linux笔记归纳18:传输层协议TCP
2026/9/13 22:15:28 网站建设 项目流程

传输层协议TCP

目录

传输层协议TCP

一、TCP协议格式

1.1.报文格式

1.2.TCP报头标志位

1.3.连接重置

1.4.紧急指针

二、确认应答机制

2.1.序列号

2.2.确认号

三、超时重传机制

3.1.丢包问题

3.2.超时重传机制

四、连接管理机制

4.1.连接状态

4.2.建立连接的状态变化

4.3.断开连接的状态变化

4.2.三次握手

4.5.四次挥手

4.8.CLOSE_WAIT状态

4.9.TIME_WAIT状态

五、滑动窗口

5.1.滑动窗口的具体实现

5.2.滑动窗口的丢包问题

5.3.快重传 VS 超时重传

六、流量控制

6.1.流量控制机制

6.2.16位窗口大小

七、拥塞控制

7.1.网络拥塞问题

7.2.慢启动机制

7.3.拥塞窗口

八、延迟应答

九、捎带应答

十、面向字节流

十一、粘包问题

十二、TCP异常

12.1.进程终止

12.2.机器重启

12.3.机器掉电

十三、TCP总结

13.1.可靠性本质

13.2.保证可靠性的手段

13.3.提高效率的手段

十四、基于TCP的应用层协议

十五、TCP/UDP用途对比

15.1.UDP的用途

15.2.TCP的用途

十六、UDP实现可靠传输

16.1.引入序号

16.2.引入确认应答

16.3.引入超时重传

十七、TCP全连接队列与tcpdump抓包

17.1.listen的第二个参数

17.2.全连接队列原理

17.3.网络连接的理解

17.4.TCP dump抓包


TCP(Transmission Control Protocol):传输控制协议

一、TCP协议格式

1.1.报文格式

分离问题:4位首部长度实现报头与有效载荷分离

分用问题:16目的端口号将有效载荷交付给上层

  • 16位源端口号:表示数据从哪个进程来
  • 16位目的端口号:表示数据去哪个进程
  • 32位序号:数据序号(自己发送的报文序号)
  • 32位确认序号:应答序号(对对方的报文做确认)
  • 16位窗口大小:接收缓冲区的剩余空间大小
  • 16位校验和:由发送端填充,进行CRC校验,接收端校验不通过就会认为数据有问题
  • 16位紧急指针:有效载荷中特定的偏移量,表示哪些部分是紧急数据

1.2.TCP报头标志位

TCP报头中标识报文类型的字段,根据报文的不同类型,接收方有不同的做法

SYN(Synchronize)标志位:同步标志位,表示请求建立连接

ACK(Acknowledgment)标志位:确认标志位,表示确认号是否有效

FIN(Finish)标志位:结束标志位,表示请求断开连接

PSH(Push)标志位:推送标志位,通知接收方立刻把接收缓冲区数据交给上层应用程序

RST(Reset)标志位:复位标志位,表示建立连接失败后,对方要求重新建立连接

URG(Urgent)标志位:紧急标志位,表示紧急指针是否有效

// linux kernel include/linux/tcp.h struct tcphdr { __be16 source; __be16 dest; __be32 seq; __be32 ack_seq; #if defined(__LITTLE_ENDIAN_BITFIELD) __u16 res1 : 4, doff : 4, fin : 1, syn : 1, rst : 1, psh : 1, ack : 1, urg : 1, ece : 1, cwr : 1; #elif defined(__BIG_ENDIAN_BITFIELD) __u16 doff : 4, res1 : 4, cwr : 1, ece : 1, urg : 1, ack : 1, psh : 1, rst : 1, syn : 1, fin : 1; #else #error "Adjust your <asm/byteorder.h> defines" #endif __be16 window; __sum16 check; __be16 urg_ptr; };

1.3.连接重置

通信时连接出现任何问题,都可以进行重置

第三次握手没有应答,可靠性无法得到保证

客户端只要发出ACK,就认为建立连接成功

服务器只要接收ACK,就认为建立连接成功

因为有时间差,所以双方对于连接建立是否成功的认知不一致

当服务端没有收到应答,客户端发送数据,服务端会发送RST

1.4.紧急指针

TCP通过序号保证可靠性,让数据按序到达接收缓冲区

接收缓冲区是一个字节流式的接收队列,如果有数据想被优先读取处理

就需要通过紧急指针,让报文数据在向上层交付时进行插队,快速响应

