UNIX网络编程笔记:网络协议

网络协议

🌐 网络协议:数字世界的"交通规则"

网络协议(Network Protocol) 是计算机在网络中通信时遵循的规则和标准,定义了数据如何封装、传输、路由和接收。就像人类使用语言交流需要语法和语义一样,网络协议确保不同设备(如手机、服务器、路由器)能够相互理解并高效协作。


1. 核心作用

统一通信标准 :不同厂商、系统的设备能互联互通(如 Windows 与 Linux 通信)。

数据可靠传输 :确保数据完整、有序、无差错地到达目的地(如 TCP 的重传机制)。

高效路由 :通过地址标识(如 IP 地址)和路径选择(如路由协议)优化传输效率。

安全与隐私:加密数据(如 HTTPS)、验证身份(如 SSH)以防止窃听和篡改。


2. 常见网络协议举例
协议 层级 作用 应用场景
HTTP 应用层 网页内容传输(文本、图片) 浏览器访问网站
HTTPS 应用层 加密的 HTTP 通信 网银、登录认证
TCP 传输层 可靠传输数据(三次握手、流量控制) 文件下载、邮件发送
UDP 传输层 快速但不可靠传输(无连接) 视频通话、在线游戏
IP 网络层 寻址和路由(IP地址标识设备) 所有互联网通信的基础
DNS 应用层 将域名解析为 IP 地址 访问 www.example.com
ARP 数据链路层 将 IP 地址转换为物理 MAC 地址 局域网内设备通信

3. 协议分层模型

网络协议通常按分层模型组织,每层专注于特定功能,下层为上层提供服务。

(1) OSI 七层模型(理论参考)
  1. 物理层:传输原始比特流(如电缆、光纤)。
  2. 数据链路层:帧传输(如 MAC 地址、交换机)。
  3. 网络层:IP 寻址和路由(如路由器)。
  4. 传输层:端到端连接管理(如 TCP/UDP 端口)。
  5. 会话层:建立/维护会话(如 SSH 会话)。
  6. 表示层:数据格式转换(如加密、压缩)。
  7. 应用层:用户接口(如 HTTP、FTP)。
(2) TCP/IP 四层模型(实际应用)
  1. 网络接口层:物理传输 + 数据链路(如以太网、Wi-Fi)。
  2. 网络层:IP 协议(IPv4/IPv6)。
  3. 传输层:TCP/UDP。
  4. 应用层:HTTP、DNS、FTP 等。

4. 协议工作原理示例(HTTP + TCP/IP)
  1. 用户输入网址https://www.example.com
  2. DNS 解析 :将域名转换为 IP 地址(如 93.184.216.34)。
  3. 建立 TCP 连接:客户端与服务器通过三次握手建立可靠通道。
  4. 发送 HTTP 请求GET /index.html HTTP/1.1
  5. 服务器响应:返回 HTML 页面数据(状态码 200 OK)。
  6. 关闭连接:四次挥手释放资源。

5. 为什么需要多种协议?

分工协作 :不同协议各司其职(如 IP 负责寻址,TCP 负责可靠传输)。

适应场景 :根据需求选择协议(如实时语音用 UDP,文件传输用 TCP)。

技术演进:新协议解决旧协议的不足(如 IPv6 解决 IPv4 地址枯竭)。


6. 网络协议的重要性

互联网的基石 :没有协议,全球数十亿设备无法协同工作。

技术兼容性 :跨平台、跨设备通信的基础(如手机与云服务器交互)。

安全与隐私:现代协议(如 TLS 1.3)保护用户数据不被窃取。


🔍 总结

网络协议是数字世界的"交通规则"和"通用语言",从底层硬件到上层应用,每一层协议共同构建了现代互联网的通信框架。理解协议的分层设计和协作原理,是掌握网络技术(如配置路由器、开发 Web 应用)的关键基础! 🚀

一、网络协议概括

IPv4

网际协议版本4 (Internet Protocol version 4)。IPv4(通常称之为IP)自20世纪80年代早期以来一直是网际协议族的主力协议。它使用32位地址(见A.4节)。IPv4给TCP、UDP、SCTP、ICMP和IGMP提供分组递送服务。

IPv6

网际协议版本6 (Internet Protocol version 6)。IPv6是在20世纪90年代中期作为IPv4的一个替代品设计的。其主要变化是使用128位更大地址(见A.5节)以应对20世纪90年代因特网的爆发性增长。IPv6给TCP、UDP、SCTP和ICMPv6提供分组递送服务。

当无需区别IPv4和IPv6时,我们经常把"IP"一词作为形容词使用,如IP层、IP地址等。

TCP

传输控制协议 (Transmission Control Protocol)。TCP是一个面向连接的协议,为用户进程提供可靠的全双工字节流。TCP套接字是一种流套接字(stream socket)。TCP关心确认、超时和重传之类的细节。大多数因特网应用程序使用TCP。注意,TCP既可以使用IPv4,也可以使用IPv6。

由TCP向应用进程提供的服务不同于由UDP提供的服务。首先,TCP提供客户与服务器之间的连接(connection)。TCP客户先与某个给定服务器建立一个连接,再跨该连接与那个服务器交换数据,然后终止这个连接。

其次,TCP还提供了可靠性(reliability)。当TCP向另一端发送数据时,它要求对端返回一个确认。如果没有收到确认,TCP就自动重传数据并等待更长时间。在数次重传失败后,TCP才放弃,如此在尝试发送数据上所花的总时间一般为4~10分钟(依赖于具体实现)。

注意,TCP并不保证数据一定会被对方接收,因为这是不可能做到的。如果有可能,TCP就把数据递送到对方端点,否则就放弃重传并中断连接这一手段)通知用户。这么说来,TCP也不能被描述成是100%可靠的协议,它提供的是数据的可靠递送或故障的通知。

TCP含有用于动态估算客户和服务器之间的往返时间(round-trip time,RTT)的算法,以便它知道等待一个确认需要多少时间。举例来说,RTT在一个局域网上大约是几毫秒,跨越一个广域网则可能是数秒钟。另外,因为RTT受网络流通各种变化因素影响,TCP还持续估算一个给定连接的RTT。

TCP通过给其中每个字节关联一个序列号对所发送的数据进行排序(sequencing)。举例来说,假设一个应用写2048字节到一个TCP套字,导致TCP发送2个分节:第一个分节所含数据的序列号为1到1024,第二个分节所含数据的序列号为1025~2048。(分节是TCP传输给IP的数据单元。)如果这些分节非顺序到达,接收端TCP将先根据它们的序列号重新排序,再把结果数据传递给接收应用。如果接收端TCP接收到来自对端的重复数据(譬如说对端认为一个分节已丢失并因此重传,而这个分节并没有真正丢失,只是网络通信过于拥挤),它可以(根据序列号)判定数据是重复的,从而丢弃重复数据。

UDP提供可靠性、UDP本身不提供确认、序列号、RTT估算、超时和重传等机制,如果一个UDP数据报在网络中被复制,接收方就可能把重复的数据报传递给应用。同样地,如果一个UDP客户发送两个数据报到同一个目的地,它们可能被网络重新排序,颠倒顺序后到达目的地。UDP应用必须处理所有这些情况(一个是协议本身会处理,另一个是需要应用自己处理)。

再次,TCP提供流量控制(flow control)。TCP总是告知对端在任何时刻它一次能够从对端接收多少字节的数据,这称为通告窗口(advertised window)。在任何时刻,该窗口指出接收缓冲区中当前可用的空间,从而确保发送端发送的数据不会使接收缓冲区溢出。该窗口的动态变化:当接收到来自发送端的数据时,窗口大小减小,但是当接收端应用从缓冲区中读取数据时,窗口大小增大。通告窗口大小减小到0是有可能的;当TCP对某个套接字的接收缓冲区已满,导致它必须等待应用从该缓冲区读取数据时,方能从对端再接收数据。

