微商城怎么开,长沙网站包年优化,wordpress文章页打不开,企业网站建设找哪家「前言」文章内容大致是传输层协议#xff0c;TCP协议讲解的第二篇#xff0c;续上篇TCP。 「归属专栏」网络编程 「主页链接」个人主页 「笔者」枫叶先生(fy) 目录 二、TCP协议2.9 TCP连接管理机制2.9.1 三次握手2.9.2 四次挥手2.9.3 演示查看TIME_WAIT和CLOSE_WAIT状态2.9.… 「前言」文章内容大致是传输层协议TCP协议讲解的第二篇续上篇TCP。 「归属专栏」网络编程 「主页链接」个人主页 「笔者」枫叶先生(fy) 目录 二、TCP协议2.9 TCP连接管理机制2.9.1 三次握手2.9.2 四次挥手2.9.3 演示查看TIME_WAIT和CLOSE_WAIT状态2.9.4 telnet命令2.9.5 演示 2.10 解决TIME_WAIT状态引起的bind失败的问题2.11 流量控制2.12 滑动窗口2.13 拥塞控制2.14 延迟应答2.15 捎带应答2.16 面向字节流2.17 粘包问题2.18 TCP异常情况2.19 TCP小结2.20 基于TCP应用层协议2.21 TCP/UDP对比 三、TCP实验理解listen的第二个参数 二、TCP协议
2.9 TCP连接管理机制
首先明确TCP是面向连接的TCP通信之前需要先建立连接就是因为TCP的各种可靠性保证都是基于连接的要保证传输数据的可靠性的前提就是先建立好连接。
TCP连接不直接保证可靠性但是会间接保证可靠性
TCP进行连接会进行三次握手断开连接会进行四次挥手。
TCP协议的客户端/服务器程序的一般流程
2.9.1 三次握手
双方在进行TCP通信之前需要先建立连接建立连接的这个过程我们称之为三次握手。
三次握手的流程如下
第一次握手客户端向服务器发送一个SYN同步报文请求与服务器建立连接。第二次握手服务器收到客户端的SYN报文后向客户端发送一个SYN/ACK同步/确认报文表示同意建立连接。第三次握手客户端收到服务器的SYN/ACK报文后向服务器发送一个ACK确认报文表示连接建立成功。
通过三次握手客户端和服务器都确认了对方的请求并建立了可靠的连接。
注意三次握手是连接通信的策略即三次握手也可能会出现失败的情况 为什么是三次握手一次握手、两次握手、四次握手行不行 首先明确三次握手是策略不一定百分之百成功也可能出现失败。
比如通信双方在进行三次握手时其中前两次握手能够保证被对方收到因为前两次握手都有对应的应答但第三次握手是没有对应的应答报文的如果第三次握手时客户端发送的ACK报文丢失了那么连接建立就会失败。 但是我们不怕失败丢包因为TCP有配套的解决方案
前面两次握手成功后在客户端看来连接已经建立好了但是在服务端看来连接还没有建立好。如果第三层握手ACK发生丢包了一段时间后服务端还是没有收到ACK报文此时服务端就会重新发起SYN/ACK同步/确认报文又或者客户端认为连接建立好之后就直接发数据报文了此时服务端还没有收到ACK确认即服务端看来连接还没有建立好此时服务端就会收到该数据报文服务端说你都没有跟我建立连接发什么数据报文重连。此时服务端就会把RST标志位设置为1发给客户端让客户端与服务端进行重连
注意第一次和第二次握手不携带数据第三次握手可能会携带数据 一次握手行不行 绝对不行的。
首先明确连接是需要被管理起来的被OS管理起来如果管理连接先描述再组织。OS维护一个连接是有成本的。即如果连接过多OS管理不过来即代表服务器要寄了
一次握手的话只要客户端发起连接就可以直接建立连接了服务端认为连接已经建立好了这样就会导致单主机下SYN洪水攻击
一次握手会发生SYN洪水攻击就是有人搞事通过大量伪造的SYN报文向目标服务器发送连接请求从而消耗服务器资源当服务器的半连接队列被耗尽后合法用户的连接请求无法被处理导致服务不可用。
还有一点就是无法验证全双工即无法保证全双工通信通道是流畅的因为TCP是全双工的
二次握手同上 为什么三次握手可以 因为三次握手是验证全双工通信信道流畅的最小次数
TCP是全双工通信的因此连接建立的核心要务实际是验证双方的通信信道是否是连通的。而三次握手恰好是验证双方通信信道的最小次数通过三次握手后双方就都能知道自己和对方是否都能够正常发送和接收数据还有一点就是有效规避单主机下的SYN洪水攻击
注意TCP的工作是建立通信信道服务器受到攻击本身就不是TCP要解决的。但是如果三次握手有明显的漏洞让客户端利用了这就是你TCP的问题了 四次握手行不行五次、六次…呢 三次握手已经是最小成本验证了全双工再多余就是浪费时间 三次握手也可以叫四次握手原因如下 第二次握手时服务器收到客户端的SYN报文后向客户端发送一个SYN/ACK同步/确认报文这个SYN/ACK报文也可以分两次发送给客户端但是这种情况几乎不存在。 三次握手时的状态变化 服务端
[CLOSED - LISTEN] 服务器端调用listen后进入LISTEN状态等待客户端连接[LISTEN - SYN_RCVD] 一旦监听到连接请求(同步报文段)就将该连接放入内核等待队列中并向客户端发送SYN确认报文[SYN_RCVD - ESTABLISHED] 服务端一旦收到客户端的确认报文就进入ESTABLISHED状态可以进行读写数据了
客户端
[CLOSED - SYN_SENT] 客户端调用connect发送同步报文段[SYN_SENT - ESTABLISHED] connect调用成功则进入ESTABLISHED状态开始读写数据
2.9.2 四次挥手
TCP通信结束之后就需要断开连接断开连接的这个过程我们称之为四次挥手。
四次挥手的过程如下
第一次挥手客户端向服务器发送一个FIN报文表示客户端不再发送数据。第二次挥手服务器接收到客户端的FIN报文后向客户端发送一个ACK报文表示已经收到客户端的断开请求。第三次挥手服务器向客户端发送一个FIN报文表示服务器也不再发送数据。第四次挥手客户端接收到服务器的FIN报文后向服务器发送一个ACK报文表示已经收到服务器的断开请求。
这样双方都确认对方已经断开连接完成四次挥手后TCP连接就彻底关闭。 为什么要四次挥手 TCP是全双工的通信协议建立连接时需要双方都确认建立连接而断开连接时需要双方都确认断开连接。因此四次挥手的过程是必要的。即断开连接需要征得双方的同意每两次挥手对应就是关闭一个方向的通信信道因此断开连接时需要进行四次挥手 四次挥手也可能变成三次挥手原因如下 第二次挥手服务器接收到客户端的FIN报文后向客户端发送一个ACK报文表示已经收到客户端的断开请求。第三次挥手服务器向客户端发送一个FIN报文表示服务器也不再发送数据这两个报文可能会合在一起发即ACKFIN因为FIN和ACK都是不同的标志位不会影响双方 四次挥手时的状态变化 客户端状态转化
[FIN_WAIT_1 - FIN_WAIT_2] 客户端收到服务器对结束报文段的确认则进入FIN_WAIT_2开始等待服务器的结束报文段[FIN_WAIT_2 - TIME_WAIT] 客户端收到服务器发来的结束报文段进入TIME_WAIT, 并发出LAST_ACK[TIME_WAIT - CLOSED] 客户端要等待一个2MSL(Max Segment Life, 报文最大生存时间)的时间才会进入CLOSED状态
服务端状态转化
[ESTABLISHED - CLOSE_WAIT] 当客户端主动关闭连接(调用close)服务器会收到结束报文段服务器返回确认报文段并进入CLOSE_WAIT[CLOSE_WAIT - LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据)当服务器真正调用close关闭连接时会向客户端发送FIN此时服务器进入LAST_ACK状态等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)[LAST_ACK - CLOSED] 服务器收到了对FIN的ACK彻底关闭连接
注触发四次挥手是上层双方调用close(sock) 主动断开连接的一方最终状态是TIME_WAIT被动断开连接的一方两次挥手完成进入CLOSE_WAIT 下面进行做实验查看这两个状态
2.9.3 演示查看TIME_WAIT和CLOSE_WAIT状态
代码直接采用socket套接字TCP多线程版的前面已经讲解过了就不再解释
初始化服务器initServer函数步骤大致如下
调用socket函数创建套接字。调用bind函数为服务端绑定一个端口号调用listen函数将套接字设置为监听状态
启动服务器start函数步骤大致如下
调用accept函数获取新链接为客户端提供服务
服务端提供的服务什么也不做等待20秒服务端就直接退出即可我们在这20秒内操作操作就晕在客户端连接好了之后客户端在20秒内主动退出即可
即演示的目的效果是
客户端主动退出最终状态是TIME_WAIT服务端是被动断开连接的一方被动断开连接的一方两次挥手完成进入CLOSE_WAIT tcpServer.hpp #pragma once#include iostream
#include string
#include strings.h
#include unistd.h
#include pthread.h
#include sys/types.h
#include sys/socket.h
#include arpa/inet.husing namespace std;static const int gbacklog 5;// 错误类型枚举
enum
{UAGE_ERR 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR
};class tcpServer; // 声明
class ThreadDate
{
public:ThreadDate(tcpServer *self, int sockfd): _self(self), _sockfd(sockfd){}public:tcpServer *_self;int _sockfd;
};class tcpServer
{
public:tcpServer(const uint16_t port): _listensock(-1), _port(port){}// 初始化服务器void initServer(){// 1.创建套接字_listensock socket(AF_INET, SOCK_STREAM, 0);if (_listensock -1){cout create socket error endl;exit(SOCKET_ERR);}// 2.绑定端口// 2.1 填充 sockaddr_in 结构体struct sockaddr_in local;bzero(local, sizeof(local)); // 把 sockaddr_in结构体全部初始化为0local.sin_family AF_INET; // 未来通信采用的是网络通信local.sin_port htons(_port); // htons(_port)主机字节序转网络字节序local.sin_addr.s_addr INADDR_ANY; // INADDR_ANY 就是 0x00000000// 2.2 绑定int n bind(_listensock, (struct sockaddr *)local, sizeof(local)); // 需要强转(struct sockaddr*)localif (n -1){cout bind socket error endl;exit(BIND_ERR);}// 3. 把_listensock套接字设置为监听状态if (listen(_listensock, gbacklog) -1){cout listen socket error endl;exit(LISTEN_ERR);}}// 启动服务器void start(){for (;;){// 4. 获取新链接accept从_listensock套接字里面获取新链接struct sockaddr_in peer;socklen_t len sizeof(peer);// 这里的sockfd才是真正为客户端请求服务int sockfd accept(_listensock, (struct sockaddr *)peer, len);if (sockfd 0) // 获取新链接失败但不会影响服务端运行{cout accept error next endl;continue;}cout accept a new line success, sockfd: sockfd endl;// 5. 为sockfd提供服务即为客户端提供服务// 多线程版pthread_t tid;ThreadDate *td new ThreadDate(this, sockfd);pthread_create(tid, nullptr, threadRoutine, td);}}static void *threadRoutine(void *args){pthread_detach(pthread_self()); // 线程分离ThreadDate *td static_castThreadDate *(args);td-_self-serviceIo(td-_sockfd);close(td-_sockfd); // 必须关闭由新线程关闭delete td;return nullptr;}// 提供服务void serviceIo(int sockfd){sleep(20); // 20秒之后线程关闭_sockfd线程也退出}~tcpServer(){}private:int _listensock; // listen套接字不是用来数据通信的是用来监听链接到来uint16_t _port; // 端口号
};tcpServer.cc #include tcpServer.hpp
#include memory// 使用手册
// ./tcpServer port
static void Uage(string proc)
{cout \nUage:\n\t proc local_port\n\n;
}int main(int argc, char *argv[])
{if (argc ! 2){Uage(argv[0]);exit(UAGE_ERR);}uint16_t port atoi(argv[1]); // string to intunique_ptrtcpServer tsvr(new tcpServer(port));tsvr-initServer(); // 初始化服务器tsvr-start(); // 启动服务器return 0;
}下面需要使用telnet命令先介绍该命令
2.9.4 telnet命令 telnet命令 Telnet是一种用于远程登录和管理网络设备的协议同时也可以用于测试网络连接和端口的连通性。Telnet客户端可以通过命令行或者图形界面进行操作。
在命令行中可以使用telnet命令来连接到远程主机或者测试网络连接。以下是使用telnet命令的一些常见用法
1、连接到远程主机
telnet hostname [port]是要连接的远程主机的域名或者IP地址[port]是要连接的端口默认为23Telnet默认端口
例如要连接到主机example.com的Telnet服务可以使用以下命令
telnet example.com2、测试端口连通性
telnet hostname porthostname是要测试的主机的域名或者IP地址port是要测试的端口号
例如要测试主机example.com的80端口是否连通可以使用以下命令
telnet example.com 802、退出Telnet会话
在Telnet会话中可以使用以下命令退出
quit或者按下Ctrl]然后输入quit。
4、安装
如果Linux没有安装先安装telnet客户端
yum install -y telnet注普通用户需要sudo提权
2.9.5 演示
先运行服务端然后使用telnet命令连接服务端
注由于没有多余的机器只在一台机器下测试
打循环查看tcpServer查看服务端的
while : ;do netstat -natp | grep tcpServer; sleep 1; echo -----------------; done打循环查看telnet查看客户端
while : ;do netstat -natp | grep telnet; sleep 1; echo -----------------; done打循环查看TIME_WAIT查看客户端
while : ;do netstat -natp | grep TIME_WAIT; sleep 1; echo -----------------; done打循环查看TIME_WAIT是因为telnet退出后查不到该进程了
准备工作完成先运行循环再启动服务端再进行telnet客户端要在20秒内退出
0、运行循环
1、启动服务端 2、telnet 3、telnet在20秒内退出连接 服务端关闭sock并退出 客户端退出后可以查到客户端的TIME_WAIT状态 上述是我演示的是四次挥手的过程 主动断开连接的一方最终状态是TIME_WAIT被动断开连接的一方两次挥手完成进入CLOSE_WAIT TIME_WAIT状态持续一段时间后才进入真正的关闭 TIME_WAIT的等待时长是多少 太长的TIME_WAIT状态会导致等待方维持连接的成本增加浪费资源。太短的TIME_WAIT状态可能无法保证ACK被对方接收数据在网络中消散。TCP协议规定主动关闭连接的一方在四次挥手后要进入TIME_WAIT状态等待两个MSL的时间才能进入CLOSED状态。这样可以确保连接的可靠关闭。
查看Linux的MSL时间长度
cat /proc/sys/net/ipv4/tcp_fin_timeoutCentos7上默认配置的值是60秒可以修改 为什么是TIME_WAIT的时间是2MSL? MSL是TCP报文的最大生存时间, 因此TIME_WAIT持续存在2*MSL的话就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启可能会收到来自上一个进程的迟到的数据但是这种数据很可能是错误的)同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失那么服务器会再重发一个FIN。这时虽然客户端的进程不在了但是TCP连接还在仍然可以重发LAST_ACK) 如果服务器出现了大量的CLOSE_WAIT状态说明服务器 服务器有bug没有做关闭文件符操作close(sock)服务器有压力可能服务端一直推送消息给客户端导致来不及close文件描述符
2.10 解决TIME_WAIT状态引起的bind失败的问题 绑定失败现象 有客户端连着服务端服务端主动退出需要进行重启紧接着服务端再次启动绑定相同的端口就会出现绑定失败的现象 绑定失败的原因 服务端主动断开连接服务端退出最终状态是TIME_WAITTIME_WAIT持续存在2*MSL即TIME_WAIT状态需要等待两个MSL的时间才能进入CLOSED状态在2*MSL时间内服务器绑定的端口还一直被占用即服务端立马退出再进行重启这时候就会存在绑定端口失败的问题 现象演示 代码依旧是上面的运行服务器客户端进行连接然后服务端主动退出再进行重启就会出现绑定端口失败 绑定端口失败的危害 比如在某些场景下618双11这时候服务器的压力会比较大如果说服务器崩溃了需要立即马上进行重启并且能够立马重启成功如果在一分钟内或两分钟内无法重启成功就会造成巨大的损失金钱618双11都是分秒必争的 解决方法 使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1表示允许创建端口号相同但IP地址不同的多个socket描述符
// 设置地址复用
int opt 1;
setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt));修改代码 在创建套接字后面设置即可 编译运行再进行测试bind绑定失败问题没有了
2.11 流量控制
这个在前面的16位窗口大小已经谈过一部分了上一篇这里再来详细介绍。
接收端处理数据的速度是有限的如果发送端发的太快导致接收端的缓冲区被打满这个时候如果发送端继续发送就会造成丢包继而引起丢包重传等等一系列连锁反应
因此TCP支持根据接收端的处理能力来决定发送端的发送速度这个机制就叫做流量控制(Flow Control)
接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段通过ACK端通知发送端窗口大小字段越大说明网络的吞吐量越高接收端一旦发现自己的缓冲区快满了就会将窗口大小设置成一个更小的值通知给发送端发送端接受到这个窗口之后就会减慢自己的发送速度如果接收端缓冲区满了就会将窗口置为0这时发送方不再发送数据但是需要定期发送一个窗口探测数据段使接收端把窗口大小告诉发送端 16位窗口大小16位最大表示65535那TCP窗口最大就是65535吗 理论上确实是这样的但实际上TCP报头当中40字节的选项字段中包含了一个窗口扩大因子M实际窗口大小是窗口字段的值左移M位得到的 第一次向对方发送数据时如何得知对方的窗口大小 双方在进行TCP通信之前需要进行三次握手建立连接。在握手过程中除了验证通信信道是否通畅双方还会交换其他信息其中包括告知对方自己的接收能力。这样在双方正式开始通信之前双方已经了解对方的接收数据能力。因此双方在发送数据时可以根据对方的接收能力进行调整避免缓冲区溢出的问题。
2.12 滑动窗口
前面已经提到过TCP的工作模式了上一篇TCP的工作模式有两种 第一种串行发送数据不是TCP真正的工作模式 一发一收的方式性能较低串行 第二种并行发送数据TCP真正的工作模式 并行发送数据可以大大的提高性能其实是将多个段的等待时间重叠在一起了 第二种情况是TCP的真正工作模式即主流但是也会存在第一种工作模式第一种情况是很少的但也会存在
发送方可以一次发送多个报文给对方此时也就意味着发送出去的这部分报文当中有相当一部分数据是暂时没有收到应答的
发送缓冲区当中的数据可以分为三部分
已经发送并且已经收到ACK的数据。已经发送还但没有收到ACK的数据。还没有发送的数据。还有一个就是剩余空间即没有数据的空间这个剩余空间可能有也可能没有
发送缓冲区的第二部分就叫做滑动窗口 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值
下图的窗口大小就是4000个字节(四个段)发送前四个段的时候不需要等待任何ACK直接发送 收到第一个ACK后滑动窗口向后移动继续发送下一个段的数据依次类推操作系统内核为了维护这个滑动窗口需要开辟发送缓冲区来记录当前还有哪些数据没有应答只有确认应答过的数据才能从缓冲区删掉窗口越大, 则网络的吞吐率就越高 例如 1-1000、1001-2000数据接收端已经收到了并且进行了ACK则1-1000、1001-2000的数据不在发送缓冲区的滑动窗口里面了现在连续发送2001-3000、3001-4000、4001-5000、5001-6001的四个段数据此时不需要等待任何ACK可以直接进行发送 当收到对方应答的确认序号为3001时说明2001-3000这个数据段已经被对方收到了此时该数据段应该被归入发送缓冲区当中的已发送已收到ACK部分而由于我们假设对方的窗口大小一直是4000因此滑动窗口现在可以向右移动继续发送6001-7000的数据段以此类推 TCP的重传机制要求暂时保存发出但未收到确认的数据这些数据实际上位于滑动窗口中。滑动窗口的左侧是已经被对方可靠接收到的数据因此只有滑动窗口左侧的数据可以被覆盖或删除。滑动窗口除了限定可以直接发送的数据还支持TCP的重传机制即当发送的数据未收到确认时可以重新发送滑动窗口中的数据。这样可以确保数据的可靠传输提高通信的可靠性和效率。 发送缓冲区建模1 缓冲区的本质就是一个char类型的数组而滑动窗口就在发送缓冲区内所以滑动窗口本质也是一个char类型的数组滑动窗口可以被看作是由两个指针限定的一个范围指向数组下标。比如我们可以用win_start指针指向滑动窗口的左侧win_end指针指向滑动窗口的右侧。在win_start和win_end之间的区间范围内的数据可以被称为滑动窗口。通过移动这两个指针可以实现滑动窗口的滑动和调整大小以适应不同的网络条件和接收方的接收能力。这样的设计可以有效地控制发送方的发送速率并支持TCP的重传机制以确保数据的可靠传输。 当发送端收到对方的ACK应答时如果响应当中的确认序号为xxx窗口大小为win此时就可以将win_start更新为xxx而将win_end更新为win_startwin暂时这样理解下面再详细解释 滑动窗口大小是怎么设定的未来怎么变化 滑动窗口的大小是根据网络条件和接收方的接收能力来设定的。通常滑动窗口的大小由发送方和接收方之间的协商来确定。在建立TCP连接时双方会通过握手过程交换窗口大小的信息。发送方通常会根据接收方的通知来设定初始的滑动窗口大小。这个初始大小可以是固定的值也可以是根据网络条件动态调整的值。未来滑动窗口的大小可以根据网络条件和接收方的反馈信息进行动态变化。TCP协议中有一种叫做拥塞控制的机制可以根据网络拥塞的程度来调整滑动窗口的大小。当网络拥塞时发送方会减小滑动窗口的大小以降低发送速率从而减轻网络负载。当网络状况改善时发送方可以增大滑动窗口的大小以提高发送速率。拥塞控制下面谈
总之滑动窗口的大小是根据网络条件和接收方的接收能力来动态设定和调整的以实现更高效的数据传输和网络拥塞控制。 16位窗口大小与滑动窗口 16位窗口大小指的是TCP协议中的窗口字段的大小它用16位二进制数来表示。窗口字段表示接收方还能接收多少字节的数据用于控制发送方的发送速率。滑动窗口是一种数据传输的机制用于控制发送方发送数据的速率和接收方接收数据的能力。滑动窗口的大小可以根据网络条件和接收方的接收能力进行调整。滑动窗口的大小可以与16位窗口大小相关联。发送方在发送数据时会根据接收方的窗口大小来确定发送窗口的大小。如果接收方的16位窗口大小为N发送方可以将发送窗口的大小设置为N以确保发送的数据不会超过接收方的窗口大小。当发送方发送了一段数据后接收方会发送确认消息ACK给发送方同时更新窗口字段的值。发送方根据接收方的窗口字段的值来调整发送窗口的大小以控制发送的数据量。
总之16位窗口大小与滑动窗口的关系是发送方根据接收方的16位窗口大小来确定发送窗口的大小并根据接收方的确认消息来动态调整发送窗口的大小以实现数据的可靠传输和流量控制。 滑动窗口会向左滑动吗滑动窗口整体一定会向右滑动么 一定不会向左滑动滑动窗口左边的数据一定是已经发送了并且是对方已经收到了在正常情况下滑动窗口整体是向右滑动的即发送方不断发送新的数据接收方不断确认收到数据并将窗口向右滑动。这样可以实现高效的数据传输。然而在某些特殊情况下滑动窗口整体可能会不滑动滑动窗口还可能一直变小。比如对方的接收缓冲区的上层不取数据缓冲区慢慢会被打满此时滑动窗口的大小也慢慢变小并且整体不会向右滑动 滑动窗口大小会一直不变吗会变小吗会变大吗 滑动窗口的大小是根据网络条件和接收方的接收能力来动态设定和调整的即一直会发生变化也可能不变不会是一直不变如上面例子确认3001、4001滑动窗口可能会变小上面的例子确认5001、6001最小就是变为0滑动窗口可能会变大比如对方的接收缓冲区的上层一下子读取了完了全部的数据此时接收缓冲区的接收能力就会变大滑动窗口也随之变大提高发送数据的速率 丢包问题 当发送端一次发送多个报文数据时此时的丢包情况也可以分为两种。
情况一 数据包已经抵达ACK丢包。
部分ACK丢包并不要紧此时可以通过后续的ACK进行确认在这种情况下由于接收方的ACK对于2001-3000和4001-5000的数据包丢失了发送方无法直接知道这些数据包是否已经成功到达。但是发送方可以通过接收到的最后一个有效ACK5001-6000数据包的确认来推断一些信息。TCP协议的规定收到一个确认ACK序号为6001的确认消息表示接收方成功接收了序号为1-6000的字节数据。因此发送方可以推测在丢失的这一段2001-3000和4001-5000之前的数据包也应该已经到达了接收方。 情况二 数据包真的丢了。
当发送端连续收到三次确认序号为1001的响应报文即接收方对于1001-2000的数据包的确认被重复确认了三次发送端会认为这些数据包丢失了。TCP协议的重传机制当发送端连续收到三次相同的确认序号时会触发快速重传。在这种情况下发送端会立即重传1001-2000的数据包以确保接收方能够正确接收到这些数据。通过快速重传发送端可以快速恢复丢失的数据包提高数据的可靠性和传输效率。因此在这个特殊情况下滑动窗口的整体位置不会发生变化但发送端会根据丢失的数据包触发快速重传机制重新发送丢失的数据包。 TCP协议的重传机制当发送端连续收到三次相同的确认序号时触发重传机制会触发高速重发控制也叫快重传 发送缓冲区建模2如果滑动窗口一直向后滑动空间大小不够了怎么办 发送缓冲区被内核组织称为了一种环形结构本质依旧是插入类型的线性数组通过数据下标进行模运算实现环形结构怎么滑动也不会发生越界问题 快重传 VS 超时重传 快速重传和超时重传是TCP协议中两种常用的重传机制用于处理丢失的数据包。它们的主要区别在于触发重传的条件和重传的时机。快速重传Fast Retransmit是指当发送方连续收到三个重复的确认序号时即接收方对同一个数据包的确认被重复确认了三次发送方会立即重传该数据包。这是因为连续收到重复确认序号通常意味着该数据包已经丢失了为了快速恢复丢失的数据包发送方会触发快速重传机制立即重传该数据包而不必等待超时重传。超时重传Timeout Retransmission是指当发送方发送一个数据包后等待一段时间超时时间后仍未收到对应的确认消息时发送方会认为该数据包丢失了会触发超时重传机制重新发送该数据包。超时时间是根据网络状况和往返时间动态调整的如果网络延迟较高或丢包较多超时时间会相应增加。快速重传和超时重传都是为了处理丢失的数据包但触发条件和重传时机不同。快速重传是基于连续收到重复确认序号触发的可以更快地恢复丢失的数据包减少了等待超时的时间。而超时重传是基于超时时间触发的适用于网络延迟较高或丢包较多的情况。
综上所述快速重传和超时重传是TCP协议中两种常用的重传机制根据不同的情况选择合适的重传策略以提高数据的可靠传输性能。 以上话题都是端到端客户端到服务端服务端到客户端没有考虑有网络的TCP也有网络问题方面的机制控制就是拥塞控制。
2.13 拥塞控制 为什么会有拥塞控制 两个主机在进行TCP通信的过程中偶尔出现个别数据包丢失的情况是很正常的此时可以通过快速重传或超时重传来补发丢失的数据包。TCP认为是自己的问题但如果双方在通信时出现大量数据包丢失的情况这就不再是正常现象了。TCP认为不是自己的问题
举个例子
比如高数考试一个班有30人考试下来全班只有张三一个人挂了张三认为这是自己的问题那如果考试下来全班只有张三一个人通过了考试其他全挂了这时其他人认为不是自己的问题也不是张三的问题而是试卷的问题同理TCP也是如此比如客户端发送了10000个报文服务端收到了9999个只丢了一个报文TCP认为这是自己的问题如果服务端只收到了10个丢了9990个报文这时TCP就不会再认为是自己的问题即不会进行重传报文而是网络的问题
所以TCP不仅考虑了通信双端主机的问题同时也考虑了网络的问题。
拥塞窗口考虑双方通信时网络的问题如果发送的数据超过了拥塞窗口的大小可能引起网络拥塞。在双方网络通信时偶尔出现少量的丢包是允许的但一旦出现大量的丢包这就是量变引起质变此时TCP不再假设是双方接收和发送数据的问题而是判断双方通信信道网络出现了拥塞问题。网络拥塞是指在计算机网络中当网络中的数据流量超过网络链路或节点的处理能力时导致网络性能下降、延迟增加、丢包率增加等现象。网络拥塞通常发生在网络的瓶颈点即网络中的某些关键节点或链路无法处理大量的数据流量。 从另一个视角看待 如果出现了网络拥塞问题网络拥塞不仅仅影响单个主机几乎会影响网络中的所有主机其他主机也是会发生大量丢包。所以出现了网络拥塞之后双方主机可以减少数据传输的速率尽量少发数据甚至不发数据等待网络状况恢复后再逐渐恢复数据传输速率减少网络负担如果出现了网络拥塞双发出现了大量的丢包双方的主机还进行重传报文的只会给网络雪上加霜网络上其他主机也是如此雪崩的时候没有一片雪花是无辜的因此所有使用TCP传输控制协议的主机都需要执行拥塞避免算法拥塞控制以统一减少发送窗口的大小从而避免网络拥塞问题的进一步恶化和传播。因此拥塞控制虽然看似只是针对单个主机的通信策略但实际上是所有主机在网络崩溃后都应该遵守的策略。当网络发生拥塞时所有主机都需要执行拥塞避免算法这样才能有效地缓解网络拥塞问题防止雪崩效应的发生并尽快恢复网络的正常运行。 拥塞控制 虽然TCP有了滑动窗口这个大杀器能够高效可靠的发送大量的数据。但是如果在刚开始阶段就发送大量的数据仍然可能引发问题
因为网络上有很多的计算机可能当前的网络状态就已经比较拥堵在不清楚当前网络状态下贸然发送大量的数据是很有可能引起雪上加霜
TCP引入 慢启动机制先发少量的数据探探路摸清当前的网络拥堵状态再决定按照多大的速度传输数据 拥塞窗口
发送开始的时候定义拥塞窗口大小为1每次收到一个ACK应答拥塞窗口加1 每次发送数据包的时候将拥塞窗口和接收端主机反馈的窗口大小做比较取较小的值作为实际发送的窗口
即自己的滑动窗口的大小 min(拥塞窗口对端窗口大小)两者取较小值
滑动窗口自己的拥塞窗口网络的窗口大小对端的对端的接收能力
像上面这样的拥塞窗口增长速度是指数级别的 “慢启动” 只是指初使时慢但是增长速度非常快
为了不增长的那么快因此不能使拥塞窗口单纯的加倍此处引入一个叫做慢启动的阈值当拥塞窗口超过这个阈值的时候不再按照指数方式增长而是按照线性方式增长当TCP开始启动的时候慢启动阈值等于对端窗口的最大值在每次超时重发的时候慢启动阈值会变成原来的一半同时拥塞窗口置回1 指数增长。刚开始进行TCP通信时拥塞窗口的值为1并不断按指数的方式进行增长加法增大。慢启动的阈值初始时为对方窗口大小的最大值图中慢启动阈值的初始值为16因此当拥塞窗口的值增大到16时就不再按指数形式增长了而变成了的线性增长。乘法减小。拥塞窗口在线性增长的过程中在增大到24时如果发生了网络拥塞此时慢启动的阈值将变为当前拥塞窗口的一半也就是12并且拥塞窗口的值被重新设置为1所以下一次拥塞窗口由指数增长变为线性增长时拥塞窗口的值应该是12。
少量的丢包我们仅仅是触发超时重传大量的丢包我们就认为网络拥塞。
当TCP通信开始后网络吞吐量会逐渐上升随着网络发生拥堵吞吐量会立刻下降。
拥塞控制归根结底是TCP协议想尽可能快的把数据传输给对方但是又要避免给网络造成太大压力的折中方案。拥塞控制也是为了保证可靠性和传输速率
2.14 延迟应答
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小 如果接收数据的主机收到数据后立即进行ACK应答此时返回的窗口可能比较小。
假设对方接收端缓冲区剩余空间大小为1M对方一次收到500K的数据后如果立即进行ACK应答此时返回的窗口就是500K。但实际接收端处理数据的速度很快10ms之内就将接收缓冲区中500K的数据消费掉了。在这种情况下接收端处理还远没有达到自己的极限即使窗口再放大一些也能处理过来。如果接收端稍微等一会再进行ACK应答比如等待200ms再应答那么这时返回的窗口大小就是1M。
延迟应答的目的不是为了保证可靠性而是为了提高数据的传输效率。保证在网络不拥塞的情况下尽量提高传输效率 那么所有的报文都可以延迟应答么? 答案肯定也不是延迟应答会有以下两个限制
数量限制每隔N个包就应答一次时间限制超过最大延迟时间就应答一次不能比超时重传的时间长
具体的数量和超时时间依操作系统不同也有差异一般N取2超时时间取200ms
2.15 捎带应答
主机A给主机B发送了一条消息当主机B收到这条消息后需要对其进行ACK应答但如果主机B此时正好也要给主机A发生消息此时这个ACK就可以搭顺风车而不用单独发送一个ACK应答此时主机B发送的这个报文既发送了数据又完成了对收到数据的响应这就是捎带应答捎带应答也是为了提高传输效率 TCP连接不直接保证可靠性但是会间接保证可靠性 TCP连接确实不直接保证可靠性但它通过一系列机制间接保证了数据的可靠传输。比如序号和确认机制、滑动窗口、拥塞控制、流量控制等这些机制是直接保障可靠性这些机制建立的基础是已经连接已经建立成功所以TCP连接确实不直接保证可靠性而是间接保证可靠性 以上便是TCP所有的策略
2.16 面向字节流
当创建一个TCP的socket时同时在内核中会创建一个发送缓冲区和一个接收缓冲区。
调用write函数就可以将数据写入发送缓冲区中此时write函数就可以进行返回了接下来发送缓冲区当中的数据就是由TCP自行进行发送的。如果发送的字节数太长TCP会将其拆分成多个数据包发出。如果发送的字节数太短TCP可能会先将其留在发送缓冲区当中等到合适的时机再进行发送。接收数据的时候数据也是从网卡驱动程序到达内核的接收缓冲区可以通过调用read函数来读取接收缓冲区当中的数据。而调用read函数读取接收缓冲区中的数据时也可以按任意字节数进行读取。
这个缓冲区在前面已经详细谈过了这里就不展开说了 由于缓冲区的存在TCP程序的读和写不需要一一匹配面向字节流例如 写100个字节数据时可以调用一次write写100字节也可以调用100次write每次写一个字节。 读100个字节数据时也完全不需要考虑写的时候是怎么写的既可以一次read100个字节也可以一次read一个字节重复100次。 于TCP来说它并不关心发送缓冲区当中的是什么数据在TCP看来这些只是一个个的字节数据它的任务就是将这些数据准确无误的发送到对方的接收缓冲区当中就行了而至于如何解释这些数据完全由上层应用来决定这就叫做面向字节流 比对面向数据报 应用层交付给UDP多长的报文UDP就原样发送既不会拆分也不会合并这就叫做面向数据报比如用UDP传输100个字节的数据发送端调用一次发送函数发送100字节那么接收端也必须调用对应的一次接收函数接收100个字节如果发送端调用十次发送函数则接收端也必须调用对应的十次接收函数即UDP协议发送函数的次数 : 接收函数的次数 1 : 1
2.17 粘包问题 什么是粘包基于TCP的应用层问题 首先要明确粘包问题中的“包”是指的应用层的数据包。在TCP的协议头中没有如同UDP一样的“报文长度”这样的字段。站在传输层的角度TCP是一个一个报文过来的按照序号排好序放在缓冲区中。但站在应用层的角度看到的只是一串连续的字节数据。那么应用程序看到了这么一连串的字节数据就不知道从哪个部分开始到哪个部分是一个完整的应用层数据包。 如何解决粘包问题 要解决粘包问题本质就是要明确报文和报文之间的边界。
对于定长的包保证每次都按固定大小读取即可。对于变长的包可以在报头的位置约定一个包总长度的字段从而就知道了包的结束位置。对于变长的包还可以在包和包之间使用明确的分隔符。因为应用层协议是程序员自己来定的只要保证分隔符不和正文冲突即可。 对于UDP协议来说是否也存在 “粘包问题” ? 对于UDP如果还没有上层交付数据UDP的报文长度仍然在同时UDP是一个一个把数据交付给应用层的有很明确的数据边界。站在应用层的角度使用UDP的时候要么收到完整的UDP报文要么不收不会出现“半个”的情况。
因此UDP是不存在粘包问题的根本原因就是UDP报头当中的16位UDP长度记录的UDP报文的长度因此UDP在底层的时候就把报文和报文之间的边界明确了而TCP存在粘包问题就是因为TCP是面向字节流的TCP报文之间没有明确的边界。
2.18 TCP异常情况 1进程终止 当客户端与服务端已经建立好连接了如果客户端进程突然终止此时建立好的连接会怎么样
当一个进程退出时该进程曾经打开的文件描述符都会自动关闭因此当客户端进程退出时相当于自动调用了close函数关闭了对应的文件描述符此时双方操作系统在底层会正常完成四次挥手然后释放对应的连接资源。也就是说进程终止时会释放文件描述符TCP底层仍然可以发送FIN和进程正常退出没有区别。 2机器重启 当客户端与服务端已经建立好连接了
选择重启主机时操作系统会先杀掉所有进程然后再进行关机重启因此机器重启和进程终止的情况是一样的此时双方操作系统也会正常完成四次挥手然后释放对应的连接资源。 3机器掉电断电源/网线断开 当客户端与服务端已经建立好连接了一端突然断电或断网了
比如是客户端断电或断网后服务器端在短时间内无法知道客户端掉线了因此在服务器端会维持与客户端建立的连接但这个连接也不会一直维持因为TCP是有保活策略的。
服务器会定期发送保活探测报文给客户端以检测客户端的存在状况。如果连续多次都没有收到客户端的应答服务器就会认为客户端已经掉线并关闭这条连接。此外客户端也可以定期向服务器发送心跳消息以确保服务器知道自己的存在。如果服务器长时间没有收到客户端的心跳消息也会认为客户端已经掉线并关闭对应的连接。综上所述TCP通过保活探测和心跳消息机制间接地检测客户端的在线状态并在一定时间内关闭掉线的连接以保证连接的有效性和资源的合理利用。
此外应用层的某些协议也有一些类似的检测机制例如基于长连接的HTTP也会定期检测对方的存在状态。
2.19 TCP小结 为什么TCP这么复杂? 因为要保证可靠性同时又尽可能的提高性能 可靠性 检验和序列号确认应答超时重传连接管理流量控制拥塞控制 提高性能 滑动窗口快速重传延迟应答捎带应答
除此之外还有一些定时器(超时重传定时器, 保活定时器, TIME_WAIT定时器等)
2.20 基于TCP应用层协议
常见的基于TCP的应用层协议如下
HTTP超文本传输协议HTTPS安全数据传输协议SSH安全外壳协议Telnet远程终端协议FTP文件传输协议SMTP电子邮件传输协议
当然也包括你自己写TCP程序时自定义的应用层协议
2.21 TCP/UDP对比
TCP是可靠连接, 那么是不是TCP一定就优于UDP呢?
TCP和UDP之间的优点和缺点不能简单绝对的进行比较不存在谁好谁不好的问题他们只是应用场景不同
TCP用于可靠传输的情况应用于文件传输重要状态更新等场景UDP用于对高速传输和实时性要求较高的通信领域。例如早期的QQ视频传输等另外UDP可以用于广播
归根结底TCP和UDP都是程序员的工具什么时机用具体怎么用还是要根据具体的需求场景去判定 如何用UDP实现可靠传输 参考TCP的可靠性机制在应用层实现类似的逻辑例如
引入序列号, 保证数据顺序引入确认应答确保对端收到了数据引入超时重传如果隔一段时间没有应答就重发数据…
三、TCP实验理解listen的第二个参数
listen函数的作用是设置套接字为监听状态该函数的第二个参数之前没有谈现在来谈一下
第二个参数backlog全连接队列的最大长度。
如果有多个客户端同时发来连接请求此时未被服务器处理的连接就会放入连接队列该参数代表的就是这个全连接队列的最大长度一般不能设置太大
下面进行做实验
该实验不进行accept获取_listensock套接字新连接什么也不干只进行监听连接的到来backlog设置为2 tcpServer.hpp #pragma once#include iostream
#include string
#include strings.h
#include unistd.h
#include pthread.h
#include sys/types.h
#include sys/socket.h
#include arpa/inet.husing namespace std;static const int gbacklog 2; // 全连接队列大小// 错误类型枚举
enum
{UAGE_ERR 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR
};
class tcpServer
{
public:tcpServer(const uint16_t port): _listensock(-1), _port(port){}// 初始化服务器void initServer(){// 1.创建套接字_listensock socket(AF_INET, SOCK_STREAM, 0);if (_listensock -1){cout create socket error endl;exit(SOCKET_ERR);}// 1.1 设置地址复用int opt 1;setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt));// 2.绑定端口// 2.1 填充 sockaddr_in 结构体struct sockaddr_in local;bzero(local, sizeof(local)); // 把 sockaddr_in结构体全部初始化为0local.sin_family AF_INET; // 未来通信采用的是网络通信local.sin_port htons(_port); // htons(_port)主机字节序转网络字节序local.sin_addr.s_addr INADDR_ANY; // INADDR_ANY 就是 0x00000000// 2.2 绑定int n bind(_listensock, (struct sockaddr *)local, sizeof(local)); // 需要强转(struct sockaddr*)localif (n -1){cout bind socket error endl;exit(BIND_ERR);}// 3. 把_listensock套接字设置为监听状态if (listen(_listensock, gbacklog) -1){cout listen socket error endl;exit(LISTEN_ERR);}}// 启动服务器void start(){for (;;){sleep(1); // 什么也不做不从_listensock套接字里面获取新连接}}~tcpServer(){}
private:int _listensock; // listen套接字不是用来数据通信的是用来监听链接到来uint16_t _port; // 端口号
};tcpServer.cc #include tcpServer.hpp
#include memory// 使用手册
// ./tcpServer port
static void Uage(string proc)
{cout \nUage:\n\t proc local_port\n\n;
}int main(int argc, char *argv[])
{if (argc ! 2){Uage(argv[0]);exit(UAGE_ERR);}uint16_t port atoi(argv[1]); // string to intunique_ptrtcpServer tsvr(new tcpServer(port));tsvr-initServer(); // 初始化服务器tsvr-start(); // 启动服务器return 0;
}编译运行服务器此时启动 3 个客户端同时连接服务器, 用 netstat 查看服务器状态, 一切正常 但是启动第四个客户端时, 发现服务器对于第四个连接的状态存在问题了 客户端状态正常, 但是服务器端出现了SYN_RECV状态, 而不是ESTABLISHED状态
这是因为, Linux内核协议栈为一个tcp连接管理使用两个队列
半连接队列用来保存处于SYN_SENT和SYN_RECV状态的请求全连接队列accpetd队列用来保存处established状态但是应用层没有调用accept取走的请求
这个全连接队列不能太长也不能没有
如果全连接队列过长会导致服务器资源的浪费。每个连接请求都会占用一定的内存和其他资源如果队列过长服务器可能无法及时处理所有的连接请求导致资源耗尽和服务质量下降相反如果全连接队列没有长度也会导致问题。如果服务器无法及时处理所有的连接请求就会导致连接请求被丢弃客户端无法建立连接。这会导致客户端的请求失败和连接超时影响用户体验和服务可用性。
而全连接队列的长度会受到 listen 第二个参数的影响
全连接队列满了的时候就无法继续让当前连接的状态进入 established 状态了这个全连接队列的长度通过上述实验可知全连接的长度是 listen 的第二个参数 1
上述实验我们设置的全连接队列大小是2前三次连接正常但是到了第四次连接的处于半链接队列处于了SYN_RECV状态 但是在客户端看来连接已经建立好了但是在服务端看来没有建立连接成功因为服务端对于第三次握手的ACK进行了忽略 TCP内容真多终于完结了TCP写了差不多三万字 --------------------- END ----------------------
「 作者 」 枫叶先生
「 更新 」 2023.7.30
「 声明 」 余之才疏学浅故所撰文疏漏难免或有谬误或不准确之处敬请读者批评指正。