TCP协议

承接上文 UDP协议_清风玉骨的博客-CSDN博客

本篇简介

TCP协议段格式样式

各个字段的初步介绍包括:

        4位首部长度

        32位序号和32位确认序号

        16位窗口大小

        6个标志位

机制包括:

        ACK确认应答机制

        超时重传机制

        连接管理机制

        流量控制

        拥塞控制

        延迟应答

        捎带应答

三个TCP延伸问题:

        如何理解面向字节流 

        粘包问题 

        TCP异常情况

TCP相关实验:

        理解 listen 的第二个参数

        使用 wireshark 分析 TCP 通信流程

补充:

        用UDP实现可靠传输(经典面试题)

        为什么要三次握手和四次挥手

        四次挥手各方的状态

        DDoS攻击

TCP协议

        TCP全称为 "传输控制协议(Transmission Control Protocol"). 人如其名, 要对数据的传输进行一个详细的控制

        为了更好的了解TCP协议在整个学习过程中,需要解决以下四个问题

1.认识TCP协议的报头——字段

2.如何封装解包,如何分用

3.如何理解TCP的报头

4.学习TCP可靠性(确认应答)&&提高传送效率

TCP协议段格式

TCP协议_第1张图片

字段1:4位首部长度(回答问题2)

TCP协议_第2张图片

至此在本文开头 问题2 已经在图中得到解决

补充1:tcp的报头里面,为什么没有有效载荷的长度

TCP协议_第3张图片

补充2:网络协议栈和文件是什么关系

即,进程是怎么读取到数据的

TCP协议_第4张图片

回答问题3:如何理解TCP报头

        报头其实就是一个结构化的数据,并且因为操作系统是C语言写的,所以添加报头的行为可以使用指针的形式去解决

TCP协议_第5张图片

理解问题4

TCP协议_第6张图片

补充3:确认应答机制

TCP协议_第7张图片

        TCP协议保证通信的可靠性,需要保证双方两个朝向的可靠性,双方在进行通信的时候,可能除了正常的数据段,通信时会覆盖确认数据段,即发送的消息带有确认历史消息的确认

        例如上图的 " 我吃了 ",就既充当了对历史消息的确认,同时也涵盖了对A问题的回答,这就是最基础的工作模式

TCP协议_第8张图片

其实第一种工作模式也存在,即A给B发消息,不需要B回消息,也就是那种一应一答式的

补充4:TCP双方的地位是对等的

        即不分服务端和客户端,这一点和应用层是不一样的,双方收到的都是数据段,是没有地位之分的,双方发送的数据段的性质都是一样的,大家都是传数据,为此我们才能更好的理解发送给对方的是什么

字段2:序号

32位序号和确认序号

TCP协议_第9张图片

补充5:为什么我们要有两组序号?

TCP协议_第10张图片

字段3:16位窗口大小

TCP协议_第11张图片

        这里填的是自己的窗口大小,因为我们所构建的所以TCP报文都是要给对方发送的!对C,S同样适用,一旦我们可以交换接收能力,那么我们就能够实现流量控制,实现合适的发送数据,至于这个16位窗口大小是怎么理解的在之后详谈,而这个反馈其实在一开始的三次握手就交换了信息了,之后再详细接收三次握手和四次分手

字段4:六个标志位(最后一个字段)

        我们需要知道的一点是,TCP报文是有类型的,比如带数据的,确认历史数据的,为此接收方对于这些不同的类型是需要有不同的反应、动作,为了区分数据的类型,我们就有了六个标志位

TCP协议_第12张图片

至此TCP报文字段介绍完毕!

补充6:标志位补充说明

带外数据如何设置

TCP协议_第13张图片

TCP协议_第14张图片

RST连接重置位

TCP协议_第15张图片

        关于选项其实如果有兴趣可以自行了解,这里面有很多关于TCP报头的设置,比如扩大大小都是有涉及的

TCP字段总结

源/目的端口号: 表示数据是从哪个进程来, 到哪个进程去;
32位序号/32位确认号: 下文详谈;
4位TCP报头长度: 表示该TCP头部有多少个32位bit(有多少个4字节); 所以TCP头部最大长度是15 * 4 = 60

