listen的第二个参数,叫做底层的全连接队列的长度,算法是:n+1,表示在不accept的情况下你最多能够维护多少个链接。
假设我们传的listen的第二个参数是2,那么最多维护全连接的个数是3个,超过3个,将不让我们成功3次握手。而是以 SYN_SENT状态给我们保留,当有全连接退出了,才会继续连接。
为什么要这样设置呢?
这样排队的策略,可以让服务器在闲置的情况下,直接从底层拿取连接,进行连接处理。但是排队的长度不能太长,原因是:1.对客户的体验不友好。2.在系统层面上,整个服务器的可用硬件资源是确定的。如果维护的队列太长,会影响服务器的原来的效率。
在上一篇文章中,讨论了确认应答策略, 对每一个发送的数据段,都要给一个ACK确认应答,收到ACK后再发送下一个数据段。这样做有一个比较大的缺点: 就是性能较差,尤其是数据往返的时间较长的时候。
但是TCP协议没有采用这样的方式,但是采用了这样的思想。
既然这样一发一收的方式性能较低,那么我们一次发送多条数据,就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)。
那么主机A如何知道主机B的接受能力,根据流量控制来判断。
那么如果客户端给对方发送数据,可能会丢包,但是我们有超时重传机制,在此期间,我们需要把数据先保存到发送方的发送缓冲区。
下面就详细介绍一下发送缓冲区:
这个是发送缓冲区的一个基本结构。发送过程如下:
当我们发送的数据被应答后,我们就可以把窗口向右移动,这样左边就为已经发送,确认应答,中间就为已经发送,没有应答,右边就为待发送数据。
那么如何理解缓冲区和滑动窗口?
我们可以把缓冲区看成一个大数组,用两个int来代表开始下标(start)和结束下标(end),当下标超过范围时,就用取余来形成环形结构。
滑动窗口的大小是由谁来决定的?
目前:是由对方的接受的能力决定,也就是我们收到的TCP报头中的窗口大小。窗口越大,则网络的吞吐率就越高。
假设,发送方的缓冲区大小是4KB,接收方此时也是4KB,我们把数据发送过去,如果对方不把数据拿上来,就放在缓冲区里,对方响应的TCP报头里窗口大小就为0。那么我们的滑动窗口end不变,start向后移动到end同样的位置,这样滑动窗口就为0。也就说明滑动窗口变小,滑动窗口不一定会向右移动,因为end不动。如果此时数据全部拿出,接收缓冲区的大小变为16KB,那么我们的滑动窗口也会变大。所以滑动窗口是会变大或者变小的。
那么滑动窗口是如何移动的呢?
当我们收到TCP报头时,开始下标就等于确认序号,结束下标就等于开始下标+报头里16位窗口大小(不准确,先这样理解)。
假设我们发送了1001到2000的数据,下一步开始下标就到2001,结束下标不变。当发送了2001到3001的数据,下一步开始下标就为3001,结束下标不变。当窗口大小为0的时候,开始下标和结束下标同时在5001位置。如果窗口大小突然增大,开始下标不变,结束下标往后。
如果发送的一批数据中只收到了2001和5001应答,3001和4001应答没有收到,该怎么办呢?
答案是:窗口大小的开始下标依旧是5001,因为报头中的确认序号规定只有前面的报文收到了才会变成5001。如果真的当中的3000和4000的报文丢失了,那么返回的确认序号里只到2001。接下来发送方就会进入超时重传机制。
不理解的可以看下面的图片:
情况一: 数据包已经抵达,ACK被丢了。
这种情况下,部分ACK丢了并不要紧,因为可以通过后续的ACK进行确认。
情况二: 数据包就直接丢了。
当某一段报文段丢失之后,发送端会一直收到 1001 这样的ACK,就像是在提醒发送端 "我想要的是 1001"一样。如果发送端主机连续三次收到了同样一个 “1001” 这样的应答,就会将对应的数据 1001 - 2000 重新发送。这个时候接收端收到了 1001 之后,再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中。这种机制被称为 “高速重发控制”(也叫 “快重传”)。
接收端处理数据的速度是有限的,如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包,继而引起丢包重传等等一系列连锁反应。因此TCP支持根据接收端的处理能力,来决定发送端的发送速度。这个机制就叫做流量控制。
变到0的时候,主机A就不会给主机B再发送消息了。但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端,不过接收端也会主动发送给发送端,这是一个双赴的过程。窗口探测数据段其实是没有有效载荷的TCP报头。
16位数字最大表示65535,那么TCP窗口最大就是65535字节么?
TCP首部40字节选项中还包含了一个窗口扩大因子M,实际窗口大小是:窗口字段的值左移 M 位。
虽然TCP有了滑动窗口能够高效可靠的发送大量的数据,但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题。
因为网络上有很多的计算机,可能当前的网络状态就已经比较拥堵,在不清楚当前网络状态下,贸然发送大量的数据,是很有可能引起雪上加霜的。
我们如何判断是网络出现了问题呢?
因为前面学习过的确认应答,超时重传,序号,滑动窗口,流量控制都是确保发送方和接收方的稳定性。如果我们发送1000报文,但有999个报文丢失,说明就是网络出现了问题,并且不能立即重传。
为什么网络出现问题,不能立即进行重传?
原因是:不仅仅是一台主机进行网络发送,可能有许多主机同时在网络中发送数据,那么网络已经非常拥堵了,如果还有大量主机进行超时重传的话,就会加剧网络拥堵。
解决办法:TCP引入 慢启动 机制,先发少量的数据,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。
此处引入一个概念程为拥塞窗口:在这个拥塞窗口大小以内,不会拥塞,超出可能拥塞。
那么一次向目标主机发送的量(滑动窗口的大小)=对方16位窗口大小和拥塞窗口大小的较小值。那么滑动窗口的结束下标=开始下标+对方16位窗口大小和拥塞窗口大小的较小值。
拥塞窗口增长速度,是指数级别的。“慢启动” 只是指初使时慢,但是增长速度非常快。
为了不增长的那么快,因此不能使拥塞窗口单纯的加倍,此处引入一个叫做慢启动的阈值,当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长。
当TCP开始启动的时候,慢启动阈值等于窗口最大值,在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回1。
为什么一开始要指数增长?
指数增长前期慢,意味着前期全部主机都可以发送少量数据。当摸清当前的网络拥堵状态,如果都可以发送成功,就需要尽快恢复网络通信的正常速度。当增长到一定程度,就让它正常的线性增长。
如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小。假设接收端缓冲区为1M,一次收到了500K的数据,如果立刻应答,返回的窗口就是500K。但实际上可能处理端处理的速度很快,10ms之内就把500K数据从缓冲区消费掉了,在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来,如果接收端稍微等一会再应答,比如等待200ms再应答,那么这个时候返回的窗口大小就是1M。
一定要记得:窗口越大,网络吞吐量就越大,传输效率就越高。我们的目标是在保证网络不拥塞的情况下尽量提高传输效率。
那么所有的包都可以延迟应答么?
肯定不是。数量限制: 每隔N个包就应答一次。或者时间限制: 超过最大延迟时间就应答一次。具体的数量和超时时间,依操作系统不同也有差异,一般N取2,超时时间取200ms。
这幅图就是以每隔2个包就应答一次。
在延迟应答的基础上,我们发现很多情况下, 客户端服务器在应用层也是 “一发一收” 的,意味着客户端给服务器说了 “How are you”, 服务器也会给客户端回一个 “Fine, thank you”,那么这个时候ACK就可以搭顺风车,和服务器回应的 “Fine, thank you” 一起回给客户端。
创建一个TCP的socket,同时在内核中创建一个发送缓冲区和一个接收缓冲区。调用write时,数据会先写入发送缓冲区中,如果发送的字节数太长,会被拆分成多个TCP的数据包发出。如果发送的字节数太短,就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其它合适的时机发送出去。接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区,然后应用程序可以调用read从接收缓冲区拿数据。另一方面,TCP的一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这一个连接,既可以读数据,也可以写数据,这个概念叫做全双工。
由于缓冲区的存在,TCP程序的读和写不需要一一匹配。例如:写100个字节数据时,可以调用一次write写100个字节,也可以调用100次write,每次写一个字节。读100个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次read100个字节,也可以一次read一个字节,重复100次。
首先要明确:粘包问题中的 “包”,是指的应用层的数据包。
在TCP的协议头中,没有如同UDP一样的 “报文长度” 这样的字段,但是有一个序号这样的字段。站在传输层的角度,TCP是一个一个报文过来的,按照序号排好序放在缓冲区中。站在应用层的角度, 看到的只是一串连续的字节数据。那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分是一个完整的应用层数据包。
那么如何避免粘包问题呢?
明确两个包之间的边界。
1.对于定长的包,保证每次都按固定大小读取即可。
2.对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置。对于变长的包,还可以在包和包之间使用明确的分隔符。
对于UDP协议来说,是否也存在 “粘包问题” 呢?
对于UDP, 如果还没有上层交付数据,UDP的报文长度仍然在。 同时,UDP是一个一个把数据交付给应用层。就有很明确的数据边界。站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收,不会出现"半个"的情况。
当主机间正常通信的时候,突然进程终止(比如服务器崩溃了),该怎么办呢?
进程终止(机器重启):会释放文件描述符,因为文件描述符是跟着进程的,在此之前仍然可以发送FIN,和正常关闭没有什么区别。
机器掉电/网线断开:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset。即使没有写入操作,TCP自己也内置了一个保活定时器,会定期询问对方是否还在。如果对方不在,也会把连接释放。
为什么TCP这么复杂? 因为要保证可靠性,同时又尽可能的提高性能。
可靠性:16位校验和(校验和不通过,则认为数据有问题。可以防止比特位的翻转)、序列号(按序到达、去重)、确认应答(确保之前的数据被收到了)、超时重发、连接管理(必须3次握手和4次挥手)、流量控制、拥塞控制(让包在往里走不大量丢失)。
提高性能:滑动窗口、快速重传、延迟应答、捎带应答。