Linux学习-TCP网络协议
一、TCP 基础认知
(一)协议定位与作用
TCP(Transmission Control Protocol,传输控制协议 )工作于 OSI 模型传输层,为应用层提供可靠、有序、面向连接的字节流传输服务,像 HTTP、FTP 等应用层协议依赖 TCP 保障数据稳定传输,弥补网络层(如 IP 协议)“尽力交付”的不可靠性。
(二)与 UDP 对比
特性 | TCP | UDP |
---|---|---|
数据导向 | 面向字节流(无明确数据包边界,按流传输 ) | 面向数据包(每个数据包独立,有明确边界 ) |
连接性 | 必须建立连接(三次握手)后通信 | 无连接,直接发送数据包 |
可靠性 | 通过确认、重传、排序等机制保障可靠传输 | 仅尽力交付,不保证到达、顺序,可能丢包 |
资源开销 | 机制复杂(连接管理、流量控制等 ),开销大 | 机制简单,开销小 |
通信模式 | 本质一对一(可通过多线程/进程并发实现“一对多” ) | 支持一对一、一对多、多对多通信 |
典型应用 | 网页浏览(HTTP)、文件传输(FTP)等 | 实时音视频(直播、视频通话 )、DNS 查询等 |
二、TCP 核心机制全解析
(一)三次握手(建立连接,保障双向可靠)
1. 核心目的
- 同步通信双方的初始序列号(ISN),为后续数据有序传输、去重做准备;
- 确认双方发送和接收能力正常,确保连接建立后能稳定收发数据。
2. 详细流程(结合状态机理解)
- 客户端状态变化:
CLOSED
→SYN_SENT
→ESTABLISHED
- 服务端状态变化:
CLOSED
→LISTEN
→SYN_RCVD
→ESTABLISHED
- 具体交互:
- 第一次握手(客户端→服务端):
- 客户端发送
SYN
包(SYN = 1
,ACK = 0
),携带客户端初始序列号(ISN_C),进入SYN_SENT
状态,等待服务端回应。 - 作用:发起连接请求,告知服务端“我要连你,这是我的初始序列号”。
- 客户端发送
- 第二次握手(服务端→客户端):
- 服务端收到
SYN
后,回复SYN + ACK
包(SYN = 1
,ACK = 1
),携带服务端初始序列号(ISN_S),并确认客户端的ISN_C
(ACK = ISN_C + 1
),进入SYN_RCVD
状态。 - 作用:同意连接请求,同步自己的序列号,同时确认客户端的序列号。
- 服务端收到
- 第三次握手(客户端→服务端):
- 客户端收到
SYN + ACK
后,发送ACK
包(ACK = 1
),确认服务端的ISN_S
(ACK = ISN_S + 1
),进入ESTABLISHED
状态;服务端收到ACK
后,也进入ESTABLISHED
状态,连接正式建立。 - 作用:最终确认服务端的序列号,双方进入“可收发数据”的稳定状态。
- 客户端收到
- 第一次握手(客户端→服务端):
(二)四次挥手(断开连接,确保数据无残留)
1. 核心目的
- 确保通信双方已发送的数据全部被接收,有序释放连接资源,避免数据丢失或“半关闭”状态导致的问题。
2. 详细流程(结合状态机理解)
- 主动关闭方(假设是客户端)状态:
ESTABLISHED
→FIN_WAIT_1
→FIN_WAIT_2
→TIME_WAIT
→CLOSED
- 被动关闭方(假设是服务端)状态:
ESTABLISHED
→CLOSE_WAIT
→LAST_ACK
→CLOSED
- 具体交互:
- 第一次挥手(主动方→被动方):
- 主动方(如客户端)发送
FIN
包(FIN = 1
),表示“我已无数据要发,准备断开”,进入FIN_WAIT_1
状态。 - 作用:发起断开请求,告知被动方“我要关闭连接了”。
- 主动方(如客户端)发送
- 第二次挥手(被动方→主动方):
- 被动方(如服务端)收到
FIN
后,回复ACK
包(ACK = 1
,ACK = 主动方FIN序列号 + 1
),进入CLOSE_WAIT
状态;主动方收到ACK
后,进入FIN_WAIT_2
状态。 - 作用:确认断开请求,此时被动方可能仍有未发完的数据,需继续发送。
- 被动方(如服务端)收到
- 第三次挥手(被动方→主动方):
- 被动方数据发送完毕后,发送
FIN
包(FIN = 1
),进入LAST_ACK
状态,告知主动方“我也没数据了,可断连”。 - 作用:被动方完成数据发送,发起最终断开请求。
- 被动方数据发送完毕后,发送
- 第四次挥手(主动方→被动方):
- 主动方收到
FIN
后,回复ACK
包(ACK = 1
,ACK = 被动方FIN序列号 + 1
),进入TIME_WAIT
状态(需等待2MSL
时间,确保被动方收到ACK
,避免旧包干扰新连接 );被动方收到ACK
后,直接进入CLOSED
状态;主动方等待2MSL
后,也进入CLOSED
状态,连接完全断开。 - 作用:最终确认断开,
TIME_WAIT
是 TCP 保障可靠性的关键设计(防止迟到的数据包影响后续新连接 )。
- 主动方收到
- 第一次挥手(主动方→被动方):
三、TCP 编程流程
(一)服务端流程与函数解析
1. socket()
:创建套接字
- 函数原型:
int socket(int domain, int type, int protocol);
- 参数:
domain
:协议族,如AF_INET
(IPv4 网络 )、AF_INET6
(IPv6 )。type
:套接字类型,SOCK_STREAM
(TCP 字节流 )、SOCK_DGRAM
(UDP 数据包 )。protocol
:协议,一般填0
(让系统自动匹配domain
和type
对应的协议,如AF_INET + SOCK_STREAM
对应IPPROTO_TCP
)。
- 返回值:成功返回套接字描述符(非负整数),失败返回
-1
。 - 作用:创建用于网络通信的“端点”,是后续操作的基础。
2. bind()
:绑定地址与端口
- 函数原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 参数:
sockfd
:socket()
创建的套接字描述符。addr
:指向地址结构体的指针(如struct sockaddr_in
用于 IPv4 ,需强转为struct sockaddr *
),包含 IP 地址、端口等信息。addrlen
:地址结构体的长度(如sizeof(struct sockaddr_in)
)。
- 返回值:成功
0
,失败-1
。 - 作用:让套接字与特定 IP + 端口绑定,服务端需明确“监听哪个地址和端口的连接”。
3. listen()
:监听连接请求
- 函数原型:
int listen(int sockfd, int backlog);
- 参数:
sockfd
:已绑定的套接字描述符。backlog
:半连接队列 + 全连接队列的最大长度(实际受系统限制,如 Linux 中somaxconn
参数影响 ),表示最多允许多少个客户端处于“等待连接”状态。
- 返回值:成功
0
,失败-1
。 - 作用:将套接字设为“监听模式”,开始接收客户端的连接请求。
4. accept()
:接收客户端连接
- 函数原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 参数:
sockfd
:监听套接字描述符(listen()
后的套接字 )。addr
:用于存储客户端地址信息的结构体指针(如struct sockaddr_in
),可NULL
(不关心客户端地址 )。addrlen
:地址结构体长度的指针(需提前赋值,如sizeof(struct sockaddr_in)
),函数会修改其值为实际客户端地址长度。
- 返回值:成功返回新的通信套接字描述符(用于与该客户端收发数据 ),失败返回
-1
。 - 关键逻辑:阻塞等待客户端的连接请求,三次握手完成后,生成独立的通信套接字,后续与该客户端的数据交互都通过此套接字,监听套接字可继续接收其他客户端连接。
5. recv()
/send()
:收发数据
recv()
函数原型:ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- 参数:
sockfd
:通信套接字描述符(accept()
返回的 )。buf
:接收数据的缓冲区指针。len
:期望接收的最大字节数。flags
:标志位,一般填0
(默认阻塞接收 ),也可设MSG_DONTWAIT
(非阻塞 )等。
- 返回值:成功返回实际接收的字节数;失败返回
-1
;对方关闭连接返回0
。
- 参数:
send()
函数原型:ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- 参数:
sockfd
:通信套接字描述符。buf
:要发送数据的缓冲区指针。len
:要发送的数据长度。flags
:标志位,一般填0
(默认发送 ),也可设MSG_DONTWAIT
(非阻塞 )等。
- 返回值:成功返回实际发送的字节数;失败返回
-1
。
- 参数:
- 作用:通过通信套接字实现应用层数据的收发,TCP 会负责底层的可靠传输(确认、重传等 )。
6. close()
:关闭套接字
- 函数原型:
int close(int fd);
(fd
为套接字描述符 ) - 作用:触发 TCP 四次挥手流程,释放套接字资源;若为监听套接字,会停止接收新连接。
(二)客户端流程与函数解析
1. socket()
:同服务端,创建套接字。
2. connect()
:发起连接请求
- 函数原型:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 参数:
sockfd
:客户端套接字描述符。addr
:服务端地址结构体指针(如struct sockaddr_in
,填服务端的 IP 和端口 )。addrlen
:地址结构体长度。
- 返回值:成功
0
,失败-1
。 - 关键逻辑:触发 TCP 三次握手,与服务端建立连接;若服务端无响应,会超时失败(可设置
SO_RCVTIMEO
等选项调整超时时间 )。
3. send()
/recv()
:同服务端,收发数据。
4. close()
:同服务端,关闭套接字,发起四次挥手。
四、TCP 粘包问题)
(一)问题本质
TCP 是面向字节流的协议,无明确“数据包边界”,发送方多次发送的应用层数据,接收方可能因TCP 底层的缓存、组包机制,或接收方处理速度慢,导致“多包数据被一次性读取”,出现“粘包”(多个应用层数据包粘连在一起 ),影响解析。
(二)产生原因
- 发送方角度:
- 发送速率快,TCP 底层为提高效率,会将应用层的小数据包合并(Nagle 算法默认开启,合并小数据包发送 ),导致接收方读取到“合并后的大包”。
- 应用层未明确数据包边界,连续发送时,TCP 流无区分标识。
- 接收方角度:
- 接收缓冲区有残留数据,
recv()
调用未按“应用层数据包大小”精准读取,导致一次读出多包数据。 - 处理数据速度慢,多个数据包在缓冲区堆积,应用层读取时“一次性取走”。
- 接收缓冲区有残留数据,
(三)解决方法
1)调整发送速率
- 原理:降低发送方数据发送频率,减少 TCP 底层因速率快对多包数据的重新组帧,让接收方有时间逐包处理。
- 适用场景:对实时性要求不高、数据量小且发送频率易调整的简单场景,如低并发的本地数据传输测试 。
- 缺点:会降低整体传输效率,不适用于高并发、低延迟的业务场景,如实时音视频传输 。
2)定长数据收发(基于结构体,需注意对齐)
- 原理:发送方按固定大小封装数据(如固定结构体长度),接收方也按相同固定大小接收,通过明确数据边界解决粘包。
- 结构体对齐问题:跨平台(如 32 位与 64 位系统)传输时,因不同平台对结构体成员的内存布局规则不同,可能导致数据解析错误。例如:
struct a {char a; int b; long c;
};
在 32 位和 64 位平台,int
long
等类型的字节长度、内存对齐方式有差异,需用编译指令(如 #pragma pack
)或属性设置(如 __attribute__((packed))
)统一对齐规则 。
- 适用场景:数据格式固定、对跨平台兼容性要求高且数据量相对稳定的场景,如硬件设备间的简单协议通信 。
- 示例流程:发送方定义
struct Data { char msg[100]; };
,每次发送 100 字节的msg
内容;接收方同样按 100 字节大小接收struct Data
数据,解析msg
字段 。
3)添加分隔符解析
- 原理:在应用层发送的数据中加入唯一分隔符(如
\n
),接收方按分隔符拆分数据包,识别数据边界。 - 示例:发送方发送
hello world\n
how are you\n
,接收方读取数据后,以\n
为标识,拆分出两个数据包 “hello world” 和 “how are you” 。 - 注意事项:若数据本身含分隔符,需提前转义(如将数据中的
\n
替换为\\n
,接收方再还原 ),否则会误判数据边界,导致解析错误 。 - 适用场景:文本类数据传输,如日志传输、简单命令交互场景,分隔符易识别且数据内容对分隔符干扰少的情况 。
4)封装自定义数据帧格式(协议)
- 原理:自定义包含帧头、帧尾、有效数据长度、校验等字段的数据帧,接收方严格按协议解析,通过帧头帧尾定位边界,校验确保数据完整。
- 帧结构说明(以示例帧
AA C0 00 00 00 F0 00 BB 10 A0 00 00 00 10 校验 BB
为例 ):- 帧头:如
AA
,固定标识数据帧开始,方便接收方识别新帧 。 - 有效数据长度:如
C0
,告知接收方有效数据的字节数,辅助解析数据范围 。 - 有效数据:如
00 00 00 F0 00 BB 10 A0 00 00 00 10
,承载实际业务数据 。 - 校验:支持 8 位和校验、16 位和校验、CRC 校验等,用于检测数据在传输中是否出错,保障数据可靠性 。
- 帧尾:如
BB
,标识数据帧结束,配合帧头完成边界识别 。
- 帧头:如
- 适用场景:对数据可靠性、安全性要求高的复杂场景,如工业控制、金融交易等领域的通信,需精准解析和错误校验的场景 。
- 解析流程:接收方先查找
AA
帧头,读取有效数据长度字段,按长度提取有效数据,通过校验字段验证数据完整性,最后识别BB
帧尾,完成一包数据解析 。