好的,我现在要思考用户的问题:"TCP的编程要点是什么"。用户之前已经询问过TCP和UDP的核心差异,现在可能想深入了解如何在实际编程中正确使用TCP协议。我需要整理出TCP编程的关键点,确保覆盖连接管理、错误处理、数据流处理等方面。

以下是TCP编程的核心要点及详细说明,帮助开发者编写高效、可靠的网络应用:


一、连接管理

1. 服务器端流程

创建套接字

c 复制代码
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

• 参数:AF_INET(IPv4),SOCK_STREAM(TCP流式套接字)。

• 错误处理:检查返回值,失败时处理errno(如EACCES, EAFNOSUPPORT)。

绑定地址与端口

c 复制代码
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有网卡
serv_addr.sin_port = htons(8080);              // 端口8080
bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

• 注意:避免端口冲突,处理EADDRINUSE错误。

监听连接请求

c 复制代码
listen(sockfd, SOMAXCONN); // SOMAXCONN为系统允许的最大队列长度

backlog参数:已完成连接队列的最大长度,影响并发连接处理能力。

接受连接

c 复制代码
struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);
int connfd = accept(sockfd, (struct sockaddr*)&cli_addr, &cli_len);

• 阻塞操作:默认阻塞直到有新连接,可设为非阻塞模式(如fcntl设置O_NONBLOCK)。

2. 客户端流程

连接服务器

c 复制代码
connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

• 错误处理:处理ECONNREFUSED(服务器未运行)、ETIMEDOUT(超时)等。

3. 连接终止

优雅关闭

c 复制代码
shutdown(connfd, SHUT_WR); // 关闭写方向,通知对端数据发送完毕
char buffer[1024];
while (read(connfd, buffer, sizeof(buffer)) > 0); // 读取剩余数据
close(connfd);

• 避免TIME_WAIT:通过设置套接字选项SO_REUSEADDR复用端口。


二、字节流处理与消息边界

1. 粘包与拆包问题

原因 :TCP是字节流协议,不保留应用层消息边界,多次send可能被合并或拆分。

解决方案

固定长度 :每条消息定长(如512字节),不足部分填充。

分隔符 :使用特殊字符(如\n)分割消息,需转义处理。

长度前缀:在消息头部添加长度字段(如4字节表示消息体长度)。

javascript 复制代码
 // 发送端示例:先发长度,再发数据
 uint32_t len = htonl(data_len);
 send(sockfd, &len, 4, 0);
 send(sockfd, data, data_len, 0);

 // 接收端示例:先读长度,再读数据
 uint32_t len;
 read(sockfd, &len, 4);
 len = ntohl(len);
 char *data = malloc(len);
 read(sockfd, data, len);
2. 读写操作注意事项

循环读写 :单次read/send可能未完成全部数据传输。

c 复制代码
// 安全发送函数
ssize_t send_all(int sockfd, const void *buf, size_t len) {
    size_t total_sent = 0;
    while (total_sent < len) {
        ssize_t sent = send(sockfd, buf + total_sent, len - total_sent, 0);
        if (sent == -1) return -1;
        total_sent += sent;
    }
    return total_sent;
}

三、错误处理与健壮性

1. 常见错误码处理

EINTR:系统调用被信号中断,需重试操作。

c 复制代码
while ((n = read(sockfd, buf, sizeof(buf))) == -1 && errno == EINTR);

ECONNRESET :连接被对端强制关闭,需清理资源。

EPIPE :向已关闭的套接字写入数据,捕获SIGPIPE信号或设置MSG_NOSIGNAL标志。

2. 超时设置

设置接收/发送超时

c 复制代码
struct timeval tv;
tv.tv_sec = 5;  // 5秒超时
tv.tv_usec = 0;
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));

四、并发模型

1. 多进程/多线程

进程模型

c 复制代码
while (1) {
    int connfd = accept(...);
    if (fork() == 0) {
        close(sockfd);    // 子进程关闭监听套接字
        handle_client(connfd);
        exit(0);
    }
    close(connfd);        // 父进程关闭已连接的套接字
}

• 缺点:进程创建开销大,适合长连接场景。

线程池

• 预创建线程处理连接,避免频繁创建销毁线程的开销。

2. I/O多路复用

select/poll/epoll

c 复制代码
// epoll示例
int epfd = epoll_create1(0);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);

while (1) {
    int nready = epoll_wait(epfd, events, MAX_EVENTS, -1);
    for (int i = 0; i < nready; i++) {
        if (events[i].data.fd == sockfd) {
            // 处理新连接
        } else {
            // 处理已连接套接字的读写
        }
    }
}

• 优势:高并发场景下资源消耗低,适合短连接或大量连接。


五、流量控制与拥塞控制

滑动窗口 :依赖内核自动管理,应用层可通过调整发送速率配合。

• 避免发送过快:在send返回EAGAIN/EWOULDBLOCK时暂停发送,等待可写事件。


六、安全考虑

防止DoS攻击

• 限制单个IP的连接数。

• 设置合理的backlog和连接超时。

加密通信:使用TLS/SSL层(如OpenSSL库)加密TCP数据。


七、调试与工具

网络分析工具

tcpdump:捕获TCP报文,分析握手、数据传输、挥手过程。
bash tcpdump -i eth0 'tcp port 8080'

• Wireshark:图形化分析流量,排查协议错误。

日志记录:记录连接状态、收发数据长度和关键错误。


总结:TCP编程检查清单

  1. 连接管理:正确处理三次握手和四次挥手,避免资源泄漏。
  2. 消息边界:设计应用层协议(长度前缀或分隔符)。
  3. 错误处理:检查所有系统调用的返回值,处理常见错误码。
  4. 并发模型:根据场景选择多进程、多线程或I/O多路复用。
  5. 流量控制:配合TCP滑动窗口,避免发送过快。
  6. 安全性:防范常见攻击,必要时加密数据。
  7. 测试:模拟网络异常(如丢包、延迟)验证健壮性。

UDP

用户数据报协议 (User Datagram Protocol)。UDP是一个无连接协议。UDP套接字是一种数据报套接字(datagram socket)。UDP数据报不能保证最终到达它们的目的地。与TCP一样,UDP既可以使用IPv4,也可以使用IPv6。

应用进程往一个UDP套接字写入一个信息,该消息随后被封装到一个UDP数据报,该UDP数据报进而又被封装到一个IP数据报,然后发送到目的地。UDP不保证UDP数据报会到达其最终目的低,不保证各个数据报的先后顺序跨网络后保持不变,也不保证每个数据报只达到一次。

使用UDP进行网络编程所遇到的问题是它缺乏可靠性。如果一个数据报到达了其最终目的地,但是校验和检测发现有错误,或者该数据报在网络传输途中被丢弃了,它就无法被投递给UDP套接字,也不会被源端自动重传。如果想要确保一个数据报到达其目的地,可以往应用程序中添置一大堆的特性;来自对端的确认、本端的超时与重传等。

每个UDP数据报都有一个长度。如果一个数据报正确地到达其目的地,那么该数据报的长度将随数据一道传递给接收端应用进程。我们已经提到TCP是一个字节流(byte-stream)协议,没有任何记录边界,这一点不同于UDP。

我们也说UDP提供无连接的(connectionless)服务,因为UDP客户与服务器之间不必存在任何长期的关系。举例来说,一个UDP客户可以创建一个套接字并发送一个数据报给一个给定的服务器,然后立即用同一个套接字发送另一个数据报给另一个服务器。同样地,一个UDP服务器可以用同一个UDP套接字从若干个不同的客户接收数据报,每个客户一个数据报。

嗯,用户数据报协议,也就是UDP,对吧。我需要理解这个协议的特点以及它在网络编程中的应用。首先,UDP是一个无连接的协议,这和TCP不同。TCP是面向连接的,所以在传输数据之前需要先建立连接,而UDP不需要。这意味着使用UDP的时候,发送方和接收方之间没有长期的连接关系,对吧?比如,客户端可以随时发送数据报给不同的服务器,而服务器也可以用同一个套接字接收不同客户端的数据。

UDP不提供流量控制。让较快的UDP发送端以一个UDP接收端难以跟上的速率发送数据报是非常容易的。