6位标志位:
URG: 紧急指针是否有效
ACK: 确认号是否有效
PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段

16位窗口大小: 下文详谈
16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也包含TCP数据部分.
16位紧急指针: 标识哪部分数据是紧急数据;
40字节头部选项: 暂时忽略;
 

机制

ACK确认应答机制

TCP协议_第16张图片

TCP协议_第17张图片

TCP协议_第18张图片

我们缓冲区先看做是一个数组,这时候,每个字节,就天然有了序号

超时重传机制

主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B;
如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发;

 

TCP协议_第19张图片

TCP协议_第20张图片

TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间.

Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍.
如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接

连接管理机制

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

TCP协议_第21张图片

引入下文

详谈连接管理机制

TCP协议_第22张图片

为什么要三次握手?

TCP协议_第23张图片

为什么要四次挥手?

TCP协议_第24张图片

关于TCP可靠性需要注意的是,是基于三次握手间接保证可靠性的,并不是说TCP协议就是可靠的

tcp为什么要连接?因为要保证可靠性!

补充1:DDoS攻击 

TCP协议_第25张图片

四次挥手的各方状态

TCP协议_第26张图片

netstat -natp

TCP协议_第27张图片

服务端状态转化:

[CLOSED -> LISTEN] 服务器端调用listen后进入LISTEN状态, 等待客户端连接;
[LISTEN -> SYN_RCVD] 一旦监听到连接请求(同步报文段), 就将该连接放入内核等待队列中, 并向客户端发送SYN确认报文
[SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文, 就进入ESTABLISHED状态, 可以进行读写数据了.
[ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用close), 服务器会收到结束报文段, 服务器返回确认报文段并进入CLOSE_WAIT;
[CLOSE_WAIT -> LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用close关闭连接时, 会向客户端发送FIN, 此时服务器进入LAST_ACK状态, 等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)
[LAST_ACK -> CLOSED] 服务器收到了对FIN的ACK, 彻底关闭连接

客户端状态转化:

[CLOSED -> SYN_SENT] 客户端调用connect, 发送同步报文段;
[SYN_SENT -> ESTABLISHED] connect调用成功, 则进入ESTABLISHED状态, 开始读写数据;
[ESTABLISHED -> FIN_WAIT_1] 客户端主动调用close时, 向服务器发送结束报文段, 同时进入FIN_WAIT_1;
[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状态

TIME_WAIT维持时间

        四次挥手动作已经完成,但是主动断开连接的一方要维持一段时间的 time_wait,这是因为当最后一个断开连接的ACK可能会丢失,为了防止最后一个ACK不能被补发,所以得维持一段时间,而这个维持时间一般为多长?

        2*MSL,刚好是去向ACK消息最大存活时间(MSL) + 来向FIN消息的最大存活时间(MSL),就是服务端补发FIN然后客户端补发ACK的最长时间

        这就可以保证下面这两点

1.保证最后一个ACK尽可能的被对方收到

2.双方在断开连接的时候,网络中还有滞留的报文 -- 保证滞留报文进行消散 -- 教材给的理由

TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态.
我们使用Ctrl-C终止了server, 所以server是主动关闭连接的一方, 在TIME_WAIT期间仍然不能再次监听同样的server端口;
MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同, 在Centos7上默认配置的值是60s;
可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看msl的值;
 

这也和解决我们之前遗留下来的,无法短时间内绑定同一个端口号

        如果做了实验我们会发现四次挥手动作已经完成,但是主动断开连接的一方要维持一段时间的 time_wait,那么为什么呢?这就得下面介绍的内容有关了,这也和解决我们之前遗留下来的,无法短时间内绑定同一个端口号

为什么要重启?怎么解决?(短时间被绑定同一个端口号)

        就好比淘宝网站在双十一的时候,可能由于用户量访问过大,导致服务器需要重启,那么在这个时间段里面是无法重启的,必须得等60s(Centos7),那么肯定是不行的,为此我们需要解决这个问题,解决这个问题其实很简单

        使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符

TCP协议_第28张图片

TCP协议_第29张图片

本文中提到的代码参考文章:HTTP协议初识·中篇_清风玉骨的博客-CSDN博客

详谈滑动窗口

        我们讨论了确认应答策略, 对每一个发送的数据段, 都要给一个ACK确认应答. 收到ACK后再发送下一个数据段.这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候

TCP协议_第30张图片

        既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了),串行变并行,即使没有应答我们也可以继续向后发送消息

TCP协议_第31张图片

        窗口大小指的是无需等待确认应答而可以继续发送数据的最大值. 上图的窗口大小就是4000个字节(四个段).
        发送前四个段的时候, 不需要等待任何ACK, 直接发送;

        收到第一个ACK后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;
        操作系统内核为了维护这个滑动窗口, 需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答; 只有确认应答过的数据, 才能从缓冲区删掉;
        窗口越大, 则网络的吞吐率就越高;

TCP协议_第32张图片

以滑动的方式进行更新滑动窗口

TCP协议_第33张图片

以上是大致内容,下面我们进行深入了解

提出关于滑动窗口的几个问题

TCP协议_第34张图片

前三个问题的回答

TCP协议_第35张图片

后三个问题的回答

TCP协议_第36张图片

        如果我们发送数据,没有收到应答之前,我们必须将自己的已经发送的数据暂时保存起来,为了支持超时重传,通过上面的了解我们可以知道,这里的数据是被保存在发送缓冲区中,具体点就是滑动窗口之中

流量控制

        接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,就会造成丢包, 继而引起丢包重传等等一系列连锁反应.
        因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);

        接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "窗口大小" 字段, 通过ACK端通知发送端;
        窗口大小字段越大, 说明网络的吞吐量越高;
        接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
        发送端接受到这个窗口之后, 就会减慢自己的发送速度;
        如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端

TCP协议_第37张图片

        接收端如何把窗口大小告诉发送端呢? 回忆我们的TCP首部中, 有一个16位窗口字段, 就是存放了窗口大小信息;那么问题来了, 16位数字最大表示65535, 那么TCP窗口最大就是65535字节么?
实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是 窗口字段的值左移 M 位

拥塞控制

        有一个比喻,在一个考试中,学生门都去考试,假如考试的结果是大多数人都过了,其中几个人没用过,那么这几个人没有过就只能在自己身上找问题,但是如果大多数人都没有过,那么问题就出现在试卷之上,可以认为试卷没有出好

        同理在网络通信之中,假如一方给对方发送1000个报文,通常如果就只有几个报文丢失了,那么发送方就会触发重传机制,但是如果有很多个报文都丢失了,比如900多个都丢失了,那么显然是网络之中出现了问题,就不能单单用重传来解决,重传了还是大概率会继续丢失,并且还会加重网络故障,同时浪费了大量的资源,基于这一点,TCP就有了拥塞控制这一机制

        首先我们得有一个认知概念,这不是单独指一台主机(发送端),而是整个网络中的访问服务端(对端)的所有主机,这个时候一旦发送了网络故障,所有访问该主机的客户端都会发送丢包问题,假如所有主机都触发重传机制,那么就会进一步加深网络故障的问题,这时候就不能大量发送数据了(正常的网络通信为了提高效率都是大量数据进行发送了),一旦一台主机把数据发送量降低下来,因为TCP是大家都遵循的通信协议,所有的主机都会降低发送量,这时候网络中的数据量就会急剧下降,网络拥塞状态会得到大幅度的减缓,又因为网络有恢复的能力,所以网络故障问题就会得到解决

        同时的理解的是,在次之前我们的模型都是端(发送端)对端(接收端)而来的,但是在拥塞控制之中我们就得把研究对象放在端与端之间,TCP既考虑的双方主机的问题,同时也考虑了路上网络的问题

        虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题.
        因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据,是很有可能引起雪上加霜的.
        TCP引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;

       

TCP协议_第38张图片