16位紧急指针表示的是在当前报文有效载荷中,在特定偏移量处有紧急数据

二、确认应答机制

2.1.序列号

序列号:TCP将每个字节的数据进行编号

作用:确认应答,按序到达,去重

2.2.确认号

确认号:每一个ACK都带有对应的确认号

作用:告诉发送方,已经接收了哪些数据,下次该从哪里发送数据

对历史报文做100%的可靠性保证

将发送缓冲区看成一个字符类型的数组,每一个字节天然就有编号

三、超时重传机制

3.1.丢包问题

发送方收到应答

意味着接收方100%收到

发送方没有收到应答

情况1:数据丢失

数据可能丢失,接收方可能没收到

情况2:应答丢失

应答没有收到,但是接收方收到数据

3.2.超时重传机制

如果发送方在特定的时间间隔,收不到应答,就判定报文丢失

这个特定的时间间隔为了适应网络环境,一定是在不断变化的

  • 网络环境好的时候,间隔短一点
  • 网络环境差的时候,间隔长一点

在Linux中,以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍

发送一次,等500ms,如果重发一次仍得不到应答,等待2 * 500ms

如果重传后仍得不到应答,等待4 * 500ms,依次以指数形式递增

积累重传一定次数,TCP认为网络或对端主机异常,强制关闭连接

四、连接管理机制

4.1.连接状态

正常情况下,TCP经过三次握手建立连接、四次挥手断开连接

一个服务端可能会同时存在非常多的连接,连接有不同的状态

  • 有的连接处于SYN_RCVD状态
  • 有的连接处于ESTABLISHED状态
  • 有的连接处于CLOSE_WAIT状态
  • 有的连接处于LAST_ACK状态
  • 有的连接处于CLOSED状态

服务端需要对各个状态的连接进行管理,就需要先描述再组织

在内核中,连接的本质是一个struct sock结构体,有状态、序号、缓冲区大小,窗口大小等属性

在服务端与客户端建立连接时,有时间和空间成本,一旦连接过多,内存不足,服务器就会崩溃

4.2.建立连接的状态变化

服务端状态变化

[CLOSE → LISTEN]:

服务端调用listen,进入LISTEN状态,等待客户端连接

[LISTEN → SYN_RCVD]:

服务端监听到客户端的连接请求,将该连接放入内核等待队列,向客户端发送确认报文

[SYN_RCVD → ESTABLISHED]:

服务端接收到客户端的确认报文,连接建立成功

客户端状态变化

[CLOSE → SYN_SENT]:

客户端调用connect,发送同步报文,申请建立连接

[SYN_SENT → ESTABLISHED]:

客户端接送到服务端的确认报文,连接建立成功

4.3.断开连接的状态变化

客户端状态变化

[ESTABLISHED → FIN_WAIT_1]:

客户端主动调用close,向服务端发送结束报文

[FIN_WAIT_1 → FIN_WAIT_2]:

客户端接收到服务端的确认报文,等待服务端的结束报文

[FIN_WAIT_2 → TIME_WAIT]:

客户端接收到服务端的结束报文,向服务端发送确认报文

[TIME_WAIT → CLOSED]:

客户端等待两个MSL时间,进入CLOSED状态,关闭连接成功

服务端状态变化

[ESTABLISHED → CLOSE_WAIT]:

服务端接收到客户端的结束报文,向客户端发送确认报文

[CLOSE_WAIT → LAST_ACK]:

服务端调用close,向客户端发送结束报文

[LAST_ACK → CLOSED]:

服务端接收到客户端的确认报文,关闭连接成功

4.2.三次握手

三次握手的概念

通信双方建立连接的过程,让通信双方获取对方接收缓冲区的数据接收能力

三次握手前,双方不能发送消息,前两次握手不能携带数据,只能发送报头

三次握手的实现

内核层:SYN,SYN + ACK,ACK

用户层:客户端调用connect发起连接,连接建立完成后,服务端调用accept接收

connect用来发起三次握手,具体流程由客户端与服务端的操作系统自己完成

accept完全不参与三次握手,只获取底层建立好的连接

三次握手的原因

理由一:以最短方式,验证全双工,确认双方网络通常

理由二:以最小成本,确认双方通信意愿

面对客户端的连接请求,服务端都要无条件接受,所以ACK与SYN在一个报文同时设置

而在断开连接时,客户端给服务端发FIN报文,服务端可能不会接受,需要进行四次挥手

4.5.四次挥手

四次挥手的概念

通信双方断开连接的过程,需要双方断开连接的共识

