TCP全称为 “传输控制协议(Transmission Control Protocol”)。要对数据的传输进行一个详细的控制;
前置知识:
0000~1111
,转成10进制0~15
;首部长度计算的基本单位:4字节,即首部长度 * 4 = 最终长度,故最终长度取值范围0~60
60-20=40
字节;TCP报头的非标准长度是20字节+选项20 / 4 = 5
,转成二进制就是0101
为什么没有有效载荷的长度呢?
因为TCP是面向字节流的,即以字节流的形式传输数据的,TCP收到数据就直接交给上层了,数据怎样用怎么解析,是你上层的工作,与我TCP无关
扩展,关TCP选项字段是如何实现的呢?
C语言中柔性数组的概念,帮助我们定义变长的结构体;前面的字段用结构体(位段)定义好,后面跟上柔性数组,你需要多少选项直接malloc空间,malloc出来的空间+20字节标准报头填到4位首部长度处,就知道了整个报文的大小。
有效载荷是如何做到交付给应用层的哪一个进程呢(分用)?
根据目的端口号向上交付给应用层,绑定该端口号的进程,即分用的过程
网络传输中不可靠的情况:
丢包(少量,大量),乱序,重复,校验失败,发送太快/太慢,网络出现问题
为什么会出现这么多不可靠的问题呢?
单纯的就是因为通信双方之间距离变长了
上面不可靠情况中最关键:丢包问题,你怎么知道这个报文丢包了呢?
这就引出了可靠性中最核心的问题:正确理解确认应答机制
客户端给服务器发送消息,服务器给客户端ACK应答,无法保证对ACK应答的确认。即使存在对ACK的应答,也无法保证对ACK应答的应该。换句话说,在长距离传输中总有最新的报文无法被确认。
所以我们认为:
误区:不要仅仅认为对方把数据收到了就是可靠性。当对方收到数据时,我知道了;当对方没有收到数据时,我也知道了;这才叫做可靠。即对于我们所做的动作都要有反馈,这套机制称为可靠性。
但是真实的情况,并不是向上面一样,你发一个数据我给你一个应答,这样做整个发送过程是串行,效率低,所以真实的发送过程是C->S一次性发送多份报文(并发),S再对这些报文处理(并发响应)
这样做:发送多个报文时间上就重叠了,提高了发送效率
但是有2个问题:
所以,就注定了,我们要想办法给报文带上编号,即序号和确认序号。
故,我们最终呈现出来的样子:
无论请求还是响应,双方在进行交互时,通信发送的都是TCP报头+有效载荷(如果有的话)
TCP将发送出去的每个字节数据都进行了编号,这个编号叫做序列号。
TCP是怎么做到对每个字节数据都进行了编号呢?
在通信的过程中,TCP并不关心自己缓冲区中的数据类型;TCP是面向字节流的,缓冲区中的数据可以把它整体看待成 char sendBuffer[N]
类型,它仅仅表示uint8_t
按字节为单位划分成一个一个的小格子,即发送缓冲区是一个大数组,数组中是以字节为单位来呈现的。上层把对应数据拷贝到缓冲区时,由于发送缓冲区是一个大数组,每个字节的数据就天然地带上了编号,即该数组的下标。
每一个ACK都带有对应的确认序列号。
对于任意一个确认序号X表示:X-1之前的报文应经全部收到了,下次发请从X编号开始发送
以上图为例,用确认序号规则,收到确认序号1001表示:1000之前的序号已经全部收到;收到确认序号2001表示:2000之前的序号已经全部收到;如果只收到确认序号2001没有收到1001,同样也能表明:2000之前的序号已经全部收到(当然包含1000),只是确认序号1001丢失了,这种确认序号的概念定义:可以允许少量的ACK丢失,更细粒度的确认丢包原因
这里就衍生出一个问题,为什么不把序号和确认序号表示成一个字段呢?
TCP是全双工的,C可能在给S发消息也可能收到S到来的数据请求,对于S也是如此;以C->S发信息,就出现了一种情况:S->C的ACK确认和S->C发送数据,这时就会把两个应答/请求压缩成一个,就称为捎带应答。
这里确认和发送数据是同时存在的:
所以,就注定了序号和确认序号必须是不同的字段
TCP协议是全双工的,本身具有接收缓冲区和发送缓冲区
在应用层调用write,read等系统接口时,并不是直接把数据从本主机发送到目标主机,而是把数据向下交给传输层,由传输层来处理
所以write,read等系统接口的本质是:拷贝,把应用层缓冲区的数据拷贝给传输层的缓冲区
发送过程:把我发送缓冲区数据拷贝到对方的接收缓冲区,把对方发送缓冲区数据拷贝到我的接收缓冲区。整个过程由TCP协议来控制。
我们以上图C->S发数据为例:
C->S发送消息太快,S来不及接收,一旦S的接收缓冲区被打满了,就无法接收来自C的消息,那么后面的数据将会直接丢弃,看似很正常,因为TCP协议存在超时重传机制,所以这些丢弃的报文可以重新发送。但是C与S通信过程中,经历长距离传输,这些要丢弃的报文已经消耗了很多网络资源了,直接丢弃是比较低效的表现,同时站在C的角度:既然S已经来不及接收了,为啥不早点告诉我呢,让我控制一下自己的发送速度。问题解决方案:S->C做ACK确认应答时,告诉C自己的接收能力,即S接收缓冲区的大小,把这个字段填入TCP报头的,称此字段为16位窗口大小。16窗口的存在实现了对方交换接收能力的作用,这种策略就称为流量控制(Flow Control)。
16位窗口大小:
TCP的报头中有一个称为窗口大小的字段,它占用了16个比特位。用来表示接收端的接收能力,即接收端接收缓冲区的大小。
注意:凡是我要构建TCP报文必定是我向对方发送消息,16窗口大小就填自己的窗口大小,表示我自己接收缓冲区的大小
客户端与服务端的通信的过程中,以服务端为例,它会在同一时刻/时间段收到各种各样的报文请求,比如有的请求是建立连接的,有的请求是发送消息的,有的请求是断开连接的。这些请求报文都是不同的,就注定了报文是有类型的,不同的请求报文要提供不同的处理动作。这就回答了标志位的一些前提问题。
标志位的本质是什么?
标识不同类型的报文。
为什么要有标志位?
标志位的不同反应了报文的不同,从而提供不同的处理动作
具体标志位,是怎么设计的呢?
前提:这6个标志位全部都是一个一个的比特位,为1表示该标志位被设置,为0则表示不被设置
ACK(acknowledgement)
:此标志表示应答域是否有效。
报文中的ACK被设置为1时,表明该报文是一个确认应答报文
报文中的ACK被设置为1同时该报文携带数据,称为捎带应答。表明既是对对方上一个报文的确认也包含了我所给对方发送的消息。
SYN(synchronous)
:此标志表示建立连接
FIN(finish)
:此标志表示断开连接
PSH(push)
:此标志为1表示提示接收端应用程序立刻从TCP缓冲区把数据读走。
当两台主机通信时,发送方不断向接收方发送数据,接收方此时也会给发送方做ACK应答同时通过16窗口大小告诉发送方自己接收缓冲区的大小,一旦窗口大小为0,表明接收方没有能力再接收数据了。此时窗口大小为0可能有两种原因:
窗口大小为0后,发送方会定期向接收方发送询问报文(不携带数据只含有报头),接收方做ACK应答后告诉发送方自己的窗口大小还是0,几次询问后发送方会再发送询问报文将自己的PSH标志位置为1,表示提示(催促)接收端应用程序立刻从TCP缓冲区把数据读走(向上交付)。
RST(reset)
:此标志表示要求对方重新建立连接
比如,客户端和服务器在正常通信时,突然拔掉服务器的网线,客户端与服务器并没有进行四次挥手,此时客户端依然认为与服务端建立着连接。重启服务器后,服务器会收到客户端发来的TCP报文。服务端会发现没有与客户端建立连接的情况下直接通信,此时服务端就会把RST设置为1,把报文发送给客户端,让客户端与服务端进行重新连接。
RST标志位表示通信双方建立连接成功后,由于某些原因导致双方认为连接状态不一致,无法进行正常通信,需要进行连接重置。
我们在访问某些网站时,就可能出现下面连接重置的情况:
URG(urgent)
:此标志表示紧急指针字段是否有效
16位紧急指针:标识哪部分数据是紧急数据
实际上现在的TCP协议中,紧急指针的使用情况很少几乎用不到
recv/send函数就有设置URG的参数
recv函数第四个参数flags有一个叫做MSG_OOB的选项,其中OOB是带外数据(out-of-band)的简称,带外数据就是一些比较重要的数据,因此上层如果想读取紧急数据,就可以在使用recv函数进行读取,并设置MSG_OOB选项
但是, 主机A未收到B发来的确认应答, 也可能是因为ACK丢失了;
因此主机B会收到很多重复数据. 那么TCP协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉。这时候我们可以利用前面提到的序列号, 就可以很容易做到去重的效果(序号相同的报文直接丢弃)。
细节:主机A已经把数据发到网络中了,不能立即在自己的发送端把数据清掉,需要把对应数据暂时维护一段时间,直到收到应答或超时情况,即把数据维护到结果确定的情况下,以支持超时重传机制。
一个已经发送出去的数据在没有收到应答时,TCP必须把此数据维护一段时间(保存起来),以支持超时重传
那么, 如果超时的时间如何确定?
TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间。
服务器初始化:
建立连接的过程:
这个建立连接的过程, 通常称为三次握手;
数据传输的过程:
断开连接的过程:
如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN(第三次);
客户端收到FIN, 再返回一个ACK给服务器(第四次);
这个断开连接的过程, 通常称为四次挥手。
在学习socket API时要注意应用程序和TCP协议层是如何交互的:
- 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段
- 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段
强调:
3次握手中发送的可不是简单的SYN,ACK。发送的是TCP报头,就是上面TCP协议数据只不过把对应标志位置为1
3次握手是建立连接的过程。
什么是连接呢?
作为一款服务器,它可能随时随地收到来自多个客户端的连接请求,它们都想要连接服务器,连接服务器就会触发3次握手。3次握手就是要建立连接,整个过程是在TCP层,由TCP协议帮我们做的,即OS帮我们做的,就意味着一定在OS内部同时存在多个建立好的连接。
那么OS要不要把这些已经建立好的连接管理起来呢?当然要
所以OS怎么对这些已经建立好的连接管理呢?先描述,再组织。所谓的连接即OS为了管理连接维护的数据结构!先描述:struct tcp_link{管理连接的字段(源ip,目的ip,源端口,目的端口,缓冲区....)}
,再组织:这个结构体里面包含struct tcp_link* next
以双向链表或其他数据结构组织起来。所以OS就把对连接的管理转化成对某种数据结构对象的管理
管理连接必须先描述,描述成结构体;建立连接成功时OS要根据连接类型帮我们new/malloc出来对应的连接对象,连接对象中包含了各种属性。所以创建并维护连接是有成本的。创建连接对象并设置数据到对象中,一定要new/malloc连接对象,此过程一定要消耗内存资源;维护定时器(保证超时重传),维护连接的状态,设置各种字段值,维护管理连接的算法等此过程,一定要消耗CPU资源
结论:连接即OS内维护的数据结构对象。维护连接是有成本的,会消耗内存和CPU资源
客户端只要把SYN报文发送出去,客户端的状态立马变成SYN_SENT(同步发送);
只要服务器收到SYN报文,服务端的状态立马变成SYN_RCVD(同步收到);
客户端收到SYN + ACK报文,并且发出ACK时,客户端的3次握手就完成了,它认为连接就建立好了;服务端收到ACK时,服务端的3次握手就完成了,它认为连接也建立好了。只有双方都认为自己的3次握手完成时,3次握手的过程才完成。
<1> 3次握手过程中,客户端到服务器的线为什么是斜着向下的呢?
这张图中存在时间维度,报文发出和对方收到这两个时间点绝对不一样,斜着向下表明时间流逝。
<2> 3次握手中,客户端到服务器从来不担心第1次,第2次丢失
3次握手不一定能握手成功。
第1个报文丢失:服务端不会收到任何影响,因为它从未收到握手请求即服务端不会为我们维护管理连接的数据结构,客户端可以给我们超时重传
第2个报文丢失:双方3次握手过程照样没有成功,双方没有建立起来连接,没有成本。同时报文是否丢失我们还存在应答
第3个报文丢失:
3次握手中客户端只要把最后的ACK报文发出,客户端就认为连接建立好了。无论服务端是否收到
即最后一个ACK是否被服务器收到,客户端并不知道到。如果最后一个ACK报文丢失怎么办呢?
客户端已经在最后一个ACK报文发出时认为连接就建立好了。此时客户端就会维护连接对应的结构体;只有服务器收到最后一个ACK报文,才对它来说接连建立好了,才会维护连接对应的结构体,双方就会正常通信。
客户端认为连接建立好了,服务器没有收到最后一个ACK报文,就出现了客户端与服务器之间连接建立状态不一致。此时客户端就直接给服务器发送消息,服务器会判定双方连接建立状态不一致,服务器立马给客户端发送RST连接重置响应,客户端收到响应后会关掉连接然后重新建立连接。
在网络中最后一个ACK能否被收到是不知道的,那我们可不可以给最后一个ACK应答呢?
只要你不断在应答就注定了永远有最新的报文无法应答,如果你对ACK的应答丢失了呢,服务端此时也无法知道对ACK的应答是否被客户端收到。
2次握手可以吗?
即2次握手非常容易受到攻击。
注:TCP即便是3次握手也会受到攻击,缓解TCP受到攻击的问题不是TCP本身该解决,防止攻击本身和TCP无关,但是TCP本身设计有明显漏洞被攻击就和TCP有关系了。
4次握手可以吗?不建议
如果是主动发起连接建立的一方,最后一定会存在最后一个ACK是没有对应的应答的。
最后一个ACK由谁发起,谁一定会先完成指定次数的握手过程。奇数次握手一定是客户端来主动发起对应的ACK,偶数次握手一定是服务端来主动发起对应的ACK。奇数次握手一定是客户端先建立好连接,服务器有没有建立好连接客户端不知道,把不确定性留给客户端;偶数次握手一定是服务器先建立好连接,客户端有没有建立好连接服务器不知道,把不确定性留给服务器。
连接握手可能会发生异常。
3次握手为什么行,其他次握手不行呢
除了上面其他次握手会存在漏洞的问题外,3次握手本身就够了
3次握手是奇数次握手,没有明显的设计漏洞,一旦建立连接出现异常,成本嫁接到客户端,服务端成本较低
双方进行通信的前提是信道通畅,3次握手是验证全双工通信信道通畅的最小成本(验证客户端和服务器必须得能收和能发。客户端:第1, 2次验证;服务器:第1, 3次验证)
3次握手也可以叫4次握手,原因如下:
我们以前在写套接字代码时并没有对服务器进行响应,所以发数据是把数据拷贝到对应的TCP缓冲区,发送数据是由自己的OS自动发送的,确认也是由对端的OS自动应答的,发送和应答本身是由OS完成的。即整个3次握手的过程本身是由两端OS自动完成的。
服务端:
CLOSED -> LISTEN
] 服务器端调用listen后进入LISTEN
状态,等待客户端连接LISTEN -> SYN_RCVD
] 一旦监听到连接请求(同步报文段),就将该连接放入内核等待队列中,并向客户端发送SYN确认报文SYN_RCVD -> ESTABLISHED
] 服务端一旦收到客户端的确认报文,就进入ESTABLISHED
状态,可以进行读写数据了客户端:
CLOSED -> SYN_SENT
] 客户端调用connect,发送同步报文段SYN_SENT -> ESTABLISHED
] connect调用成功,则进入ESTABLISHED
状态,开始读写数据4次挥手也可能变成三次挥手,原因如下:
客户端状态转化:
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,彻底关闭连接注:4次挥手是上层双方各自调用close(sock),调用一次close(sock)就是2次挥手。
TIME_WAIT
CLOSE_WAIT
状态被动断开连接的一方,收到别人发送来的FIN,在给对方ACK时,不调用close(sock),最终会进入CLOSE_WAIT
状态。
CLOSE_WAIT
状态:主动断开连接的一方断开连接,被动断开连接的一方不断开连接,它此时就会维持CLOSE_WAIT
状态
我们此时注释代码中的:close函数
开始测试:
启动服务器,用telnet作为客户端连接服务器,连上后发现两者ESTABLISHED
状态
此时我们立马关掉客户端,客户端进入FIN_WAIT_2
状态,服务器进入CLOSE_WAIT
状态
过了一会,继续查看,此时客户端已经退出了,服务器会维持很长时间的CLOSE_WAIT
状态
如果服务器出现了大量的CLOSE_WAIT
状态,说明服务器:
主动断开连接的一方,会发送最后一次ACK,最终会进入TIME_WAIT
状态。
TIME_WAIT
状态:发送方发送完最后一次ACK后,等待一段时间尽可能保证最后一次ACK被对方收到。
放开close函数,演示正常的四次握手
启动服务器,用telnet作为客户端连接服务器,连上后发现两者ESTABLISHED
状态
主动关掉客户端,客户端进入TIME_WAIT
状态
TIME_WAIT
状态持续一段时间后才进入真正的关闭,可见TIME_WAIT
状态是一种临时性状态,过一会就没了
还是上面的演示,此时我们主动关掉服务器,服务器进入TIME_WAIT
状态
绑定失败现象:
客户端连着服务端,服务端主动退出,紧接着服务端再次启动绑定相同的端口就会出现绑定失败的现象
为什么会出现上面的情况呢?
TIME_WAIT
状态,是一种临界状态,对于连接资源数据结构还是维护的,与此连接相关联的,进程所绑定的端口号依旧在被使用
在服务器的TCP连接没有完全断开之前不允许重新监听,某些情况下可能是不合理的。来进一步回答这个问题
是否会存在服务器主动关闭连接的场景呢?
存在,比如双11期间服务器上有大量的连接,服务器压力越来越大。直到来了某个连接,服务器实在撑不住挂掉了,连接断开,服务器进入TIME_WAIT状态。想要立马恢复服务,可是此时状态还是TIME_WAIT,无法立即重启成功,就会造成巨大的金钱损失。
上面的问题如何解决呢?如果服务器不想等待或更换端口?
使用setsockopt()
设置socket描述符的选项SO_REUSEADDR
为1,表示允许创建端口号相同但IP地址不同的多个socket描述符
直接修改代码
继续测试,bind绑定失败问题没有了
查看TIME_ WAIT状态的时间
cat /proc/sys/net/ipv4/tcp_fin_timeout
等待两个MSL(maximum segment lifetime)
MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同, 在Centos7上默认配置的值是60s;
为什么是TIME_WAIT的时间是2MSL?
前面16窗口大小学习时,我们已经引入了流量控制。这里详细介绍一下。
接收端处理数据的速度是有限的。如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,就会造成丢包, 继而引起丢包重传等等一系列连锁反应。
因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度。这个机制就叫做流量控制(Flow Control);
接收端如何把窗口大小告诉发送端呢?
回忆我们的TCP首部中, 有一个16位窗口字段, 就是存放了窗口大小信息;
那么问题来了, 16位数字最大表示65535, 那么TCP窗口最大就是65535字节么?
实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是 窗口字段的值左移 M 位;
三次握手过程中可以携带数据吗?
第一次、第二次握手不可以携带数据,第三次握手可以携带数据
理由:
第一次握手不可以放数据,其中一个简单的原因就是会让服务器更加容易受到攻击了。而对于第三次的话,此时客户端已经处于 ESTABLISHED 状态。对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据也没啥毛病。
针对上面的补充问题,我们来回答一下流量控制中一个细节问题:
首次发送的时候,发送方咋知道接收方的窗口大小,即保证第一次发送数据不会出现发送太快太慢的问题?
TCP协议通信时,并不是双方在进行第一次报文交换的时候,双方在3次握手期间已经有过至少一次报文交换
客户端建立连接的时候不会携带数据只发送报头,不会使用服务器的接收缓冲区,不存在把数据给发多的情况;客户端发送连接建立请求时,除SYN被置1,客户端一定会将自己16位窗口大小通告给服务器,第2次握手时,服务器给客户端响应,即SYN+ACK同时被置1,同时服务器的窗口大小也会告诉客户端
于是双方在 1,2次握手时不携带任何数据只发送TCP报头也就不会存在报文发送快慢的问题,即不使用双方的接收缓冲区的情况下, 交换了双方的接收能力
注:其实发送数据就是不断把自己发送缓冲区的数据填入对方的接收缓冲区中。由应用层调用read,recv接口把数据往上拿
基于确认应答策略, 对每一个发送的数据段, 都要给一个ACK确认应答。收到ACK后再发送下一个数据段(串行)。收发效率非常低下
既然这样一发一收的方式性能较低,那么我们一次发送多条数据(并行), 就可以大大的提高收发效率(其实是将多个段的等待时间重叠在一起了)
实际通信时上面两种方案都会存在,从上面两种方案对比,可以看到:
TCP协议,可靠性是主要研究的问题,但是不是全部。效率问题,也是TCP考虑的问题。
学习滑动窗口的前提:
发送方数据速度会受到接收方接收能力的约束。如果接收方接收能力有限,就注定了发送方只能把发送缓冲区中的一部数据给对方。
所谓的滑动窗口,其实是发送缓冲区的一部分。
于是根据滑动窗口,可以把发送缓冲区分割为3部分
目前我们认为:滑动窗口的大小?和对方的接收能力有关,应答报文中win窗口大小
滑动窗口,可以看做用户往滑动窗口中尚未发送的数据区域放数据,发送时发送滑动窗口中的数据,是一个基于队列的生产消费者模型。
我们前面在确认应答时的确认序号说过:缓冲区中的数据可以把它整体看待成 char sendbuffer[N]
类型数组,将上层数据拷贝下来时每个字节数据就天然有编号。
只能向右滑动,不能向左滑动。滑动窗口左侧是已经发送已经收到确认的数据区域,再把重复的数据发送给对方,没有意义。
滑动窗口的变化是取决于对方的接收能力的,即对方给我通告的win的大小
可以变大,变大即winend+=某些值
可以变小,变小即winstart+=某些值
可以变0,变0表示接收方已经不能再接收数据了,它对应的接收缓冲区剩余空间大小为0,滑动窗口的两指针winstart,winend指向同一位置,就会进入流量控制的窗口探测和通知过程
基于以上几点,表明滑动窗口的大小是浮动的
将发送缓冲区设置成环状结构即可,winstart和winend不可能越界,只有打满或为空的情况
winstart = seq
(起始位置); 应答的win,winend = winstart + win
(结束位置)第一个丢失:
中间丢失:
最后一个丢失:
即,在滑动窗口中所有的丢失情况,全部会变成最左侧丢失问题
如果出现了丢包, 如何进行重传?
情况一: 数据包已经抵达, ACK被丢了。
情况二: 数据包就直接丢了。
这种机制被称为 “高速重发控制”(也叫 “快重传”)。
超时重传 VS 快重传
超时重传:决定重传的下限,侧重于帮助重传兜底。
快重传:决定重传的上限,连续收到3个以上的重复确认序号就会快重传,侧重于重传时的效率问题
两台主机在通信时如果出现少量的丢包,是很正常的现象,此时可以通过超时重传或快重传来补发丢失的数据;可是如果出现大量的丢包情况,就是不正常的现象了。
举一个例子:
现在大家要考试,班级里有30个同学。如果这30个人中只有1,2个挂科那么可以说是这1,2个同学自己的问题 ;如果这30个人中只有1,2个通过,那么挂科的这些同学会认为不是自己的问题,而是试卷的问题,是不是考试卷子出难了…
同样对于TCP来说也是如此:
那么出现大量丢包时,发送方的发送策略是什么呢?
出现大量丢包时,说明处于网络拥塞状态,此时我们的策略要保证两点:
既然发生了网络拥塞,发送方就要基本得知网络拥塞的严重程度,所以必须要进行网络状态的探测,此处引入一个概念称为拥塞窗口。
TCP引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;
拥塞窗口:
即自己滑动窗口大小 = min(对端主机的接收能力win,网络的拥塞窗口)
winstart = seq
, winend = min(seq_win,拥塞窗口)
,
像上面这样的拥塞窗口增长速度, 是指数级别的。 “慢启动” 只是指初使时慢, 但是增长速度非常快。
少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;
当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;
拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案。
两台主机在通信时,接收方会给发送方应答,告诉发送方自己的接收能力,即win
我们只要给发送方通告更大的win大小,发送方可能就会更新出更大的滑动窗口大小,就可以一次性并发给我们发送更多数据,就能在较大概率上提高传送效率
那么如何更新出一个更大的接收窗口呢?只有让上层尽快交付,才有可能更新出更大的接收能力,即给上层更多的时间来进行读取:
为了给上层更多的时间来读取,我们引入延迟应答的概念
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小。
一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高。我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;
那么所有的包都可以延迟应答么? 肯定也不是;
数量限制: 每隔N个包就应答一次;
时间限制: 超过最大延迟时间就应答一次;
具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms;
创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
由于缓冲区的存在, TCP程序的读和写不需要一一匹配,例如:
对于TCP来说,它并不关心缓冲区里的数据类型,在它看来都是一个一个的字节数据,它只负责把这些数据准确无误地发送对接收方的缓冲区中,至于如何读这些数据完全交给上层处理,这就叫做面向字节流。
首先要明确, 粘包问题中的 “包” , 是指的应用层的数据包。
那么如何避免粘包问题呢?
归根结底就是一句话, 明确两个包之间的边界。
思考: 对于UDP协议来说, 是否也存在 “粘包问题” 呢?
因此UDP是不存在粘包问题的,根本原因就是UDP报头当中的16位UDP长度记录的UDP报文的长度,因此UDP在底层的时候就把报文和报文之间的边界明确了,而TCP存在粘包问题就是因为TCP是面向字节流的,TCP报文之间没有明确的边界。
进程终止:
双方通信的进程,一方出现了进程崩溃,该进程曾经打开的文件描述符会被OS自动关闭,自己看来是进程崩溃,实际OS看来就是进程的正常退出,双方OS在底层会正常完成四次挥手,然后释放对应的连接资源。
即进程终止会释放文件描述符, 仍然可以发送FIN。和正常关闭没有什么区别。
机器重启:
关机重启之前OS在底层会正常完成四次挥手,然后释放对应的连接资源,对应的进程会被杀掉,和进程终止的情况相同。
机器掉电/网线断开:
接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset。即使没有写入操作, TCP自己也内置了一个保活定时器, 会定期询问对方是否还在。如果对方不在, 也会把连接释放。
另外, 应用层的某些协议, 也有一些这样的检测机制。例如HTTP长连接中, 也会定期检测对方的状态。例如我们用QQ时长时间挂着不退出,也不发消息,如果使用长连接就决定腾讯内部一定要维护大量的连接;所以一定是在本地检测到用户不活跃了,就把连接断开了;活跃时,再重新建立连接。
为什么TCP这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能。
可靠性:
提高性能:
其他:
当然, 也包括你自己写TCP程序时自定义的应用层协议;
我们说了TCP是可靠连接, 那么是不是TCP一定就优于UDP呢? TCP和UDP之间的优点和缺点, 不能简单, 绝对的进行比较
归根结底, TCP和UDP都是程序员的工具, 什么时机用, 具体怎么用, 还是要根据具体的需求场景去判定。
参考TCP的可靠性机制, 在应用层实现类似的逻辑;
例如:
引入序列号, 保证数据顺序;
引入确认应答, 确保对端收到了数据;
引入超时重传, 如果隔一段时间没有应答, 就重发数据;
我们修改之前的代码,不进行accept获取listensock
套接字新连接,什么也不干,只进行监听连接的到来,backlog
设置为1
编译运行服务器,开启2个客户端,发现连接没有问题;一旦启动第3个客户端,客户端状态正常, 但是服务器端出现了SYN_RECV
状态, 而不是ESTABLISHED
状态
这是因为, Linux内核协议栈为一个tcp连接管理使用两个队列:
而全连接队列的长度会受到 listen 第二个参数的影响。
全连接队列满了的时候, 就无法继续让当前连接的状态进入 established 状态了。
通过上述实验可知, 全连接队列的长度 = listen 的第二个参数 + 1。
上述实验,我们设置的全连接队列大小是2,前2次连接正常,但是到了第3次连接的处于半连接队列,处于了SYN_RECV
状态,在客户端看来,连接已经建立好了,但是在服务端看来没有建立连接成功,因为服务端对于第三次握手的ACK进行了忽略。
过一段时间去看,SYN_RECV
状态已经没了,服务器长时间不受理ACK,为了防止服务器压力过大,直接关掉连接
误区:
在上面全连接队列长度为2,是不是意味着服务器一次最多可以处理两个连接?
不是,这个队列表示已经处于连接成功状态但是还没来得及被上层读取的,不是说上层一共能处理多少连接,这些连接一旦被accept被拿走,就会在此队列中被移走。就像你去餐厅吃饭,餐厅门口在排队,可是里面有上百号人在吃饭,能说餐厅只能供这几个排队的人吃饭吗
全连接队列长度问题:
举一个例子:
比如你在就餐高峰期去某个饭店吃饭,你到门口先问服务员是否有位置,他说没有后你直接离开,去下一家饭店;到这家饭店你还是问服务员是否有位置,他说位置满了,但是在5分钟内就有人要离开,你可以排队等待。对比这两家饭店:在就餐高峰期时间段内,总会有几分钟桌椅上是没有人的,第二饭店通过在外部排队的方式时时刻刻保证了自己饭店的内部资源(桌椅)使用率是100%,提高了盈利。
老板看到了这种方式,于是买了大量的椅子供用户在饭店外排队,以保证自己饭店的盈利。可是他这样做真的有用吗?(1) 一旦这个队伍过长,那么处于队尾的人就不太愿意排队了,他有排队的时间不如去另一家饭店吃饭 (2) 老板本人也是比较愚蠢的,他有买椅子的钱,为啥不用来扩张一下店面,以供更多的用户就餐。
所以,我们在全连接队列不能没有的前提下,保证这个队列不能太长: