在进行嵌入式Linux开发或C语言网络编程时,传输层的TCP协议是我们绕不开的核心知识点。作为流式套接字,TCP在保证数据安全可靠的同时,也带来了相对复杂的机制。
本文基于我的个人学习笔记,将知识点结构化梳理,帮助同样是新手的你快速吃透TCP协议。
一、 TCP初探:特点与应用场景
TCP(传输控制协议)位于传输层,采用流式套接字。
我们可以用一张表来总结TCP的核心特点:
| 特点分类 | 具体描述 |
|---|---|
| 连接性 | 有连接 |
| 数据形式 | 面向字节流 |
| 可靠性 | 安全可靠的传输协议(依靠三次握手、四次挥手、应答机制、超时重传机制等) |
| 优劣性 | 机制复杂,实时性和效率没有UDP高 |
| 常见应用 | HTTPS、MQTT、FTP |
二、 TCP生命周期:三次握手与四次挥手
TCP的所有可靠性机制,都建立在连接的基础之上。为了确保通信双方都已经准备就绪,TCP通过三次握手建立连接,通过四次挥手断开连接。
1. 三次握手(由客户端发起)
text
客户端 服务端
| ------------- SYN ----------------> |
| <--------- SYN + ACK -------------- |
| ------------- ACK ----------------> |
- 作用:TCP建立连接时,通过三次握手,来确保通信双方都已经准备就绪。
2. 数据收发
text
客户端 服务端
| ------------- PSH ----------------> |
| <------------- ACK ---------------- |
- 发送数据使用 PSH 标志,接收方回复 ACK 确认。
3. 四次挥手
text
客户端 服务端
| ------------- FIN ----------------> |
| <------------- ACK ---------------- |
| <------------- FIN ---------------- |
| ------------- ACK ----------------> |
- 作用:TCP断开连接时,通过四次挥手,确保通信双方数据都已经收发结束。
三、 C语言实战:TCP编程核心流程与API
在C语言中实现TCP通信,客户端和服务端的流程有所不同。
编程流程图
客户端流程:
socket()创建套接字 -> connect()请求建立连接(触发三次握手) -> send()发送 -> recv()接收 -> close()关闭
服务端流程:
socket()创建套接字(监听套接字) -> bind()绑定IP和端口 -> listen()监听 -> accept()接收完成三次握手的客户端(产生通讯套接字) -> recv()接收 -> send()发送 -> close()关闭
注意 :服务端存在两个状态的客户端队列:已完成三次握手的客户端(如
cli1,等待accept处理)和未完成三次握手的客户端(如cli2,cli3,在排队等待)。
核心API详解
以下是新手必须掌握的几个关键函数,我整理成了表格方便查阅:
| 函数名 | 函数原型 | 功能 | 核心参数说明 | 返回值 |
|---|---|---|---|---|
| connect | int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
请求建立连接 | sockfd: 套接字 addr: 服务端的地址 addrlen: 地址长度 |
成功: 0 失败: -1 |
| listen | int listen(int sockfd, int backlog); |
监听客户端的三次握手 | sockfd: 监听套接字 backlog: 最多允许监听的客户端个数 |
成功: 0 失败: -1 |
| accept | int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); |
接收完成三次握手的客户端,并返回一个通讯套接字 | sockfd: 监听套接字 addr: 保存接入的客户端地址信息的指针 addrlen: 地址信息长度的指针 |
成功: 通讯套接字 失败: -1 |
| recv | ssize_t recv(int sockfd, void *buf, size_t len, int flags); |
接收网络数据 | sockfd: 通讯套接字 buf: 存放接收数据的空间首地址 len: 期待接收的字节数 flags: 0 (默认方式) |
成功: 实际收到的字节数 失败: -1 0: 发送断开连接 |
四、 深入底层:TCP报文头部与标志位
理解TCP报文头部,是排查网络问题和理解底层机制的基础。TCP报文段 = TCP首部 + TCP数据部分。
TCP首部固定为20字节,其中包含了关键的标志位:
| 标志位 | 含义 |
|---|---|
| SYN | 请求建立连接标志位 |
| ACK | 效应报文标志位(确认) |
| PSH | 携带数据的报文标志位 |
| FIN | 请求断开连接标志位 |
| URG | 紧急数据标志位 |
| RST | 重置标志位 |
在报文头部中,还包含:源端口/目的端口、序号、确认号、数据偏移、保留、窗口、检验和、紧急指针、选项(长度可变)和填充。
五、 TCP核心机制思维导图
为了保障可靠性,TCP内部设计了非常复杂的机制。用思维导图梳理如下:
-
TCP核心机制
-
连接管理:三次握手机制、四次挥手机制
-
可靠性保障
-
应答机制:TCP为发送的数据进行编号。发送数据时,报文头部的序列号是这包数据的第一个数据的编号;将来接收方需要给这包数据发送ACK,ACK报文中确认号是收到的最后一个字节编号+1。
-
超时重传机制:TCP每发送一包数据后都要等待应答,如果超时时间之内没有收到应答,则重新发送这包数据。
-
捎带应答机制:ACK可以和应用层发送的数据一起发出,表示对上包数据的响应。
-
-
效率与流量控制
-
滑动窗口机制:缓冲区,保存已发送并收到应答的数据、已发送未收到应答的数据、未发送但在对方处理范围内的数据。
-
延迟应答机制:TCP可以发送多组数据,发送的同时等待应答。
-
流量控制机制:TCP会根据发送端的数据处理和接收能力调整自己的发送速率,根据ACK中窗口值的大小进行动态调整流量。
-
-
六、 避坑指南:TCP粘包与解决方案
什么是粘包?
发送端发送速度太快,接收端处理速度比较慢,导致数据在缓冲区缓存,应用层读出数据时,多包数据发生了粘连。
如何解决?(三种常用方案)
-
收发指定大小数据(收发结构体)
-
注意:跨平台发送时平台的位数。
-
示例代码:
c
struct data { xxx; long num; }; send(sockfd, &data, sizeof(struct data), 0); recv(sockfd, &data, sizeof(struct data), 0);
-
-
给发送的数据明显的分割符,应用层根据分隔符解析
- 示例:发送
hello\nworld\n,接收后根据\n拆分解析。
- 示例:发送
-
以自定义方式定义发送的数据帧格式,接收方严格按照协议方式解析
-
帧格式:帧头、数据长度、消息类型、校验、帧尾
-
帧头示例 :
5A 0101 1010、A5、7E、E7、EB90 -
消息类型:区分数据包的数据类型
-
校验:8位和校验 (checksum)、16位和校验、CRC校验
-
协议帧示例分析
在实际抓包或调试中,数据通常如下所示:
text
AA 0C 01 00 00 00 10 00 00 01 00 00 00 11 00 sum BB
如果发生粘包,可能会把两个完整的帧连在一起读取:
text
AA 0C 01 00 00 00 10 00 00 01 00 00 00 BB 11 00 sum BB AA 0F 02 00 00 00 01 00 00 00 10 00 00 01 00 00 00 11 00 BB
此时,接收端必须严格按照"自定义帧格式"中的帧头(如 AA)、数据长度、校验和帧尾(如 BB)来截取每一段完整的数据包。
总结:作为嵌入式和C语言新手,理解TCP不仅仅是为了应付面试,更是为了在编写网络通信模块时,能够写出健壮、不粘包、处理异常断线的高质量代码。从理论到API,再到报文结构和机制,扎实的基础是进阶的必经之路!