20242817李臻《Linux⾼级编程实践》第8周
20242817李臻《Linux⾼级编程实践》第8周
一、AI对学习内容的总结
计算机网络概述
1. 计算机网络概述
- 计算机网络的定义:通过通信线路将地理位置不同的多台计算机连接起来,实现资源共享和信息传递。
- 网络的组成:
- 硬件:计算机、网络设备(路由器、交换机等)。
- 软件:网络操作系统、网络协议、应用软件。
- 网络协议:规定通信时信息的格式和规则,常见的协议有TCP/IP、IPX/SPX、NetBEUI等。
- 网络协议分层:
- 物理层:硬件设备的电气特性。
- 数据链路层:数据传输的规程。
- 网络层:IP协议,负责数据包的转发。
- 传输层:TCP和UDP,负责端到端的连接。
- 应用层:定义应用程序的使用规程。
2. TCP/IP协议
- 协议分层模型:
- 应用层:定义应用程序的网络使用规则。
- 传输层:TCP(面向连接、可靠)和UDP(无连接、不可靠)。
- 网络层:IP协议,负责数据包的转发。
- 数据链路层:以太网协议,负责数据帧的传输。
- 物理层:硬件设备的通信。
- TCP和UDP的区别:
- TCP:面向连接、可靠传输,适用于需要高可靠性的场景。
- UDP:无连接、不可靠传输,适用于简单的请求-响应通信和网络条件较好的场景。
3. Client/Server模型
- C/S模型:客户端负责与用户交互,服务器端负责处理请求并返回结果。
- 应用场景:大多数网络应用(如QQ、邮箱服务)都基于C/S模型。
4. Linux网络编程概述
- Linux网络功能:支持多种互联网协议(如TCP/IP),在服务器领域广泛应用。
- 网络协议栈:数据在内核中的处理流程,包括应用层、协议层和接口层。
5. Socket编程
- Socket的定义:一种通信机制,用于跨网络、跨操作系统的进程间通信。
- Socket的类型:
- SOCK_STREAM(TCP):面向连接、可靠传输。
- SOCK_DGRAM(UDP):无连接、不可靠传输。
- SOCK_RAW:原始套接字,允许直接构造协议头部。
- Socket编程的基本系统调用:
socket()
:创建套接字。bind()
:绑定套接字到本地地址和端口。listen()
:监听套接字。accept()
:接受客户端连接请求。connect()
:向服务器发送连接请求。send()
/recv()
:发送和接收数据。close()
:关闭套接字。
- 网络字节序与主机字节序:
- 网络字节序:大端模式。
- 主机字节序:小端或大端模式。
- 转换函数:
htonl()
、htons()
、ntohl()
、ntohs()
。
- 网络地址转换函数:
inet_addr()
:将点分十进制IP地址转换为二进制。inet_ntoa()
:将二进制IP地址转换为点分十进制。
6. Socket编程实例
- TCP服务器和客户端:
- 服务器:监听端口,接受连接请求,处理数据。
- 客户端:连接服务器,发送和接收数据。
- UDP服务器和客户端:
- 服务器:接收数据并返回处理结果。
- 客户端:发送数据并接收服务器的响应。
- 原始套接字:
- 用于直接构造协议头部,监听网络数据包。
- 应用:网络诊断工具(如ping、traceroute)。
7. 高级I/O模型
- 阻塞与非阻塞Socket:
- 阻塞模式:调用会等待操作完成。
- 非阻塞模式:调用立即返回,需轮询检查结果。
- 多路复用:
select()
:轮询文件描述符,检测是否可读写。poll()
:与select()
类似,但更高效。epoll()
:Linux内核提供的高效I/O事件处理机制,适用于大量连接。
8. 并发编程
- 多进程模型:
- 父进程监听连接请求,子进程处理客户端数据。
- 优点:提高并发性,响应时间快。
- 缺点:进程管理开销较大。
- 多线程模型:
- 单进程内创建多个线程处理客户端请求。
- 优点:线程间共享内存,通信效率高。
- 缺点:线程同步复杂。
9. 实例代码
- TCP服务器和客户端:
- 服务器接收客户端发送的整型数据,加1后返回。
- 客户端发送数据并接收服务器的响应。
- UDP服务器和客户端:
- 服务器接收数据并返回处理结果。
- 客户端发送数据并接收服务器的响应。
- 原始套接字:
- 监听网络数据包,打印IP和TCP头部信息。
10. 总结
- 网络编程的核心:Socket编程、协议选择(TCP/UDP)、阻塞与非阻塞模式、并发模型。
- 高级I/O模型:
select()
、poll()
、epoll()
,适用于不同场景。 - 并发编程:多进程和多线程模型,提高服务器性能。
二、 对AI总结的反思与补充
反思
1. 网络协议分层的表述问题
原文将TCP/IP模型划分为五层(包含物理层),但标准的TCP/IP四层模型不单独定义物理层。应明确说明:
- 应用层(HTTP/FTP)
- 传输层(TCP/UDP)
- 网络层(IP/ICMP)
- 网络接口层(以太网/WiFi)
建议增加OSI七层模型对比说明,避免与混合模型混淆。
2. 过时协议引用
原文列举的IPX/SPX、NetBEUI属于已淘汰协议。现代网络应聚焦:
- IPv4/IPv6协议族
- QUIC(HTTP/3底层协议)
- TLS/SSL安全协议
3. TCP服务器并发处理缺失
示例代码未体现多客户端处理,可能导致阻塞。需补充:
// 多线程处理示例
while(1) {int client_fd = accept(...);pthread_t tid;pthread_create(&tid, NULL, handle_client, &client_fd);pthread_detach(tid); // 避免僵尸线程
}
补充
1. IPv6编程扩展
- 双栈支持:通过
AI_V4MAPPED
标志同时支持IPv4/IPv6struct addrinfo hints = {0}; hints.ai_family = AF_INET6; hints.ai_flags = AI_V4MAPPED;
- 流式套接字差异:
struct sockaddr_in6 server_addr; server_addr.sin6_flowinfo = 0; // 流量标识必须显式初始化
2. 现代I/O框架
- epoll的触发模式:
- 水平触发(LT):数据未处理会重复通知
- 边缘触发(ET):只在状态变化时通知,需搭配非阻塞IO
struct epoll_event ev; ev.events = EPOLLIN | EPOLLET; // 启用ET模式
- 零拷贝技术:
sendfile(out_fd, in_fd, &offset, count); // 内核直接传输文件数据
3. 网络诊断增强
- 原始套接字权限管理:
sudo setcap cap_net_raw+ep ./program # 避免root运行
- BPF过滤器:
struct sock_filter code[] = {BPF_STMT(BPF_LD|BPF_H|BPF_ABS, 12), // 加载以太网类型字段BPF_JUMP(BPF_JMP|BPF_JEQ, 0x0800, 0, 1), // 匹配IPv4BPF_STMT(BPF_RET|BPF_K, 0xFFFF), // 允许通过BPF_STMT(BPF_RET|BPF_K, 0) // 拒绝其他 };
三、学习思维导图
第九章
mindmaproot((Linux网络编程))计算机网络概述计算机网络组成及特点资源共享自治计算机网络协议网络协议TCP/IP协议IPX/SPX协议NetBEUI协议网络协议分层物理层数据链路层互联网层传输层应用层TCP/IP协议TCP协议UDP协议IP地址Client/Server模型客户端服务器端Linux网络编程概述支持多种协议网络功能丰富网络协议栈协议栈架构数据传输流程Socket编程Socket概述通信机制由IP地址和端口描述Socket编程基本系统调用socketbindlistenacceptconnectsend/recvcloseSocket类型SOCK_STREAMSOCK_DGRAMSOCK_RAWSocket流服务TCP协议服务器端和客户端模型Socket数据包服务UDP协议服务器端和客户端模型原始套接字服务数据链路层原始套接字网络层原始套接字网络编程高级I/O阻塞与非阻塞模式阻塞模式非阻塞模式select/poll/epoll多路复用selectpollepoll非阻塞Socket用法非阻塞连接非阻塞数据发送非阻塞数据读取非阻塞接收连接网络并发编程基于多进程的网络编程父进程子进程基于多线程的网络编程线程池事件处理并发模型的优缺点多进程模型多线程模型select/poll/epoll模型
四、基于AI的学习
1、关于TCP/IP协议
-
我:我在学习LinuxTCP/IP协议知识点,请你以苏格拉底的方式对我进行提问,一次一个问题
-
AI:好的,我们开始吧。那么,请你先告诉我,你目前对TCP/IP协议有什么初步的理解呢?
-
我:TCP/IP协议是互联网的基础协议,它由传输控制协议(TCP)和互联网协议(IP)组成,用于在网络中传输数据。TCP负责可靠的数据传输,IP负责数据包的路由和寻址。
-
AI:很好,那么请你思考一下,TCP/IP协议的主要功能是什么?
-
我:TCP/IP协议的主要功能是实现不同网络之间的互联互通,确保数据能够在复杂的网络环境中可靠、高效地传输。它通过分层架构来实现这一目标,每一层负责不同的功能,比如IP层负责寻址和路由,TCP层负责可靠传输。
-
AI:非常棒!那么,你认为TCP/IP协议的分层架构是如何工作的呢?
-
我:TCP/IP协议采用分层架构,通常分为四层:应用层、传输层、互联网层和网络接口层。数据从应用层开始,逐层向下封装,最终通过网络接口层发送到物理网络;接收时则相反,数据逐层向上解封装,最终到达应用层。
-
AI:很好,那么请你思考一下,TCP和UDP协议的主要区别是什么?
-
我:TCP是一种面向连接的、可靠的传输协议,它通过三次握手建立连接,确保数据按顺序、无差错地传输。UDP是一种无连接的、不可靠的传输协议,它不保证数据的可靠传输,但传输速度快,适用于对实时性要求较高的应用。
-
AI:非常棒!那么,你认为在实际应用中,如何选择使用TCP协议还是UDP协议呢?
-
我:选择TCP还是UDP主要取决于应用的需求。如果应用对数据的可靠性要求很高,比如文件传输、网页浏览等,通常选择TCP;如果应用对实时性要求很高,比如视频流、语音通话等,通常选择UDP。
-
AI:非常棒!那么,你认为在实际开发中,使用TCP/IP协议时,需要特别注意哪些潜在问题呢?
- 我:你的理解是什么?
- AI:在实际开发中,使用TCP/IP协议时,需要特别注意以下潜在问题:
- 网络延迟和丢包:TCP/IP协议在网络层和传输层都有机制来处理丢包和延迟,但这些机制可能会导致额外的延迟。例如,TCP的重传机制可能会导致数据传输时间延长。
- 连接管理:TCP是一种面向连接的协议,需要正确管理连接的建立和关闭。如果连接没有正确关闭,可能会导致资源泄漏。
- 安全性问题:TCP/IP协议本身并不提供加密机制,因此在网络传输中数据可能会被窃取或篡改。需要结合其他安全协议(如TLS/SSL)来保护数据。
- 协议兼容性:不同的操作系统和网络设备可能对TCP/IP协议的实现存在细微差异,这可能导致兼容性问题。
- 性能优化:在高负载情况下,TCP/IP协议的性能可能会受到影响。需要通过优化网络配置、调整协议参数等方式来提高性能。
- 我:针对我该知识点,我理解了吗?我的回答结束了,请对我的回答进行评价总结。
- AI:好的,让我们来总结一下你对TCP/IP协议知识点的理解。
从你的回答来看,你对TCP/IP协议的基本概念、主要功能、分层架构、TCP和UDP的区别,以及实际应用中的选择和潜在问题都有较为清晰的认识。你能够准确地描述TCP/IP协议的工作原理和应用场景,并且能够结合实际开发场景思考可能遇到的问题,这表明你对这个知识点有较为深入的理解。
不过,还可以进一步完善的地方是,对于TCP/IP协议的详细实现机制(如TCP的拥塞控制算法、IP的路由选择算法等)可以再深入研究一下。此外,对于TCP/IP协议的安全性问题(如常见的攻击类型和防御机制)也可以进一步探讨。
总体来说,你对TCP/IP协议的知识点已经有了很好的掌握,继续保持这种学习态度,你会在这个领域取得更大的进步!
2、关于多路复用
-
我:我在学习Linux多路复用知识点,请你以苏格拉底的方式对我进行提问,一次一个问题
-
AI:好的,我们开始吧。那么,请你先告诉我,你目前对多路复用(I/O多路复用)有什么初步的理解呢?
-
我:多路复用是一种允许程序同时管理多个I/O操作的技术,它可以让单个进程同时监视多个文件描述符(如套接字),并根据哪个文件描述符准备好进行读写操作来执行相应的处理。
-
AI:很好,那么请你思考一下,多路复用的主要应用场景是什么?
-
我:多路复用主要用于需要同时处理多个网络连接或I/O操作的场景,比如服务器程序需要同时监听多个客户端的请求,或者同时处理多个文件的读写操作。
-
AI:非常棒!那么,你认为多路复用有哪些主要的实现方式呢?
-
我:多路复用主要有三种实现方式:
select
、poll
和epoll
。select
通过轮询文件描述符集合来检测是否有I/O事件发生;poll
通过数组来管理文件描述符并检测事件;epoll
则通过内核提供的事件通知机制来高效地处理大量文件描述符。 -
AI:很好,那么请你思考一下,
select
、poll
和epoll
的主要区别是什么?
-
我:
select
和poll
都是通过轮询的方式来检测文件描述符的状态,但poll
没有最大文件描述符数量的限制,而select
有。epoll
则是基于事件通知机制,它不需要轮询,效率更高,尤其是在处理大量文件描述符时。 -
AI:非常棒!那么,你认为在实际开发中,如何选择使用
select
、poll
还是epoll
呢?
-
我:在实际开发中,如果需要处理的文件描述符数量较少,
select
和poll
都可以使用。但如果需要处理大量的文件描述符,或者对性能要求较高,epoll
是更好的选择,因为它可以显著提高效率。 -
AI:非常棒!那么,你认为在实际开发中,使用多路复用技术时,需要特别注意哪些潜在问题呢?
- 我:你的理解是什么?
- AI:在实际开发中,使用多路复用技术时,需要特别注意以下潜在问题:
- 资源限制:虽然
poll
和epoll
没有文件描述符数量的限制,但系统资源(如文件描述符表的大小)仍然是有限的。需要合理管理文件描述符,避免资源耗尽。 - 性能问题:虽然
epoll
的性能优于select
和poll
,但在某些情况下,如果文件描述符的活跃度较低,epoll
的性能优势可能不明显。需要根据实际场景选择合适的多路复用机制。 - 事件处理逻辑:多路复用需要处理多种事件(如读事件、写事件、错误事件等),需要设计合理的事件处理逻辑,避免事件处理过程中的死锁或竞态条件。
- 兼容性问题:不同的操作系统和库对多路复用的支持可能有所不同,需要确保代码在目标平台上具有良好的兼容性。
- 复杂性管理:多路复用的代码逻辑相对复杂,需要合理设计程序结构,避免代码难以维护。
- 我:针对我该知识点,我理解了吗?我的回答结束了,请对我的回答进行评价总结。
- AI:好的,让我们来总结一下你对多路复用知识点的理解。
从你的回答来看,你对多路复用的基本概念、主要应用场景、实现方式(select
、poll
、epoll
),以及它们的区别和实际开发中的选择和潜在问题都有较为清晰的认识。你能够准确地描述多路复用的工作原理和应用场景,并且能够结合实际开发场景思考可能遇到的问题,这表明你对这个知识点有较为深入的理解。
不过,还可以进一步完善的地方是,对于多路复用的具体实现细节(如 epoll
的内核实现机制、select
和 poll
的性能瓶颈等)可以再深入研究一下。此外,对于多路复用在不同场景下的优化策略(如动态调整监听的文件描述符集合)也可以进一步探讨。
总体来说,你对多路复用的知识点已经有了很好的掌握,继续保持这种学习态度,你会在这个领域取得更大的进步!
五、基于AI的代码实践
使用Gitee进行代码托管
仓库链接:
https://gitee.com/li-zhen1215/homework/tree/master/Week8
例1
test.c
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>void main(int argc, char *argv[]) {char ipbuf[20]; // save ip addressunsigned int ipint; // ip address 的整数形式struct in_addr addrstr;char *ch=NULL;while (1) {printf("please input ip address\n");fgets(ipbuf, 20, stdin);if (!strcmp(ipbuf, "exit\n")) break;ipbuf[strlen(ipbuf)-1] = '0'; // 去掉 fgets 输入的 \nipint = inet_addr(ipbuf);printf("The binary of ip address is %s\n", ipbuf);printf("The host byte of binary ip address is %u\n", inet_network(ipbuf));inet_aton(ipbuf, &addrstr);ch = (char *)&addrstr;printf("The inet_aton result is %x\n", addrstr.s_addr);printf("The network byte of ip address is %x.%x.%x.%x\n", (*ch)&0xff, *(ch+1)&0xff, *(ch+2)&0xff, *(ch+3)&0xff);printf("The integer value of ip address is %u\n", ipint);}
}
实践截图
例2
tcpservber.c
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#define PORT 8888int main() {int sock, new_sock;struct sockaddr_in my_addr, client_addr;socklen_t len;char buf[100];char buf2[128];int recdata=0;if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {perror("socket create error\n");exit(1);}memset(&my_addr, 0, sizeof(my_addr));my_addr.sin_family = AF_INET;my_addr.sin_port = htons(PORT);my_addr.sin_addr.s_addr = INADDR_ANY;if (bind(sock, (struct sockaddr *)&my_addr, sizeof(my_addr)) == -1) {perror("bind error\n");exit(1);}if (listen(sock, 5) < 0) {perror("listen error\n");exit(1);}while (1) {len = sizeof(struct sockaddr);if ((new_sock = accept(sock, (struct sockaddr *)&client_addr, &len)) < 0) {perror("accept error\n");exit(1);} else {printf("server get connection from ip:port %s:%d socket id %d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), new_sock);}len = recv(new_sock, buf, 100, 0);if (len < 0) {printf("recv error\n");exit(1);} else if (len == 0) {printf("the client quit\n");break;} else {buf[len] = '\0';printf("receive message is %s\n", buf);recdata = atoi(buf);recdata++;sprintf(buf2, "%d", recdata);if (send(new_sock, buf2, strlen(buf2), 0) < 0) {perror("send data failed\n");}}close(sock);close(new_sock);}
}
tcpclient.c
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8888int main(int argc, char **argv) {int sock;struct sockaddr_in my_addr;int len;char buf[100];char recbuf[100];if (argc < 2) {printf("Usage: %s <ip>\n", argv[0]);exit(1);}if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {perror("socket create error\n");exit(1);}my_addr.sin_family = AF_INET;my_addr.sin_port = htons(8888);if (inet_aton(argv[1], (struct in_addr *)&my_addr.sin_addr.s_addr) == 0) {perror("change error\n");exit(1);}if (connect(sock, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) < 0) {printf("connect error\n");exit(1);}printf("connected\n");printf("Input data to send\n ");fgets(buf, 100, stdin);len = send(sock, buf, strlen(buf) - 1, 0);if (len < 0) {perror("send error\n");exit(1);}len = recv(sock, recbuf, 100, 0);recbuf[len] = '\0';if (len < 0) {perror("recv error\n");exit(1);}printf("the received data from server is %s\n", recbuf);close(sock);
}
实践截图:
例3
udpserver.c
/*udpserver.c*/
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h> // 添加这一行
int main(int argc, char **argv){struct sockaddr_in s_addr,c_addr;int sock;socklen_t addr_len;int len;char buf[128];char buf2[128];int recdata=0;if ((sock = socket(AF_INET, SOCK_DGRAM,'0')) == -1) {perror("socket");exit(1);} elseprintf("create socket connected.\n");memset(&s_addr, 0, sizeof(struct sockaddr_in));s_addr.sin_family = AF_INET;s_addr.sin_port = htons(8888);s_addr.sin_addr.s_addr = INADDR_ANY;if (bind(sock, (struct sockaddr *) &s_addr, sizeof(s_addr)) == -1) {perror("bind failed\n");exit(1);} elseprintf("bind address to socket.\n");addr_len = sizeof(c_addr);while (1) {memset(buf,0,sizeof(buf));len = recvfrom(sock, buf, sizeof(buf) - 1, 0, (struct sockaddr *) &c_addr, &addr_len);if (len < 0) {perror("recvfrom error.\n");exit(errno);}buf[len] = '\0';printf("recive come from %s:%d message:%s\n\r",inet_ntoa(c_addr.sin_addr), ntohs(c_addr.sin_port), buf);recdata=atoi(buf);recdata++;sprintf(buf2,"%d",recdata);sendto(sock, buf2, strlen(buf2),0, (struct sockaddr *) &c_addr, addr_len);}close(sock); // 这里使用 close 函数return 0;
}
udpclient.c
/*udpclient.c*/
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h> // 添加这一行
int main(int argc, char **argv){int sock;int addr_len;int len;char buff[128];struct sockaddr_in s_addr;if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {perror("socket create failed");exit(1);} elseprintf("socket create successful.\n");s_addr.sin_family = AF_INET;s_addr.sin_port = htons(8888);memset(s_addr.sin_zero, '\0', sizeof(s_addr.sin_zero));if (argc>2){s_addr.sin_addr.s_addr = inet_addr(argv[1]);strcpy(buff,argv[2]);} else {printf("input server ip and parameter!\n");exit(0);}addr_len = sizeof(s_addr);len = sendto(sock, buff, strlen(buff), 0, (struct sockaddr *) &s_addr, addr_len);if (len < 0) {perror("send error.\n");exit(1);}sleep(1); // 这里使用 sleep 函数len = recvfrom(sock, buff, sizeof(buff) - 1, 0, (struct sockaddr *) &s_addr, &addr_len);if (len < 0) {perror("recvfrom error.\n");exit(1);}printf("receive from server %s\n",buff);close(sock); // 这里使用 close 函数return 0;
}
实践截图:
例4
sk.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <unistd.h>int Open_Raw_Socket(void);
int Set_Promisc(char *interface, int sock);int main() {int sock;sock = Open_Raw_Socket();printf("raw socket is %d\n", sock);char buffer[65535];int bytes_received;socklen_t fromlen;struct sockaddr_in from;struct iphdr *iphdr;struct tcphdr *tcphdr;Set_Promisc("eth0", sock);printf("IP header is %zu \n", sizeof(struct iphdr));printf("TCP header is %zu \n", sizeof(struct tcphdr));while (1) {fromlen = sizeof(from);bytes_received = recvfrom(sock, buffer, sizeof(buffer),0, (struct sockaddr*)&from, &fromlen);printf("\nBytes received: %d\n", bytes_received);printf("Source address: %s\n", inet_ntoa(from.sin_addr));iphdr = (struct iphdr*)buffer;if (iphdr->protocol == IPPROTO_TCP) {printf("Dest address: %s\n", inet_ntoa((struct in_addr){iphdr->daddr}));printf("IP Header Length: %d bytes\n", iphdr->ihl * 4);printf("Protocol: %d\n", iphdr->protocol);printf("Type of Service: %d\n", iphdr->tos);printf("Time to Live: %d\n", iphdr->ttl);printf("Checksum: %d\n", ntohs(iphdr->check));int ip_header_len = iphdr->ihl * 4;tcphdr = (struct tcphdr*)(buffer + ip_header_len);printf("Dest port: %d\n", ntohs(tcphdr->dest));printf("Source port: %d\n", ntohs(tcphdr->source));printf("Seq number: %u\n", ntohl(tcphdr->seq));printf("Ack number: %u\n", ntohl(tcphdr->ack_seq));// 组合TCP标志位u_int8_t flags = ((tcphdr->urg << 5) | (tcphdr->ack << 4) | (tcphdr->psh << 3) | (tcphdr->rst << 2) | (tcphdr->syn << 1) | tcphdr->fin);printf("Flags: 0x%02x (URG:%d ACK:%d PSH:%d RST:%d SYN:%d FIN:%d)\n",flags,tcphdr->urg, tcphdr->ack, tcphdr->psh,tcphdr->rst, tcphdr->syn, tcphdr->fin);}}close(sock);return 0;
}int Open_Raw_Socket(void) {int sock;if ((sock = socket(AF_INET, SOCK_RAW, IPPROTO_TCP)) < 0) {perror("socket");exit(EXIT_FAILURE);}return sock;
}int Set_Promisc(char *interface, int sock) {struct ifreq ifr;strncpy(ifr.ifr_name, interface, IFNAMSIZ - 1);ifr.ifr_name[IFNAMSIZ - 1] = '\0';if (ioctl(sock, SIOCGIFFLAGS, &ifr) == -1) {perror("ioctl SIOCGIFFLAGS");close(sock);exit(EXIT_FAILURE);}ifr.ifr_flags |= IFF_PROMISC;if (ioctl(sock, SIOCSIFFLAGS, &ifr) == -1) {perror("ioctl SIOCSIFFLAGS");close(sock);exit(EXIT_FAILURE);}return 0;
}
实践截图:
例5
test.c
#include <sys/types.h>
#include <sys/socket.h>
#include <error.h>
#include <errno.h>
#include <fcntl.h>
#include <time.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <errno.h>void main(int argc, char** argv) {int sock;struct sockaddr_in my_addr;int asynflag=1;time_t t1,t2;if((sock=socket(AF_INET,SOCK_STREAM,0))<0){perror("socket create error!\n");exit(1);}my_addr.sin_family=AF_INET;my_addr.sin_port=htons(8888);if(inet_aton(argv[1],(struct in_addr *)&my_addr.sin_addr.s_addr) == 0){printf("%s chage error!\n",argv[1]);exit(1);}t1=time(NULL);/*ioctl (sock, FIONBIO, &asynflag);*/if(connect(sock,(struct sockaddr*)&my_addr,sizeof(struct sockaddr))<0){if (errno!=EINPROGRESS) {perror("connect error!\n");}}t2=time(NULL);printf("connect used time is %d second\n",(int)(t2-t1));close(sock);
}
实践截图
例7
a.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <poll.h> // 包含 poll.h 头文件#define MAXLINE 10240
#ifndef OPEN_MAX
#define OPEN_MAX 40960
#endifvoid handle(struct pollfd* clients, int maxClient, int readyClient);int main(int argc, char **argv) {int servPort = 6888;int listening = 1024;int listenfd, connfd;struct pollfd clients[OPEN_MAX];int maxi;socklen_t socklen = sizeof(struct sockaddr_in);struct sockaddr_in cliaddr, servaddr;char buf[MAXLINE];int ready;int opt = 1;int nready; // 声明 nready 变量if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {perror("socket error");exit(-1);}if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {perror("setsockopt error");exit(-1);}memset(&servaddr, 0, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);servaddr.sin_port = htons(servPort);if (bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) == -1) {perror("bind error");exit(-1);}if (listen(listenfd, listening) < 0) {perror("listen error");exit(-1);}clients[0].fd = listenfd;clients[0].events = POLLIN;int i;for (i = 1; i < OPEN_MAX; i++) {clients[i].fd = -1;clients[i].events = 0;}maxi = listenfd + 1;printf("pollechoserver startup, listen on port:%d\n", servPort);printf("max connection is %d\n", OPEN_MAX);for (;;) {ready = poll(clients, maxi + 1, -1);if (ready == -1) {perror("poll error");continue;}for (i = 0; i < maxi; i++) {if (clients[i].fd == -1) continue;if (clients[i].revents & POLLIN) {if (i == 0) { // listenfdconnfd = accept(listenfd, (struct sockaddr *) &cliaddr, &socklen);if (connfd < 0) {perror("accept error");continue;}if (connfd >= OPEN_MAX) {printf("too many connection, more than %d\n", OPEN_MAX);close(connfd);continue;}clients[i].fd = connfd;clients[i].events = POLLIN;} else { // clientfdnready = read(clients[i].fd, buf, MAXLINE);if (nready < 0) {perror("read error");close(clients[i].fd);clients[i].fd = -1;continue;}if (nready == 0) {printf("client close the connection\n");close(clients[i].fd);clients[i].fd = -1;continue;}buf[nready] = '\0';printf("Server received data is %s\n", buf);}}}}
}void handle(struct pollfd* clients, int maxClient, int readyClient) {int connfd;int i, nread;char buf[MAXLINE];if (readyClient == 0)return;for (i = 1; i < maxClient; i++) {connfd = clients[i].fd;if (connfd == -1)continue;if (clients[i].revents & POLLIN) {nread = read(connfd, buf, MAXLINE);if (nread < 0) {perror("read error");close(connfd);clients[i].fd = -1;continue;}if (nread == 0) {printf("client close the connection\n");close(connfd);clients[i].fd = -1;continue;}buf[nread] = '\0';printf("Server received data is %s\n", buf);}}
}
实践截图:
习题1
使用socket()函数创建一个套接字,该函数需要指定地址族(如AF_INET)、套接字类型(如SOCK_STREAM或SOCK_DGRAM)以及协议(如IPPROTO_TCP或IPPROTO_UDP)。接着,通过setsockopt()函数设置套接字的选项,例如允许端口重用(SO_REUSEADDR)。然后,使用bind()函数将套接字绑定到特定的IP地址和端口上,以便监听和接受连接请求。
对于服务器端,调用listen()函数使套接字进入监听状态,准备接受客户端的连接。当有客户端请求连接时,服务器使用accept()函数接受连接,该函数返回一个新的套接字用于与客户端进行数据交换。
数据传输过程中,客户端和服务器端可以使用send()和recv()函数来发送和接收数据。这些函数可以指定发送或接收的数据量,并处理网络字节序的转换。
最后,完成数据交换后,使用close()函数关闭套接字,释放相关资源。在整个过程中,需要对每个函数调用进行错误检查,以确保网络通信的稳定性和可靠性。此外,还可以使用poll()或select()等函数来管理多个套接字,实现非阻塞或多路复用的网络编程模型。
习题2
TCP和UDP是两种主要的传输层协议,它们在可靠性和连接方式上有本质区别。TCP是面向连接的可靠协议,通过三次握手建立连接,提供数据确认、重传和流量控制机制,确保数据有序到达,编程时需要先建立连接再传输数据;而UDP是无连接的不可靠协议,直接发送数据报,不保证顺序和到达,但开销小速度快,编程时直接发送接收数据包无需连接。TCP适用于文件传输、网页浏览等需要可靠性的场景,UDP适合视频流、实时游戏等对延迟敏感的应用。
习题3
IP地址10.20.30.40的二进制形式是00001010.00010100.00011110.00101000;主机字节序在不同系统中可能不同(小端序为0x281e140a,大端序为0x0a141e28);网络字节序(大端序)固定为0x0a141e28;网络地址和主机地址的划分取决于子网掩码,若使用默认的C类掩码255.255.255.0,则网络地址是10.20.30.0,主机地址是0.0.0.40。
习题4
select调用和多进程/线程实现是两种不同的并发处理方式。select属于I/O多路复用技术,通过单进程监控多个文件描述符的状态变化,适合处理大量并发连接但计算量小的场景,优点是资源占用少、避免进程切换开销,缺点是无法充分利用多核CPU且编程复杂度高。多进程/线程则为每个连接创建独立执行单元,能充分利用多核并行处理,适合计算密集型任务,优点是编程模型简单、隔离性好,缺点是进程/线程创建和切换开销大,并发数受系统资源限制。select适合高并发低负载的网络服务,而多进程/线程更适合需要并行计算或强隔离性的应用。
习题5
server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define PORT 8080
#define BUFFER_SIZE 1024int main() {int server_fd, new_socket;struct sockaddr_in address;int opt = 1;int addrlen = sizeof(address);char buffer[BUFFER_SIZE] = {0};char filename[256] = {0};// 创建 socket 文件描述符if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}// 绑定 socket 到端口address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(PORT);if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {perror("bind failed");exit(EXIT_FAILURE);}if (listen(server_fd, 3) < 0) {perror("listen");exit(EXIT_FAILURE);}if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {perror("accept");exit(EXIT_FAILURE);}// 接收文件名read(new_socket, filename, 255);printf("Receiving file: %s\n", filename);FILE *file = fopen(filename, "wb");if (file == NULL) {perror("File opening failed");exit(EXIT_FAILURE);}// 接收文件数据int bytes_read;while ((bytes_read = read(new_socket, buffer, BUFFER_SIZE)) > 0) {fwrite(buffer, 1, bytes_read, file);}fclose(file);printf("File transfer completed.\n");close(new_socket);close(server_fd);return 0;
}
client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define PORT 8080
#define BUFFER_SIZE 1024int main(int argc, char const *argv[]) {if (argc != 3) {fprintf(stderr, "Usage: %s <filename> <server IP>\n", argv[0]);exit(EXIT_FAILURE);}int sock = 0;struct sockaddr_in serv_addr;char buffer[BUFFER_SIZE] = {0};char *filename = argv[1];char *server_ip = argv[2];// 创建 socket 文件描述符if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {printf("\n Socket creation error \n");return -1;}serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(PORT);// 转换 IPv4 地址从文本到二进制if (inet_pton(AF_INET, server_ip, &serv_addr.sin_addr) <= 0) {printf("\nInvalid address/ Address not supported \n");return -1;}// 连接到服务器if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {printf("\nConnection Failed \n");return -1;}// 发送文件名send(sock, filename, strlen(filename), 0);printf("Sending file: %s\n", filename);FILE *file = fopen(filename, "rb");if (file == NULL) {perror("File opening failed");exit(EXIT_FAILURE);}// 发送文件数据int bytes_read;while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, file)) > 0) {send(sock, buffer, bytes_read, 0);}fclose(file);printf("File transfer completed.\n");// 发送结束信号const char *end_signal = "END";send(sock, end_signal, strlen(end_signal), 0);close(sock);return 0;
}
实践截图:
习题6
server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>#define PORT 8080
#define BUFFER_SIZE 1024void *handle_client(void *client_socket) {int sock = *((int *)client_socket);free(client_socket);char buffer[BUFFER_SIZE] = {0};char filename[256] = {0};// 接收文件名read(sock, filename, 255);printf("Receiving file: %s\n", filename);FILE *file = fopen(filename, "wb");if (file == NULL) {perror("File opening failed");close(sock);return NULL;}// 接收文件数据int bytes_read;while ((bytes_read = read(sock, buffer, BUFFER_SIZE)) > 0) {fwrite(buffer, 1, bytes_read, file);}fclose(file);printf("File transfer completed.\n");close(sock);return NULL;
}int main() {int server_fd, new_socket;struct sockaddr_in address;int opt = 1;int addrlen = sizeof(address);pthread_t thread_id;// 创建 socket 文件描述符if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}// 绑定 socket 到端口address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(PORT);if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {perror("bind failed");exit(EXIT_FAILURE);}if (listen(server_fd, 3) < 0) {perror("listen");exit(EXIT_FAILURE);}while (1) {if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {perror("accept");exit(EXIT_FAILURE);}int *client_sock = malloc(sizeof(int));*client_sock = new_socket;pthread_create(&thread_id, NULL, handle_client, (void *)client_sock);}close(server_fd);return 0;
}
client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define PORT 8080
#define BUFFER_SIZE 1024int main(int argc, char const *argv[]) {if (argc != 3) {fprintf(stderr, "Usage: %s <filename> <server IP>\n", argv[0]);exit(EXIT_FAILURE);}int sock = 0;struct sockaddr_in serv_addr;char buffer[BUFFER_SIZE] = {0};const char *filename = argv[1];const char *server_ip = argv[2];// 创建 socket 文件描述符if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {printf("\n Socket creation error \n");return -1;}serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(PORT);// 转换 IPv4 地址从文本到二进制if (inet_pton(AF_INET, server_ip, &serv_addr.sin_addr) <= 0) {printf("\nInvalid address/ Address not supported \n");return -1;}// 连接到服务器if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {printf("\nConnection Failed \n");return -1;}// 发送文件名send(sock, filename, strlen(filename), 0);printf("Sending file: %s\n", filename);FILE *file = fopen(filename, "rb");if (file == NULL) {perror("File opening failed");exit(EXIT_FAILURE);}// 发送文件数据int bytes_read;while ((bytes_read = fread(buffer, 1, BUFFER_SIZE, file)) > 0) {send(sock, buffer, bytes_read, 0);}fclose(file);printf("File transfer completed.\n");// 发送结束信号const char *end_signal = "END";send(sock, end_signal, strlen(end_signal), 0);close(sock);return 0;
}
习题7
FTP协议采用双通道设计(控制连接和数据连接),通过21端口建立控制连接传输命令,再动态创建数据连接(主动模式20端口/被动模式随机端口)传输文件。Linux中的FTP实现(如vsftpd/proftpd)源码通常包含用户认证、命令解析、文件传输等模块,通过socket编程处理连接,使用状态机管理会话流程,结合系统调用实现文件操作。其设计注重安全(如chroot隔离)、性能(异步I/O)和扩展性(模块化架构),源代码分析可追踪socket绑定、PORT/PASV模式切换及文件读写等核心逻辑的实现细节。
习题8
test.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <netinet/ether.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <linux/if_packet.h>#define BUFFER_SIZE 65536int main() {int sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));if (sockfd < 0) {perror("socket");exit(1);}unsigned char buffer[BUFFER_SIZE];while (1) {int data_size = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, NULL, NULL);if (data_size < 0) {perror("recvfrom");close(sockfd);exit(1);}struct ethhdr *eth = (struct ethhdr *)buffer;printf("\n以太网头部:源MAC %.2X:%.2X:%.2X:%.2X:%.2X:%.2X → 目标MAC %.2X:%.2X:%.2X:%.2X:%.2X:%.2X 类型 %.4X\n",eth->h_source[0], eth->h_source[1], eth->h_source[2],eth->h_source[3], eth->h_source[4], eth->h_source[5],eth->h_dest[0], eth->h_dest[1], eth->h_dest[2],eth->h_dest[3], eth->h_dest[4], eth->h_dest[5],ntohs(eth->h_proto));if (ntohs(eth->h_proto) == ETH_P_IP) {struct iphdr *ip = (struct iphdr *)(buffer + sizeof(struct ethhdr));printf("IP头部:%s → %s 协议 %d TTL %d\n",inet_ntoa(*(struct in_addr *)&ip->saddr),inet_ntoa(*(struct in_addr *)&ip->daddr),ip->protocol, ip->ttl);if (ip->protocol == IPPROTO_TCP) {struct tcphdr *tcp = (struct tcphdr *)(buffer + sizeof(struct ethhdr) + ip->ihl*4);printf("TCP头部:源端口 %d → 目标端口 %d 标志 [%s%s%s%s%s%s] 序列号 %u\n",ntohs(tcp->source), ntohs(tcp->dest),tcp->urg ? "URG " : "", tcp->ack ? "ACK " : "",tcp->psh ? "PSH " : "", tcp->rst ? "RST " : "",tcp->syn ? "SYN " : "", tcp->fin ? "FIN " : "",ntohl(tcp->seq));}}}close(sockfd);return 0;
}
实践截图:
六、学习实践过程遇到的问题与解决方式
1. Socket套接字使用失败
问题描述
不了解Socket套接字使用失败的原因和解决方法。
解决方式
- Socket套接字使用失败(Socket Failure):是指在使用Socket进行网络编程时,由于各种原因导致Socket无法正常工作。这可能包括无法建立连接、数据传输失败、连接中断等情况。Socket套接字使用失败通常会影响程序的正常运行,导致网络通信无法完成。
- 失败的原因:
- 网络问题:网络连接不可用、网络延迟过高或网络配置错误可能导致Socket无法建立连接或数据传输失败。
- 资源限制:系统资源不足(如文件描述符耗尽、内存不足)可能导致Socket创建失败或无法正常工作。
- 错误的API调用:错误的Socket API调用,如绑定错误的地址、端口冲突、错误的协议类型等,可能导致Socket无法正常工作。
- 对端问题:对端服务器或客户端可能关闭连接、拒绝连接或发送错误的响应,导致Socket使用失败。
- 解决方法:
- 检查网络连接:确保网络配置正确,网络连接可用。可以使用
ping
或traceroute
等工具检查网络连通性。 - 检查资源限制:确保系统资源充足,特别是文件描述符和内存资源。可以通过调整系统参数(如
ulimit
)来增加资源限制。 - 验证API调用:仔细检查Socket API调用的参数和逻辑,确保没有错误。可以参考API文档或示例代码进行验证。
- 处理对端问题:确保对端服务正常运行,端口开放且没有防火墙限制。可以使用
telnet
或nc
等工具检查对端端口是否可用。 - 错误处理:在代码中添加适当的错误处理逻辑,捕获并处理可能出现的错误。例如,使用
try-catch
块捕获异常,并根据错误类型进行相应的处理。
- 检查网络连接:确保网络配置正确,网络连接可用。可以使用
- 应用场景:Socket套接字使用失败是网络编程中常见的问题,特别是在复杂的网络环境或高并发场景下。了解失败的原因和解决方法可以帮助开发者更好地调试和优化网络程序,提高系统的可靠性和稳定性。
2. 多路复用
问题描述
不了解多路复用(I/O多路复用)的概念、原理及应用场景。
解决方式
- 多路复用(I/O Multiplexing):是一种允许单个进程同时管理多个I/O操作的技术,它通过监控多个文件描述符(如套接字、文件等),确定哪些文件描述符已经准备好进行读写操作,从而实现高效地处理多个I/O请求。多路复用在高并发网络编程中尤为重要,能够显著提高程序的性能和效率。
- 多路复用的原理:
select
:通过轮询的方式检查一组文件描述符集合,确定哪些描述符已经准备好进行I/O操作。select
的效率较低,因为它需要遍历整个文件描述符集合,且存在最大文件描述符数量的限制。poll
:通过数组管理一组文件描述符,并检查每个描述符的状态。poll
没有最大文件描述符数量的限制,但仍然需要轮询,效率在处理大量文件描述符时不如epoll
。epoll
:基于事件通知机制,内核会主动通知程序哪些文件描述符已经准备好进行I/O操作。epoll
不需要轮询,效率更高,尤其适合处理大量文件描述符的场景。
- 应用场景:
- 高并发服务器:如Web服务器、聊天服务器等,需要同时处理多个客户端的连接请求和数据传输。多路复用技术可以高效地管理这些连接,避免为每个连接创建一个线程或进程,从而节省系统资源。
- 实时数据处理:如金融交易系统、实时监控系统等,需要快速响应多个数据源的输入和输出。多路复用可以确保及时处理每个数据源的I/O操作,提高系统的实时性。
- 资源管理:在资源受限的环境中(如嵌入式系统),多路复用可以有效利用有限的系统资源,同时处理多个任务,提高系统的整体性能。
- 使用多路复用时的注意事项:
- 资源限制:虽然
poll
和epoll
没有文件描述符数量的限制,但系统资源(如文件描述符表的大小)仍然是有限的。需要合理管理文件描述符,避免资源耗尽。 - 性能优化:根据实际应用场景选择合适的多路复用机制。例如,在处理大量文件描述符时,
epoll
的性能优势更为明显。 - 错误处理:在多路复用的代码中,需要添加适当的错误处理逻辑,以应对文件描述符关闭、网络错误等情况。
- 兼容性问题:不同的操作系统和库对多路复用的支持可能有所不同,需要确保代码在目标平台上具有良好的兼容性。
- 代码复杂性:多路复用的代码逻辑相对复杂,需要合理设计程序结构,避免代码难以维护。
- 资源限制:虽然
七、参考资料
- AI工具(你使用的AI工具及其链接)
- 问小白
- Kimi
- 图书
- 《Linux编程基础.李养群》电子版
- 网站
- CSDN
“20242817李臻 原创作品转载请注明出处 《Linux高级编程实践》”