此处引入一个概念程为拥塞窗口
发送开始的时候, 定义拥塞窗口大小为1;
每次收到一个ACK应答, 拥塞窗口加1;
每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口; 

        其实拥塞窗口和滑动窗口一样就是一个单纯的数字,只是名字取得是这样罢了,意思就是当达到这个数字的时候可能会发生网络拥塞的问题,表示网络的接收能力,发送量尽量不要超过这个数字,这个数字在报文中并没有显示出来,所以我们看不到,基于这一点,我们的重新认识一下窗口大小具体是多少,真正的滑动窗口大小应该是下面这一个表达式

        滑动窗口大小 = min(拥塞窗口,窗口大小(自己的接收能力))

增长速度模型 

TCP协议_第39张图片

        每一次的拥塞窗口都是为了下一次更新窗口的阈值,ssthresh指的是上一次探测的网络拥塞数值的减半,通过不断的摸索来让网络的通信达到稳定

        像上面这样的拥塞窗口增长速度, 是指数级别的. "慢启动" 只是指初使时慢, 但是增长速度非常快,这样既可以让网络得到喘息的时间,并且可以快速恢复正常的网络通信

为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍.
为此引入一个叫做慢启动的阈值
当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长

当TCP开始启动的时候, 慢启动阈值等于窗口最大值;
在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1

        少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;
        当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;
        拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案.TCP拥塞控制这样的过程, 就好像 热恋的感觉

        就像堵车一样,如此如此拥塞控制既做到了保障通信的可靠性(网络故障了就不要大量发送数据不然还是丢包),又提高了效率(慢是为了快,让网络恢复就是为了提高效率)!

        

延迟应答

如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小,这是一个概率问题,不过提高大量的实验证明,这大概率会有更大的窗口

假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;
但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;
在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M;

        一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率

那么所有的包都可以延迟应答么? 肯定也不是;


数量限制: 每隔N个包就应答一次;
时间限制: 超过最大延迟时间就应答一次;


具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms;

TCP协议_第40张图片

捎带应答

在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 "一发一收" 的. 意味着客户端给服务器说了 "How are you", 服务器也会给客户端回一个 "Fine, thank you";
那么这个时候ACK就可以搭顺风车, 和服务器回应的 "Fine, thank you" 一起回给客户端

TCP协议_第41张图片

这一点我们只需要知道ACK其实就是报头里面的一个标志位,那么这一个机制其实很容易理解,这也是为什么大多数情况下ACK都会置为1,表示对历史消息的确认,这是为了解决效率的问题

三个TCP延伸问题

如何理解面向字节流

创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;

调用write时, 数据会先写入发送缓冲区中;
如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
然后应用程序可以调用read从接收缓冲区拿数据;
另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据. 这个概念叫做 全双工

我们现在已经明白了,read和write这类的数据本质其实就是拷贝函数,read的时候,当缓冲区没有数据的时候,操作系统就会将它挂起,导致阻塞


由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 例如:

写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;

TCP并不知道这些数据是什么含义,也并不需要知道,因为TCP是基于应用层需要多少它就拷贝多少,要多少读多少,想怎么读就怎么读(缓冲区)

这一点和UDP是不一样的,报文之间是有明显边界的情况,我们就称它为面向数据报的

UDP不需要保证报文是不是一个完整报文的情况,读上来就一定是一个完整的报文,但是TCP就不一样,它并不知道数据是什么含义,数据是应用层关心的,需要应用层自己去处理(怎么读取一个完整的报文),体现在之前我们写的网络版计算器中的内容一样,以\r\n为分隔符,这一部分就是在应用层中对数据进行管理维护,这也是为什么我们在应用层写的时候,需要自己保证读取到的是一个完整的报文

这也是为什么TCP报文中没有描述数据多少的字段,因为它并不需要描述,应用层需要读取多少,那么下层就读取多少,TCP把数据都塞进了缓冲区之中,上层只需要读读读就行啦

粘包问题
 