通信一方发出申请后,另一方应答,通信另一方再发出申请,一方应答

四次挥手的实现

Client→Server:

客户端FIN:我要发的数据发完了,我要和你断开连接

服务端ACK:我收到了

Server→Client:

服务端FIN:我要发的数据发完了,我要和你断开连接

客户端ACK:我收到了

四次挥手的原因

理由一:以最短次数,完成全双工断开连接的请求

理由二:服务端ACK与FIN的时间间隔不确定

shutdown系统调用:关闭套接字的读端、写段或者读写端

4.8.CLOSE_WAIT状态

客户端退出后,如果服务端没有调用close关闭套接字,服务端则会一直处于CLOSE_WAIT状态

依旧占用sockfd,连接没有释放,sockfd用完后,必须要进行关闭,否则会造成文件描述符泄漏

#include "Socket.hpp" #include <iostream> #include <memory> #include <sys/wait.h> #include <functional> using namespace SocketModule; using namespace LogModule; // IO服务 using ioservice_t = std::function<void(std::shared_ptr<Socket> &sock, InetAddr &client)>; // TCP服务器: 实现连接, 不需要关心传递的数据 class TcpServer { public: TcpServer(uint16_t port) : _port(port), _listensockptr(std::make_unique<TcpSocket>()), _isrunning(false) { _listensockptr->BuildListenSocketMethod(_port); } void Start(ioservice_t callback) { _isrunning = true; while (_isrunning) { InetAddr client; auto sock = _listensockptr->Accept(&client); if (sock == nullptr) { continue; } LOG(LogLevel::DEBUG) << "accept success..." << client.StringAddr(); // // 多进程实现 // pid_t id = fork(); // if (id < 0) // { // LOG(LogLevel::FATAL) << "fork error..."; // exit(FORK_ERR); // } // else if (id == 0) // { // // 子进程 // // 关闭listensockfd // _listensockptr->Close(); // if (fork() > 0) // { // exit(0); // } // // 孙子进程 // callback(sock, client); // 执行IO服务 // sock->Close(); // exit(OK); // } // else // { // // 父进程 // // 关闭sockfd // sock->Close(); // pid_t rid = ::waitpid(id, nullptr, 0); // (void)rid; // } } _isrunning = false; } ~TcpServer() { } private: uint16_t _port; std::unique_ptr<Socket> _listensockptr; bool _isrunning; };

客户端退出,操作系统会自动关闭客户端的套接字

服务端如果没有调用close,则该套接字不会释放

4.9.TIME_WAIT状态

主动断开连接的一方要进入TIME_WAIT状态,等待两个MSL时间后才能回到CLOSED状态

MSL(Maximun Segment Lifetime):单个TCP报文在网络中存活的最大时间

保证两个传输方向上,未被接收或者迟到的报文自动消失

报文没有丢,但是被发送方判定为超时,当连接关闭后,该报文依旧在网络中残存

此时如果服务器立刻重启,就可能会收到这个迟到的数据,会造成一些异常的情况

#include "Socket.hpp" #include <iostream> #include <memory> #include <sys/wait.h> #include <functional> using namespace SocketModule; using namespace LogModule; // IO服务 using ioservice_t = std::function<void(std::shared_ptr<Socket> &sock, InetAddr &client)>; // TCP服务器: 实现连接, 不需要关心传递的数据 class TcpServer { public: TcpServer(uint16_t port) : _port(port), _listensockptr(std::make_unique<TcpSocket>()), _isrunning(false) { _listensockptr->BuildListenSocketMethod(_port); } void Start(ioservice_t callback) { _isrunning = true; while (_isrunning) { InetAddr client; auto sock = _listensockptr->Accept(&client); if (sock == nullptr) { continue; } LOG(LogLevel::DEBUG) << "accept success..." << client.StringAddr(); // 多进程实现 pid_t id = fork(); if (id < 0) { LOG(LogLevel::FATAL) << "fork error..."; exit(FORK_ERR); } else if (id == 0) { // 子进程 // 关闭listensockfd _listensockptr->Close(); if (fork() > 0) { exit(0); } // 孙子进程 callback(sock, client); // 执行IO服务 sock->Close(); exit(OK); } else { // 父进程 // 关闭sockfd sock->Close(); pid_t rid = ::waitpid(id, nullptr, 0); (void)rid; } } _isrunning = false; } ~TcpServer() { } private: uint16_t _port; std::unique_ptr<Socket> _listensockptr; bool _isrunning; };