最后,TCP连接是全双工的(full-duplex)。这意味着在一个给定的连接上应用可以在任何时刻在进出两个方向上既发送数据又接收数据。因此,TCP必须为每个数据流方向跟踪诸如序列号和通告窗口大小等状态信息。建立一个全双工连接后,需要的话可以把它转换成一个单工连接。

UDP可以是全双工的。

以下是UDP编程的核心要点及详细说明,帮助开发者编写高效、灵活的网络应用:


一、UDP核心特性

  1. 无连接:无需建立连接,直接通过IP和端口发送数据报。
  2. 不可靠性:不保证数据报顺序、不重传丢失包、不检测重复。
  3. 低开销:头部仅8字节(TCP至少20字节),适合高频小数据场景。
  4. 支持广播/多播:可向多个目标同时发送数据。

二、UDP编程基本流程

1. 服务器端流程

创建套接字

c 复制代码
int sockfd = socket(AF_INET, SOCK_DGRAM, 0); // SOCK_DGRAM表示UDP

• 错误处理:检查sockfd == -1,处理errno(如EACCES权限问题)。

绑定地址与端口

c 复制代码
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有网卡
serv_addr.sin_port = htons(8080);              // 端口8080
bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

• 注意:绑定后套接字可接收目标为该端口的数据报。

2. 客户端流程

无需连接:直接发送数据到目标地址:

c 复制代码
struct sockaddr_in dest_addr;
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.sin_family = AF_INET;
dest_addr.sin_addr.s_addr = inet_addr("192.168.1.100"); // 目标IP
dest_addr.sin_port = htons(8080);                       // 目标端口

char msg[] = "Hello UDP!";
sendto(sockfd, msg, strlen(msg), 0, (struct sockaddr*)&dest_addr, sizeof(dest_addr));
3. 数据收发函数

发送数据sendto()

c 复制代码
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

• 关键参数:dest_addr指定目标地址,每次发送可指向不同目标。

• 返回值:成功返回发送字节数,失败返回-1(如EMSGSIZE数据过大)。

接收数据recvfrom()

c 复制代码
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

• 关键参数:src_addr保存发送方地址,用于后续回复。

• 阻塞行为:默认阻塞直到有数据到达,可设为非阻塞模式(fcntl设置O_NONBLOCK)。


三、关键编程注意事项

1. 数据报边界处理

保留消息边界 :每个sendto()调用对应一个独立数据报,recvfrom()读取完整数据报。

• 示例:若客户端发送两次sendto("A")sendto("B"),服务器可能收到"A""B"两个独立数据报,而非合并的"AB"

缓冲区大小:需足够容纳最大可能的数据报(通常≤65507字节,受IP层限制)。

c 复制代码
char buffer[65507]; // 最大UDP数据报长度
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer), 0, ...);
2. 错误与异常处理

丢包处理

• UDP不提供重传机制,需应用层自行实现(如添加序列号和ACK确认)。

• 示例:发送方为每个数据包添加序号,接收方返回ACK;超时未收到ACK则重传。

乱序与重复

• 接收方需缓存已处理的数据包序号,丢弃重复包并按需排序。

ICMP错误处理

• 若发送到未监听的端口,可能收到ICMP"端口不可达"错误,但默认不通知应用层。

• 可通过SO_ERROR套接字选项或recvmsg()捕获ICMP错误(需高级配置)。

3. 超时控制

设置接收超时

c 复制代码
struct timeval tv;
tv.tv_sec = 3;  // 3秒超时
tv.tv_usec = 0;
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

• 超时后recvfrom()返回-1errno设为EAGAINEWOULDBLOCK


四、高级特性应用

1. 广播与多播

广播(Broadcast)

• 发送到子网广播地址(如255.255.255.255):
c int broadcast_enable = 1; setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast_enable, sizeof(broadcast_enable));

• 接收方需绑定到特定端口并监听INADDR_ANY

多播(Multicast)

• 加入多播组(如239.255.255.250):
c struct ip_mreq mreq; mreq.imr_multiaddr.s_addr = inet_addr("239.255.255.250"); mreq.imr_interface.s_addr = htonl(INADDR_ANY); setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));

• 发送到多播地址,组内所有成员均可接收。

2. 非阻塞I/O与多路复用

非阻塞模式

c 复制代码
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

• 适合单线程同时处理多个套接字。

多路复用(select/poll/epoll)

c 复制代码
// select示例:监控UDP套接字可读事件
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);
select(sockfd + 1, &readfds, NULL, NULL, &tv);
if (FD_ISSET(sockfd, &readfds)) {
    // 调用recvfrom读取数据
}

五、性能优化

1. 减少系统调用

批量发送 :合并多个小数据报为一个大数据报发送(需权衡延迟与效率)。

使用sendmmsg()/recvmmsg()(Linux特有):单次系统调用处理多个数据报。

c 复制代码
struct mmsghdr msgs[10];
// 填充msgs数组...
int n = recvmmsg(sockfd, msgs, 10, 0, NULL);
2. 缓冲区调整

增大接收缓冲区:避免丢包(尤其在高速场景):

c 复制代码
int buf_size = 1024 * 1024; // 1MB
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &buf_size, sizeof(buf_size));

六、安全与防御

  1. 来源验证 :通过recvfrom()获取发送方地址,仅处理可信IP。
  2. 速率限制:防止DDoS攻击,限制每秒接收数据报数量。
  3. 数据校验:添加校验和(如CRC32)或使用应用层加密(如DTLS)。

七、调试工具与最佳实践

网络分析

tcpdump抓包:tcpdump -i eth0 udp port 8080

• Wireshark分析数据报内容与顺序。

日志记录

• 记录发送/接收的源地址、端口和数据长度。

• 标记数据报序列号和时间戳,用于排查丢包问题。

测试策略

• 模拟丢包:使用tc工具(Linux)模拟网络丢包。
bash tc qdisc add dev eth0 root netem loss 20% # 20%丢包率

• 压力测试:发送高频率数据报验证系统稳定性。


UDP编程检查清单

  1. 套接字创建 :使用SOCK_DGRAM类型,正确处理地址绑定。
  2. 数据报处理:设计应用层协议处理消息边界、丢包、乱序和重复。
  3. 错误处理:检查所有系统调用返回值,处理超时和ICMP错误。
  4. 性能优化:调整缓冲区大小,使用批量IO接口。
  5. 安全措施:验证数据来源,限制速率,加密敏感数据。
  6. 测试覆盖:覆盖正常、异常及高负载场景。

总结

UDP以无连接和高效性为核心,适用于对实时性要求高、可容忍数据丢失的场景。开发者需权衡可靠性与性能,必要时在应用层补充可靠性机制,或选择混合方案(如HTTP/3基于UDP的QUIC)。理解UDP的优劣有助于在合适场景发挥其最大价值。

SCTP

流控制传输协议 (Stream Control Transmission Protocol)。SCTP是一个提供可靠全双工关联的面向连接的协议,我们使用"关联"一词来指称SCTP中的连接,因为SCTP是多宿的,从而每个关联的两端均涉及一组IP地址和一个端口号。SCTP提供消息服务,也就是维护来自应用层的记录边界。与TCP和UDP一样,SCTP既可以使用IPv4,也可以使用IPv6,而且能够在同一个关联中同时使用它们。

SCTP提供的服务与UDP和TCP提供的类似。SCTP在客户和服务器之间提供关联(association),并像TCP那样给应用提供可靠性、排序、流量控制以及全双工的数据传送。SCTP中使用"关联"一词取代"连接"是为了避免这样的内涵:一个连接只涉及两个IP地址之间的通信。一个关联指代两个系统之间的一次通信,它可能因为SCTP支持多宿而涉及不止两个地址。

与TCP不同的是,SCTP是面向消息的(message-oriented)。它提供各个记录的按序递送服务。与UDP一样,由发送端写入的每条记录的长度随数据一道传递给接收端应用。

SCTP能够在所连接的端点之间提供多个流,每个流各自可靠地按序递送消息。一个流上某个消息的丢失不会阻塞同一关联其他流上消息的投递。这种做法与TCP正好相反,就TCP而言,在单一字节流中任何位置的字节丢失都将阻塞该连接上其后所有数据的递送,直到该丢失被修复为止。

SCTP还提供多宿特性,使得单个SCTP端点能够支持多个IP地址。该特性可以增强应对网络故障的健壮性。一个端点可能有多个冗余的网络连接,每个网络又可能有各自接入因特网基础设施的连接。当该端点与另一个端点建立一个关联后,如果它的某个网络或某个跨越因特网的通路发生故障,SCTP就可以通过切换到使用已与该关联相关的另一个地址来规避所发生的故障。

类似的健壮性在路由协议的辅助下也可以从TCP中获得。举例来说,由iBGP实现的同一域内的BGP连接往往把赋予路由器内某个虚拟接口的多个地址用作TCP连接的端点。该域的路由协议确保两个路由器之间只要存在一条路由,该路由就会被用上,从而保证这两个路由器之间的BGP连接可用;要是使用属于某个物理接口的地址来建立BGP连接,该物理接口又变得不工作了,这一点就不可能做到。SCTP的多宿特性允许主机(而不仅仅是路由器)也多宿,而且允许多宿跨越不同的服务供应商发生,这些基于路由的TCP多宿方法都无法做到。

好的,用户之前已经询问了TCP和UDP的编程要点,现在转向SCTP。用户提供了关于SCTP的详细描述,包括关联、多流、消息导向、多宿等特性,并比较了与TCP和UDP的区别。需要总结SCTP的核心特性、与TCP/UDP的差异,可能还要涉及编程要点和应用场景。

以下是SCTP(流控制传输协议)的核心特性、与TCP/UDP的关键差异及适用场景的总结:


SCTP核心特性总结

1. 关联(Association)与多宿(Multihoming)

关联替代连接

• SCTP在两个端点间建立关联 而非连接,允许单个关联绑定多个IP地址(多宿)。

多宿容灾 :若主路径故障(如网络接口失效),自动切换到备用IP地址,提升网络健壮性。

支持跨服务商多宿:不同于基于路由的TCP多宿方案,SCTP允许主机通过不同ISP连接实现冗余。

2. 消息导向(Message-Oriented)

保留消息边界 :类似UDP,每个send操作发送的完整消息(称为"块")在接收端通过recv完整读取。

有序/无序传输

按序递送 :消息可配置为按发送顺序到达(类似TCP)。

无序模式:允许消息以任意顺序到达(类似UDP),减少队头阻塞影响。

3. 多流(Multistreaming)

独立数据流 :一个关联内可创建多个逻辑流 ,每个流独立传输消息。

避免队头阻塞 :流A的消息丢失仅阻塞流A的重传,不影响流B的消息投递。

• 对比TCP:TCP单一字节流中任何丢失都会阻塞后续所有数据。

4. 可靠性与控制机制

可靠传输

• 提供ACK确认、超时重传、动态RTT估算(类似TCP)。

• 选择性重传(SACK):仅重传丢失的块,而非整个窗口数据。

流量与拥塞控制:类似TCP的滑动窗口和拥塞避免算法,但针对多流优化。


SCTP与TCP/UDP的关键差异对比

特性 SCTP TCP UDP
传输模式 消息导向(保留边界) 字节流(无边界) 数据报(保留边界)
连接模型 多宿关联(支持多个IP地址) 单一连接(一对IP地址) 无连接
可靠性 可靠传输(可配置有序/无序) 可靠传输(严格有序) 不可靠
多路复用 多流(逻辑通道独立) 单一流(队头阻塞问题) 无流概念
网络容错 自动切换多宿路径 依赖路由协议或应用层容错 无容错机制
典型应用 电信信令(SIP)、WebRTC数据通道、金融交易 文件传输、HTTP、数据库事务 实时音视频、DNS查询、游戏

SCTP适用场景

  1. 高可靠性且需多路径容灾的场景

    电信核心网 :如SIP信令传输(4G/5G中的S1-MME接口)。

    金融交易系统:需冗余网络链路保证交易连续性。

  2. 需要避免队头阻塞的实时应用

    WebRTC数据通道 :通过多流传输文件与控制命令,避免音视频流被阻塞。

    物联网网关:同时管理多个设备数据流,确保关键指令优先到达。

  3. 复杂网络环境下的可靠通信

    跨ISP多宿主机 :企业服务器通过不同运营商线路接入互联网,提升可用性。

    卫星通信:处理高延迟、易中断的链路,利用多宿快速切换路径。

  4. 消息边界敏感的应用

    数据库复制 :每条SQL语句作为独立消息传输,确保操作原子性。

    工业控制系统:传输独立控制指令,避免TCP粘包问题。


SCTP编程要点

1. 建立关联

服务器端

c 复制代码
int sockfd = socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP); // SOCK_SEQPACKET表示消息导向
struct sockaddr_in addr = {.sin_family=AF_INET, .sin_port=htons(8080), .sin_addr=INADDR_ANY};
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sockfd, 5); // 监听关联请求

客户端

c 复制代码
struct sctp_initmsg initmsg = {.sinit_num_ostreams=10}; // 初始化10个流
setsockopt(sockfd, IPPROTO_SCTP, SCTP_INITMSG, &initmsg, sizeof(initmsg));
connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
2. 多流与消息处理

发送消息到指定流

c 复制代码
struct sctp_sndinfo sndinfo = {.snd_sid=stream_id}; // 指定流ID
sctp_sendmsg(sockfd, data, data_len, NULL, 0, 0, 0, stream_id, 0, 0);

接收消息并获取流ID

c 复制代码
struct sctp_rcvinfo rcinfo;
ssize_t n = sctp_recvmsg(sockfd, buffer, sizeof(buffer), NULL, 0, &rcinfo, 0);
int stream_id = rcinfo.rcv_sid;
3. 多宿配置

添加备用地址

c 复制代码
struct sockaddr_in addr2 = {.sin_family=AF_INET, .sin_port=htons(8080), .sin_addr=inet_addr("192.168.2.1")};
sctp_bindx(sockfd, (struct sockaddr*)&addr2, 1, SCTP_BINDX_ADD_ADDR);
4. 错误处理

关联中断通知

c 复制代码
struct sctp_event_subscribe events = {.sctp_shutdown_event=1};
setsockopt(sockfd, IPPROTO_SCTP, SCTP_EVENTS, &events, sizeof(events));

SCTP的局限性

  1. 中间设备支持不足:部分防火墙/NAT设备未正确处理SCTP,可能导致通信失败。
  2. 应用生态较弱:相比TCP,SCTP的库和工具链支持有限(如HTTP/3选择基于UDP的QUIC而非SCTP)。
  3. 复杂性高:多流、多宿特性增加了协议栈实现和应用设计的复杂度。

总结:何时选择SCTP?

需要多宿容灾 :如关键基础设施的冗余通信。

避免队头阻塞 :实时系统中需独立处理多个数据流。

消息边界敏感 :传输独立逻辑消息(非连续字节流)。

替代自定义可靠UDP方案:SCTP内置可靠性机制,减少重复造轮子。

ICMP

网际控制消息协议 (Internet Control Message Protocol)。ICMP处理在路由器和主机之间流通的错误和控制消息。这些消息通常由TCP/IP网络支持软件本身(而不是用户进程)产生和处理,不过图中展示的ping和traceroute程序同样使用ICMP。有时我们称这个协议为ICMPv4,以便与ICMPv6相区别。

IGMP

网际组管理协议 (Internet Group Management Protocol)。IGMP用于多播(见第21章),它在IPv4中是可选的。

ARP

地址解析协议 (Address Resolution Protocol)。ARP把一个IPv4地址映射成一个硬件地址(如以太网地址)。ARP通常用于诸如以太网、令牌环网和FDDI等广播网络,在点到点网络上并不需要。

RARP