首先要明确, 粘包问题中的 "包" , 是指的应用层的数据包.
在TCP的协议头中, 没有如同UDP一样的 "报文长度" 这样的字段, 但是有一个序号这样的字段.
站在传输层的角度, TCP是一个一个报文过来的. 按照序号排好序放在缓冲区中.
站在应用层的角度, 看到的只是一串连续的字节数据.
那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包

那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界

对于定长的包, 保证每次都按固定大小读取即可; 例如之前的Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;
对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔符不和正文冲突即可)

粘包问题其实就是应用层应该解决的事情,之前网络版计算器就是如此,我们定制一个协议采取子描述的方式其实就是在解决粘包问题,这也是所以基于TCP协议的应用层都应该解决的问题

那么对于UDP协议来说, 是否也存在 "粘包问题" 呢?

对于UDP, 如果还没有上层交付数据, UDP的报文长度仍然在. 同时, UDP是一个一个把数据交付给应用层. 就有很明确的数据边界.
站在应用层的站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现"半个"的情况

在基于UDP协议的应用层是不会发送粘包问题的,因为在应用层眼中,读取的就是一个一个完整的报文,有很明确的数据边界,所以UDP协议是不存在粘包问题的

TCP异常情况
 

进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别.
机器重启: 和进程终止的情况相同.
机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset. 即使没有写入操作, TCP自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放.

        关于前面两种情况,无论是进程挂掉了,还是机器重启原理都是一样的,都有操作系统进行维护管理,进程挂掉了操作系统会回收进程资源,然后在进入四次挥手,这里是由操作系统完成的,机器重启也是如此,当你关闭计算机的时候,当有些正在运行进程还存在的时候它会询问因为该程序还在运行,是否继续关闭重启,当你继续的时候,操作系统就会和进程终止执行相同的流程,这也是为什么有很多软件运行的时候关机会比较慢的原因

        后者就不一样了,这时候即使是操作系统也没有任何办法执行四次挥手的操作了,因为物理手段最为致命,这时候和之前我们提到的一样,我们以客户端关机为例,客户端会认为连接已经不存在了,如果它后面又接通了电源的话,但是服务端认为连接存在(没有四次挥手),这时候客户端是不会再发送数据了,服务端这边不可能会一直等下去,维护连接需要资源,所以客户端就会发送询问消息来确认客户端是否还在,当几次之后,如果还没有答复服务端就会直接关掉连接回收资源


        另外, 应用层的某些协议, 也有一些这样的检测机制. 例如HTTP长连接中, 也会定期检测对方的状态. 例如QQ, 在QQ断线之后, 也会定期尝试重新连接,比如有时候当你没有用QQ的时候,就是单纯的挂起,做着其他的事情(比如看视频),你会发现你的QQ会进入到忙碌状态,这其实就是一种运用了检查机制做的,因为维护TCP连接是需要资源的,你不用当然不能为此占用资源,只有当你使用的时候,它就会自动重新发起连接请求

  

TCP相关实验

理解 listen 的第二个参数

        其实这里的 listen 的第二个参数涉及到来排队的问题

        为什么需要这个排队呢?这里我们就要想象一下在一家海底捞中,当生意红火的时候,来吃饭的人很多,在高峰期甚至占满座位了,那么当没有座位的时候,后面来的客人想要吃的时候,你是否会发现,即使座位满了,服务员会给你一个序号,说如果你想要吃的话,可以稍微等待一段时间,根据其中的商业思路,我们会发现,一部分因为满座而走的客人会被留了下来,当一个座位客人走了之后,后面排队的人立马就可以坐下用餐,那么不就是就提高了座位的利用率了吗?但是还有一个问题就是,那么为什么老板不多安排几个位置呢?这显然是有成本的,每一张座位都是需要消耗成本的,为此我们需要从中找到一个平衡

        基于这一点,listen的第二个参数因此而生,即为了提高提供服务时间段的利用率,又节约了成本,半连接状态就是为了节约维持连接成本而存在的,稍后实验会证明该现象

准备工作

TCP协议_第42张图片

TCP协议_第43张图片

实验结果

TCP协议_第44张图片

