目录
1. 解密 Linux TCP 网络协议栈的工作原理
【摘要】 深入探索 Linux TCP 网络协议栈的内部机制, 揭开其背后的神秘面纱。通过对 TCP 协议在 Linux 系统中的实现方式进行详细解析, 了解到它是如何实现可靠的数据传输、拥塞控制和流量管理等关键功能的。 从 TCP 协议栈的基本构建模块开始, 逐步展示数据包在协议栈中的传递过程。通过剖析各个层级的功能模块, 包括物理层、链路层、网络层和传输层, 我们将揭示每个模块的作用和相互配合的工作方式。
1.1. TCP 网络开发 API
TCP, 全称传输控制协议(Transmission Control Protocol), 是一种面向连接的、可靠的、基于字节流的传输层通信协议。
1.1.1. TCP 服务器调用的 API
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
// 1
int socket(int domain, int type, int protocol);
// 2
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 3
int listen(int sockfd, int backlog);
// 4
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// 5
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 6
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// 7
int close(int fd);
// 8
int shutdown(int sockfd, int how);
1.1.2. TCP 客户端调用的 API
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
// 1
int socket(int domain, int type, int protocol);
// 2
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 3
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 4
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// 5
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// 6
int close(int fd);
// 7
int shutdown(int sockfd, int how);
1.1.3. API 函数的作用
-
int socket(int domain, int type, int protocol)
在文件系统中分配一个 fd, 并创建 TCB 数据结构。 -
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
为 TCP 的 socket 绑定本地 IP 地址和端口。 -
int listen(int sockfd, int backlog)
将 TCP 置于 LISTEN 状态。 -
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
从全连接队列中取出一个节点, 并分配一个 fd。 -
ssize_t recv(int sockfd, void *buf, size_t len, int flags)
在对应 fd 中, 从读缓冲区中拷贝出数据。 -
ssize_t send(int sockfd, const void *buf, size_t len, int flags)
把 fd 对应的 TCB 数据拷贝到写缓冲区中。 -
int close(int fd)
准备一个 FIN 包, 放到写缓冲区, 是否 fd。 -
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
准备一个 SYN 包, 交给协议栈发送出去, 等待三次握手完成后才返回。
1.2. TCP 的三个阶段
1.2.1. TCP 建立连接
TCP 连接的建立主要依靠 socket()、bind()、listen()、connect()、accept() 这几个函数。
1.2.2. TCP 的三次握手
示意图:
三次握手在 kernel 协议栈中进行, 那么三次握手是在哪几个函数中发送的呢?
- 第一次, 由 connect() 函数触发 发起握手, 也就是发送 syn 包到服务端;
- 第二次, 在 listen() 之后 accept() 之前, 服务器接收到 syn 包后发送 syn&&ack 包到客户端;
- 第三次, 客户端发送 ack 包到服务端完成连接的建立。
TCP 报头:
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 |
+-------+-----------+-+-+-+-+-+-+-------------------------------+
| Header| Reserve |U|A|P|R|S|F| Window |
| Length| |R|C|S|S|Y|I| |
| | |G|K|H|T|N|N| |
+-------------------------------+-------------------------------+
| Checksum | Urgent Pointer |
+---------------------------------------------------------------+
| Option |
+---------------------------------------------------------------+
| Data |
| ... |
+---------------------------------------------------------------+
- SYN: 即 synchronous, 同步。
- ACK: 即 acknowledgement, 确认。
- PSH: 即 push, 推送。
- FIN : 即 finish, 结束。
- RST: 即 reset, 重置。
- URG: 即 urgent, 紧急。
- Sequence Number: 是数据包本身第一个字节的序列号。
- Acknowledge Number: 是期望对方继续发送的那个确认数据包的序列号其值一般为接收到的 Sequence Number 加 1。
从报文中可以看出, SYN 包最重要的是将 SYN 位设为 1, 设置 Sequence Number; ACK 包最重要的是将 ACK 位设为 1, 设置 Acknowledgment Number。
半连接队列和全连接队列:
在三次握手中, Linux kener 协议栈会维护两个队列: 半连接队列和全连接队列。
- 半连接队列(也叫 SYN 队列): 半连接队列在第一握手中, 当客户端发送 SYN 包到服务端时, 服务端的半连接队列会加入一个节点, 表示此连接处于半连接状态。
- 全连接队列(也叫 ACCEPT 队列): 全连接队列在第三握手中, 当客户端发送 ACK 包到服务端时, 服务端会检查半连接队列中是否存在此连接节点(通过五元组进行查找), 如果存在就将此连接节点加入全连接队列中; 否则将抛弃此连接。
accpt() 函数在三次握手完成后, 从全连接队列中取出连接节点, 为节点分配 socket fd, 返回到用户态。
那么, accept() 函数如何知道全连接队列中有节点呢?
当三次握手完成后, 全连接队列创建节点的同时会释放一个有连接接入的信号(single 或信号量), 这个信号决定了 accept() 函数是否可以从全连接队列中取节点; 也决定 epoll 等 IO 多路复用器能不能检查这个连接 fd 是否可读。
- 在阻塞模式下, accept() 函数一直等待信号, 直到全连接队列中有节点才返回。
- 在非阻塞模式下, 全连接队列为空 accept() 函数就返回-1, 否则返回 socket fd。
在 listen() 函数有, 有一个 backlog 参数, 这个参数表示的是全连接队列的大小还是半连接队列的大小呢?
随着 TCP 协议的不断迭代, backlog 参数在不同的版本中代表的含义也不相同; 它可以是半连接队列大小, 也可以是全连接队列大小, 也可以是半连接队列+全连接队列的大小总和。不过, 效果不会有太大差异。目前版本中主要表示全连接队列的大小。
DDOS 攻击:
根据三次握手原理, 产生一种对服务器的攻击方式: DDOS 攻击。所谓 DDOS 攻击, 就是客户端伪造一些不存在的 IP, 一直发送 SYN 包, 使服务器的半连接队列不断增大, 当半连接队列的大小达到极限时, 造成网络阻塞就会导致服务器无法再接受连接, 从而使服务器奔溃。
1.2.3. TCP 状态转换
TCP 状态转换图:
- 从状态转换图看出, LISTEN 状态可以通过发送 SYN 和数据转换到 SYN_SEND 状态; 也就是 LISTEN 状态可以发送数据。
- SYN_SEND 状态可以收到 SYN, 并发送 SYN 和 ACK 转换到 SYN_RECV 状态; 也就是两个设备可以互发 SYN 包, 建立连接。
1.3. TCP 传输数据
TCP 传输数据主要依靠 send() 和 recv() 两个函数。
使用 send() 函数发送数据时, 返回正数不一定代表发送成功。因为 send() 函数仅仅只是将数据拷贝到协议栈的写缓冲区, 由协议栈发送; 发送过程中会经过 N 个网关, 可能存在丢包或链路断开导致未能发送到目的地。如果要知道数据是否发送成功, 需要加上确认机制(ACK)。
1.3.1. 传输控制块 TCB
为了保证数据能正确分发, TCP 使用一种 TCB(传输控制块)的数据结构, 把发送给不同设备的数据封装起来。这个 TCB 会存在整个 TCP 周期, 知道断开连接。
一个 TCB 数据块包含数据发送双方对应的 socket 信息以及拥有存放数据的缓冲区。建立连接连接发送数据之前, 通信双方必须做一个准备工作: 分配内存建立 TCB 数据块。当双方准备好自己的 socket 和 TCB 数据结构后, 就可以进入"三次握手"建立连接。
1.3.2. TCP 分包
TCP 分包就是要传输的数据很大, 超出发送缓存区剩余空间, 将会进行分包; 待发送的数据大于最大报文长度, TCP 在传输前将进行分包。
分包在应用程序的处理一般是发送循环 send(), 接收方循环 recv()。
1.3.3. TCP 粘包及解决方案
TCP 粘包就是发送方发送的若干数据包到接收方接收时粘成一个包, 从接收缓冲区看就是后数据包的头紧接着前数据包的尾。
常见解决方案:
- (推荐)应用层协议头前面添加包长度。分两次接收数据; 第一次先接收包的长度, 然后根据包的长度一次性读取或循环读取数据。
例如:
// ...
ssize count=0;
ssize size=0;
while(count<tcpHdr->length)
{
size=recv(fd,buffer,buffersize,0);
count+=size;
}
// ...
- 为每个包添加分隔符。在数据末尾添加分隔符, 这会导致解数据可能需要有合包操作; 因为分割数据包后, 需要记录后一个数据包, 用于与该包后面部分数据进行合并。
1.4. TCP 四次挥手
断开连接是比建立连接和传输数据还复杂的一个过程, 断开连接主要分为主动关闭和被动关闭两种。
四次挥手示意图:
需要注意的是, 调用 close()
不是立即完成断开, 而是关闭了数据传输, 进入了四次挥手阶段, TCB 数据结构还没有释放。四次挥手结束才真正把 TCB 释放。
根据四次挥手流程, 可以思考一些问题:
-
传输数据过程中, 网线断了之后立刻连接, TCP 如何知道?
网线掉线网卡会停止供电, 再次连接后网卡恢复供电, 网卡服务重启, 网络连接重连。应用程序设计通过心跳包检测。 -
服务器如何知道客户端是否宕机?
一样需要通过心跳包机制来检测。 -
服务器如何甄别网络阻塞和宕机?
服务器发送心跳包时, 不仅仅发一次, 而是要发送多次的; 如果是网络阻塞, 那么在一定时间内一定有回复信息; 如果是宕机, 无论多长时间都没有客户端的回复。 -
如果出现大量的
CLOSING
状态, 如何处理?
出现大量CLOSING
状态, 基本上业务上要处理的逻辑过多, 导致一直在CLOSING
状态; 可以使用异步, 将网络层和业务层分离, 单独处理。 -
四次挥手中, 为什么存在
TIME_WAIT
状态?
防止没有LAST_ACK
或LAST_ACK
丢失, 导致一直重发已经不存在的 socket。
1.5. 总结
需要掌握 TCP 三次握手和四次挥手的过程, 熟悉 TCP 状态转换。清楚什么是 SYN 包和 ACK 包。
- 三次握手是 由客户端发起 SYN, 服务端收到 SYN 后发送 SYN 和 ACK, 客户端回复 ACK; 完成连接的建立。
- 断开连接主要有主动断开和被动断开。
- 四次挥手是 由发起方调用 close(), 同时发送 FIN 包; 接收端接收到 FIN 包返回 ACK 包, 接收端发送 FIN 包; 发起方接收到 FIN 包返回 ACK 包; 完成断开。
- 理解 TCP 的状态转换图。LISTEN 状态到 SYN_RCVD 状态和 SYN_SEND 状态, 如何进入 ESTABLISHED 状态; 四次挥手 FIN_WAIT_1、FIN_WAIT_2、TIME_WAIT、CLOSING 直接的转换, CLOSE_WAIT 和 LAST_ACK 的处理等。
- 理解 API 的底层原理, 以及全连接队列和半连接队列。
- TCP 的分包场景以及 TCP 粘包的处理方式。
TCP 通信完整过程: