一、TCP 协议概述
TCP(Transmission Control Protocol,传输控制协议)是 TCP/IP 协议族中的核心协议之一,位于传输层。它的设计目标是在不可靠的 IP 网络之上,提供一种面向连接的、可靠的、基于字节流的通信服务。
1.1 TCP 的核心特性
| 特性 | 说明 |
|---|---|
| 面向连接 | 通信前需建立连接(三次握手),结束后释放连接(四次挥手) |
| 可靠传输 | 通过确认与重传机制,保证数据完整、无差错、不丢失、不重复 |
| 全双工通信 | 连接双方可同时发送和接收数据 |
| 字节流服务 | 应用层数据被视为连续的字节流,无固定边界,由应用层自行解析 |
| 流量控制 | 使用滑动窗口,防止发送方发送过快导致接收方缓冲区溢出 |
| 拥塞控制 | 动态调整发送速率,避免网络过载 |
二、TCP 报文段头部格式
理解 TCP 的工作机制,离不开对报文段头部的认识。TCP 头部长度通常为 20~60 字节,各字段含义如下:
| 字段名 | 长度(bit) | 说明 |
|---|---|---|
| 源端口号 | 16 | 发送方应用程序端口 |
| 目标端口号 | 16 | 接收方应用程序端口 |
| 序列号(SEQ) | 32 | 数据段的第一个字节序号 |
| 确认号(ACK) | 32 | 期望收到的下一个序号,确认号之前的数据已收到 |
| 数据偏移 | 4 | 头部长度(单位 4 字节) |
| 控制位 | 6+2 | 包含 SYN、ACK、FIN、RST、PSH、URG 等标志 |
| 窗口大小 | 16 | 接收窗口大小,用于流量控制 |
| 校验和 | 16 | 头部和数据的校验和 |
| 紧急指针 | 16 | 紧急数据的位置 |
其中,控制位(flags) 是管理连接状态的关键:
- SYN:同步序号,用于建立连接
- ACK:确认字段有效
- FIN:发送方完成数据发送,请求关闭
- RST:异常重置连接
- PSH:接收方应尽快将数据交给应用层
- URG:紧急数据
三、TCP 的建立连接------三次握手
TCP 在传输数据前必须通过三次握手建立连接,其根本目的是确保双方都具备发送和接收数据的能力,并同步初始序列号。
3.1 握手过程
text
客户端 服务器
| |
| SYN, SEQ=x |
|----------------->| (第一次:客户端请求连接)
| |
| SYN, ACK=x+1, |
| SEQ=y |
|<-----------------| (第二次:服务器同意,并携带自己的SYN)
| |
| ACK=y+1 |
|----------------->| (第三次:客户端确认,连接建立)
| |
| 数据传输... |
- 第一次 :客户端发送 SYN 报文段,指明自己的初始序列号
x,表示希望建立连接。 - 第二次 :服务器回复 SYN+ACK,确认号是
x+1,同时告知自己的初始序列号y。 - 第三次 :客户端发送 ACK,确认号为
y+1。此包可携带应用层数据。
3.2 为什么是三次,而不是两次或四次?
- 两次:服务器无法确认客户端已收到自己的 SYN-ACK,可能造成半连接或重复连接。
- 三次:最小的握手次数,足以同步双方的序列号并确认双方向通信能力。
- 四次或更多:增加不必要的开销。
四、TCP 的断开连接------四次挥手
当一方完成数据发送后,可以发起关闭连接。由于 TCP 是全双工的,关闭需要分别关闭两个方向的传输。
4.1 挥手过程
text
客户端 服务器
| |
| FIN, SEQ=u |
|----------------->| (第一次:客户端不再发送数据)
| |
| ACK=u+1 |
|<-----------------| (第二次:服务器确认,此时若还有数据可继续发送)
| |
| FIN, SEQ=v |
|<-----------------| (第三次:服务器数据发送完毕,请求关闭)
| |
| ACK=v+1 |
|----------------->| (第四次:客户端确认,进入TIME_WAIT)
| |
| 连接彻底关闭 |
- 第一次:主动关闭方(如客户端)发送 FIN 报文,表示不再发送数据。
- 第二次:被动关闭方回复 ACK,表示已收到 FIN,但此时若还有数据可继续发送。
- 第三次:被动关闭方数据发完后,发送自己的 FIN。
- 第四次:主动关闭方回复 ACK,随后等待 2MSL 后关闭。
4.2 为什么是四次而不是三次?
因为关闭连接时,被动关闭方可能仍有数据要发送,不能像握手那样合并 ACK 和 FIN,因此必须分两步(先 ACK,再 FIN)。
五、TCP 的可靠传输机制
TCP 通过以下机制保证数据传输的可靠性:
5.1 序列号与确认应答
每个报文段都带有唯一的 序列号 (SEQ),接收方回复 确认号 (ACK = 已收到的最后一个字节序号 + 1),告知发送方所有数据已收到。发送方若在超时时间内未收到确认,则重传该报文段。
5.2 滑动窗口
TCP 采用 滑动窗口 实现流量控制和高效传输。发送方维护一个窗口,窗口内的数据可以连续发送而不必等待逐个确认。窗口大小由接收方的接收窗口(rwnd)和拥塞窗口(cwnd)共同决定。
- 发送窗口:允许发送但尚未确认的数据范围。
- 接收窗口:接收方可用的缓冲区大小,在 ACK 中通告。
5.3 校验和
TCP 头和数据都参与校验和计算,接收方校验失败则丢弃报文段,相当于触发重传。
5.4 分片与排序
TCP 会根据网络 MTU 对数据进行合理分片,接收方将乱序到达的数据包缓存并按序重组,再交给应用层。
六、流量控制
流量控制 是端到端的机制,解决"收发速度不匹配"的问题。
TCP 通过滑动窗口实现流量控制:
- 接收方在每次 ACK 中捎带 窗口大小(win) 字段,告知发送方自己还能接收多少数据。
- 发送方根据该窗口值调整发送速率。
- 如果接收方处理不及时,可以缩小窗口,甚至通告
win=0,让发送方停止发送。
七、拥塞控制
拥塞控制 是全局性的,防止过多的数据注入网络导致网络拥堵。TCP 使用以下四大算法:
7.1 慢启动(Slow Start)
连接刚建立时,拥塞窗口(cwnd)初始为 1 个 MSS(最大报文段长度),每收到一个 ACK,cwnd 加倍(指数增长),快速探测网络容量。
7.2 拥塞避免(Congestion Avoidance)
当 cwnd 达到慢启动阈值(ssthresh)后,进入拥塞避免阶段,cwnd 每经过一个 RTT(往返时间)线性增加 1 个 MSS(加法增大)。
7.3 快重传(Fast Retransmit)
当发送方连续收到 3 个重复的 ACK 时,立即重传丢失的报文段,而不必等待超时。
7.4 快恢复(Fast Recovery)
发生快重传后,TCP 将 ssthresh 设为当前 cwnd 的一半,cwnd 设为 ssthresh + 3 个 MSS,然后线性增长。
八、TCP Socket 编程接口
Linux 的 Socket API 是 POSIX 标准的一部分,几乎所有操作系统都支持。下面逐一介绍核心函数,包含原型、参数、返回值和使用示例。
8.1 socket() ------ 创建套接字
c
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
- 参数
domain:协议族,常用AF_INET(IPv4)、AF_INET6(IPv6)、AF_UNIX(本地)。type:套接字类型,SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)。protocol:通常设为0,自动选择协议(TCP 为IPPROTO_TCP)。
- 返回值 :成功返回文件描述符(非负整数),失败返回
-1。
c
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket");
exit(1);
}
8.2 bind() ------ 绑定本地地址
c
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 用途:将套接字与指定 IP 和端口绑定。服务器端通常需要绑定固定端口。
- 参数 :
sockfd:socket 返回的描述符。addr:指向struct sockaddr_in或sockaddr_in6的指针。addrlen:地址结构大小。
- 返回值 :成功
0,失败-1。
c
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 所有本地 IP
servaddr.sin_port = htons(8888);
if (bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
perror("bind");
exit(1);
}
建议 :在
bind前设置SO_REUSEADDR,避免端口被占用(TIME_WAIT导致)。
c
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
8.3 listen() ------ 监听连接
c
int listen(int sockfd, int backlog);
- 用途:将主动套接字变为被动(监听)套接字,并设置已完成连接队列的最大长度。
- 参数 :
sockfd:已绑定的套接字。backlog:排队等待accept的连接数上限(通常设为 10~128)。
- 返回值 :成功
0,失败-1。
c
if (listen(sockfd, 10) < 0) {
perror("listen");
exit(1);
}
8.4 accept() ------ 接受连接
c
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 用途 :从监听套接字的完成队列中取出第一个连接,并生成一个新的已连接套接字,用于与该客户端通信。
- 参数 :
sockfd:监听套接字。addr:输出客户端地址信息(可为NULL)。addrlen:输入输出,传入大小,返回实际长度。
- 返回值 :成功返回新套接字描述符,失败返回
-1。
c
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int connfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
if (connfd < 0) {
perror("accept");
exit(1);
}
printf("客户端 %s:%d 已连接\n",
inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
8.5 connect() ------ 发起连接(客户端)
c
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 用途:客户端发起 TCP 三次握手,连接到指定服务器。
- 参数 :与
bind类似,地址为目标服务器。 - 返回值 :成功
0,失败-1。
c
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8888);
inet_pton(AF_INET, "192.168.1.100", &servaddr.sin_addr);
if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
perror("connect");
exit(1);
}
8.6 send() ------ 发送数据(TCP)
c
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- 用途:通过已连接的套接字发送数据。
- 参数 :
sockfd:已连接套接字。buf:发送缓冲区。len:要发送的字节数。flags:通常0,可用MSG_DONTWAIT等。
- 返回值 :成功返回实际发送的字节数(可能小于
len),失败-1。
c
char msg[] = "Hello TCP!";
ssize_t sent = send(connfd, msg, strlen(msg), 0);
if (sent < 0) {
perror("send");
exit(1);
}
注意:TCP 是流式协议,
send可能只发送部分数据,需循环发送直到全部发送完毕。
8.7 recv() ------ 接收数据(TCP)
c
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- 用途:从已连接套接字接收数据。
- 参数 :同
send,buf为接收缓存,len为缓存大小。 - 返回值 :成功返回实际接收的字节数(可能小于
len),返回0表示对方已关闭,失败返回-1。
c
char buffer[1024];
ssize_t n = recv(connfd, buffer, sizeof(buffer) - 1, 0);
if (n < 0) {
perror("recv");
exit(1);
} else if (n == 0) {
printf("连接关闭\n");
close(connfd);
return;
}
buffer[n] = '\0';
printf("收到: %s\n", buffer);
8.8 close() 与 shutdown()
c
int close(int fd);
int shutdown(int sockfd, int how);
close:关闭套接字,释放资源,若为最后一个引用则触发关闭流程。shutdown:更精细地控制关闭方向:how = SHUT_RD:不再接收how = SHUT_WR:不再发送how = SHUT_RDWR:两者都禁止
shutdown 不会释放套接字,需配合 close 使用。常用于优雅关闭。
8.9 地址转换辅助函数
inet_pton:将 IP 字符串转为网络字节序二进制(推荐)。
c
int inet_pton(int af, const char *src, void *dst);
inet_ntop:将二进制 IP 转回字符串。htons/ntohs:端口号转换(主机 ↔ 网络)。htonl/ntohl:IP 地址转换。
九、完整示例:TCP 回射服务器与客户端
9.1 服务器端代码
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>
int main() {
int listenfd, connfd;
struct sockaddr_in servaddr, clientaddr;
socklen_t len = sizeof(clientaddr);
char buffer[1024];
// 1. 创建 socket
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0) {
perror("socket");
exit(1);
}
// 2. 设置端口重用
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 3. 绑定
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(8888);
if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
perror("bind");
exit(1);
}
// 4. 监听
if (listen(listenfd, 10) < 0) {
perror("listen");
exit(1);
}
printf("服务器启动,监听端口 8888\n");
// 5. 循环接受连接
while (1) {
connfd = accept(listenfd, (struct sockaddr*)&clientaddr, &len);
if (connfd < 0) {
perror("accept");
continue;
}
printf("客户端 %s:%d 连接\n",
inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
// 回显所有收到的数据
ssize_t n;
while ((n = recv(connfd, buffer, sizeof(buffer)-1, 0)) > 0) {
buffer[n] = '\0';
printf("收到: %s\n", buffer);
send(connfd, buffer, n, 0); // 回显
}
close(connfd);
printf("客户端断开\n");
}
close(listenfd);
return 0;
}
9.2 客户端代码
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>
int main() {
int sockfd;
struct sockaddr_in servaddr;
char buffer[1024];
// 1. 创建 socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket");
exit(1);
}
// 2. 设置服务器地址
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8888);
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr); // 本机测试
// 3. 连接服务器
if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
perror("connect");
exit(1);
}
printf("连接服务器成功\n");
// 4. 发送消息并接收回显
printf("请输入消息: ");
fgets(buffer, sizeof(buffer), stdin);
send(sockfd, buffer, strlen(buffer), 0);
ssize_t n = recv(sockfd, buffer, sizeof(buffer)-1, 0);
if (n > 0) {
buffer[n] = '\0';
printf("回显: %s\n", buffer);
}
// 5. 关闭连接
close(sockfd);
return 0;
}
十、总结
TCP 作为互联网最基础的传输协议之一,通过三次握手 建立连接、可靠传输 (序列号、确认重传、滑动窗口)保障数据完整性、流量控制 与拥塞控制平衡网络性能。理解这些原理,是网络编程和网络问题排查的基石。
在编程层面,Linux 提供的 Socket API(socket/bind/listen/accept/connect/send/recv/close 等)简洁而强大,掌握它们即可开发出稳定高效的 TCP 应用。实际项目中,还需进一步学习多线程/多进程并发、select/poll/epoll 事件驱动模型,以及超时处理等高级主题。
希望本文能帮助读者建立起从协议原理到代码实践的完整认知。若有疑问或建议,欢迎在评论区交流!
文章首发于 CSDN,转载请联系作者。