客户端先关闭

服务端先关闭

TIME_WAIT引起bind失败

服务端主动关闭连接,进入TIME_WAIT状态,IP与端口号仍然被系统占用

此时杀掉服务端进程,立刻重启服务端程序,再次绑定同一个接口就会报错

setsockopt函数

作用:创建端口号相同,但IP地址不同的多个套接字描述符

// Socket.hpp void SocketOrDie() override { _sockfd = ::socket(AF_INET, SOCK_STREAM, 0); if (_sockfd < 0) { LOG(LogLevel::FATAL) << "socket error"; exit(SOCKET_ERR); } int opt = 1; setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); LOG(LogLevel::INFO) << "socket success"; }

五、滑动窗口

5.1.滑动窗口的具体实现

滑动窗口的大小

无需等待确认应答而可以继续发送数据的最大值

对方的接收缓冲区剩余空间的大小

对方的接收缓冲区的数据接收能力

滑动窗口的移动

start与end下标的增加

start:报文确认序号

end:start + 对方接收窗口大小

滑动窗口的本质

实现流量控制的方案

滑动窗口的特点

滑动窗口不可向左滑动

已发送的数据不代表是无效的,而是指这部分空间可以再次被利用

不需要刻意地清空发送缓冲区,序号在发送的过程中数字依次增大

滑动窗口可以任意改变大小,根据接收方的窗口大小动态调整更新

5.2.滑动窗口的丢包问题

情况一:最左侧丢失

最左侧报文数据丢失,滑动窗口左侧不变,让对应的报文保存在滑动窗口中,方便后续重传

最左侧报文应答丢失,滑动窗口正常工作,可以通过后续的应答进行确认

确认的消息一定是连续的,在发送时也必须连续发送,保证了滑动窗口不能跳跃

情况二:中间丢失

滑动窗口会向右滑动到中间丢失的位置,变成最左侧丢失的情况

情况三:最右侧丢失

滑动窗口会向右滑动到最右侧丢失的位置,变成最左侧丢失的情况

5.3.快重传 VS 超时重传

高速重发机制

触发条件:发送报文数据丢失,接收方发送三个同样的确认应答

作用:提高效率

超时重传机制

触发条件:超时并且没有应答

作用:无法触发快重传时用来兜底

六、流量控制

6.1.流量控制机制

意义:将数据发送的流量变得合理,慢了加快,快了减慢

接收缓冲区填满后,发送方发送的报文直接丢弃,浪费资源,效率低下

按量发送时,发送方必须要知道接收方的接收缓冲区中剩余空间的大小

在报头中填写自己接收缓冲区中剩余空间的大小,表示自己的接收能力

将滑动窗口设置为0后,主机A就不会向主机B发送携带数据的报文

主机B的操作系统需要等待上层消费者把数据读走才能释放缓冲区

主机A会周期性地发送窗口探测,等待主机B发送窗口更新的通知

当主机A多次询问主机B无果后,发送RST报文,直接关闭TCP连接

6.2.16位窗口大小

16位窗口大小,16位数字最大表示为65535,TCP的窗口理论上最大就是65535个字节

但是TCP首部的40字节选项中包含了窗口扩大因子M,实际窗口大小是窗口值左移M位

七、拥塞控制

7.1.网络拥塞问题

同样是丢包,丢包少和丢包多,得出的结论是不同的

就像学校考试,200人有2、3个人挂科,是学生问题,但198、199人挂科,就是课程问题

TCP不仅要考虑双方主机的问题,还要考虑网络本身的问题

如果TCP通信中出现大量丢包,就不是双方主机问题,而是网络本身问题

此时如果立即重发,可能会增加网络的负载,使网络更加拥堵

7.2.慢启动机制

先发送少量的数据探路,摸清当前的网络状态,再决定按照多大的速度传输数据

指数增幅发送数据,前期发送缓慢,直到网络不拥堵后,就会尽快恢复网络通信

7.3.拥塞窗口

拥塞窗口的本质

一个临界值,低于该值时网络大概率不拥塞,高于该值时网络可能发生拥塞

拥塞窗口的作用

实现拥塞控制,衡量网络是否会拥堵的指标

拥塞窗口的实现

拥塞窗口的数值开始是以指数增长的,当达到慢启动阈值后,就会变为线性增长

发生网络拥塞后,从0开始,新的慢启动阈值是上次网络拥塞时窗口大小的一半

理论上拥塞窗口不会一直增大,网络带宽会限制拥塞窗口的大小

