linux学习进展 网络编程——TCP协议详解
上一节我们学习了多进程、多线程并发服务器,解决了基础Socket服务器“单客户端处理”的局限,实现了多个客户端同时与服务器通信。但在实操中,我们只关注了Socket系统调用的使用,却没有深入思考:为什么TCP能保证数据的可靠传输?“面向连接”到底是什么意思?数据在网络中传输时,TCP是如何避免丢失、乱序的?
本节课,我们将深入TCP协议的底层,从TCP协议的核心定义、报文格式、核心机制(连接建立、数据传输、连接关闭),到Linux网络编程中TCP的实操注意事项,全面解析TCP协议,让我们不仅会用Socket编写TCP程序,更能理解其背后的原理,为后续编写更稳定、高效的网络程序打下基础。
核心重点:掌握TCP协议的核心特性、报文格式,理解三次握手、四次挥手的原理,熟悉TCP可靠传输、流量控制、拥塞控制的核心逻辑,结合Linux Socket编程场景,规避TCP使用中的常见问题。
先给大家些图
TCP 报文格式

TCP 三次握手

TCP 四次挥手

TCP 状态转换


一、TCP协议核心概述(必懂)
1. TCP的定义与定位
TCP(Transmission Control Protocol,传输控制协议),是TCP/IP五层模型(或OSI七层模型)中传输层的核心协议之一,与UDP协议并列,主要作用是为应用层提供可靠、面向连接、面向字节流的端到端数据传输服务。
定位:TCP位于应用层(如HTTP、FTP)和网络层(IP协议)之间,承接应用层的数据,将其封装成TCP报文,交给网络层通过IP协议传输;同时接收网络层传来的TCP报文,解析后交给应用层,屏蔽底层IP协议的不可靠性(IP协议无连接、不可靠,可能出现数据丢失、乱序、重复)。
2. TCP的三大核心特性(重点)
TCP之所以能成为网络编程中最常用的传输层协议(如Socket编程、HTTP/HTTPS协议底层都是TCP),核心在于其三大特性,也是我们上节实现TCP程序时“可靠通信”的底层保障:
(1)面向连接
“面向连接”是指在数据传输之前,通信双方必须先建立一个明确的连接,数据传输完成后,再主动关闭连接,就像打电话:拨号(建立连接)→ 通话(数据传输)→ 挂电话(关闭连接)。
注意:TCP的连接是双向连接,一旦建立连接,通信双方可同时发送和接收数据(全双工通信);而我们上节编写的Socket程序中,accept()和connect()的交互,本质就是TCP连接建立的过程。
(2)可靠传输
可靠传输是TCP最核心的特性,指数据从发送端发出后,确保能准确、完整地到达接收端,不会出现丢失、乱序、重复的情况。TCP实现可靠传输的核心手段包括:确认机制、重传机制、序号与确认号、校验和等(后续详细讲解)。
对比:UDP协议是不可靠传输,发送端发送数据后,不关心接收端是否收到,也不处理数据丢失、乱序问题,适合实时性要求高(如视频直播、游戏)的场景。
(3)面向字节流
TCP将应用层传递的数据视为“连续的字节流”,不区分数据的边界(与UDP的“面向数据报”相反)。发送端会根据TCP报文的最大长度(MSS),将字节流拆分成分段(TCP Segment)发送;接收端会将收到的分段重新拼接成完整的字节流,交给应用层。
举个例子:应用层发送1000字节的数据,TCP可能会拆分成2个分段(如500字节+500字节)发送,接收端收到后,会将两个分段拼接成1000字节,完整交给应用层,应用层无需关心拆分细节。
3. TCP的适用场景
基于TCP的可靠传输、面向连接特性,其适用场景主要是“对数据可靠性要求高”的场景,也是我们Linux网络编程中最常接触的场景:
Socket网络编程(如我们实现的客户端/服务器通信);
HTTP/HTTPS协议(网页浏览、接口调用);
文件传输(FTP协议)、远程登录(SSH协议);
邮件传输(SMTP协议)等。
二、TCP报文格式(核心重点,必掌握)
TCP数据在网络中传输时,会被封装成“TCP报文段(TCP Segment)”,TCP报文段由“首部(Header)”和“数据(Data)”两部分组成,其中首部包含了TCP协议的核心控制信息,是理解TCP机制的关键。
TCP首部最小长度为20字节(无选项字段),最大长度为60字节(包含选项字段),其核心字段如下(按顺序讲解,重点记关键字段):
1. TCP报文首部核心字段(图解式讲解)
我们无需死记硬背所有字段,重点掌握以下7个核心字段,结合实际场景理解其作用:
(1)源端口号(16位)与目的端口号(16位)
两个字段均为16位,共同标识TCP连接的“端点”,与IP地址结合,可唯一确定网络中的一个应用程序:
源端口号:发送端应用程序的端口(如客户端Socket的随机端口);
目的端口号:接收端应用程序的端口(如服务器的8888端口)。
类比:源端口号和目的端口号,就像邮寄包裹时的“发件人电话”和“收件人电话”,确保包裹能准确送达对应的应用程序。例如,浏览器(源端口随机)连接网站的HTTP服务(目的端口80),就是通过这两个字段定位通信端点的。
(2)序号(32位)
序号字段用于标识TCP报文段中“数据部分的第一个字节”的编号,核心作用是解决数据乱序问题,让接收端能按正确顺序重组数据。
关键细节:
TCP连接建立时,会随机生成一个初始序号(ISN,Initial Sequence Number),后续发送的报文段序号,会基于初始序号递增(序号 = 上一个序号 + 上一个报文段的数据长度);
例如:发送端初始序号为1000,第一个报文段数据长度为500字节,则该报文段序号为1000;第二个报文段数据长度为300字节,则序号为1000+500=1500,以此类推。
(3)确认序号(32位)
确认序号与序号对应,用于确认接收端已成功收到的数据,表示接收端“期望接收的下一个字节的序号”,核心作用是实现“可靠传输的确认机制”。
举例:接收端收到序号为1000、数据长度为500字节的报文段(即收到1000~1499字节的数据),则接收端回复的确认报文中,确认序号为1500,表示“已收到1000~1499字节,下一个希望收到的是1500字节及以后的数据”。
注意:确认序号只有在“ACK标志位为1”时才有效(后续讲解标志位)。
(4)首部长度(4位)
4位字段,用于表示TCP首部的长度,单位是“32位字”(即4字节)。
计算方式:首部长度 = 字段值 × 4字节;例如,字段值为5,表示首部长度为5×4=20字节(无选项字段);字段值为6,表示首部长度为24字节(包含4字节选项)。
核心作用:告诉接收端,TCP首部的结束位置,以及数据部分的开始位置(避免首部和数据混淆)。
(5)标志字段(6位,重点!)
6位字段,每一位对应一个控制标志,用于控制TCP连接的建立、数据传输、连接关闭,核心标志位有4个,其余2个(URG、PSH)了解即可:
SYN(同步位):值为1时,表示“请求建立连接”,用于TCP三次握手的前两次;
ACK(确认位):值为1时,表示“确认有效”,确认序号字段生效,几乎所有TCP报文(除了三次握手的第一个报文)都会设置ACK=1;
FIN(结束位):值为1时,表示“请求关闭连接”,用于TCP四次挥手;
RST(重置位):值为1时,表示“连接异常,需要重置”,常用于处理连接错误(如客户端强行关闭,服务器返回RST报文)。
补充:URG(紧急位)=1时,表示报文段中包含紧急数据,需优先处理;PSH(推送位)=1时,表示接收端应立即将数据交付给上层应用,无需等待缓冲区满。
(6)窗口大小(16位)
16位字段,用于流量控制,表示接收端当前“可用的缓冲区大小”(以字节为单位),告诉发送端“最多能发送多少字节的数据”,避免发送端发送过快,导致接收端缓冲区溢出。
举例:接收端缓冲区还剩1000字节可用,则窗口大小字段值为1000,发送端收到后,最多只能发送1000字节的数据,直到接收端处理完部分数据、更新窗口大小后,再发送剩余数据。
窗口大小是TCP流量控制的核心字段,后续会结合“滑动窗口机制”详细讲解。
(7)校验和(16位)
用于校验TCP报文段的完整性,发送端计算报文段(首部+数据)的校验和,填入该字段;接收端收到报文后,重新计算校验和,若与字段值不一致,则认为报文段丢失或损坏,直接丢弃,不进行确认(触发发送端重传)。
2. TCP报文格式总结
TCP报文段 = 首部(20~60字节) + 数据(应用层传递的数据);
核心字段记忆口诀:端口定端点,序号防乱序,确认保可靠,标志控连接,窗口做限流;
理解TCP报文格式,是掌握TCP核心机制(三次握手、四次挥手、可靠传输)的基础,后续讲解机制时,会反复用到这些字段。
三、TCP核心机制(重中之重,结合实操理解)
TCP的核心机制围绕“可靠传输”和“连接管理”展开,我们重点讲解4个核心机制:连接建立(三次握手)、连接关闭(四次挥手)、可靠传输机制、流量控制与拥塞控制,结合我们上节的Socket编程场景,让理论落地。
1. 连接建立:三次握手(Three-Way Handshake)
TCP是面向连接的协议,数据传输前必须建立连接,这个连接建立的过程就是“三次握手”,本质是通信双方交换初始序号、确认连接的过程,对应我们Socket编程中“服务器listen()、accept()”和“客户端connect()”的交互过程。
(1)三次握手的核心目的
让通信双方(客户端和服务器)互相确认对方的“接收能力”和“发送能力”;
交换双方的初始序号(ISN),为后续可靠传输、序号同步打下基础;
确保连接是双向的(双方都能发送和接收数据)。
(2)三次握手的详细流程(结合Socket编程)
假设客户端(Client)要连接服务器(Server),服务器已启动并调用listen()进入监听状态,三次握手流程如下(重点记标志位、序号、确认序号):
-
第一次握手(客户端 → 服务器):
-
客户端调用connect(),发送TCP报文段;
-
标志位:SYN=1,ACK=0(表示请求建立连接,无确认信息);
-
序号:客户端的初始序号(ISN_C,随机生成,如1000);
-
目的:告诉服务器“我要连接你,请你确认我的发送能力”;
-
对应Socket场景:客户端执行connect(),触发第一次握手,此时服务器的accept()仍处于阻塞状态。
-
-
第二次握手(服务器 → 客户端):
-
服务器收到客户端的SYN报文后,确认客户端的发送能力,回复TCP报文段;
-
标志位:SYN=1,ACK=1(表示确认客户端的连接请求,同时请求客户端确认自己的发送能力);
-
序号:服务器的初始序号(ISN_S,随机生成,如2000);
-
确认序号:ISN_C + 1(1001,表示已收到客户端序号1000的报文,期望下一个收到1001及以后的数据);
-
目的:告诉客户端“我收到你的连接请求了,我的发送能力正常,请你确认我的接收能力”;
-
对应Socket场景:服务器收到SYN报文后,完成连接的“半连接”,此时accept()仍未返回。
-
-
第三次握手(客户端 → 服务器):
-
客户端收到服务器的SYN+ACK报文后,确认服务器的发送和接收能力,回复确认报文;
-
标志位:ACK=1(仅确认,无需再发SYN);
-
序号:ISN_C + 1(1001,基于客户端初始序号递增);
-
确认序号:ISN_S + 1(2001,表示已收到服务器序号2000的报文,期望下一个收到2001及以后的数据);
-
目的:告诉服务器“我已确认你的接收和发送能力,连接可以建立,我们可以开始传输数据了”;
-
对应Socket场景:客户端发送第三次握手报文后,connect()返回(连接成功);服务器收到该报文后,accept()返回(得到通信Socket cfd),双方正式建立连接,可开始读写数据。
-
(3)为什么是“三次握手”,不是两次或四次?
核心原因:确保“双向连接”的可靠性,避免“无效连接”浪费资源:
两次握手:客户端发送SYN,服务器回复SYN+ACK后,服务器会认为连接已建立,但客户端可能没收到服务器的回复(报文丢失),此时客户端不会发送数据,而服务器会一直等待客户端数据,造成资源浪费;
三次握手:客户端必须回复服务器的SYN+ACK,服务器收到回复后,才确认连接建立,双方都能确认对方的收发能力,避免无效连接;
四次握手:多余的步骤,三次握手已能完成双向确认,四次会增加连接建立的延迟,降低效率。
2. 连接关闭:四次挥手(Four-Way Handshake)
TCP连接是双向的,因此关闭连接时,需要双方分别关闭自己的“发送通道”,这个过程就是“四次挥手”,对应我们Socket编程中“close()”函数的调用过程(客户端和服务器调用close(),都会触发四次挥手)。
(1)四次挥手的核心目的
让通信双方互相确认“数据已全部发送完毕”,避免数据丢失;
关闭双方的发送通道,释放连接占用的资源(如Socket文件描述符、缓冲区等)。
(2)四次挥手的详细流程(结合Socket编程)
假设客户端先主动关闭连接(实际中客户端或服务器都可主动关闭),双方已完成数据传输,流程如下:
-
第一次挥手(客户端 → 服务器):
-
客户端调用close(),发送TCP报文段;
-
标志位:FIN=1,ACK=1(表示客户端已完成数据发送,请求关闭自己的发送通道);
-
序号:客户端当前的序号(如1500,基于之前的传输递增);
-
确认序号:服务器当前的确认序号(如2500,表示客户端已收到服务器所有数据);
-
对应Socket场景:客户端调用close()后,无法再向服务器发送数据,但仍能接收服务器的数据。
-
-
第二次挥手(服务器 → 客户端):
-
服务器收到客户端的FIN报文后,确认客户端的关闭请求,回复确认报文;
-
标志位:ACK=1;
-
序号:服务器当前的序号(如2500);
-
确认序号:客户端的序号 + 1(1501,表示已收到客户端的关闭请求);
-
目的:告诉客户端“我已收到你的关闭请求,我会尽快关闭我的发送通道,请你等待我剩余的数据发送完毕”;
-
对应Socket场景:服务器收到FIN后,会继续向客户端发送未完成的数据,此时客户端仍能接收数据。
-
-
第三次挥手(服务器 → 客户端):
-
服务器发送完所有剩余数据后,调用close(),发送FIN报文;
-
标志位:FIN=1,ACK=1(表示服务器已完成数据发送,请求关闭自己的发送通道);
-
序号:服务器当前的序号(如2800,基于之前的传输递增);
-
确认序号:客户端的序号 + 1(1501,与第二次挥手一致);
-
对应Socket场景:服务器调用close()后,无法再向客户端发送数据,仅能接收客户端的确认报文。
-
-
第四次挥手(客户端 → 服务器):
-
客户端收到服务器的FIN报文后,确认服务器已完成数据发送,回复确认报文;
-
标志位:ACK=1;
-
序号:客户端当前的序号(1501);
-
确认序号:服务器的序号 + 1(2801,表示已收到服务器的关闭请求);
-
目的:告诉服务器“我已收到你的关闭请求,你可以释放资源了”;
-
对应Socket场景:客户端发送确认报文后,会等待一段时间(TIME_WAIT状态,后续讲解),确保服务器收到确认报文后,双方关闭连接,释放Socket资源。
-
(3)为什么是“四次挥手”,不是三次?
核心原因:TCP连接是双向的,双方的发送通道需要分别关闭,且服务器可能有未发送完的数据:
客户端发送FIN后,仅关闭自己的发送通道,服务器可能还有数据要发送,无法立即发送FIN,只能先回复ACK确认;
服务器发送完所有数据后,才能发送FIN,关闭自己的发送通道;
客户端收到服务器的FIN后,回复ACK,完成双向关闭,因此需要四次挥手。
3. 可靠传输机制(TCP核心中的核心)
TCP的可靠传输,本质是“确保发送端发送的数据,能准确、完整地被接收端收到”,核心依靠以下4种手段,结合我们之前学的报文字段,逐一理解:
(1)确认机制(基于确认序号)
接收端收到TCP报文段后,会立即回复一个“确认报文”(ACK=1),通过确认序号告诉发送端“我已收到哪些数据,下一个希望收到什么数据”;发送端只有收到确认报文,才会认为对应的数据已成功发送,否则会触发重传。
举例:发送端发送序号1000、长度500字节的报文,接收端收到后,回复确认序号1500,发送端收到确认后,才会发送下一个报文(序号1500)。
(2)重传机制(解决数据丢失)
发送端发送报文后,会启动一个“重传计时器”,如果在计时器超时前,未收到接收端的确认报文,就认为该报文丢失,重新发送该报文,直到收到确认或重传次数达到上限(避免无限重传)。
补充:重传计时器的超时时间会动态调整(基于网络延迟),网络延迟大时,超时时间变长,避免不必要的重传。
(3)序号与确认序号(解决数据乱序、重复)
发送端给每个字节的数据分配唯一的序号,接收端根据序号重组数据,即使报文在网络中乱序到达,也能按序号排列成正确的字节流;
同时,接收端会根据确认序号,忽略重复收到的报文(如报文重传后,接收端收到重复数据,会直接丢弃,不回复确认)。
(4)校验和(解决数据损坏)
发送端计算TCP报文段(首部+数据)的校验和,填入报文首部;接收端收到报文后,重新计算校验和,若与首部的校验和不一致,说明报文在传输过程中损坏,直接丢弃,不回复确认,触发发送端重传。
4. 流量控制与拥塞控制(避免数据溢出和网络拥堵)
TCP不仅要保证可靠传输,还要避免“发送端发送过快”和“网络拥堵”,因此设计了流量控制和拥塞控制机制,两者作用不同,相辅相成:
(1)流量控制(端到端控制)
核心目的:控制发送端的发送速度,匹配接收端的接收能力,避免接收端缓冲区溢出(接收端处理数据的速度跟不上发送端发送速度)。
实现方式:基于TCP报文首部的“窗口大小”字段,接收端会根据自己的缓冲区空闲大小,动态调整窗口大小,发送端严格按照接收端的窗口大小发送数据:
-
接收端缓冲区空闲大 → 窗口大小大 → 发送端发送速度快;
-
接收端缓冲区空闲小 → 窗口大小小 → 发送端发送速度慢;
-
接收端缓冲区满 → 窗口大小为0 → 发送端停止发送,直到接收端处理完数据,更新窗口大小。
(2)拥塞控制(全局控制)
核心目的:控制发送端的发送速度,避免网络拥堵(当网络中数据过多时,会出现报文丢失、延迟增加,导致重传,进一步加剧拥堵)。
实现方式:TCP会维护一个“拥塞窗口”(cwnd),发送端的实际发送窗口 = min(接收端窗口大小,拥塞窗口),通过动态调整拥塞窗口的大小,适应网络状态:
-
网络通畅(无报文丢失):拥塞窗口逐渐增大,发送速度逐渐加快;
-
网络拥堵(出现报文丢失):拥塞窗口立即减小,发送速度变慢,避免加剧拥堵。
补充:拥塞控制是TCP协议的复杂机制之一,核心是“探测网络容量”,在不拥堵的前提下,最大化发送效率,我们无需深入其算法细节,只需理解其核心作用即可。
四、Linux Socket编程中TCP的实操注意事项
结合我们前两节的Socket编程(单客户端、多进程多线程服务器),以及本节课的TCP协议原理,总结几个实操中容易踩坑的点,避免出现通信异常:
1. close()函数与四次挥手的关系
调用close()函数,本质是触发TCP的FIN报文(发起关闭连接请求):
-
客户端调用close():触发第一次挥手,客户端无法再发送数据,但仍能接收服务器的数据,直到服务器完成关闭;
-
服务器调用close():触发第三次挥手,服务器无法再发送数据,直到客户端回复确认;
-
注意:多进程/多线程服务器中,子进程/子线程处理完客户端通信后,必须调用close(cfd),否则会导致连接资源泄漏,甚至出现“TIME_WAIT”状态积累。
2. TIME_WAIT状态(重点,避免端口占用)
四次挥手的第四次握手后,客户端会进入TIME_WAIT状态(默认等待2MSL,约1分钟),核心目的是:
-
确保服务器能收到客户端的确认报文(若服务器未收到,会重传FIN报文,客户端在TIME_WAIT状态下能重新回复);
-
避免旧的连接报文(延迟到达的报文)干扰新的连接。
实操问题:TIME_WAIT状态下,客户端的端口会被占用,若频繁重启客户端,可能出现“端口已被占用”的错误;服务器若主动关闭连接,也会进入TIME_WAIT状态,导致端口无法立即复用。
解决方法:在Socket创建后,设置SO_REUSEADDR选项,允许端口复用:
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
3. 字节流的“无边界”问题
TCP是面向字节流的,发送端发送的数据会被拆分成多个报文段,接收端接收时,可能会将多个报文段拼接成一个“大数据块”,也可能只接收一个报文段的部分数据(如接收端缓冲区未满),因此:
-
接收端不能假设“一次read()就能读完发送端发送的所有数据”,必须循环read(),直到读取到预期的字节数或read()返回0(对方关闭连接);
-
实操中,建议在数据中添加“长度标识”(如先发送数据长度,再发送数据),避免接收端读取不完整。
4. 避免“粘包”问题
“粘包”是TCP面向字节流的必然现象:发送端连续发送多个小数据,TCP会将其合并成一个大的TCP报文段发送(提高传输效率),导致接收端一次read()读取到多个数据,出现“粘包”。
解决方法(实操常用):
-
固定数据长度:每次发送固定长度的数据,接收端每次读取固定长度;
-
添加分隔符:在数据末尾添加特殊分隔符(如“\n”“\r\n”),接收端根据分隔符拆分数据;
-
长度+数据:先发送数据的长度(如4字节),再发送数据,接收端先读取长度,再根据长度读取对应的数据。
五、学习小结
1. TCP协议是传输层核心协议,核心特性是面向连接、可靠传输、面向字节流,为应用层提供稳定的数据传输服务,适用于对可靠性要求高的场景。
2. TCP报文首部是核心,重点掌握“端口号、序号、确认序号、标志位、窗口大小”,这些字段是理解TCP机制的基础。
3. 三次握手建立连接,四次挥手关闭连接,核心是“双向确认”,避免无效连接和数据丢失;三次握手对应Socket的connect()、listen()、accept(),四次挥手对应close()。
4. 可靠传输依靠确认、重传、序号、校验和四大机制;流量控制控制端到端速度,拥塞控制控制网络全局速度,两者共同保证TCP传输的高效与稳定。
5. 实操中需注意TIME_WAIT状态、字节流无边界、粘包等问题,结合setsockopt()选项和数据处理技巧,避免通信异常。
下一节,我们将学习UDP协议,对比TCP协议的优缺点,实现简单的UDP客户端和服务器,同时补充TCP/UDP的适用场景对比,完善我们的Linux网络编程知识体系。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)