反向地址解析协议 (Reverse Address Resolution Protocol)。RARP把一个硬件地址映射成一个IPv4地址。它有时用于无盘节点的引导。

ICMPv6

网际控制消息协议版本6 (Internet Control Message Protocol version 6)。ICMPv6综合了ICMPv4、IGMP和ARP的功能。

BPF

BSD分组过滤器 (BSD packet filter)。该接口提供对于数据链路层的访问能力,通常可以在源自Berkeley的内核中找到。

DLPI

数据链路提供者接口 (datalink provider interface)。该接口也提供对于数据链路层的访问能力,通常随SVR4内核提供。

二、TCP连接的建立和终止

TCP连接

建立一个TCP连接时会发生下述情形:

(1)服务器必须准备好接受外来的连接。这通常通过调用socket(创建套接字)、bind(将套接字与特定ip进行绑定)和listen(将套接字状态设置为监听)这3个函数来完成,我们称之为被动打开。

(2)客户通过调用connect发起主动打开。这导致客户TCP发送一个SYN(同步分节),它告诉服务器客户将在(待建立的)连接中发送的数据的初始序列号。通常SYN分节不携带数据,其所在IP数据报只含有一个IP首部、一个TCP首部及可能有的TCP选项。

(3)服务器必须确认(ACK)客户的SYN,同时自己也得发送一个SYN分节,它含有服务器将在同一连接中发送的数据的初始序列号。服务器在单个分节中发送SYN和对客户SYN的ACK(确认)。

(4)客户必须确认服务器的SYN。

这种交换至少需要3个分组,因此称之为TCP的三路握手:

图中给出的客户的初始序列号为J,服务器的初始序列号为K。ACK中的确认号是发送这个ACK的一端所期待的下一个序列号。因为SYN占据一个字节的序列号空间,所以每一个SYN的ACK中的确认号就是该SYN的初始序列号+1.类似的,每一个FIN(表示结束)的ACK中的确认号为该FIN的序列号+1。

建立TCP连接就好比一个电话系统[Nemeth 1997],socket函数等同于有电话可用,bind函数是在告诉别人你的电话号码,这样他们可以呼叫你。listen函数是打开电话机,等待来电,accept函数是在接到电话时,返回对方的电话号码。connect函数是拨打对方的电话号码,并拨打它。accept函数发生在被呼叫的人应答电话之时。由accept退回客户的标识(即客户的IP地址和端口号)类似于让电话机的呼叫者ID功能部件显示呼叫者的电话号码。然而两者的不同之处在于accept只在连接建立之后返回客户的标识,而呼叫者ID功能部件却在我们选择应答或不应答电话之前显示呼叫者的电话号码。如果使用域名系统DNS,它就提供了一种类似于电话簿的服务。getaddrinfo类似于在电话簿中查找某个个人的电话号码码,getnameinfo则是类似于有一本按照电话号码而不是按照用户名排序的电话簿。

每一个SYN可以含有多个TCP选项。下面是常用的TCP选项。

  • MSS选项。发送SYN的TCP一端使用本选项通告对端的最大分节大小(maximum segment size)即MSS,也就是它在本连接的每个TCP分节中愿意接受的最大数据量。发送端TCP使用接收端的MSS值作为所发送分节的最大大小。
  • 窗口规模选项。TCP连接任何一端能够通告对端的最大窗口大小是65535,因为在TCP首部中相应的字段占16位。然而当今因特网上业已普及的高速网络连接(45 Mbit/s或更快,如RFC 1323 [Jacobson, Braden, and Borman 1992]所述)或长延迟路径(卫星链路)要求有更大的窗口以获得尽可能大的吞吐量。这个新选项指定TCP首部中的通告窗口必须扩大(即左移)的位数(0~14),因此所提供的最大窗口接近1 GB(65535×2^14)。在一个TCP连接上使用窗口规模的前提是它的两个端系统必须都支持这个选项。
    为提供与不支持这个选项的较早实现间的互操作性,需应用如下规则。TCP可以作为主动打开的部分内容随其SYN发送该选项,只是在对方也随其SYN发送该选项的前提下,它才能扩大自己窗口的规模。类似地,服务器的TCP只有接收到随客户的SYN到达的该选项时,才能发送该选项。本逻辑假定实现者不理解它们不理解的选项,如此忽略是必需的要求,也已普遍满足。但无法保证所有实现都满足此要求。

• 时间戳选项。这个选项对于高速网络连接是必要的,它可以防止由失而复现的分组可能造成的数据损坏。它是一个较新的选项,也类似于窗口规模选项的方式协商处理。作为网络编程人员,我们无需考虑这个选项。

TCP的多数实现都支持这些选项。后两个选项有时称为"RFC 1323选项",因为它们是在RFC 1323 [Jacobson, Braden, and Borman 1992]中说明的。既然高管带或长延迟的网络被称为"长胖管道"(long fat pipe),这两个选项也称为"长胖管道选项"。TCPv1的第24章对这些选项有详细的叙述。

TCP终止

TCP建立一个连接需3个分节,终止一个连接则需4个分节。