拥塞窗口的增加并不代表发送的数据量一定在增加,发送的数据量由滑动窗口大小决定

滑动窗口的大小 == min{对方接收缓冲区的剩余大小,拥塞窗口}

八、延迟应答

如果接收数据的主机立刻应答,此时返回的窗口可能比较小

在延迟期间,接收方的上层可能会拿走接收缓冲区中的数据

此时返回的窗口大小会更大,网络吞吐量大,提高传输效率

数量限制:每隔N个包应答一次

时间限制:超过最大延迟时间应答一次

九、捎带应答

在ACK报文中加入有效载荷发送给对方,大部分的报文ACK都是设置为1的

捎带应答的作用:可以减少报文数量,降低网络开销,提高网络通信的效率

十、面向字节流

TCP协议会在内核中创建一个发送缓冲区和接收缓冲区,使得TCP读写不需要一一匹配

写100个字节数据,可以调用一次write,写100个字节,也可以调用100次write,每次写一个字节

读100个字节数据,可以调用一次read,读100个字节,也可以调用100次read,每次读一个字节

十一、粘包问题

应用层的数据包在TCP协议中没有明确的边界,可能会接收半个或者一个半的数据

由应用层解决,通过自定义协议加序列化和反序列化,明确报文与报文之间的边界

十二、TCP异常

12.1.进程终止

进程终止会释放文件描述符,仍然可以发送FIN,自动进行四次挥手,和正常关闭没有区别

12.2.机器重启

和进程终止的情况相同,关机之前,操作系统需要终止所有的进程

12.3.机器掉电

客户端网线断开,但服务端认为连接还在

服务端向客户端发送报文,服务端发现连接已经不在,就会发送RESET重置连接

TCP内置保活定时器,定期询问连接是否还在,如果不在则释放连接

内置的保活机制是几十分钟级,一般的保活机制由应用层自己来完成

十三、TCP总结

13.1.可靠性本质

确认应答可以保证历史报文的可靠性,而通信周期中最新的报文永远没有应答,无法保证可靠性

13.2.保证可靠性的手段

校验和、序列号、确认应答

超时重发、连接管理、流量控制、拥塞控制

13.3.提高效率的手段

滑动窗口、快速重传、延迟应答、捎带应答

十四、基于TCP的应用层协议

HTTP、HTTPS、SSH、Telnet、FTP、SMIP

十五、TCP/UDP用途对比

15.1.UDP的用途

用于对高速传输和实时性要求较高的通信领域

比如:广播、直播、视频传输

15.2.TCP的用途

用于可靠传输的情况

比如:文件传输、银行转账、登录注册

十六、UDP实现可靠传输

16.1.引入序号

保证数据顺序

16.2.引入确认应答

确保对端收到数据

16.3.引入超时重传

如果隔一段时间没有应答,就重新发送数据

十七、TCP全连接队列与tcpdump抓包

17.1.listen的第二个参数

backlog + 1:表示在全连接队列中,能够三次握手成功的连接个数

17.2.全连接队列原理

全连接队列的结构本质:生产者消费者模型

全连接队列的连接是上层来不及处理的连接

如果连接数超过队列个数,多余连接就会无法完成握手,保持SYN_SENT状态

全连接队列不能为空,否则会增加服务的闲置率,减少给用户提供服务的效率

全路径队列不能太长,否则会浪费空间,用户的体验差

17.3.网络连接的理解

17.4.TCP dump抓包

捕捉所有网络接口上的TCP报文

sudo tcpdump -i any tcp

捕获指定网络接口上的TCP报文

ifconfig eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 172.18.45.153 netmask 255.255.192.0 broadcast 172.18.63.255 inet6 fe80::216:3eff:fe03:959b prefixlen 64 scopeid 0x20<link> ether 00:16:3e:03:95:9b txqueuelen 1000 (Ethernet) RX packets 34367847 bytes 9360264363 (9.3 GB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 34274797 bytes 6954263329 (6.9 GB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 sudo tcpdump -i eth0 tcp

捕获特定源或目的IP地址的TCP报文

sudo tcpdump src host 192.168.1.100 and tcp sudo tcpdump dst host 192.168.1.200 and tcp sudo tcpdump src host 192.168.1.100 and dst host 192.168.1.200 and tcp

捕获特定端口的TCP报文

sudo tcpdump port 80 and tcp

保存捕获的数据包到文件

sudo tcpdump -i eth0 port 80 -w data.pcap

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询