【Linux高级全栈开发】2.2.1 Linux服务器百万并发实现2.2.2 Posix API与网络协议栈
【Linux高级全栈开发】2.2.1 Linux服务器百万并发实现2.2.2 Posix API与网络协议栈
高性能网络学习目录
基础内容(两周完成):
-
2.1网络编程
- 2.1.1多路复用select/poll/epoll
- 2.1.2事件驱动reactor
- 2.1.3http服务器的实现
-
2.2网络原理
- 百万并发
- PosixAPI
- QUIC
-
2.3协程库
- NtyCo的实现
-
2.4dpdk
- 用户态协议栈的实现
-
2.5高性能异步io机制
项目内容(两周完成):
- 9.1 KV存储项目
- 9.2 RPC项目
- 9.3 DPDK项目
2.2.1 Linux服务器百万并发实现
1 同步处理与异步处理的数据差异
同步处理:
- 服务器线程在读取或写入数据时会阻塞,直到操作完成
- 数据处理是顺序的,一个连接处理完成后才会处理下一个
- 示例:使用
read()
/write()
系统调用时线程会等待 I/O 完成
异步处理:
- 服务器线程不会阻塞,I/O 操作由内核或线程池异步处理
- 通过回调、事件通知机制获取 I/O 完成状态
- 数据处理是非阻塞的,线程可以同时处理多个连接
- 示例:使用
epoll
/kqueue
事件通知或异步 I/O 接口(如aio_*
系列函数)
数据差异影响:
- 同步模式下需要为每个连接分配独立线程 / 进程,内存消耗大(约 1MB / 线程)
- 异步模式下单个线程可以管理数万连接,内存效率高(约 1KB / 连接)
2 网络io线程池异步处理
实现方式:
- 主 Reactor 线程:负责 accept 新连接,将 socket 分配给 Worker 线程
- Worker 线程池:每个线程维护一个
epoll
实例,处理读写事件 - 任务队列:线程间通过队列传递任务(如新建连接、关闭连接)
代码示例(伪代码):
// 主Reactor线程
while (1) {int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);for (int i = 0; i < nfds; i++) {if (events[i].data.fd == listen_fd) {// 接受新连接int conn_fd = accept(listen_fd, ...);// 选择一个Worker线程worker = get_least_busy_worker();// 将conn_fd加入该Worker的任务队列enqueue(worker->task_queue, conn_fd);// 唤醒Worker线程wakeup(worker);}}
}// Worker线程
while (1) {// 从任务队列获取新连接int conn_fd = dequeue(task_queue);// 将conn_fd加入epoll监听epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev);// 处理epoll事件int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);for (int i = 0; i < nfds; i++) {if (events[i].events & EPOLLIN) {// 读取数据并处理read_and_process(events[i].data.fd);}}
}
优势:
- 避免单线程处理瓶颈
- 充分利用多核 CPU 资源
- 隔离 I/O 处理和业务逻辑
3 ulimit的fd的百万级别支持
限制原因:
- Linux 默认每个进程最多打开 1024 个文件描述符(FD)
- 百万并发需要修改多个系统限制
修改步骤:
-
临时修改(当前会话有效):
ulimit -n 1048576 # 设置最大FD数量
-
永久修改(所有用户):
编辑/etc/security/limits.conf
:* soft nofile 1048576 * hard nofile 1048576 root soft nofile 1048576 root hard nofile 1048576
-
系统级限制:
编辑/etc/sysctl.conf
:fs.file-max = 1048576 # 系统最大文件数
执行
sysctl -p
生效
验证方法:
ulimit -n # 查看当前FD限制
cat /proc/sys/fs/file-max # 查看系统文件限制
4 sysctl.conf的rmem与wmem的调优
TCP 内存参数说明:
net.core.rmem_max
:单个 socket 接收缓冲区最大字节数net.core.wmem_max
:单个 socket 发送缓冲区最大字节数net.ipv4.tcp_rmem
:TCP 接收缓冲区大小范围(min, default, max)net.ipv4.tcp_wmem
:TCP 发送缓冲区大小范围(min, default, max)
推荐配置:
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
net.ipv4.tcp_mem = 786432 1048576 1572864
参数作用:
- 增大缓冲区可以提高吞吐量,但会增加内存压力
- 动态调整范围允许系统根据负载自动优化内存使用
5 conntrack的原理分析(在ubuntu16.04中需要修改)
功能:
- 跟踪 IP 数据包的连接状态(如 TCP 的 SYN_SENT、ESTABLISHED 等)
- 为 NAT(网络地址转换)提供状态保持功能
- 用于防火墙规则匹配(如状态防火墙)
问题场景:
- 高并发下 conntrack 表会耗尽内存
- 每个连接在表中占用约 300-600 字节内存
- 默认限制较低(如 65536 条记录)
优化方法:
-
增加表大小:
net.netfilter.nf_conntrack_max = 1048576 net.netfilter.nf_conntrack_buckets = 262144 # 建议为max的1/4
-
缩短超时时间:
net.netfilter.nf_conntrack_tcp_timeout_established = 1200 # 默认432000秒
-
禁用不必要的跟踪:
# 对特定端口禁用conntrack iptables -t raw -A PREROUTING -p tcp --dport 80 -j NOTRACK iptables -t raw -A PREROUTING -p tcp --dport 443 -j NOTRACK
验证命令:
cat /proc/sys/net/netfilter/nf_conntrack_count # 当前连接数
cat /proc/sys/net/netfilter/nf_conntrack_max # 最大连接数
总结
实现百万并发需要综合优化:
- 架构层面:采用异步非阻塞 I/O 模型 + 线程池
- 系统限制:调整 FD 数量、内存参数、conntrack 表大小
- 网络栈优化:调整 TCP 缓冲区、窗口大小、超时参数
- 内存管理:使用内存池减少内存碎片
- 监控工具:使用
ss
、netstat
、conntrack
等工具监控系统状态
2.2.2 Posix API与网络协议栈
1 网络基础知识复习
1.1 客户端部分
- socket()
- 功能:创建一个套接字描述符(文件描述符 fd ),用于后续的网络通信。它在内核中分配一个网络 I/O 控制块(如 tcp control block ,简称 tcb ),并返回一个整数类型的文件描述符(fd ,是一个整数)。对于 TCP 或 UDP 协议,这是网络编程的起始操作。例如在 C 语言中,
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
表示创建一个 IPv4 的 TCP 套接字 。
- 功能:创建一个套接字描述符(文件描述符 fd ),用于后续的网络通信。它在内核中分配一个网络 I/O 控制块(如 tcp control block ,简称 tcb ),并返回一个整数类型的文件描述符(fd ,是一个整数)。对于 TCP 或 UDP 协议,这是网络编程的起始操作。例如在 C 语言中,
- bind()(可选 )
- 功能:对于服务器端,一般是必须操作,但客户端有时可省略。它将套接字与特定的 IP 地址和端口号绑定。在客户端使用时,可指定客户端自己监听的 IP 和端口,若不调用,系统会自动分配可用端口等。如
bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
,其中addr
是包含 IP 和端口信息的结构体。
- 功能:对于服务器端,一般是必须操作,但客户端有时可省略。它将套接字与特定的 IP 地址和端口号绑定。在客户端使用时,可指定客户端自己监听的 IP 和端口,若不调用,系统会自动分配可用端口等。如
- connect()(udp协议 )
- 功能:在 TCP 协议中,用于客户端发起与服务器端的连接请求。它向服务器的 IP 地址和端口发送连接请求报文(SYN 包),并经历三次握手过程来建立可靠连接。对于 UDP ,也可使用
connect
来指定目标 IP 和端口,后续可直接用send
和recv
收发数据,而不必每次都指定地址信息 。例如connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
,这里servaddr
是服务器的地址信息。
- 功能:在 TCP 协议中,用于客户端发起与服务器端的连接请求。它向服务器的 IP 地址和端口发送连接请求报文(SYN 包),并经历三次握手过程来建立可靠连接。对于 UDP ,也可使用
- send()
- 功能:用于向已连接的套接字发送数据。在 TCP 中,它将应用层数据复制到内核发送缓冲区,由 TCP 协议负责分段、编号、重传等操作,将数据可靠地发送到对端。在 UDP 中,它直接将数据封装成 UDP 报文发送出去,不保证可靠性 。如
send(sockfd, buffer, strlen(buffer), 0);
,buffer
是存放要发送数据的缓冲区。
- 功能:用于向已连接的套接字发送数据。在 TCP 中,它将应用层数据复制到内核发送缓冲区,由 TCP 协议负责分段、编号、重传等操作,将数据可靠地发送到对端。在 UDP 中,它直接将数据封装成 UDP 报文发送出去,不保证可靠性 。如
- recv()
- 功能:从已连接的套接字接收数据。在 TCP 中,它从内核接收缓冲区读取数据到应用层缓冲区。在 UDP 中,接收 UDP 报文并将数据存储到指定的缓冲区。例如
recv(sockfd, buffer, sizeof(buffer), 0);
,接收的数据存放在buffer
中。
- 功能:从已连接的套接字接收数据。在 TCP 中,它从内核接收缓冲区读取数据到应用层缓冲区。在 UDP 中,接收 UDP 报文并将数据存储到指定的缓冲区。例如
- close()
- 功能:关闭套接字,释放与该套接字相关的资源。在 TCP 中,会发起四次挥手过程来终止连接。例如
close(sockfd);
,通知内核不再使用该套接字进行通信 。
- 功能:关闭套接字,释放与该套接字相关的资源。在 TCP 中,会发起四次挥手过程来终止连接。例如
1.2 服务器端部分
- socket()
- 功能:同客户端的
socket()
,创建套接字描述符,分配网络 I/O 控制块等资源,为后续网络通信做准备 。
- 功能:同客户端的
- bind()
- 功能:将套接字绑定到特定的 IP 地址和端口号上,这是服务器端的关键步骤,使得服务器能够在指定的地址和端口上监听客户端请求。如
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
,让服务器监听servaddr
对应的 IP 和端口 。
- 功能:将套接字绑定到特定的 IP 地址和端口号上,这是服务器端的关键步骤,使得服务器能够在指定的地址和端口上监听客户端请求。如
- listen()
- 功能:将套接字设置为监听状态,在内核中为该套接字创建同步队列(syn_queue )和接受队列(accept_queue ),用于存放客户端的连接请求。它将套接字的状态设置为
TCP_STATUS_LISTEN
,并准备好接收客户端的连接请求 。例如listen(listenfd, BACKLOG);
,BACKLOG
表示监听队列的最大长度 。
- 功能:将套接字设置为监听状态,在内核中为该套接字创建同步队列(syn_queue )和接受队列(accept_queue ),用于存放客户端的连接请求。它将套接字的状态设置为
- accept()
- 功能:从监听队列中取出一个已完成三次握手的客户端连接请求,创建一个新的套接字(与监听套接字不同 )用于与该客户端进行后续的数据通信。返回的新套接字描述符用于和对应的客户端进行数据收发等操作 。如
int connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
,cliaddr
存放客户端地址信息,clilen
是其长度 。
- 功能:从监听队列中取出一个已完成三次握手的客户端连接请求,创建一个新的套接字(与监听套接字不同 )用于与该客户端进行后续的数据通信。返回的新套接字描述符用于和对应的客户端进行数据收发等操作 。如
- recv()
- 功能:同客户端的
recv()
,从与客户端连接的套接字接收数据,将内核接收缓冲区的数据读取到应用层缓冲区 。
- 功能:同客户端的
- send()
- 功能:同客户端的
send()
,向与客户端连接的套接字发送数据,将应用层数据复制到内核发送缓冲区进行发送 。仅限于把数据从应用程序拷贝到内核中,而内核的发送时间和大小send无法决定。
- 功能:同客户端的
- close()
- 功能:同客户端的
close()
,关闭与客户端连接的套接字,释放相关资源,在 TCP 情况下发起四次挥手终止连接 。
- 功能:同客户端的
1.3 epoll 相关函数(epoll_create ()、epoll_ctl ()、epoll_wait () )
- epoll_create()
- 功能:创建一个 epoll 实例,在内核中分配一个数据结构用于管理事件,返回一个 epoll 句柄(文件描述符 ),后续通过这个句柄对 epoll 实例进行操作 。如
int epfd = epoll_create(1024);
,这里参数可指定初始大小(现在该参数意义不大 )。
- 功能:创建一个 epoll 实例,在内核中分配一个数据结构用于管理事件,返回一个 epoll 句柄(文件描述符 ),后续通过这个句柄对 epoll 实例进行操作 。如
- epoll_ctl()
- 功能:用于对 epoll 实例进行事件的添加、修改或删除操作。它可以将套接字与感兴趣的事件(如可读、可写、异常等 )关联起来。例如
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
,表示向epfd
对应的 epoll 实例中添加sockfd
套接字的相关事件,ev
是包含事件类型等信息的结构体 。
- 功能:用于对 epoll 实例进行事件的添加、修改或删除操作。它可以将套接字与感兴趣的事件(如可读、可写、异常等 )关联起来。例如
- epoll_wait()
- 功能:阻塞等待在 epoll 实例中注册的套接字上有事件发生。当有事件发生时,它会返回发生事件的套接字数量,并将发生事件的相关信息填充到用户提供的数组中。如
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
,events
是存放事件信息的数组,MAX_EVENTS
是数组大小,-1
表示一直阻塞直到有事件发生 。
- 功能:阻塞等待在 epoll 实例中注册的套接字上有事件发生。当有事件发生时,它会返回发生事件的套接字数量,并将发生事件的相关信息填充到用户提供的数组中。如
1.4 fcntl()
- 功能:用于对文件描述符(包括套接字描述符 )进行各种控制操作,如设置文件描述符为非阻塞模式(
fcntl(fd, F_SETFL, O_NONBLOCK);
) ,获取或设置文件描述符的标志等 ,还可用于复制文件描述符等操作 。
2 connect, listen, accept 与三次握手
connect 是客户端发起连接请求的函数;listen 用于服务器端,将套接字设置为监听状态,准备接收客户端连接;accept 是服务器端接受客户端连接请求的函数 。三次握手是 TCP 协议中建立连接的过程,客户端发送 SYN 包,服务器回复 SYN+ACK 包,客户端再回复 ACK 包,以此确认双方都能正常收发数据。
2.1 TCP三次握手过程
-
第一次握手:客户端发送一个带有 SYN(同步序列号)标志的 TCP 报文段,其中包含客户端初始序列号(ISN),向服务器发起连接请求 ,此时客户端进入 SYN_SENT 状态。
-
第二次握手:服务器收到客户端的 SYN 报文后,会回复一个 SYN + ACK 报文,其中 SYN 标志表示同意建立连接,ACK 标志用于确认客户端的请求,同时包含服务器的初始序列号以及对客户端序列号的确认号(客户端序列号 + 1) ,服务器进入 SYN_RCVD 状态。
-
第三次握手:客户端收到服务器的 SYN + ACK 报文后,再发送一个 ACK 报文给服务器,确认收到服务器的响应(确认号为服务器序列号 + 1 ),至此客户端和服务器都进入 ESTABLISHED 状态,连接建立成功。
-
建立连接三次握手原因:主要是为了防止已失效的连接请求报文段突然又传送到服务器,产生错误。通过三次握手,客户端和服务器都能确认彼此的发送和接收能力正常,保证连接可靠建立。
- TCP 首部长度:TCP 首部长度可变,最小为 20 字节(没有选项时),最大为 60 字节(包含 40 字节选项) 。首部长度以 4 字节为单位来表示,通过首部中的 “首部长度” 字段(4 位)来指明。
- 字段:
- 源端口和目的端口:各占 2 字节,标识发送端和接收端应用程序的端口号。
- 序号:4 字节,用于标识发送字节流中的位置,保证数据按序传输。
- 确认号:4 字节,期望收到对方下一个报文段的第一个数据字节的序号。
- 首部长度:4 位,指出 TCP 首部的长度。
- 标志位:6 位,包含 SYN(同步)、ACK(确认)、FIN(结束)、RST(复位)、PSH(推送)、URG(紧急)等标志。
- 窗口:2 字节,用于流量控制,表示接收方当前允许接收的字节数。
- 校验和:2 字节,对 TCP 首部和数据进行校验,确保数据的完整性。
- 紧急指针:2 字节,配合 URG 标志使用,指出紧急数据的末尾在数据流中的位置 。
- 选项:长度可变,最多 40 字节,常见选项有 MSS(最大报文段长度)、窗口扩大因子、时间戳等 。
2.2 三次握手发生在哪些函数里面
connect()
- 功能:在 TCP 协议中,用于客户端发起与服务器端的连接请求。它向服务器的 IP 地址和端口发送连接请求报文(SYN 包),并经历三次握手过程来建立可靠连接。对于 UDP ,也可使用
connect
来指定目标 IP 和端口,后续可直接用send
和recv
收发数据,而不必每次都指定地址信息 。例如connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
,这里servaddr
是服务器的地址信息。
listen()
- 功能:将套接字设置为监听状态,在内核中为该套接字创建一个监听队列,用于存放客户端的连接请求。它将套接字的状态设置为
TCP_STATUS_LISTEN
,并准备好接收客户端的连接请求 。例如listen(listenfd, BACKLOG);
,BACKLOG
表示监听队列的最大长度 。
accept()
- 功能:服务器在接收到客户端的第三次握手 ACK 报文,完成三次握手流程,建立起与客户端的连接后,处于 ESTABLISHED 状态时,调用 accept 函数从监听队列中取出一个已完成三次握手的客户端连接请求,创建一个新的套接字(与监听套接字不同 )用于与该客户端进行后续的数据通信。返回的新套接字描述符用于和对应的客户端进行数据收发等操作 。如
int connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
,cliaddr
存放客户端地址信息,clilen
是其长度 。
2.3 三次握手过程中有哪些不安全性
- SYN 洪泛攻击:攻击者向服务器发送大量带有 SYN 标志的 TCP 报文,且源 IP 地址是伪造的不可达地址。服务器收到这些 SYN 报文后,会分配资源并进入 SYN_RCVD 状态,等待第三次握手的 ACK 报文。但由于源 IP 不可达,服务器收不到 ACK,这些半连接会一直占用服务器资源,导致正常客户端的连接请求无法被处理,使服务器拒绝服务。
- 中间人攻击:攻击者在客户端和服务器之间的通信路径上,拦截双方的通信报文。在三次握手过程中,攻击者可以截获客户端的 SYN 报文,然后伪装成客户端向服务器发送自己的 SYN 报文,同时伪装成服务器向客户端发送 SYN + ACK 报文,使客户端和服务器都误以为与对方直接建立了连接,从而攻击者可以窃取和篡改双方传输的数据。
3 listen 参数 backlog
在 TCP 服务器端调用 listen 函数时,backlog 参数用于指定处于半连接状态(SYN_RCVD)和完全连接状态(ESTABLISHED)的连接队列的最大长度。它表示内核为该套接字维护的两个队列的总长度上限(未分配fd的tcb的数量)。当队列已满,再有新的连接请求到达时,客户端可能会收到 ECONNREFUSED 错误(取决于具体实现)。合理设置 backlog 有助于服务器应对不同的并发连接请求情况,避免资源耗尽。
- 含义:backlog 是 listen 函数的一个参数,它定义了等待连接队列的最大长度。即服务器在还没调用 accept 处理连接请求时,能够缓存的最大连接请求数量。
- 作用:合理设置 backlog 可平衡服务器处理连接的能力和客户端连接请求的流量,避免过多连接请求丢失,防止 syn 泛洪。
- 老版本:syn链接长度,中版本syn+accept总长度,新版本:accept队列长度
4 syn 泛洪的解决方案
- 含义:syn 泛洪是一种网络攻击,攻击者短时间内发送大量 SYN 包,占用服务器的半连接队列资源,使正常客户端无法建立连接。
- 解决方案:常见的有 SYN 缓存、SYN cookie 等。SYN 缓存通过记录半连接信息,减少内存占用;SYN cookie 则是在收到 SYN 包时,根据特定算法生成 cookie 代替传统的半连接记录,在后续 ACK 包到来时验证 cookie 合法性,过滤掉非法连接请求。
5 close 与四次挥手
四次挥手
-
含义:close 是关闭套接字连接的函数。四次挥手是 TCP 协议中关闭连接的过程,当一方发起关闭请求(FIN 包),另一方回复 ACK 包,然后另一方也准备好关闭时发送 FIN 包,最初发起关闭的一方再回复 ACK 包,完成连接关闭。
- 第一次挥手:客户端想要断开连接,发送一个带有 FIN(结束标志)的 TCP 报文段给服务器,请求释放连接,此时客户端进入 FIN_WAIT_1 状态。
- 第二次挥手:服务器收到客户端的 FIN 报文后,发送一个 ACK 报文给客户端,确认收到客户端的断开请求,此时服务器进入 CLOSE_WAIT 状态,客户端收到 ACK 后进入 FIN_WAIT_2 状态。
- 第三次挥手:服务器数据传输完毕,也准备好断开连接时,向客户端发送一个 FIN 报文,请求断开连接,此时服务器进入 LAST_ACK 状态。
- 第四次挥手:客户端收到服务器的 FIN 报文后,发送一个 ACK 报文给服务器,确认收到服务器的断开请求,然后客户端进入 TIME_WAIT 状态,服务器收到 ACK 后进入 CLOSED 状态。经过 2MSL(最长报文段寿命)时间后,客户端也进入 CLOSED 状态,连接彻底断开。
-
断开连接四次挥手原因:TCP 是全双工通信,当一方请求断开连接(发送 FIN)时,只是表示它自己不再发送数据,但仍可以接收数据。另一方收到 FIN 后,先回复 ACK 确认,等自己数据传输完后再发送 FIN 断开连接,所以需要四次挥手来确保双方数据都能完整传输和连接正常释放。
close流程
close
函数的调用与触发
- 主动关闭方:当应用程序调用
close(sockfd)
或shutdown(sockfd, SHUT_WR)
(不建议)时(假设是客户端主动关闭),TCP 协议会:- 停止发送数据:关闭发送缓冲区,不再允许应用程序通过该套接字发送新数据。
- 发送 FIN 包:TCP 协议自动向对端(服务器)发送一个 FIN 包,表示 “我已完成数据发送,请准备关闭连接”。
- 进入
FIN_WAIT_1
状态:等待对端的 ACK 确认。
2. 四次挥手流程中的 close
交互
第一次挥手:
- 客户端操作:调用
close(sockfd)
→ 发送 FIN 包 → 进入FIN_WAIT_1
。 - 含义:客户端告诉服务器 “我不再发送数据,但仍可接收”。
第二次挥手:
- 服务器操作:收到 FIN 包 → 发送 ACK 包 → 进入
CLOSE_WAIT
。 - 客户端状态:收到 ACK 后 → 从
FIN_WAIT_1
进入FIN_WAIT_2
。 - 关键点:此时客户端的发送通道已关闭,但接收通道仍开放,可继续接收服务器的数据。
第三次挥手:
- 服务器操作:
- 若服务器还有数据要发送,会先完成数据传输。
- 数据发送完毕后,应用程序调用
close(sockfd)
→ 发送 FIN 包 → 进入LAST_ACK
。
- 含义:服务器告诉客户端 “我也完成数据发送,准备关闭”。
第四次挥手:
- 客户端操作:收到服务器的 FIN 包 → 发送 ACK 包 → 进入
TIME_WAIT
。 - 服务器状态:收到 ACK 后 → 进入
CLOSED
状态,连接彻底关闭。 - 客户端的
TIME_WAIT
:需等待 2MSL(最大段生存期) 时间后才进入CLOSED
,目的是确保最后一个 ACK 能被服务器收到(若丢失,服务器会重发 FIN)。
TIME_WAIT
状态的意义
- 确保连接可靠关闭:若客户端的最后一个 ACK 丢失,服务器会重发 FIN,客户端在
TIME_WAIT
状态可响应重发的 FIN。 - 防止旧连接数据干扰:2MSL 时间足够让本次连接的所有数据包在网络中自然消失,避免新连接复用相同端口号时,旧数据包被误认为是新连接的数据。
6. 常见问题与优化
-
TIME_WAIT
过多:高并发短连接场景下,大量
TIME_WAIT
状态可能耗尽系统资源。可通过以下方式优化:
- 调整内核参数:
net.ipv4.tcp_tw_reuse = 1
(允许复用处于TIME_WAIT
的端口)。 - 使用长连接:减少频繁建立 / 关闭连接的开销。
- 调整内核参数:
-
半关闭场景:若需要双向数据传输的精细控制(如文件传输),可使用
shutdown(SHUT_WR)
实现半关闭,而非直接close
。
6 11 个状态迁移
- 简介:TCP 连接存在多种状态,如 LISTEN(监听)、SYN_SENT(发送同步包)、SYN_RECV(收到同步包并回复 )、ESTABLISHED(已建立连接 )、FIN_WAIT_1(主动关闭方发送 FIN 包后 )、FIN_WAIT_2(收到对方 ACK 后 )、CLOSE_WAIT(被动关闭方收到 FIN 包后 )、CLOSING(双方同时发送 FIN 包时 )、LAST_ACK(被动关闭方发送 FIN 包后等待 ACK )、TIME_WAIT(主动关闭方收到对方 FIN 包并回复 ACK 后 )、CLOSED(连接关闭 ) ,这些状态之间依据连接的建立、数据传输、关闭等操作进行迁移。
状态转换图相关补充
- CLOSED:表示初始状态,套接字没有在使用,不占用任何资源。
- LISTEN:服务器端调用
listen
函数后进入此状态,此时套接字处于监听状态,等待客户端的连接请求 。 - SYN_SENT:客户端调用
connect
后发送 SYN 包,进入此状态,等待服务器的 SYN + ACK 包 。 - SYN_RCVD:服务器接收到客户端的 SYN 包后,发送 SYN + ACK 包,自身进入此状态,等待客户端的 ACK 包 。
- ESTABLISHED:经过三次握手完成后,客户端和服务器端都进入此状态,此时可以进行数据的正常收发 。
- FIN_WAIT_1:主动关闭连接的一方(通常是客户端 )发送 FIN 包后进入此状态,等待对方的 ACK 包 。
- FIN_WAIT_2:收到对方 ACK 包后进入此状态,此时等待对方发送 FIN 包 。
- TIME_WAIT:主动关闭方收到对方 FIN 包,发送 ACK 包后进入此状态,在此状态停留 2 倍的报文段最大生存时间(MSL ),用于确保网络中延迟的报文段都消失,防止新连接中出现旧连接的残留数据 。
- CLOSE_WAIT:被动关闭连接的一方收到对方 FIN 包后,进入此状态,此时表示已经收到对方关闭连接请求,但还未完全处理完自身的工作(如数据发送等 ) 。
- LAST_ACK:被动关闭方处理完自身工作后,发送 FIN 包,进入此状态,等待对方的 ACK 包 。
- CLOSING:双方同时发送 FIN 包时可能出现的状态,即双方都在关闭连接的过程中 。
7 大量 close_wait 与 time_wait 的原因与解决方案
close_wait
- 大量 close_wait 原因:通常是被动关闭连接的一方,没有及时调用 close 函数关闭连接,导致资源未释放,积累大量处于 close_wait 状态的连接。
- 大量 close_wait 解决方案:检查代码中关闭连接的逻辑,确保在合适时机调用 close 关闭连接。
time_wait
-
time_wait 持续时间:通常为 2 倍的 MSL(Maximum Segment Lifetime,最长报文段寿命),在大多数系统中 MSL 默认值为 1 分钟,所以 TIME_WAIT 一般为 2 分钟 。
-
原因:一是为了确保最后一个 ACK 报文能够到达服务器,如果服务器没有收到这个 ACK,会重发 FIN 报文,客户端在 TIME_WAIT 状态可以重新发送 ACK;二是防止在本连接中出现的报文段在网络中延迟,在新的连接中被误认,保证旧连接的所有报文段都从网络中消失,避免对新连接造成干扰。若短时间内有大量连接主动关闭,就会产生大量 time_wait 状态连接。
-
解决方案:调整系统参数,如设置合适的 2MSL 时长;使用 SO_REUSEADDR 套接字选项,允许在 TIME_WAIT 状态下重用本地地址。
8 tcp keepalive 与应用层心跳包
- tcp keepalive:是 TCP 协议提供的一种保活机制,在一段时间内如果没有数据传输,TCP 会自动发送探测报文,以检测连接是否正常。若对端无响应,会尝试多次后判定连接已失效。
- 应用层心跳包:是在应用程序层面实现的类似机制,应用程序定时向对端发送特定的心跳报文,以确认连接状态,相比 tcp keepalive 更灵活,可自定义检测逻辑和频率。
- 作用:二者都是为了检测连接是否存活,及时发现并处理失效连接,保障网络通信的可靠性。
9 拥塞控制与滑动窗口
- 简介: TCP 协议为防止网络出现拥塞而采取的机制。当网络出现拥塞时,通过调整发送方的发送速率来缓解网络压力。常见算法有慢开始、拥塞避免、快重传、快恢复等。慢开始是从一个较小的拥塞窗口开始,逐渐增大;拥塞避免是在拥塞窗口达到一定阈值后,线性增加;快重传是在收到多个重复 ACK 时,快速重传丢失的报文段;快恢复是在快重传后,调整拥塞窗口大小。
慢启动
- 原理:发送方从一个较小的拥塞窗口(cwnd ,Congestion Window)大小开始发送数据,初始值常为 1 个 MSS(Maximum Segment Size,最大报文段长度 )。每收到一个 ACK(确认报文 ),拥塞窗口大小就增加 1 个 MSS ,呈现指数增长趋势。例如,初始窗口为 1 个 MSS ,收到 1 个 ACK 后变为 2 个 MSS ,收到 2 个 ACK 后变为 4 个 MSS ,以此类推。直到拥塞窗口达到慢开始阈值(ssthresh ,Slow Start Threshold ),或者发生丢包情况,此时慢开始阶段结束。
- 作用:在连接建立初期,以较小速率试探网络状况,避免一开始就发送大量数据导致网络拥塞,逐步探测网络的承载能力。
拥塞控制
- 原理:当慢开始阶段的拥塞窗口达到慢开始阈值后,进入拥塞避免阶段。此阶段不再采用指数增长方式,而是每经过一个往返时间(RTT ,Round - Trip Time ),拥塞窗口大小线性增加 1 个 MSS 。若发生丢包,ssthresh 设置为当前拥塞窗口大小的一半,同时拥塞窗口大小设置为 ssthresh 加 1 个 MSS ,然后继续在拥塞避免阶段调整。
- 作用:在网络已经有一定数据传输量基础上,更平稳地增加发送速率,防止因增长过快再次引发拥塞 。
滑动窗口
- 简介:是 TCP 协议中用于流量控制的机制。接收方通过通告窗口大小给发送方,发送方根据这个窗口大小来控制发送数据量,窗口大小会随着接收方处理数据情况动态调整,实现发送方和接收方数据传输速率的匹配,避免接收方因来不及处理数据而丢包。
- 窗口结构
- 已发送且已确认:这部分数据已经成功发送,并且也收到了接收方的确认。
- 已发送但未确认:数据已经发送出去了,不过还没有收到接收方的确认。
- 未发送但可发送:这部分数据虽然还没有发送,但处于发送窗口允许的范围内,是可以发送的。
- 未发送且不可发送:数据不在发送窗口范围内,暂时不可以发送。
- 工作流程:
- 初始化:在 TCP 连接建立时,双方会协商初始的窗口大小。一般来说,接收方会在 SYN 包中通告自己的初始窗口大小。
- 数据传输:发送方依据接收方通告的窗口大小,发送相应数量的数据。每发送一个数据段,发送方都会启动一个计时器。
- 确认机制:接收方在收到数据后,会发送 ACK 确认报文,其中包含两个关键信息:
- 确认号(ACK Number):表示期望接收的下一个字节的序号。
- 窗口大小(Window Size):用于告知发送方自己当前的接收窗口大小。
- 窗口滑动:当发送方收到 ACK 确认后,发送窗口会向前滑动,从而允许发送新的数据。窗口滑动的距离取决于确认号的增长情况。
- 窗口动态调整:接收方可以根据自身的处理能力,动态地调整通告窗口的大小。例如,当接收缓冲区快满时,接收方可以减小窗口大小,甚至将窗口大小设置为 0,此时发送方需要暂停发送数据,直到收到新的窗口更新通知。
- 窗口大小的动态变化:
- 窗口增大:如果接收方处理数据的速度很快,能够及时清空接收缓冲区,就会增大通告窗口的大小,这样发送方就可以以更高的速率发送数据。
- 窗口减小:当接收方处理数据的速度变慢,接收缓冲区接近满状态时,会减小通告窗口的大小,以此限制发送方的发送速率。
- 零窗口(Zero Window):当接收缓冲区完全满时,接收方会通告窗口大小为 0。这时,发送方必须停止发送数据,进入等待状态,直到收到接收方发送的窗口更新通知(Window Update)。
- 窗口探测(Window Probe):在发送方收到零窗口通知后,为了避免因接收方的窗口更新通知丢失而导致的死锁情况,发送方会定期发送窗口探测报文,以确认接收方的窗口是否有变化。
- 优点:
- 高效性:滑动窗口机制允许发送方在等待确认的同时继续发送后续数据,这大大提高了数据传输的效率,实现了流水线式的数据传输。
- 流量控制:通过动态调整窗口大小,接收方可以有效地控制发送方的发送速率,防止自己因处理能力不足而导致数据溢出和丢包。
- 可靠性:结合 ACK 确认和重传机制,滑动窗口确保了数据传输的可靠性,即使在网络出现丢包的情况下,也能保证数据准确无误地到达接收方。
- 与拥塞控制的关系:
- 滑动窗口主要负责流量控制,关注的是接收方的处理能力,它决定了发送方在一定时间内可以发送的数据量上限。
- 拥塞控制则着眼于整个网络的拥塞状况,通过调整拥塞窗口(cwnd)来控制发送方的发送速率,防止因网络过载而导致的性能下降。
- 在实际运行中,发送窗口的大小是接收窗口(rwnd)和拥塞窗口(cwnd)中的最小值,即发送窗口大小 = min (rwnd, cwnd)。
延迟确认
延迟确认(Delayed Acknowledgment)是 TCP 协议中的一种机制,主要作用是减少网络中 ACK 报文数量,降低网络开销 。具体解释如下:
- 常规确认:在传统 TCP 确认机制中,接收方每收到一个报文段,就会立即发送一个 ACK 报文来确认收到的数据。
- 延迟确认:接收方在收到报文段后,并不马上发送 ACK 报文,而是等待一段时间(通常在 200ms 以内 )。在这段时间内,如果接收方又收到了发送方的新数据,那么可以将多个确认合并在一个 ACK 报文中发送出去;若等待超时,即使期间没有收到新数据,接收方也会发送 ACK 报文。
超时重传
发送方每发送一个报文段,就启动一个定时器。在定时器规定时间内(即重传超时时间 RTO,Retransmission Time - Out ),若未收到接收方针对该报文段的 ACK(确认)报文,TCP 协议就认为这个报文段中的数据可能已丢失或损坏,于是重新组织并发送该报文段 ,直到成功收到确认或达到最大重传次数。
工作流程示例:
- 发送报文段:发送方发送一个报文段,并启动定时器。
- 等待确认:等待接收方的 ACK 报文。
- 超时判断:若定时器超时,仍未收到 ACK,发送方重传该报文段,并重置定时器。
- 重复过程:若重传后还是未收到 ACK,继续重传,直到收到 ACK 或达到系统设定的最大重传次数。不同系统对重传次数设置不同,有些系统一个报文最多重传 3 次,之后若仍无确认,就重置 TCP 连接;高要求业务系统可能会不断重传 。
-
快重传(Fast Retransmit)
- 原理:当接收方检测到某个报文段丢失时,后续收到其他报文段会不断发送重复的 ACK 给发送方。当发送方接收到三个重复的 ACK 时,就认为对应的报文段丢失,不等超时就立即重传该丢失的报文段。重传后,将 ssthresh 设置为当前拥塞窗口大小的一半,然后进入快速恢复阶段。
- 作用:快速检测到丢包情况并及时重传,避免因等待超时才重传导致的传输延迟,提高网络传输效率 。
-
快恢复(Fast Recovery)
-
原理:进入快恢复阶段后,发送窗口大小设置为 ssthresh 加 1 个 MSS 。此后,每收到一个非重复的 ACK,发送窗口大小增加 1 个 MSS 。当收到所有丢失报文段的 ACK 后,退出快速恢复阶段,进入拥塞避免阶段继续调整。
-
作用:在快重传之后,让发送方较快地恢复数据发送速率,避免长时间处于低速率传输状态,维持网络的高效传输 。
-
2 「代码实现」
2.1 作业:实现 TCP 点对点 P2P 通信的代码
客户端代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>#define PORT 8888
#define BUFFER_SIZE 1024int main(int argc, char const *argv[]) {int sock = 0;struct sockaddr_in serv_addr;char buffer[BUFFER_SIZE] = {0};const char *hello = "Hello from client";// 检查是否提供了服务器IP地址if (argc < 2) {printf("Usage: %s <server_ip>\n", argv[0]);return -1;}// 创建socket文件描述符if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {printf("\nSocket creation error\n");return -1;}serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(PORT);// 将IPv4地址从点分十进制转换为二进制形式if (inet_pton(AF_INET, argv[1], &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, hello, strlen(hello), 0);printf("Hello message sent to server\n");// 接收消息int valread = read(sock, buffer, BUFFER_SIZE);printf("Server: %s\n", buffer);close(sock);return 0;
}
服务端代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>#define PORT 8888
#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};const char *hello = "Hello from server";// 创建socket文件描述符if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {perror("socket failed");exit(EXIT_FAILURE);}// 设置socket选项if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {perror("setsockopt");exit(EXIT_FAILURE);}address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(PORT);// 绑定socket到指定地址和端口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);}printf("Server listening on port %d...\n", PORT);// 接受连接if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {perror("accept");exit(EXIT_FAILURE);}// 接收消息int valread = read(new_socket, buffer, BUFFER_SIZE);printf("Client: %s\n", buffer);// 发送消息send(new_socket, hello, strlen(hello), 0);printf("Hello message sent to client\n");close(new_socket);close(server_fd);return 0;
}
下一章:2.2.3 UDP的可靠传输协议QUIC
https//github.com.0voice