(1) 某个应用进程首先调用close,我们称该端执行主动关闭(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。

(2) 接收到这个FIN的对端执行被动关闭(passive close)。这个FIN由TCP确认。它的接收作为一件文件结束符(end-of-file)传递给接收端应用进程(放在已排队等候该应用进程接收的任何其他数据之后),因为FIN的接收意味着应用进程在相应连接上再无额外数据可接收。

(3) 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。

(4) 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。

既然每个情形都需要一个FIN和一个ACK,因此通常需要4个分节。我们使用限定词"通常"是因为某些情形下的FIN随数据一起发送;另外,步骤3发送的分节可以由执行被动关闭的一方发送。有可能会出现只有一个FIN分节的情况。

类似SYN,一个FIN也占据1个字节的序列号空间。因此,每个FIN的ACK确认号就是这个FIN的序列号加1。

在步骤2与步骤3之间,从执行被动关闭一端到执行主动关闭一端流动数据是可能的。这称为半关闭(half-close)。

当套接字被关闭时,其所在端TCP各自发送了一个FIN。我们在图中指出,这是由应用进程调用close而发生的,不过需认识到,当一个Unix进程无论自愿地(调用exit或从main函数返回)还是非自愿地(收到一个终止本进程的信号)终止时,所有打开的描述符都被关闭,这也导致仍然打开的任何TCP连接上也发出一个FIN。

上图展示了客户执行主动关闭的情形,不过我们指出,无论是客户还是服务器,任何一端都可以执行主动关闭。通常情况是客户执行主动关闭,但是某些协议(譬如值得注意的HTTP/1.0)却由服务器执行主动关闭。

TCP涉及连接建立和连接终止的操作可以用状态转换图(state transition diagram)来说明。

TCP为一个连接定义了11种状态,并且TCP规则规定如何基于当前状态及在该状态下所接收的分节从一个状态转换到另一个状态。举例来说,当某个应用进程在CLOSED状态下执行主动打开时,TCP将发送一个SYN,且新的状态是SYN_SENT。如果这个TCP接着接收到一个带ACK的SYN,它将发送一个ACK,且新的状态是ESTABLISHED。这个最终状态是绝大多数数据传送发生的状态。

自ESTABLISHED状态引出的两个箭头处理连接的终止。如果某个应用进程在接收到一个FIN之前调用close(主动关闭),那就转换到FIN_WAIT_1状态。但如果某个应用进程在ESTABLISHED状态期间接收到一个FIN(被动关闭),那就转换到CLOSE_WAIT状态。

我们用粗实线表示通常的客户状态转换,用粗虚线表示通常的服务器状态转换。图中还注明存在两个我们未曾讨论的转换:一个为同时打开(simultaneous open),发生在两端几乎同时发送SYN并且这两个SYN在网络中交错的情形下,另一个为同时关闭(simultaneous close),发生在两端几乎同时发送FIN的情形下。TCPv1的第18章中有这两种情况的例子和讨论,它们是可能发生的,不过非常罕见。

展示状态转换图的原因之一是给出11种TCP状态的名称。这些状态可使用netstat显示,它是一个在调试客户/服务器应用时很有用的工具。

一、状态转换图总体结构

  1. 核心状态节点
    图示完整覆盖TCP协议中11种标准状态(RFC 793定义):
    CLOSED :初始/终止状态
    LISTEN :服务器等待SYN
    SYN_SENT :客户端发送SYN后等待响应
    SYN_RCVD :服务器收到SYN后等待ACK
    ESTABLISHED :连接建立完成
    FIN_WAIT_1/2 :主动关闭方等待ACK/FIN
    CLOSING :双方同时关闭
    TIME_WAIT :等待2MSL(报文最大生存时间)
    CLOSE_WAIT :被动关闭方等待应用层关闭
    LAST_ACK:被动关闭方发送FIN后等待ACK

二、关键流程解析

1. 连接建立(三次握手)

标准流程
客户端 服务器 CLOSED → SYN_SENT SYN CLOSED → SYN_RCVD SYN+ACK SYN_SENT → ESTABLISHED ACK SYN_RCVD → ESTABLISHED 客户端 服务器


2. 连接终止(四次挥手)

标准流程
主动关闭方 被动关闭方 ESTABLISHED → FIN_WAIT_1 FIN ESTABLISHED → CLOSE_WAIT ACK FIN_WAIT_1 → FIN_WAIT_2 FIN CLOSE_WAIT → LAST_ACK ACK FIN_WAIT_2 → TIME_WAIT LAST_ACK → CLOSED TIME_WAIT → CLOSED (2MSL超时) 主动关闭方 被动关闭方


3. 异常处理路径

关键异常场景

  1. RST(连接重置)处理

    LISTEN状态收到RST:忽略

    SYN_SENT收到RST:回退到CLOSED

  2. 超时处理

    SYN_SENT超时:重传SYN(最多重试/proc/sys/net/ipv4/tcp_syn_retries次)

    FIN_WAIT_2超时:直接关闭(需tcp_fin_timeout参数控制)


三、技术细节补充

1. 状态持续时间
状态 典型持续时间 系统参数
TIME_WAIT 2*MSL(通常60秒) net.ipv4.tcp_fin_timeout
FIN_WAIT_2 可配置(默认60秒) net.ipv4.tcp_fin_timeout
SYN_RCVD 75秒 net.ipv4.tcp_synack_retries
2. 网络编程关联

bind() :将套接字从CLOSED移至LISTEN

connect() :触发CLOSED → SYN_SENT转换

close() :触发ESTABLISHED → FIN_WAIT_1


总结

此状态图基本覆盖了TCP协议的核心状态机,但存在少量标注错误。通过结合RFC 793标准和Linux实现细节,可以更准确理解TCP连接生命周期中的复杂状态转换,这对网络调试(如netstat -ton分析)和高性能服务器开发具有重要指导意义。

上图展示了一个完整的TCP连接所发生的实际分组交换情况,包括连接建立、数据传送和连接终止3个阶段。图中还展示了每个端点所经历的TCP状态。

本例中的客户通告一个值为536的MSS(表明该客户只实现了最小重组缓冲区大小),服务器通告一个值为1460的MSS(以太网上IPV4的典型值)。不同方向上MSS值不相同不成问题。

一旦建立连接,客户就构成一个请求并发送给服务器。这使我们假设该请求来自于单个TCP端(即请求大小对于服务器适宜的值为1460字节的消息)。服务器处理该请求并发送两个答复。我们假设该答复也适合于单个分节(例个案小于35字节)。图中使用粗线表示这些数据分节。注意,服务器对请求的确认是伴随其答复发送的。这种做法称为捎带(piggybacking),它通常在服务器处理请求并产生回答的时间小于200 ms时发生。如果服务器耗用更长时间,譬如说1秒,那么我们将看到先是确认后是应答。(TCP数据流机理在TCPv1第18章和第20章中细述。)

图中随后展示的是终止连接的4个分节,注意,执行主动关闭的第一端(本例子中为客户)进入图所示的状态。该端必须发送完整的终止序列,即发送一个 FIN然后接收一个 ACK。关闭该端的4个分节使用8个分节的空间。如果改用UDP,那么只需交换两个请求和一个应答,一个请求一个答复。然而从TCP切换到UDP将丧失TCP提供的许多可靠性,这种可靠性是通过一个大的端结层(TCP)转移到UDP应用程序。TCP提供的另一个重要性即拥塞控制,因为它们需要应用进程数据。尽管如此,我们仍然需要知道许多网络应用是使用UDP构建的,必须由UDP进行更多的处理,如UDP忽略了TCP连接建立和终止所需的前开销。

TIME_WAIT状态

毫无疑问,TCP中有关端结层最不容易理解的是它的TIME_WAIT状态。在前面的图中我们看到执行主动关闭的端结进入了这个状态。该端结在该状态的持续时间是最大生存期(maximum segment lifetime, MSL)的两倍,RFC 1122 [Braden 1989] 的建议是2分钟,不过源自Berkeley的实现传统上改用30秒这个值。这意味着TIME_WAIT状态的持续时间在1分钟到4分钟之间。MSL是任何IP数据报能够在因特网中存活的最长时间。我们知道这个时间是有界的,因为每个数据报都有一个称为跳限(hop limit)的位字段(见图A-1中IPv4的TTL字段和图A-2中IPv6的跳限字段)。它的最大值为255。尽管这是一个跳数的限制而不是真正的时间限制,我们仍然假设:具有最大跳限(255)的分组在网络中存在的时间不可能超过MSL秒。

分组在网络中的"迷途"通常是路由异常的结果。某个路由器崩溃或某条路由之间的某个链路断开,路由协议在数秒钟到数分钟的时间里才能稳定并找出另一条通路。在这段时间内有可能会发生分组迷途(路由器A把分组发送给路由器B,而B再把它们返回A),我们关心的分组可能就此陷入这样的循环。假设迷途的分组是一个TCP分节,在它迷途期间,发送端TCP超时并重传该分组,而重传的分组通过某条候选路径最终到达目的地。然而不久后(自迷途的分组开始其旅程起最多MSL秒以内)路由环恢复,早先迷失在这个循环中的分组最终也到达目的地,该分组的到达被判定为迟到的重复分组(lost duplicate)或漫游的重复分组(wander duplicate)。TCP必须正确处理这些迟到的分组。

TIME_WAIT状态有两个存在的理由:

(1)可靠地实现TCP双工连接的终止;

(2)允许老的重复分节在网络中消逝。

第一个理由可以通过查看上图并假设最终的ACK丢失了来解释。服务器将重新发送它的最终FIN,因此客户必须维护状态信息,以允许它重新发送最终的ACK。要是客户不维护状态信息,它将响应以一个RST(另一种类型的TCP分节),这个分节将被服务器解释成一个错误。如果TCP执行所有必要的工作以彻底终止某个连接上两个方向的数据流(即无差错为什么它必须正确处理那一端终止于TIME_WAIT状态中的每一个分节的原因。本例子也说明了理解为ACK的那个。

为理解存在于TIME_WAIT状态的第二个理由,我们假设在12.106.32.254的1500端口和206.168.112.219的端口之间有一个TCP连接。我们关闭这个连接,过一段时间后在相同的IP地址和端口号之间建立另一个连接。后一个连接被称为前一个连接的复活(incarnation),因为它们的IP地址和端口号都相同。TCP必须防止来自一个复活的连接的老分组在该连接已终止后再现。从理论上讲,这种情况可能发生,譬如说,某个应用进程锁定在某个僵局中,导致连接暂停一段时间后再继续。既然TIME_WAIT状态的持续时间是MSL的2倍,这就足以让某个方向上的分组最多存活MSL秒即被丢弃,另一个方向上的应答同时也会在MSL秒内被丢弃。通过这只消失的分组,新老连接将不会混淆。

这个规则存在一个例外:如果到达的SYN的序列号大于前一化身的结束序列号,源自Berkeley的实现将当前处于TIME_WAIT状态的连接启动新的化身,它要求服务器执行主动关闭,因为接收下一个SYN的那一端必须处于TIME_WAIT状态。rsh命令具备这种能力。

TCP协议中的TIME_WAIT状态是网络通信中一个关键但容易引起困惑的设计。以下是针对其核心逻辑的总结与分析:


TIME_WAIT的本质与作用

  1. 存在的必要性

    可靠终止全双工连接

    若主动关闭方(如客户端)发送的最后一个ACK丢失,被动关闭方(如服务器)会重传FIN包。处于TIME_WAIT状态的客户端能重新发送ACK,避免连接异常终止(否则会响应RST导致错误)。

    消除旧报文干扰

    等待2MSL时间(通常1-4分钟)确保网络中残留的旧报文彻底消失,避免新连接(相同四元组)收到前一次连接的延迟报文,导致数据错乱。

  2. 2MSL的数学逻辑

    MSL(最大报文生存时间) :IP报文在网络中的最大存活时间(RFC 1122建议2分钟,实际实现如Linux默认为60秒)。

    2MSL的合理性

    报文单向存活最多MSL时间,ACK响应最多再存活MSL时间。2MSL覆盖了"发送FIN→重传FIN→接收ACK"的最坏情况。


TIME_WAIT的挑战与解决方案

  1. 常见问题场景

    高并发短连接 :频繁创建/关闭连接时,主动关闭方(如HTTP服务器)积累大量TIME_WAIT状态,导致端口耗尽(Cannot assign requested address错误)。

    延迟敏感场景:TIME_WAIT状态占用端口资源,影响新连接建立速度。

  2. 优化策略

    内核参数调优(Linux示例):

    bash 复制代码
    # 允许复用TIME_WAIT状态的端口(需内核4.1+)
    sysctl -w net.ipv4.tcp_tw_reuse=1
    # 启用快速回收(注意NAT环境下可能导致问题)
    sysctl -w net.ipv4.tcp_tw_recycle=1
    # 缩短TIME_WAIT超时时间(非RFC合规)
    sysctl -w net.ipv4.tcp_fin_timeout=30

    应用层设计

    ◦ 使用连接池(如HTTP Keep-Alive)减少短连接。

    ◦ 客户端与服务端角色调换(如rsh服务端主动关闭,利用序列号校验复用连接)。

  3. 例外情况处理

    序列号避让机制:若新SYN的初始序列号大于前一次连接的最大序列号,允许跳过TIME_WAIT(需内核支持)。此机制依赖TCP序列号的严格单调递增特性,避免旧报文被误认为有效。


关键争议与实践权衡

  1. TIME_WAIT是否多余?

    反对观点 :现代网络设备可靠性高,路由收敛快,MSL可能被高估。

    支持观点:在复杂网络(如卫星链路、高延迟路径)中,2MSL仍是避免数据错乱的唯一可靠方法。

  2. 实践建议

    默认遵守RFC :除非明确性能瓶颈由TIME_WAIT引起,否则避免缩短2MSL。

    监控工具 :使用ss -tan | grep TIME-WAITnetstat监控状态分布,针对性优化。


总结

TIME_WAIT是TCP协议容错设计的核心机制,通过短暂牺牲资源换取连接的可靠终止与数据一致性。理解其原理后,可通过内核调优、协议设计等手段平衡可靠性与性能,但需谨慎避免破坏协议语义(如禁用TIME_WAIT可能导致数据污染)。在实际工程中,结合具体场景(如是否处于NAT环境、是否允许非RFC行为)选择最优策略。

三、SCTP的连接与终止

与TCP一样,SCTP也是面向连接的,因而也有关联的建立与终止的握手过程。不过SCTP的握手过程不同于TCP,我们在此加以说明。

四路握手

建立一个SCTP关联的时候会发生下述情形(类似于TCP)。

(1)服务器必须准备好接受外来的关联。这通常通过调用socket、bind和listen这3个函数来完成,称为被动打开。

(2)客户通过调用connect或者发送一个隐式打开该关联的消息进行主动打开。这使得客户SCTP发送一个INIT消息(初始化),该消息告诉服务器客户的IP地址清单、初始序列号、用于标识本关联中所有分组的起始标记、客户请求的外出流的数目以及客户能够支持的外来流的数目。

(3)服务器以一个INITACK消息确认客户的INIT消息,其中含有服务器的IP地址清单、初始序列号、起始标记、服务器请求的外出流的数目、服务器能够支持的外来流的数目以及一个状态cookie。状态cookie包含服务器用于确信本关联有效所需的所有状态,它是数字化签名过的,以确保其有效性。

(4)客户以一个COOKIE ECHO消息回射服务器的状态cookie。除COOKIE ECHO外,该消息可能在同一个分组中还捆绑了用户数据。

(5)服务器以一个COOKIE ACK消息确认客户回射的cookie是正确的,本关联于是建立。该消息也可能在同一个分组中还捆绑了用户数据。

以上交换过程至少需要4个分组,因此称之为SCTP的四路握手(four-way handshake)。下图展示了这4个分节。

SCTP的四路握手在很多方面类似于TCP的三路握手,差别主要在于作为SCTP整体一部分的cookie的生成。INIT(随其众多参数一道)承载一个验证标记Ta和一个初始序列号J。在关联的有效期内,验证标记Ta必须在对端发送的每个分组中出现。初始序列号J用作承载用户数据的DATA块的起始序列号。对端也在INIT ACK中承载一个验证标记Tz,在关联的有效期内,验证标记Tz也必须在其发送的每个分组中出现。除了验证标记Tz和初始序列号K外,INIT的接收端还在作为响应的INIT ACK中提供一个cookie C。该cookie包含设置本SCTP关联所需的所有状态,这样服务器的SCTP栈就不必保存所关联客户的有关信息。SCTP关联设置的细节参见[Stewart and Xie 2001]的第4章。

四路握手过程结束时,两端各自选择一个主目的地址(primary destination address)。当不存在网络故障时,主目的地址将用作数据要发送到的默认目的地。

在SCTP中使用四路握手是为了避免一种拒绝服务攻击。

SCTP使用cookie的四路握手定形了一种防护这种攻击的方法。TCP的许多实现也使用类似的方法。两者的主要差别在于,TCP中cookie状态必须编码到只有32位长的初始序列号中。SCTP为此提供了一个任意长度的字段,并且要求实施基于加密的安全性以防护攻击。

关联终止

SCTP不像TCP那样允许"半关闭"的关联。当一端关闭某个关联时,另一端必须停止发送新的数据。关联关闭请求的接收端发送完已经排队的数据(如果有的话)后,完成关联的关闭。下图展示了这一交换过程。

SCTP没有类似于TCP的TIME_WAIT状态,因为SCTP使用了验证标记。所有后续块都在捆绑它们的SCTP分组的公共首部标记了初始的INIT块和INIT ACK块中作为起始标记交换的验证标记;由来自旧连接的块通过所在SCTP分组的公共首部间接携带的验证标记对于新连接来说是不正确的。因此,SCTP通过放置验证标记值就避免了TCP在TIME_WAIT状态保持整个连接的做法。

SCTP涉及关联建立和关联终止的操作可以用状态转换图(state transition diagram)来说明,如下图所示。

与前面一样,本状态机中从一个状态到另一个状态的转换由SCTP规则基于当前状态及在该状态下所接收的块规定。举例来说,当某个应用进程在CLOSED状态下执行主动打开时,SCTP将发送一个INIT,且新的状态是COOKIE-WAIT。如果这个SCTP接着接收到一个INIT ACK,它将发送一个COOKIE ECHO,且新的状态是COOKIE-ECHOED。如果该SCTP随后接收到一个COOKIE ACK,它将转换成ESTABLISHED状态。这个最终状态是绝大多数数据传送发生点的状态,尽管DATA块也可以由COOKIE ECHO块或COOKIE ACK块所在消息捆绑捎带。

从 ESTABLISHED 状态引出的两个箭头处理关联的终止。如果某个应用进程在接收到一个 SHUTDOWN 之前调用 close(主动关闭),那就转换到 SHUTDOWN-PENDING 状态。否则,如果某个应用进程在 ESTABLISHED 状态期间接收到一个 SHUTDOWN(被动关闭),那就转换到 SHUTDOWN-RECEIVED 状态。

下图展示一个作为样例的 SCTP 关联所发生的实际分组交换情况,包括关联建立、数据传送和关联终止 3 个阶段。图中还展示了每个端点所历经的 SCTP 状态。

本例中,客户在COOKIE ECHO块所在分组中捎带了它的第一个DATA块,服务器则在作为应答的COOKIE ACK块所在分组中捎带了数据。一般而言,当网络应用采用一到多接口式样时(我们将在后面讨论一到一和一到多这两种接口式样),COOKIE ECHO通常捎带一个或多个DATA块。

SCTP分组中信息的单位称为块(chunk)。块是自描述的,包含一个块类型、若干个块标记和一个块长度。这样做方便了多个块的绑缚,只要把它们简单地组合到一个SCTP外出消息中([Stewart and Xie 2001]的第5章给出了块捆绑和常规数据传输过程的细节)。

1. 状态转换图的核心作用

描述协议行为 :通过状态和消息交互,定义 SCTP 如何建立关联、传输数据、处理关闭请求。

指导实现与调试 :帮助开发者和网络工程师理解 SCTP 如何通过状态管理确保可靠性和多路径通信。

对比 TCP :突出 SCTP 的特性(如无 TIME_WAIT 状态、四路握手、多宿支持等)。


2. 关键状态解释

(1) 关联建立阶段
  1. CLOSED:初始状态,无关联存在。
  2. COOKIE-WAIT :客户端发送 INIT 消息后进入此状态,等待服务器响应 INIT ACK
  3. COOKIE-ECHOED :客户端收到 INIT ACK 后发送 COOKIE ECHO 消息,进入此状态,等待服务器确认。
  4. ESTABLISHED:关联成功建立,双方可双向传输数据。
(2) 数据传输阶段

ESTABLISHED 是主要的数据传输状态,支持多流(stream)和多宿(multi-homing)特性。

(3) 关联终止阶段
  1. SHUTDOWN-PENDING(主动关闭方):应用发起关闭请求后,需等待未决数据发送完毕。
  2. SHUTDOWN-SENT (主动关闭方):发送 SHUTDOWN 消息后等待确认。
  3. SHUTDOWN-RECEIVED (被动关闭方):收到 SHUTDOWN 后,发送未决数据并响应 SHUTDOWN ACK
  4. CLOSED:最终终止状态。

3. 关键消息交互

  1. 四路握手建立关联

    INITINIT ACKCOOKIE ECHOCOOKIE ACK

    通过四次消息交换验证双方身份,避免拒绝服务攻击(DoS)。

    Cookie 机制:服务器生成加密的 Cookie 保存关联信息,无需提前存储状态,增强安全性。

  2. 关联终止

    SHUTDOWNSHUTDOWN ACK :双方协商关闭,确保数据完整性和有序性。

    无半关闭状态:SCTP 不允许单向传输,关闭请求需完全终止关联。


4. 图中箭头与标注的含义

黑色实线箭头 :客户端的正常状态转换(主动发起关联建立或关闭)。

黑色虚线箭头 :服务器的正常状态转换(被动响应关联建立或关闭)。

箭头标签

接收:消息类型 (如 INITSHUTDOWN):触发状态转换的事件。

发送:消息类型 (如 INIT ACKCOOKIE ECHO):在当前状态下的响应动作。


5. 与 TCP 的对比

  1. 关联建立
    SCTP 四路握手 vs. TCP 三路握手:SCTP 使用 Cookie 增强安全性,避免 SYN 洪泛攻击。
  2. 关联终止
    SCTP 无 TIME_WAIT 状态:通过验证标记(Verification Tag)避免旧连接干扰,简化状态管理。
  3. 多流与多宿:SCTP 支持多独立数据流和多个 IP 地址,适合高可靠性场景(如 VoIP、5G 核心网)。

6. 实际应用场景

实时通信 :如 WebRTC、视频会议,依赖 SCTP 的多流和抗丢包能力。

移动网络 :5G 核心网使用 SCTP 传输信令,支持多宿和动态地址切换。

金融交易:需要高可靠性和严格有序性的场景。


这张图清晰地展示了 SCTP 如何通过状态机管理关联生命周期,结合 Cookie 机制、多路径支持和无 TIME_WAIT 等特性,提供了比 TCP 更灵活、安全的传输解决方案。

SCTP选项

SCTP使用参数和块方便增设可选特性。新的特性通过添加这两个条目之一加以定义,并允许通常的SCTP处理规则汇报未知的参数和未知的块。参数类型字段和块类型字段的高两位指明SCTP接收端该如何处置未知的参数或未知的块。

当前如下两个对SCTP的扩展正在开发中。

(1)动态地址扩展,允许协作的SCTP端点从已有的某个关联中动态增删IP地址。

(2)不完全可靠性扩展,允许协作的SCTP端点在应用进程的指导下限制数据的重传。当一个消息变得过于陈旧而无须发送时(按照应用进程的指导),该消息将被跳过而不再发送到对端。这意味着不是所有数据都确保到达关联的另一端。

四、总结

UDP是一个简单、不可靠、无连接的协议,而TCP是一个复杂、可靠、面向连接的协议。SCTP组合了这两个协议的一些特性,并提供了TCP所不具备的额外特性。尽管绝大多数因特网应用(Web、Telnet、FTP和电子邮件)使用TCP,但这3个协议对传输层都是必要的。在22.4节中我们将阐述选用UDP替代TCP的理由。在23.12节中我们将阐述选用SCTP替代TCP的理由。

TCP使用三路握手建立连接,使用四分组交换序列终止连接。当一个TCP连接被建立时,它从CLOSED状态转换到ESTABLISHED状态;当该连接被终止时,它又回到CLOSED状态。个TCP连接可处于11种状态之一,其状态转换图给出了从一种状态转换到另一种状态的规则。理解状态转换图是使用netstat命令诊断网络问题的基础,也是理解当某个应用进程调用诸如connect、accept和close等函数时所发生过程的关键。

TCP的TIME_WAIT状态一直是一个造成网络编程人员混淆的来源。存在这一状态是为了实现TCP的全双工连接终止(即处理最终那个ACK丢失的情形),并允许老的重复分节从网络中消逝。

SCTP使用四路握手建立关联;使用三分组交换序列终止关联。当一个SCTP关联被建立时,它从CLOSED状态转换到ESTABLISHED状态;当该关联被终止时,它又回到CLOSED状态。个SCTP关联可处于8种状态之一,其状态转换图给出从一种状态转换到另一种状态的规则。SCTP不像TCP那样需要TIME_WAIT状态,因为它使用了验证标记。

相关推荐
Long_poem17 分钟前
【自学笔记】智能合约基础知识点总览-持续更新
笔记·区块链·智能合约
思考的橙子35 分钟前
初识HTTP
网络·网络协议·http
辰辰大美女呀1 小时前
【Zephyr】【一】学习笔记
前端·笔记·学习
中科岩创1 小时前
某地基坑及周边建筑物自动化监测项目
大数据·网络·物联网
灏瀚星空1 小时前
运行时智控:PanLang 开发者指南(一)运行时系统核心模块实现——PanLang 原型全栈设计方案与实验性探索5
开发语言·人工智能·经验分享·后端·重构·模板方法模式·源代码管理
ssr——ssss1 小时前
网络华为HCIA+HCIP WLAN
网络
要下雨了吗1 小时前
C语言三大程序结构 & 单分支语句
c语言·c++·visual studio
mrbone112 小时前
C++-C++中的几种cast
java·开发语言·c++
White の algo2 小时前
【Linux系统】Linux权限讲解!!!超详细!!!
linux·运维·服务器
小宋要上岸2 小时前
基于TCP/QT/C++的网络调试助手测试报告
网络·c++·qt·网络协议·tcp/ip