# linux-network-programming **Repository Path**: zkchuhu/linux-network-programming ## Basic Information - **Project Name**: linux-network-programming - **Description**: linux-network-programming - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-08-08 - **Last Updated**: 2025-08-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Linux网络编程学习项目 - 详细教程 ## 项目简介 这是一个Linux网络编程学习项目,涵盖了从基础的UDP/TCP通信到高级网络编程技术的完整示例。项目旨在帮助初学者系统地学习Linux网络编程,从简单的套接字通信开始,逐步深入到并发处理、I/O多路复用、原始套接字等高级主题。 ## 网络编程基础理论 ### 计算机网络体系结构 在深入学习网络编程之前,我们需要理解计算机网络的基本体系结构。目前广泛采用的是OSI七层模型和TCP/IP四层模型: #### OSI七层模型 1. **物理层(Physical Layer)**: 定义物理设备标准,如网线接口类型、光纤接口类型、传输介质等。主要功能是传输比特流(0和1)。 2. **数据链路层(Data Link Layer)**: 定义了如何格式化数据以进行传输,以及对物理介质的访问控制。主要协议包括以太网协议,负责MAC地址寻址、差错校验等。 3. **网络层(Network Layer)**: 实现数据包的路由选择,选择合适的路径将数据包传输到目标节点。主要协议是IP协议,负责IP地址寻址、路由选择等。 4. **传输层(Transport Layer)**: 定义了两种数据传输协议TCP和UDP,负责建立端到端的连接,提供可靠或不可靠的数据传输服务。 5. **会话层(Session Layer)**: 负责建立、管理和终止会话,控制应用程序之间的对话。 6. **表示层(Presentation Layer)**: 数据的表示、安全、压缩等,确保一个系统应用层的信息可以被另一个系统的应用层读取。 7. **应用层(Application Layer)**: 为应用程序提供网络服务,如HTTP、FTP、SMTP等协议。 #### TCP/IP四层模型 1. **链路层(Link Layer)**: 对应OSI模型的物理层和数据链路层,负责处理与物理网络的接口细节。 2. **网络层(Internet Layer)**: 对应OSI模型的网络层,主要协议是IP协议,负责数据包的路由和转发。 3. **传输层(Transport Layer)**: 对应OSI模型的传输层,主要协议是TCP和UDP,负责端到端的数据传输。 4. **应用层(Application Layer)**: 对应OSI模型的会话层、表示层和应用层,为应用程序提供网络服务。 #### 两种模型的对比 TCP/IP模型更加简洁实用,而OSI模型更加理论化。在实际应用中,TCP/IP模型更为广泛使用。 ### TCP协议详解 TCP(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。 #### TCP的特点 1. **面向连接**: 在数据传输之前需要先建立连接(三次握手),传输结束后需要断开连接(四次挥手) 2. **可靠传输**: 通过确认机制、重传机制、流量控制和拥塞控制保证数据的完整性和顺序 3. **全双工通信**: 通信双方可以同时发送和接收数据 4. **面向字节流**: 将数据看作字节流,不保留数据边界 5. **有状态**: TCP会记录连接的各种状态信息 #### TCP报文段格式 TCP报文段由首部和数据两部分组成,首部最小长度为20字节,最大为60字节: ``` 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Source Port | Destination Port | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Sequence Number | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Acknowledgment Number | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Data | |U|A|P|R|S|F| | | Offset| Reserved |R|C|S|S|Y|I| Window | | | |G|K|H|T|N|N| | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Checksum | Urgent Pointer | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Options | Padding | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | data | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ``` 各字段说明: - **Source Port**和**Destination Port**: 源端口号和目的端口号,各占16位 - **Sequence Number**: 序列号,占32位,用来标识从TCP发送端向TCP接收端发送的数据字节流 - **Acknowledgment Number**: 确认号,占32位,期望收到对方下一个报文段的第一个数据字节的序号 - **Data Offset**: 数据偏移,占4位,指出TCP报文段的数据起始处距离TCP报文段的起始处有多远 - **URG**: 紧急位,当URG=1时,表明紧急指针字段有效 - **ACK**: 确认位,当ACK=1时,确认号字段才有效 - **PSH**: 推送位,当PSH=1时,接收方应尽快将这个报文段交给应用层 - **RST**: 复位位,当RST=1时,表明TCP连接中出现严重差错,必须释放连接 - **SYN**: 同步位,当SYN=1时,表明这是一个连接请求或连接接受报文 - **FIN**: 终止位,当FIN=1时,表明此报文段的发送方的数据已发送完毕,并要求释放连接 - **Window**: 窗口,占16位,用来让对方设置发送窗口的依据 - **Checksum**: 检验和,占16位,检验首部和数据 - **Urgent Pointer**: 紧急指针,占16位,指出在本报文段中紧急数据共有多少个字节 #### TCP三次握手详解 TCP连接的建立需要通过三次握手来完成: 1. **第一次握手**: 客户端发送连接请求报文段,SYN=1,seq=x,进入SYN-SENT状态 2. **第二次握手**: 服务器收到连接请求报文段后,如果同意连接,则发送确认报文段,SYN=1,ACK=1,seq=y,ack=x+1,进入SYN-RCVD状态 3. **第三次握手**: 客户端收到服务器的确认报文段后,还要向服务器发送确认报文段,ACK=1,seq=x+1,ack=y+1,进入ESTABLISHED状态 三次握手的目的是为了防止已失效的连接请求报文段突然又传送到服务器,从而产生错误。 #### TCP四次挥手详解 TCP连接的释放需要通过四次挥手来完成: 1. **第一次挥手**: 客户端发送连接释放报文段,FIN=1,seq=u,进入FIN-WAIT-1状态 2. **第二次挥手**: 服务器收到连接释放报文段后,发送确认报文段,ACK=1,seq=v,ack=u+1,进入CLOSE-WAIT状态 3. **第三次挥手**: 服务器发送连接释放报文段,FIN=1,ACK=1,seq=w,ack=u+1,进入LAST-ACK状态 4. **第四次挥手**: 客户端收到连接释放报文段后,发送确认报文段,ACK=1,seq=u+1,ack=w+1,进入TIME-WAIT状态 客户端需要等待2MSL(最长报文段寿命)后才进入CLOSED状态,这是为了确保服务器能收到客户端发出的确认报文段。 #### TCP状态转换图 ```mermaid stateDiagram-v2 [*] --> CLOSED CLOSED --> LISTEN: 被动打开 CLOSED --> SYN_SENT: 主动打开 SYN_SENT --> ESTABLISHED: 收到SYN+ACK SYN_SENT --> CLOSED: 超时或拒绝 LISTEN --> SYN_RCVD: 收到SYN SYN_RCVD --> ESTABLISHED: 收到ACK SYN_RCVD --> CLOSED: 超时或RST ESTABLISHED --> FIN_WAIT_1: 主动关闭 FIN_WAIT_1 --> FIN_WAIT_2: 收到ACK FIN_WAIT_1 --> TIME_WAIT: 同时收到FIN FIN_WAIT_2 --> TIME_WAIT: 收到FIN TIME_WAIT --> CLOSED: 2MSL超时 ESTABLISHED --> CLOSE_WAIT: 收到FIN CLOSE_WAIT --> LAST_ACK: 发送FIN LAST_ACK --> CLOSED: 收到ACK ``` 关键状态说明: - **CLOSED**: 初始状态,没有连接活动或正在进行 - **LISTEN**: 服务器等待客户端连接请求 - **SYN_SENT**: 客户端已发送连接请求,等待匹配的连接请求 - **SYN_RCVD**: 服务器已接收到客户端连接请求,发送确认后等待确认连接请求 - **ESTABLISHED**: 连接已经建立,可以进行数据传输 - **FIN_WAIT_1**: 主动关闭方已发送关闭连接请求,等待ACK - **FIN_WAIT_2**: 主动关闭方已收到关闭连接请求的确认,等待连接关闭请求 - **CLOSE_WAIT**: 被动关闭方已接收到关闭连接请求,等待本地用户关闭连接 - **CLOSING**: 两端同时尝试关闭连接,等待对方确认 - **LAST_ACK**: 被动关闭方发送关闭连接请求后,等待确认 - **TIME_WAIT**: 主动关闭方在收到被动关闭方的关闭连接请求后,等待可能丢失的最后一个ACK 状态转换触发条件: - **SYN**: 同步位,用于建立连接 - **ACK**: 确认位,确认收到数据 - **FIN**: 终止位,用于释放连接 - **RST**: 复位位,用于重置连接 #### TCP滑动窗口机制 滑动窗口是TCP流量控制的核心机制,它解决了以下问题: 1. 接收方处理能力不足时防止数据丢失 2. 提高网络带宽利用率 3. 实现端到端的流量控制 窗口工作原理: 1. 发送方维护发送窗口,窗口内的数据可以发送但未确认 2. 接收方维护接收窗口,表示还能接收多少数据 3. 确认号表示期望接收的下一个字节序号 4. 窗口大小动态调整,根据接收方缓冲区和网络状况 典型场景示例: 1. 发送方窗口大小为4000字节,发送1000字节数据 2. 接收方确认收到1000字节,通告窗口大小为3000字节 3. 发送方窗口滑动,继续发送数据 4. 接收方处理速度变慢,通告窗口减小为1000字节 5. 发送方调整发送速率,避免缓冲区溢出 窗口大小动态调整因素: - 接收方缓冲区剩余空间 - 网络拥塞程度 - 往返时间(RTT) - 应用程序读取数据的速度 典型问题处理: - **零窗口**: 接收方缓冲区满,发送方暂停发送并定期探测窗口状态 - **窗口探测**: 发送方定期发送1字节数据查询窗口状态 - **糊涂窗口综合征**: 避免发送和接收小数据块,提高传输效率 #### TCP可靠传输的实现机制 1. **确认和重传机制**: 接收方收到数据后会发送确认,发送方在一定时间内未收到确认就会重传 2. **流量控制**: 通过滑动窗口机制控制发送方的发送速度,避免接收方缓冲区溢出 3. **拥塞控制**: 当网络拥塞时,降低发送方的发送速度,包括慢开始、拥塞避免、快重传和快恢复等算法 #### TCP拥塞控制详解 TCP拥塞控制是防止过多的数据注入到网络中,避免网络负载过大导致性能下降。拥塞控制主要包含四个算法: ##### 1. 慢开始(Slow Start) - 发送方维持一个拥塞窗口(cwnd)的状态变量 - 初始值通常为1个最大报文段大小(MSS) - 每经过一个传输轮次(RTT),拥塞窗口加倍 - 呈指数增长,快速探测网络容量 ##### 2. 拥塞避免(Congestion Avoidance) - 当拥塞窗口达到慢开始门限(ssthresh)时启动 - 每经过一个RTT,拥塞窗口只增加1个MSS - 呈线性增长,谨慎增加发送速率 ##### 3. 快重传(Fast Retransmit) - 当发送方收到三个重复确认时,立即重传丢失的报文段 - 不必等待重传定时器超时 - 提高重传效率,减少传输延迟 ##### 4. 快恢复(Fast Recovery) - 当发送方收到三个重复确认时执行快恢复算法 - 慢开始门限设置为当前拥塞窗口的一半 - 拥塞窗口设置为慢开始门限值 - 直接进入拥塞避免阶段 拥塞控制状态转换: ``` 慢开始 --> 拥塞避免 --> 快重传 --> 快恢复 ^ | | v +------------------------------------+ ``` 典型拥塞控制流程示例: 1. 初始cwnd=1 MSS,ssthresh=65535字节 2. 慢开始阶段:cwnd按1,2,4,8...增长 3. 当cwnd>=ssthresh时进入拥塞避免阶段 4. cwnd线性增长:按1,2,3,4...MSS增长 5. 检测到丢包(超时)时: - ssthresh=cwnd/2 - cwnd=1 MSS - 重新进入慢开始阶段 6. 收到3个重复ACK时: - ssthresh=cwnd/2 - cwnd=ssthresh+3 - 进入快恢复阶段 现代TCP拥塞控制算法: - **Tahoe**: 基础算法,包含慢开始、拥塞避免和快重传 - **Reno**: 在Tahoe基础上增加快恢复算法 - **NewReno**: 改进的Reno,优化了部分ACK处理 - **CUBIC**: 现代Linux默认算法,使用三次函数调整窗口 - **BBR**: Google开发的基于带宽的拥塞控制算法 ### UDP协议详解 UDP(User Datagram Protocol)是一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。 #### 什么是UDP?为什么需要UDP? UDP就像寄普通信件,不需要挂号,不需要回执,寄出后就不关心是否送达。虽然不如TCP可靠,但速度更快,开销更小。 UDP适用于以下场景: - 实时应用:视频直播、在线游戏(可以容忍少量丢包,但要求低延迟) - 简单查询:DNS查询(通常只需要一个请求和一个响应) - 广播和多播:网络发现、流媒体广播 #### UDP的特点 1. **无连接**: 发送数据之前不需要建立连接,减少了开销和发送数据之前的时延 2. **不可靠**: 不保证数据能够到达目标节点,也不保证数据的顺序,不使用确认和重传机制 3. **面向报文**: 应用层交给UDP多长的报文,UDP就照样发送,即一次发送一个报文,不会对报文进行分割或合并 4. **没有拥塞控制**: 网络出现的拥塞不会使源主机的发送速率降低,这对某些实时应用是很重要的 5. **支持一对一、一对多、多对一和多对多的交交互通信** 6. **首部开销小**: UDP的首部只有8个字节,比TCP的20个字节的首部要短 #### UDP报文段格式 UDP报文段由首部和数据两部分组成,首部只有8个字节: ``` 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Source Port | Destination Port | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Length | Checksum | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | data | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ``` 各字段详细说明: - **Source Port**(16位): 源端口号,需要时可选字段,全零时表示没有使用端口号 用于标识发送方的应用程序 - **Destination Port**(16位): 目的端口号,使目的主机的UDP能够将报文交付到正确的目的应用进程 用于标识接收方的应用程序 - **Length**(16位): UDP报文段的总长度(首部+数据),其最小值是8(仅有首部) 用于确定UDP报文段的边界 - **Checksum**(16位): 检验和,可选字段,用于检错,该字段可选,在IPV4中可以为全0,而在IPV6中该字段是必须的 用于检测传输过程中是否出现错误 #### UDP的适用场景 - 实时应用(如IP电话、视频会议、直播等) - 简单的请求-应答应用 - 广播应用 - 对可靠性要求不高的应用 #### UDP校验和的计算 UDP校验和的计算方法是: 1. 伪首部 + UDP首部 + 数据 2. 按16位字进行二进制反码求和 3. 若数据长度为奇数,则在数据末尾添加一个全零字节 4. 将得到的校验和写入UDP首部的校验和字段 伪首部包括: - 32位源IP地址 - 32位目的IP地址 - 8位保留字段(值为0) - 8位协议字段(UDP为17) - 16位UDP长度 **详细计算步骤示例**: 假设我们要计算一个UDP数据包的校验和,内容如下: - 源IP: 192.168.1.100 - 目的IP: 192.168.1.200 - 源端口: 1234 - 目的端口: 5678 - 数据: "Hello" 1. 构造伪首部(12字节): ``` 源IP: C0 A8 01 64 (192.168.1.100) 目的IP: C0 A8 01 C8 (192.168.1.200) 保留: 00 协议: 11 (UDP=17) 长度: 00 0D (13=8头+5数据) ``` 2. 构造UDP首部(8字节): ``` 源端口: 04 D2 (1234) 目的端口: 16 2E (5678) 长度: 00 0D (13) 校验和: 00 00 (初始为0) ``` 3. 数据部分(5字节): ``` 48 65 6C 6C 6F ("Hello") 补零: 6F 00 (奇数长度补1字节) ``` 4. 计算校验和: ``` C0A8 + 0164 + C0A8 + 01C8 + 0011 + 000D + 04D2 + 162E + 000D + 0000 + 4865 + 6C6C + 6F00 = 2D32C 进位相加: 2 + D32C = D32E 反码: FFFF - D32E = 2CD1 ``` 最终校验和为0x2CD1 **校验和注意事项**: 1. 校验和为0表示发送方未计算校验和 2. 接收方必须验证校验和 3. IPv6中校验和是强制的 4. 校验错误的数据包会被静默丢弃 #### UDP实际应用场景分析 1. **实时音视频传输**: - 优势:低延迟,容忍少量丢包 - 示例:Zoom/Skype使用UDP作为主要传输协议 - 技术:RTP/RTCP协议基于UDP实现 2. **DNS查询**: - 优势:简单请求-响应模型 - 示例:DNS默认使用UDP端口53 - 注意:大响应会切换到TCP 3. **在线游戏**: - 优势:实时性高于可靠性 - 示例:FPS/MOBA类游戏多用UDP - 技术:自定义可靠UDP协议 4. **IoT设备通信**: - 优势:低功耗,简单实现 - 示例:传感器数据上报 - 协议:CoAP/MQTT-SN基于UDP 5. **广播/多播应用**: - 优势:一对多传输 - 示例:视频直播、网络发现 - 协议:SSDP/mDNS基于UDP ### 字节序问题 在网络编程中,字节序是一个重要概念。不同的计算机系统可能使用不同的字节序: #### 大端序(Big Endian) 最高有效字节存储在最低的内存地址处,最低有效字节存储在最高的内存地址处。例如数字0x12345678在内存中的存储顺序为: ``` 地址: 0x1000 0x1001 0x1002 0x1003 内容: 0x12 0x34 0x56 0x78 ``` #### 小端序(Little Endian) 最低有效字节存储在最低的内存地址处,最高有效字节存储在最高的内存地址处。例如数字0x12345678在内存中的存储顺序为: ``` 地址: 0x1000 0x1001 0x1002 0x1003 内容: 0x78 0x56 0x34 0x12 ``` #### 网络字节序 网络传输中统一使用大端序,也称为网络字节序。当主机字节序与网络字节序不同时,需要进行转换: - `htons()`: 主机字节序转网络字节序(short类型,16位) - `htonl()`: 主机字节序转网络字节序(long类型,32位) - `ntohs()`: 网络字节序转主机字节序(short类型,16位) - `ntohl()`: 网络字节序转主机字节序(long类型,32位) #### 字节序检测方法 可以通过联合体或指针的方式来检测系统的字节序: ```c union { short s; char c[sizeof(short)]; } un; un.s = 0x0102; if (un.c[0] == 1 && un.c[1] == 2) printf("big-endian\n"); else if (un.c[0] == 2 && un.c[1] == 1) printf("little-endian\n"); else printf("unknown\n"); ``` ### 套接字(Socket)编程详解 套接字是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部。 可以把套接字想象成电话机,通过它我们可以与网络上的其他计算机进行通信。 #### 套接字类型 1. **流式套接字(SOCK_STREAM)**: 提供面向连接的、可靠的数据传输服务,使用TCP协议 就像打电话,需要先建立连接,保证数据可靠传输 特点: - 面向连接 - 可靠传输 - 有序数据传输 - 双向通信 2. **数据报套接字(SOCK_DGRAM)**: 提供无连接的、尽最大努力的数据传输服务,使用UDP协议 就像寄信,不需要建立连接,不保证数据一定能到达 特点: - 无连接 - 不可靠传输 - 保留数据边界 - 支持广播和多播 3. **原始套接字(SOCK_RAW)**: 允许对较低层次的协议直接访问,如IP、ICMP协议 就像直接操作邮局的分拣设备,可以自定义信件格式 特点: - 可以读写内核不处理的IP包 - 可以构造任意的IP首部 - 需要root权限 #### 套接字地址结构 在IPv4中,套接字地址结构为sockaddr_in: ```c struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET */ in_port_t sin_port; /* port in network byte order */ struct in_addr sin_addr; /* internet address */ }; /* Internet address. */ struct in_addr { uint32_t s_addr; /* address in network byte order */ }; ``` 字段说明: - sin_family: 地址族,AF_INET表示IPv4 - sin_port: 端口号,必须是网络字节序 - sin_addr: IP地址,必须是网络字节序 在IPv6中,套接字地址结构为sockaddr_in6: ```c struct sockaddr_in6 { sa_family_t sin6_family; /* AF_INET6 */ in_port_t sin6_port; /* port number */ uint32_t sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */ }; ``` 通用的套接字地址结构为sockaddr: ```c struct sockaddr { sa_family_t sa_family; char sa_data[14]; }; ``` #### 基本套接字函数详解 ##### socket()函数 - 创建套接字 创建一个套接字 ```c #include int socket(int domain, int type, int protocol); ``` **详细参数说明**: 1. domain (协议域): - AF_INET: IPv4协议(最常用) - AF_INET6: IPv6协议 - AF_UNIX/AF_LOCAL: 本地套接字(进程间通信) - AF_PACKET: 底层数据包接口 - AF_NETLINK: 内核与用户空间通信 2. type (套接字类型): - SOCK_STREAM: 面向连接的字节流(TCP) - 特点: 可靠、有序、双向 - 适用: HTTP/FTP等应用 - SOCK_DGRAM: 无连接的数据报(UDP) - 特点: 不可靠、保留边界 - 适用: DNS/视频流等 - SOCK_RAW: 原始套接字 - 特点: 直接访问网络层 - 需要root权限 - SOCK_SEQPACKET: 有序分组套接字 3. protocol (协议): - 0: 自动选择默认协议 - IPPROTO_TCP: 显式指定TCP - IPPROTO_UDP: 显式指定UDP - IPPROTO_ICMP: ICMP协议 **返回值处理**: - 成功: 返回非负文件描述符 - 失败: 返回-1,错误码存储在errno中 - EACCES: 无权限创建指定类型套接字 - EAFNOSUPPORT: 不支持地址族 - EINVAL: 未知协议或不可用组合 - EMFILE/ENFILE: 进程/系统文件描述符耗尽 - ENOBUFS/ENOMEM: 内存不足 **典型用法示例**: ```c // 创建IPv4 TCP套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("socket creation failed"); exit(EXIT_FAILURE); } // 创建IPv6 UDP套接字 int sockfd6 = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP); // 创建原始套接字(需要root权限) int raw_sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); ``` **最佳实践建议**: 1. 总是检查返回值 2. 创建后立即设置SO_REUSEADDR选项 3. 非特权用户避免使用原始套接字 4. IPv6程序应同时支持IPv4(使用IPv4映射地址) 5. 资源使用完毕后及时close() **常见错误处理**: 1. 地址族不支持:检查系统支持的协议 2. 权限不足:使用sudo或调整权限 3. 资源耗尽:优化程序或增加系统限制 4. 协议组合无效:参考man手册确认有效组合 ##### bind()函数 - 绑定地址 将套接字绑定到指定地址 ```c #include int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); ``` 参数说明: - sockfd: 套接字描述符 - addr: 指向特定协议地址结构的指针 - addrlen: 地址结构的长度 返回值:成功返回0,失败返回-1 绑定时需要注意: 1. 端口号应在1024以上,避免与系统服务冲突 2. 可以使用INADDR_ANY绑定到所有网络接口 3. 需要确保端口未被其他进程占用 示例: ```c struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(8080); // 端口号转换为网络字节序 server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有接口 bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)); ``` ##### listen()函数 - 监听连接 开始监听客户端连接请求(仅TCP) ```c #include int listen(int sockfd, int backlog); ``` 参数说明: - sockfd: 套接字描述符 - backlog: 指定同时能处理的最大连接要求 backlog参数的含义: - 在Linux 2.2之前的版本中,backlog是指已完成连接队列的最大长度 - 在Linux 2.2及之后的版本中,backlog是指未完成连接队列和已完成连接队列的长度之和 示例: ```c // 监听连接,最多允许128个连接排队 listen(sockfd, 128); ``` ##### accept()函数 - 接受连接 接受客户端连接请求(仅TCP) ```c #include int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); ``` 参数说明: - sockfd: 套接字描述符(监听套接字) - addr: 用于返回已连接客户端的协议地址(可选,NULL表示不关心客户端地址) - addrlen: 值-结果参数,调用前为addr所指的套接字地址结构的长度,返回时为该结构的实际长度 返回值:成功返回已连接套接字描述符,失败返回-1 accept函数会阻塞等待客户端连接,返回一个新的套接字描述符用于与客户端通信,原来的监听套接字继续监听新的连接请求。 示例: ```c struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int connfd = accept(listenfd, (struct sockaddr*)&client_addr, &client_len); ``` ##### connect()函数 - 连接服务器 主动初始化TCP服务器连接 ```c #include int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); ``` 参数说明: - sockfd: 套接字描述符 - addr: 指向服务器协议地址结构的指针 - addrlen: 地址结构的长度 返回值:成功返回0,失败返回-1 对于TCP套接字,connect函数会发起三次握手过程,只有连接建立成功或出错时才返回。对于UDP套接字,connect函数只是简单地保存服务器地址,不进行任何网络操作。 示例: ```c struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(80); inet_pton(AF_INET, "192.168.1.100", &server_addr.sin_addr); connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)); ``` ##### send()和recv()函数 - 发送和接收数据 TCP数据发送和接收 ```c #include ssize_t send(int sockfd, const void *buf, size_t len, int flags); ssize_t recv(int sockfd, void *buf, size_t len, int flags); ``` 参数说明: - sockfd: 套接字描述符 - buf: 发送或接收数据缓冲区 - len: 数据长度 - flags: 标志位,通常为0 返回值:成功返回发送或接收的字节数,失败返回-1 send函数并不保证一次发送所有数据,可能需要多次调用才能发送完所有数据。recv函数可能返回少于请求的字节数。 示例: ```c // 发送数据 char *message = "Hello, Server!"; send(connfd, message, strlen(message), 0); // 接收数据 char buffer[1024]; int n = recv(connfd, buffer, sizeof(buffer)-1, 0); buffer[n] = '\0'; printf("Received: %s\n", buffer); ``` ##### sendto()和recvfrom()函数 - UDP发送和接收数据 UDP数据发送和接收 ```c #include ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); ``` 参数说明: - sockfd: 套接字描述符 - buf: 发送或接收数据缓冲区 - len: 数据长度 - flags: 标志位,通常为0 - dest_addr/src_addr: 目标/源地址结构 - addrlen: 地址结构长度 返回值:成功返回发送或接收的字节数,失败返回-1 示例: ```c // UDP发送数据 struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(8080); inet_pton(AF_INET, "192.168.1.100", &server_addr.sin_addr); char *message = "Hello, Server!"; sendto(udp_socket, message, strlen(message), 0, (struct sockaddr*)&server_addr, sizeof(server_addr)); // UDP接收数据 char buffer[1024]; struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int n = recvfrom(udp_socket, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&client_addr, &client_len); buffer[n] = '\0'; printf("Received from %s: %s\n", inet_ntoa(client_addr.sin_addr), buffer); ``` ##### close()函数 - 关闭套接字 关闭套接字 ```c #include int close(int sockfd); ``` 参数说明: - sockfd: 套接字描述符 返回值:成功返回0,失败返回-1 close函数会释放套接字描述符及相关资源,对于TCP套接字,还会发送FIN报文段关闭连接。 示例: ```c close(sockfd); ``` #### 套接字选项 可以使用setsockopt()和getsockopt()函数来设置和获取套接字选项: ```c #include int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen); ``` 常用套接字选项: - SO_REUSEADDR: 允许重用本地地址,通常在服务器重启时避免"Address already in use"错误 - SO_RCVBUF: 接收缓冲区大小 - SO_SNDBUF: 发送缓冲区大小 - SO_KEEPALIVE: 保活机制,检测连接是否仍然有效 - TCP_NODELAY: 禁用Nagle算法,减少小包延迟 示例: ```c // 设置地址重用 int opt = 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); ``` ### 地址转换函数 - IP地址的处理 在网络编程中,经常需要在点分十进制IP地址和二进制IP地址之间进行转换: #### inet_addr()和inet_aton() - 字符串转IP地址 将点分十进制IP地址转换为网络字节序的二进制IP地址: ```c #include in_addr_t inet_addr(const char *cp); int inet_aton(const char *cp, struct in_addr *inp); ``` inet_addr()函数在出错时返回INADDR_NONE,而inet_aton()函数在出错时返回0。 示例: ```c // 使用inet_addr struct in_addr addr1; addr1.s_addr = inet_addr("192.168.1.100"); // 使用inet_aton struct in_addr addr2; inet_aton("192.168.1.100", &addr2); ``` #### inet_ntoa() - IP地址转字符串 将网络字节序的二进制IP地址转换为点分十进制IP地址: ```c #include char *inet_ntoa(struct in_addr in); ``` 注意:inet_ntoa()函数返回的是一个静态分配的字符串,每次调用都会覆盖之前的结果。 示例: ```c struct in_addr addr; addr.s_addr = 0xC0A80164; // 192.168.1.100 char *ip_str = inet_ntoa(addr); printf("IP: %s\n", ip_str); ``` #### inet_pton()和inet_ntop() - 支持IPv6的地址转换 支持IPv4和IPv6的地址转换函数: ```c #include int inet_pton(int af, const char *src, void *dst); const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); ``` af参数可以是AF_INET(IPv4)或AF_INET6(IPv6)。inet_ntop()函数需要提供目标缓冲区dst和其大小size。 示例: ```c // IPv4地址转换 struct in_addr addr4; inet_pton(AF_INET, "192.168.1.100", &addr4); char ip_str4[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &addr4, ip_str4, INET_ADDRSTRLEN); // IPv6地址转换 struct in6_addr addr6; inet_pton(AF_INET6, "2001:db8::1", &addr6); char ip_str6[INET6_ADDRSTRLEN]; inet_ntop(AF_INET6, &addr6, ip_str6, INET6_ADDRSTRLEN); ``` #### 地址转换函数比较与选择 | 函数 | 支持协议 | 线程安全 | 返回值类型 | 备注 | |------|----------|----------|------------|------| | inet_addr() | IPv4 | 是 | in_addr_t | 已过时,不推荐使用 | | inet_aton() | IPv4 | 是 | int | 比inet_addr更可靠 | | inet_ntoa() | IPv4 | 否 | char* | 返回静态内存,非线程安全 | | inet_pton() | IPv4/IPv6 | 是 | int | 现代推荐使用 | | inet_ntop() | IPv4/IPv6 | 是 | const char* | 现代推荐使用 | 推荐使用inet_pton()和inet_ntop()函数,因为: 1. 支持IPv4和IPv6 2. 线程安全 3. 符合现代网络编程标准 4. 提供更明确的错误处理 常见使用场景: - 服务器程序:使用inet_pton()解析配置文件中的IP地址 - 客户端程序:使用inet_ntop()显示对端地址信息 - 网络工具:使用这两个函数进行地址格式转换 错误处理示例: ```c // 安全的地址转换 struct in_addr addr; if (inet_pton(AF_INET, "192.168.1.100", &addr) <= 0) { fprintf(stderr, "Invalid address format\n"); exit(EXIT_FAILURE); } char str[INET_ADDRSTRLEN]; if (inet_ntop(AF_INET, &addr, str, INET_ADDRSTRLEN) == NULL) { perror("inet_ntop"); exit(EXIT_FAILURE); } ``` ## 项目结构与内容详解 ### 1. UDP代码示例 ([01udp代码](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/01udp代码)) UDP是一种无连接的协议,通信双方不需要建立连接就可以直接发送数据包。UDP适用于对实时性要求高但对可靠性要求不高的场景。 #### 1.1 字节序示例 - [01大小端识别.c](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/01udp代码/02day/01大小端识别.c): 通过联合体检测系统字节序 - [02htonl.c](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/01udp代码/02day/02htonl.c): htonl函数使用示例 - [03ntohl.c](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/01udp代码/02day/03ntohl.c): ntohl函数使用示例 - [04ntohs.c](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/01udp代码/02day/04ntohs.c): ntohs函数使用示例 这些示例演示了字节序转换的重要性。在不同架构的系统间传输数据时,必须确保数据以网络字节序(大端序)传输。 #### 1.2 地址转换示例 - [05inet_pton.c](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/01udp代码/02day/05inet_pton.c): 点分十进制IP地址转网络二进制格式 这个示例演示了如何将人类可读的IP地址转换为网络传输所需的二进制格式。 #### 1.3 UDP通信示例 - [06udp_client.c](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/01udp代码/02day/06udp_client.c): UDP客户端实现 - [udp_server.c](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/01udp代码/02day/udp_server.c): UDP服务端实现 UDP通信流程: 1. 服务端创建套接字 2. 服务端绑定地址和端口 3. 客户端创建套接字 4. 客户端发送数据到服务端 5. 服务端接收数据并可选择回复 UDP通信不需要建立连接,因此代码相对简单,但需要自己处理数据丢失和乱序问题。 #### 1.4 TFTP协议示例 - [07_tftp_up_down.c](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/01udp代码/02day/07_tftp_up_down.c): TFTP协议上传下载实现 TFTP(Trivial File Transfer Protocol)是简单文件传输协议,基于UDP实现,常用于网络启动等场景。TFTP协议特点: - 使用UDP端口69 - 每次传输的数据块大小为512字节 - 通过确认机制保证数据传输的可靠性 - 协议简单,实现容易 TFTP操作码: - 1: Read Request (RRQ) - 2: Write Request (WRQ) - 3: Data (DATA) - 4: Acknowledgment (ACK) - 5: Error (ERROR) ### 2. TCP代码示例 ([02tcp代码](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/02tcp代码)) TCP是一种面向连接的、可靠的传输协议,适用于对数据完整性要求高的场景。 #### 2.1 基础TCP通信 - [01tcp_client.c](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/02tcp代码/01tcp_client.c): TCP客户端实现 - [02tcp_server.c](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/02tcp代码/02tcp_server.c): TCP服务端实现 TCP通信流程: 1. 服务端创建监听套接字 2. 服务端绑定地址和端口 3. 服务端开始监听 4. 客户端创建套接字 5. 客户端连接服务端 6. 服务端接受连接,创建新的连接套接字 7. 双方通过连接套接字进行数据传输 TCP通信需要先建立连接,通过三次握手确保双方都准备好进行数据传输,然后才能进行数据交换。 #### 2.2 基于进程的TCP服务端 - [03_process_tcp_server.c](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/02tcp代码/03_process_tcp_server.c): 基于进程的TCP服务端实现 这种方式为每个客户端连接创建一个新进程来处理,可以同时处理多个客户端连接。实现要点: 1. 使用fork()系统调用创建子进程 2. 父进程继续监听新的连接请求 3. 子进程处理具体的客户端请求 4. 子进程处理完毕后退出 进程模型的优点是编程相对简单,隔离性好,一个进程崩溃不会影响其他进程。缺点是创建和切换开销大,进程间通信复杂。 ### 3. TCP并发处理 ([03tcp并发](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/03tcp并发)) 为了处理多个客户端连接,需要使用并发技术。 #### 3.1 基于线程的TCP服务端 - [04_pthread_tcp_server.c](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/03tcp并发/04_pthread_tcp_server.c): 基于线程的TCP服务端实现 线程是轻量级的进程,创建和切换开销比进程小,适合处理大量并发连接。实现要点: 1. 使用pthread_create()创建新线程 2. 主线程继续监听新的连接请求 3. 新线程处理具体的客户端请求 4. 线程处理完毕后退出 线程模型的优点是创建和切换开销小,共享内存空间,通信方便。缺点是需要处理线程安全问题,一个线程崩溃可能影响整个进程。 #### 3.2 基于select模型的TCP服务端 - [05select_tcp_server.c](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/03tcp并发/05select_tcp_server.c): 使用select模型的TCP服务端实现 select是一种I/O多路复用技术,允许一个进程监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。 select函数原型: ```c #include int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); ``` select模型的优点是可以同时处理多个连接,资源消耗少。缺点是单个进程能够监视的文件描述数有限(通常为1024),效率较低。 ### 4. Epoll技术 ([04-epoll](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/04-epoll)) epoll是Linux特有的I/O多路复用技术,相比select具有更好的性能。 #### 4.1 基本epoll使用 - [01epoll_tcp_server.c](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/04-epoll/01epoll_tcp_server.c): 基本的epoll TCP服务端实现 epoll的主要函数: - `epoll_create()`: 创建epoll实例 - `epoll_ctl()`: 控制epoll实例,添加/修改/删除监视的文件描述符 - `epoll_wait()`: 等待事件发生 epoll相比select的优势: 1. 没有文件描述符数量限制 2. 效率更高,使用事件通知机制 3. 内存拷贝更少 #### 4.2 Reactor模式 - [reactor_epoll反应堆.c](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/04-epoll/reactor_epoll反应堆.c): Reactor模式的epoll实现 Reactor模式是一种事件处理模式,用于处理一个或多个并发输入,将服务请求多路分解并同步分发给请求的处理器。 Reactor模式的核心组件: 1. **Reactor**: 负责监听和分发事件 2. **Handlers**: 处理具体事件的回调函数 3. **Demultiplexer**: 等待事件发生,如select、poll、epoll Reactor模式的优点: 1. 通过事件驱动,提高系统并发处理能力 2. 将事件的等待和处理分离,提高代码可维护性 3. 避免了多线程/多进程的复杂性 ### 5. 原始套接字 ([06-原始套接字](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/06-原始套接字)) 原始套接字允许直接访问网络层协议,可以构造和解析各种网络协议包。 #### 5.1 数据包分析 - [01_anay_mac_ip_port分析MAC数据包.c](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/06-原始套接字/01_anay_mac_ip_port分析MAC数据包.c): MAC数据包分析 这个示例演示了如何使用原始套接字捕获和分析网络数据包,包括MAC地址、IP地址和端口号的解析。 #### 5.2 MAC地址获取 - [02_raw_scan_mac获取MAC地址.c](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/06-原始套接字/02_raw_scan_mac获取MAC地址.c): 获取MAC地址 通过ARP协议获取目标主机的MAC地址。 #### 5.3 网络欺骗技术 - [04飞秋欺骗.c](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/06-原始套接字/04飞秋欺骗.c), [05_raw_udp_fq.c](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/06-原始套接字/05_raw_udp_fq.c): 飞秋网络欺骗示例 这些示例演示了如何构造自定义的网络数据包,实现网络欺骗。注意:这类技术仅用于学习目的,实际使用时需遵守相关法律法规。 原始套接字需要root权限才能使用,因为它可以绕过系统的一些安全机制。 ### 6. Web服务器实现 ([07webserver](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/07webserver)) 基于前面学习的网络编程技术,实现一个简单的Web服务器。 #### 6.1 核心实现 - [server.c](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/07webserver/server.c): Web服务器主程序 Web服务器实现要点: 1. 解析HTTP请求 2. 根据请求返回相应的内容 3. 处理静态文件服务 4. 错误处理 #### 6.2 HTTP协议基础 HTTP(HyperText Transfer Protocol)是应用层协议,基于TCP实现。 HTTP请求格式: ``` GET /index.html HTTP/1.1 Host: www.example.com User-Agent: Mozilla/5.0 Accept: text/html ``` HTTP响应格式: ``` HTTP/1.1 200 OK Content-Type: text/html Content-Length: 1234 ... ``` HTTP状态码: - 1xx: 信息性状态码 - 2xx: 成功状态码,如200 OK - 3xx: 重定向状态码,如301 Moved Permanently - 4xx: 客户端错误状态码,如404 Not Found - 5xx: 服务器错误状态码,如500 Internal Server Error ### 7. Libevent库应用 ([08libevent](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/08libevent)) libevent是一个事件驱动的网络库,提供了跨平台的事件通知机制。 #### 7.1 基础使用 - [hello-world.c](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/08libevent/09day/hello-world.c): Libevent基础使用示例 #### 7.2 TCP服务端实现 - [libevent_tcp_server.c](file:///home/zkc/Desktop/study/3/linux_tcp_udp_code/08libevent/09day/libevent_tcp_server.c): 基于libevent的TCP服务端实现 libevent的主要优势: 1. 事件驱动,高性能 2. 跨平台支持(Linux的epoll, BSD的kqueue, Windows的IOCP等) 3. 简化网络编程复杂度 使用libevent的基本步骤: 1. 创建event_base事件集合 2. 创建event事件对象 3. 添加事件到事件集合 4. 进入事件循环 ## 高级网络编程技术 ### 并发模型比较 #### 1. 多进程模型 优点: - 编程简单,易于理解 - 进程间隔离性好,一个进程崩溃不会影响其他进程 - 可以利用多核CPU的并行处理能力 缺点: - 创建和切换开销大 - 进程间通信复杂 - 占用内存资源多 适用场景:连接数较少,对隔离性要求高的场景 #### 2. 多线程模型 优点: - 创建和切换开销比进程小 - 线程间共享内存空间,通信方便 - 可以利用多核CPU的并行处理能力 缺点: - 线程安全问题 - 一个线程崩溃可能影响整个进程 - 需要处理同步问题 适用场景:连接数适中,对性能有一定要求的场景 #### 3. I/O多路复用技术详解 #### 基础概念 1. **I/O本质**: - Input/Output指计算机与外部世界的数据交换 - 网络I/O特指通过网卡进行的网络数据传输 - 本质是内核缓冲区与用户缓冲区之间的数据拷贝 2. **阻塞 vs 非阻塞**: - 阻塞I/O:进程发起调用后一直等待数据就绪 - 非阻塞I/O:立即返回状态,通过轮询检查就绪状态 3. **同步 vs 异步**: - 同步I/O:就绪后需要进程自己完成数据拷贝 - 异步I/O:内核完成所有操作后通知进程 #### 多路复用原理 1. **核心思想**: - 使用单个线程监控多个I/O描述符 - 当任意描述符就绪时通知应用程序 - 避免为每个连接创建线程/进程的开销 2. **技术实现**: - 文件描述符(fd)集合管理 - 内核事件通知机制 - 用户空间与内核空间的数据交互 #### 具体技术对比 | 技术 | 触发方式 | 最大fd数 | 效率 | 平台支持 | 内存拷贝 | |------|----------|---------|------|----------|----------| | select | 水平触发 | 1024 | O(n) | 跨平台 | 每次调用需拷贝fd集 | | poll | 水平触发 | 无限制 | O(n) | 跨平台 | 同select | | epoll | 边缘触发 | 无限制 | O(1) | Linux特有 | 注册时一次拷贝 | | kqueue | 边缘触发 | 无限制 | O(1) | BSD系 | 同epoll | #### select深度解析 1. **实现机制**: - 使用fd_set结构体存储描述符集合 - 通过FD_ZERO/FD_SET/FD_CLR宏操作集合 - 每次调用需将集合从用户空间拷贝到内核空间 2. **性能瓶颈**: - 线性扫描所有fd(时间复杂度O(n)) - 默认1024个fd的限制 - 频繁的内存拷贝开销 3. **典型使用场景**: - 跨平台程序开发 - 连接数较少的场景(<1000) - 需要同时监控多种I/O类型的场景 #### poll深度解析 1. **改进点**: - 使用pollfd结构体替代fd_set - 突破1024个fd的限制 - 支持更精细的事件类型区分 2. **现存问题**: - 仍然需要线性扫描 - 大量fd时性能下降明显 - 内存拷贝问题未解决 #### epoll深度解析 1. **核心优势**: - 红黑树存储fd(查询效率O(1)) - 就绪链表维护活跃fd - mmap内存映射减少拷贝 2. **工作模式**: - LT模式(水平触发):类似select - ET模式(边缘触发):更高性能但编程复杂 3. **API详解**: - epoll_create:创建epoll实例 - epoll_ctl:管理监控的fd - epoll_wait:等待I/O事件 4. **性能优化**: - EPOLLONESHOT:避免惊群效应 - EPOLLEXCLUSIVE:解决多线程epoll_wait竞争 #### 编程实践建议 1. **fd管理**: - 使用非阻塞fd避免进程阻塞 - 合理设置socket缓冲区大小 - 及时关闭不需要的fd 2. **事件处理**: - ET模式下必须循环读取直到EAGAIN - 注意处理EINTR中断情况 - 心跳机制检测死连接 3. **性能调优**: - 调整/proc/sys/fs/epoll/max_user_watches - 使用timerfd整合定时器 - 配合内存池减少内存分配 #### 典型应用场景 1. **高并发服务器**: - Web服务器(Nginx) - 即时通讯服务器 - 游戏服务器 2. **网络代理**: - 负载均衡器 - API网关 - 反向代理 3. **实时系统**: - 金融交易系统 - 物联网网关 - 视频流媒体服务器 #### 扩展阅读 1. Reactor模式实现 2. 零拷贝技术(sendfile/splice) 3. 多线程epoll协作模型 #### 4. 异步I/O模型 优点: - 非阻塞操作,性能最高 - 资源消耗最少 缺点: - 编程复杂度最高 - Linux下支持不够完善 适用场景:连接数非常多,对性能要求极高的场景 ### 性能优化技巧 #### 1. 缓冲区管理 合理设置接收和发送缓冲区大小,避免频繁的系统调用。可以通过setsockopt()函数设置SO_RCVBUF和SO_SNDBUF选项。 #### 2. 连接池 复用连接,减少连接建立和断开的开销。对于短连接频繁的场景特别有效。 #### 3. 零拷贝技术 使用sendfile等系统调用,减少数据在内核空间和用户空间之间的拷贝。适用于文件传输等场景。 #### 4. 内存池 预先分配内存,避免频繁的malloc/free操作。 #### 5. 非阻塞I/O 使用非阻塞I/O避免线程阻塞,提高并发处理能力。 ## 学习路径建议 ### 第一阶段:基础网络编程 1. 理解TCP/UDP协议的基本概念和区别 2. 学习套接字编程基础API 3. 实践UDP通信示例 4. 实践TCP通信示例 ### 第二阶段:并发处理技术 1. 学习多进程和多线程编程 2. 理解select/poll/epoll等I/O多路复用技术 3. 实践各种并发服务器模型 ### 第三阶段:高级网络编程 1. 学习原始套接字编程 2. 了解网络协议分析 3. 学习事件驱动编程(libevent等库) ### 第四阶段:实际应用开发 1. 实现简单的Web服务器 2. 学习HTTP协议 3. 实践网络应用开发 ## 编译和运行说明 ### 基本编译方法 大多数C程序可以通过以下方式编译和运行: ```bash gcc -o program program.c ./program ``` ### 特殊编译要求 #### 多线程程序 对于使用线程的程序,需要链接pthread库: ```bash gcc -o program program.c -lpthread ``` #### 使用libevent的程序 需要安装libevent库并链接: ```bash gcc -o program program.c -levent ``` #### 原始套接字程序 原始套接字程序需要root权限运行: ```bash sudo ./program ``` ### 注意事项 1. 原始套接字程序需要root权限运行 2. epoll相关程序只能在Linux系统上运行 3. 确保防火墙设置允许相应端口的通信 4. 某些程序可能需要特定的网络环境才能正常运行 ## 常见问题解答 ### 1. 如何选择TCP还是UDP? - 如果需要保证数据的完整性和顺序,选择TCP - 如果对实时性要求高,可以容忍少量数据丢失,选择UDP - 如果需要广播或多播通信,选择UDP - 如果需要实现自定义的可靠性机制,选择UDP ### 2. 如何处理大量并发连接? - 对于连接数较少(几百个)的情况,可以使用多进程或多线程 - 对于连接数较多(几千到几万个)的情况,建议使用I/O多路复用技术(select/poll/epoll) - 对于连接数非常多(几十万以上)的情况,考虑使用异步I/O或专门的网络库 ### 3. 如何调试网络程序? - 使用tcpdump抓包分析 - 使用netstat查看网络连接状态 - 使用strace跟踪系统调用 - 使用wireshark进行协议分析 - 使用gdb进行程序调试 ### 4. 如何提高网络程序性能? - 使用I/O多路复用技术 - 合理设置缓冲区大小 - 使用连接池复用连接 - 使用零拷贝技术 - 优化数据结构和算法 ### 5. 如何处理网络异常? - 处理连接超时 - 处理网络中断 - 处理数据传输错误 - 实现重连机制 - 实现优雅关闭 ## 参考资料 1. 《UNIX网络编程》 - W. Richard Stevens 2. 《TCP/IP详解 卷1:协议》 - W. Richard Stevens 3. 《Linux高性能服务器编程》 - 游双 4. Linux man pages 5. Linux内核源码 6. libevent官方文档 7. 《深入理解计算机系统》 - Randal E. Bryant 通过这个项目的学习,你应该能够掌握Linux网络编程的核心技术,并具备开发网络应用的能力。每个示例都从简单到复杂,循序渐进地展示了各种网络编程技术的实现方式。