基于上述实验,Linux内核协议栈为一个tcp连接管理使用两个队列:

1. 半链接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求)
2. 全连接队列(accpetd队列)(用来保存处于established状态,但是应用层没有调用accept取走的请求)
而全连接队列的长度会受到 listen 第二个参数的影响.
全连接队列满了的时候, 就无法继续让当前连接的状态进入 established 状态了.
这个队列的长度通过上述实验可知, 是 listen 的第二个参数 + 1.

这样可以看出accept其实不参与三次握手,拿取的是已经握手成功的连接

这里的backlog就是 listen 的第二个参数,注意这里指的是队列的长度,因为服务器是不断在处理连接请求的,一旦队列中的连接请求被拿走了,就相当于队列里面的请求少了一个,一旦到了正在处理就不是这边需要考虑的了,就相当于海底捞排队,一旦你做上座位开始吃饭的时候,你的那个序号就没有意义了,这条队列是平衡两者之间的存在,其实可以看成一段短暂的缓冲区用来给上层服务器拿取

使用 wireshark 分析 TCP 通信流程
 

下载 wireshark
 

https://1.na.dl.wireshark.org/win64/Wireshark-win64-2.6.3.exe

下载完成之后直接双击安装, 没啥太多注意的
 

启动 wireshark 并设置过滤器
 

由于机器上的网络数据报可能较多, 我们只需要关注我们需要的. 因此需要设置过滤器
在过滤器栏中写入

ip.addr == [服务器 ip]

则只抓取指定ip的数据包.

 或者在过滤器中写入

tcp.port == 9090

则只关注 9090 端口的数据


更多过滤器的设置, 参考

https://blog.csdn.net/donot_worry_be_happy/article/details/80786241

观察三次握手过程

启动好服务器.
使用 telnet 作为客户端连接上服务器

telnet [ip] [port]

 观察三个报文各自的序列号和确认序号的规律,和之前的一样一样的
 

观察四次挥手

在 telnet 中输入 ctrl + ], 回到 telnet 控制界面, 输入 quit 退出

实际上是 "三次挥手", 由于捎带应答, 导致其中的两次重合在了一起

TCP总结

什么TCP这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能

可靠性
校验和
序列号(按序到达)
确认应答
超时重发
连接管理
流量控制
拥塞控制


提高性能
滑动窗口
快速重传
延迟应答
捎带应答


其他

定时器(超时重传定时器, 保活定时器, TIME_WAIT定时器等)

基于TCP应用层协议

HTTP
HTTPS
SSH
Telnet
FTP
SMTP

当然, 也包括你自己写TCP程序时自定义的应用层协议

TCP/UDP对比

我们说了TCP是可靠连接, 那么是不是TCP一定就优于UDP呢? TCP和UDP之间的优点和缺点, 不能简单、绝对的进行比较
TCP用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景;
UDP用于对高速传输和实时性要求较高的通信领域, 例如, 早期的QQ, 视频传输等. 另外UDP可以用于广播;
归根结底, TCP和UDP都是程序员的工具, 什么时机用, 具体怎么用, 还是要根据具体的需求场景去判定. 

如果不知道用什么协议还是用TCP协议算了,不过具体什么场景就用什么协议

用UDP实现可靠传输(经典面试题)

参考TCP的可靠性机制, 在应用层实现类似的逻辑;

引入序列号, 保证数据顺序;

引入确认应答, 确保对端收到了数据;
引入超时重传, 如果隔一段时间没有应答, 就重发数据;

等等按照现在普遍常用的协议TCP实现就行了

 

结束语 

        至此传输层结束,下一站我们将进入网络层,当我们进入到这一层的时候就会知道为什么我们一般都叫做TCP/IP协议,我们就可以知道这两个的关系密切程度了,我们都明白,当数据在传输层中被处理完之后,就会被交付给下一层网络层,这里也可以提前透露,TCP我们是用来提供策略,而IP则是用来行动的,前者保证网络通信的可靠性,后者则是直接采取行动来实现网络通信

你可能感兴趣的:(网络基础,网络,tcp)