传输层协议

一 传输层前置知识

        1 应用层和传输层

      我们现在只要知道传输层协议负责将数据发送给对端,怎么发送后面提,我前面说过了应用层,应用层就是把自己的报文发给传输层,不是直接发到网络,同理后面学到ip层我们也就知道传输层也是交给下一层,应用层就在我们写的代码中,而传输层在os中,这句话有助于我们分清楚传输层和应用层。

        2 再谈recv和read

        我们之前调用的recv,read我们以为是和网络交互,调用send,write也以为是和网络交互,其实os在内部会提供读写缓冲区,上述函数都是和这两个缓冲区打交道。我们应用层把数据给了传输层就不关心了,怎么发这都是传输层的发送策略的事。学习过程中我还接触到了阻塞式和非阻塞式套接字,用阻塞式套接字调用recv,send这些输入输出api的时候,如果资源未就绪,会陷入阻塞,而非阻塞套接字则会直接返回,返回值为特殊的宏。

        3 再谈端口号

        0-1023:知名端口号,HTTP,FTP,SSP等这些广为使用的应用层协议,他们的端口号都是固定的。1024-65535:os动态分配的端口号,例如客户端程序端口号。可是主机间不是分离的吗,http在服务端用的80端口,为什么我客户端上不能用呢? 因为os上不知道你这个主机会搭载客户端还是服务端程序,所以在所有主机上都不能由你占用端口,以免这个主机上无法搭载应用层协议的服务端。

        4 网络指令了解

netstat,选项介绍如下,一般都是直接-nltp,我们对比一下看看一个个选项。

        -n拒绝显示别名,能显示成数字就显示成数字,如下,把协议名用端口号代替。

        -t,-u分别表示和tcp和udp协议的进程。-p,process进程,表示显示进程相关的信息,例如pid啦。

-a 可以把所有进程显示出来,不会仅仅显示用户起的,会把自己云服务器上的所有连接显示出来。

        pidof:通过进程名查看进程id,grep text会把进程中包含test都搜出来,比较乱,用pidof应该可以过滤不少其它的进程信息。

二 udp协议

        学习udp协议和先前的http,https一样,我们只要了解一堆数据报文中如何把一个完整的数据报文读出来,又是如何解析报文的即可。如下,udp报文由8字节报头和数据构成,这个数据就是应用层报文。

        数据怎么给的,send或者write给的。那接下来我们还有几个问题。

1 报头和有效载荷如何分离,报头固定长度 8字节,报头字段中16位UDP长度用记录着udp报文长度,长度减去八字节,剩下的就是载荷。

2 如何向上交付

        当服务端的传输层收到了下层传递的udp报文,如何向上交付给指定进程呢,有目的端口号就可以找到指定进程。 16位校验和,这个保证在传输时没有缺漏,修改

3 可是图中的报文本质是什么呢? 什么是封装,解包呢? 其实所有的报头都是struct,可能是结构体 或者位段,属性字段就是对应的成员,那前面八个字节就一定能涵盖所有报头吗,想想内存对齐,上面八个字节可以用两个int表示,内存对齐不同平台可不一样,那8个字节就不一定能够读取完udp长度等字段可以,之前是为了空间换时间,现在为了一致性,我们必须让网络报文的结构体大小一样,所以就不允许对网络报文做内存对齐了

        而之前应用层读写,我们却不能直接发一个结构体,不能让上层发的数据也没有内存对齐吗? 我想这就会引发一个新的效率问题,从网络中写入的数据和读取的数据都要消除内存对齐,报头就几个字节,但是有效载荷却可以很大,会导致cpu在处理网络来的数据时因为没有内存对齐而效率降低。 

三 udp特点

        无连接,不可靠,因为不关心数据是否达到对端,不关心是否和对端链接,还有面向数据报。

        如何理解面向数据报? udp是知道报文长度的,所以每次交付的一定是独立的,完整的报文,不是说不考虑可靠性吗? udp只会向上交互完整的,不完整的会因为udp校核失败,直接丢弃,简单理解就是它收到完整报文就交付并非表示可靠性,还不够被称为可靠,可靠应该是丢失了会让对方补发重传的,而且我们udp通信的时候是发送端调用一次sendto发10字节,如果接收端调用recv接收只接收一个字节,我们再次调用recv不会读到剩下的9字节,这9字节都在上次读取中被丢弃了,udp底层就是这么设计的,每次向上交付一个完整报文,如果我们没读完整,剩下的就直接丢弃了。

        udp无发送缓冲区,因为不考虑可靠性,所以直接把一个数据报给下层了,不需要一个发送缓冲区来暂存发送数据,还有一点是tcp用这个缓冲区是用来补发重传的,udp不考虑重传,而且网络带宽高,可以容纳数据,所以udp直接向下交付,可是有收数据缓冲区, 因为上层可能来不及处理,所以需要暂时存储,缓冲区满了就开始丢包。

        接收缓冲区不保证接收报文顺序,因为数据在路上的情况是不同的,例如我这边先发了5,4,3,可能3先到了,udp不会按顺序排列接收的数据。

        全双工概念: 就是可以边读边写,是线程安全的,因为读写有不同的缓冲区,虽然udp没有写缓冲区,但是也不会去用读缓冲区,显然半双工就是读写共用一个缓冲区。

        什么是无连接,这个点非常有意思,我们udp每次发送数据的时候都要指明目的ip和端口,而tcp通信则是只需要read,和write,就是因为tcp是有链接的,发的时候不用每次指定ip和端口,还有就是udp服务端收数据的时候一个套接字接收所有客户端的消息,而tcp套接字(除了监听套接字)在接收数据时则是和客户端绑定的,只接收某个客户端发来的数据。

四 Tcp协议

        还是回答几个问题1 报头和有效载荷如何分离 2 如何读取一个完整报文 3 有效载荷如何交付。

        1 分离报头载荷

        无选项时报头就是二十字节,和udp一样,直接读二十字节,报头就读完了,但是tcp我们还得知道选项的大小。

        这个位置是用位段成员记录报头的行数,一行四个字节,所以行数乘以4就是报头+选项的总大小,则选项最大可以有四十个字节,因为4个比特位最大是15,也就表示60字节。所以我们可以直接提取前二十字节,获取首部长度字段*4 - 20,就是选项长度。

        有效载荷长度呢? 首先,这个有效载荷就是应用层报文,tcp报头中没有记录有效载荷的长度,答案在最后tcp总结中的粘包问题提及,tcp就不需要有效载荷长度。选项也是存在结构体内的吗?是的,放在柔性数组中,包含柔性数组的结构可以由malloc来分配空间。

2 tcp可靠性

        接下来我们有些报头字段和tcp可靠性和提高效率有关,我们得先了解了解什么是可靠性,什么是不可靠,丢包,乱序,重复(补发的重复报文如何处理,我认为是丢弃),校验报文,发送太快太慢,网络出问题都算不可靠。

        为什么会有这么多不可靠问题,就是因为距离变长了。下面会一个个说如何识别上述问题和解决,解决不难,难的是识别。

        如何确认丢包呢? - 确认应答机制确认应答就是客户端发数据后,服务端收到后给客户端发一个确认消息。这个确认消息就是应答,又称为ACK客户端收到应答后就知道服务端收到消息了,如果等待了一段时间还没收到应答就会启动超时重传,(每次发送报文后,都会启动一个定时器RTO,这个时间是随着一直没收到报文会逐渐增加,重传多了就关闭链接了)那是否要确认ACK是否被收到呢? 难道再发一个确认,那这个确认要不要也被确认一下,显然总是会有一个最新的消息无法确认

        确认不了所有数据是否收到,也叫有可靠性吗,算,因为我们能确认消息是否送到,只要有应答消息我们就认为消息到了,客户端没收到应答就超时重传,后面讲完应答序号我们会理解,应答丢失不一定会超时重传,这是一种局部的可靠性。好吧,你说服务端收到消息会给客户端发应答,那服务端给客户端发消息,如何确认客户端是否收到呢,客户端也会发应答呗。

        那服务端客户端发一次报文就一直等着应答啥也不干吗,怎么可能,实际上它们是并发发送多个报文(滑动窗口会提及),然后响应是陆续到来的,这样每个报文在网络传输的时间是重叠的,等待多个响应的时间就也是重叠的。

        服务端能发多个报文,这些报文虽然是先后发出的,但是不一定保证谁先到客户端,那客户端也就会收到多个报文,那如何保证接收顺序呢? 显然我们需要给报文编号,任一端收到报文后都要根据报文编号排序,这样就会按顺序处理报文了。

        还有就是客户端还会收到多个确认应答,应答如何和发送报文一 一对应呢,我怎么知道这个应答是对哪个报文的应答,上面刚说报文是有编号的,例如10号,那我应答的时候在确认序号写个10,这样收到报文一解析就知道10号报文已经送到了。具体设计是不是这样,还得了解下面两个字段。

        报文编号是怎样的,例如发了1000个字节的数据,编号应该填什么? 和报文在内存中的地址有关,后面讲面向字节流我们就知道了。

        32位序号是报文编号,确认序号是用来确认报文编号的,为什么是两个字段?发应答也是在发符合tcp协议的数据,所以一定是一个tcp报文,由于可能不携带数据,我们就认为这是个tcp报头,会对报头中的ACK标志位置1,表明是个应答报文,方便识别报文调用对应的方法,这不就是c++中的多态吗,根据对象的类型调用不同的方法。

        看完上面我们可能对应答序号还是模模糊糊,还得接下来解释一些问题我们就能理解应答的大部分知识。

3 确认序号

        客户端发送报文,编号为10,保存32位序号中,服务端应答报文在确认序号填11,为什么发11等会说,

确认序号含义: x - 1之前的报文都收到了,确认序号含义有点奇怪,怎么不是发10,然后表示10号报文收到了呢?

        我说个场景,我们就能体会上面这种奇怪的确认序号含义的意义了,当我们客户端发送了10和11号两个报文,应答编号为11和12号,如果应答11丢失了,但是12号却到了,此时我们也可以识别出数据报文没丢失,是其中一个应答丢失了,显然我们允许少部分应答丢失,这一点非常关键,如果是一个报文一个应答,一旦应答丢失,那客户端以为数据报文丢了,就要重传数据报文,这比较低效,而上述设计中,只要后面的应答到了,我们就知道不是报文丢了,是应答丢了,不用重传,可以更加细粒度地判断收不到应答的原因,减少重传次数。那确认报文也可以发10号啊,表示0-10号的报文都收到了,都行,只是大佬采用了发11号,并表示10之前的报文都收到了。

        好吧,那为什么要有两个字段,我客户端发报文只用序号,发应答只用确认序号,那是不是会浪费四个字节,我们的报文设计之所以同时有确认序号和序号,是将两个类型的响应合并成一个了,既是发送消息,也是发应答,就不用发送两次了。这种称为捎带应答。此时我们大致理解了序号和确认序号,但是实际应该填什么,我们还不理解,下面会反复强调。

4 流量控制

        接下来还剩下三个字段,我们逐一解释,先来说说16位窗口大小。

        这个要结合先前说过的缓冲区来说。任何一端收消息都是将数据放在缓冲区,等待上层处理,若是某一端的接收缓冲区被打满了,后来的报文就会被丢弃,虽然会被补发,但是被丢弃的报文已经消耗了很多的网络资源才到达的对端,直接丢弃太低效了。

        我们应该让发送方知道对端的接收缓冲区是否充裕,这就是流量控制。

如何控制,在应答报文的窗口大小字段填入自己接受缓冲区剩余空间大小,这样发送端发送的时候就不会发超过这个空间大小的报文。可以变快也可以变慢,例如剩余空间低于某个阈值,我们一次就少发几个报文,高于某个阈值,一次性就多发几个报文。

然后我们有几个问题要再次强调,tcp发送缓冲区就是和read,write打交道的缓冲区吗? 是的,然后序号就是有效载荷在缓冲区的首字节, 所以缓冲区内的都是有效载荷? 是的

5 标志位分析

        再来说说那六个标记位。

        服务器会在同一个时间段收到各种各样的报文请求,例如应答和完整请求,断开连接请求,如何区分这些不同的报文请求,就是靠标记位。根据不同类型的报文,上层对应不同的处理动作,所以就有了标记位,本质是什么?  这就是多态。在大量的工程实践中,多态特性时常被用到,所以c++,java这些高级语言在设计时实现了多态特性。标志位具体含义如下。

ACK被置为1,这个报文可能是纯ACK,也可能是捎带应答。

SYN:表明是链接请求的报文。后面我们讲tcp三次握手我们就要用到这个SYN,在三次握手完成前,我们不能发数据报文,只可以发链接报文,所以必须要能区分数据报文和链接报文。

FIN:链接断开报文

PSH: 先前说接收方会给发送方发个应答,应答报文内部的窗口大小会记录接收能力,如果接收方没剩余,那发送方就不能再发数据,那接收方就没有应答给发送方,我该如何知道接收方的剩余空间呢? 此时发送方就会定期发一个询问报文,让对方发个应答看看有空间了没,如果一直都没有,(注意,这里面缓冲区放的是tcp报文的有效载荷,问询报文等不带有数据的报文被识别后就直接响应了,不会有数据要放到缓冲区)发送方会发一个特殊报文,将PSH标志置为1,催促服务端快速处理数据。如何让服务端快速处理数据(多路转接)。

RST: 若是双方建立好链接了,服务端突然释放了自己的通信链接,例如系统崩溃,然后管理的进程肯定都没了,更何况网络链接,然后服务器网线断了,此时也就无法给客户端发断开连接的报文了,后来服务端网线好了,此时客户端给服务端发数据时,此时服务端认为双方没有三次握手,就会发一个RST(reset)报文,让客户端断开连接,重新开始三次握手。

URG和紧急指针的作用

        首先接收缓冲区就像个生成消费者模型,内部是队列,按照序号排列,这样上层拿的时候就会按顺序拿,如果我们想插队呢? 就是让这个报文带上URG。紧急数据大小呢? 因为仅能有一个字节。在同一报文中还会有普通数据? 是的,所以如何找到紧急数据呢? 就是紧急指针的作用啦。

紧急数据作用? 一般是状态码,可以表示暂停,取消上传等状态。当上层取走后,报文要继续入队列,但是本身属性要做修改,因为紧急数据已经被处理了。这个紧急数据又称带外数据。是普通报文额外携带的数据。如何读到带外数据,flag置成某个标志位即可。所以tcp层收到这个报文的时候会检测这个标记位,就直接读紧急数据,剩下的数据报文就放到缓冲区。

        MSG_OOB就表示从其它输入流获取紧急数据。

        但是有点鸡肋,传输数据过少,使用场景,有时候我们取消下载,取消上传,都是立刻就获得响应,这个其实就适用紧急指针,如果没有紧急指针,服务端响应我们取消的动作会比较慢,因为我们发的取消请求还在排队,客户端就还在执行下载上传,就有点影响用户体验。

五 链接管理机制

    1 三次握手

        问题:1 什么是连接 2 为什么是三次

服务端可以和多个客户端建立连接,而连接是os中的tcp协议帮我们做的,所以在os内会存在多个建立好的连接,我们要把这些连接管理起来,要管理,那就要将连接的属性信息记录下来,例如源端口,目的ip,状态信息。那所谓的链接,不过就是一个struct结构体。

客户端发起connect就是在发起三次握手,服务端accept阻塞等待三次握手完成。从图中我们可以看到在三次握手中客户端和服务端的状态变化。

        如果第一个报文丢失,显然客户端直接超时重传,第二个报文呢? 服务端客户端都收不到应答,都会超时重传,第三次若是丢失了,由于此时客户端一发出就开始维护连接数据了,如上图,状态已经为链接状态,成本就来了,那之后会做什么呢? 此时服务端也会超时重传,也有可能客户端先发消息,导致连接重置,因为服务端没收到最后一次应答,就认为还没链接还没建立好。服务端收到应答才会转为连接状态。

问题2 为什么是三次握手,1,2,4...次呢?

        大多数情况是客户端发起请求,很少由服务端主动和你建立链接,此时如果是奇数次握手,那就是客户端发最后一个报文,所以这个报文发出后客户端的状态就是已经建立好链接了,而服务端是收到报文才建立链接,可是最后一个报文若是丢失了,客户端就要维护这个异常链接。

        如果是偶数次握手,客户端可以在服务端建立了链接后就不接受服务端报文,此时服务端就要维持这个异常链接一段时间,而客户端却没有任何压力,所以就要奇数次握手,让客户端先维护链接,服务端才愿意也分配资源来维护链接。因为服务端会面临很多很多客户端,如果这些客户端都能无成本让服务端维护链接,那对服务端来说是很容易被攻击的漏洞。

5次,7次呢?,因为三次握手是最小代价验证双方通信信道是通畅的,都是可以收发消息的。其实三次握手是一个四次握手。把中间那一次拆成两次不就变成四次了,当然一般是合并转发的。

2 四次挥手

        四次挥手,中间可以合并吗?可以由于断开连接的时候,可以不用立刻断开,要给双方一个冷静期,都同意断开才可以。为什么是四次,因为达成断开连接的共识最小成本是四次,所以5,6,7就不行了,成本增加了,那123就更不用说了,都无法达成共识。

        注意,一方close,另一方也会read读到0,一般也会选择调用close,所以close一般都是直接四次挥手了,不过在还没调用close之前,这一方认为还没断开,还是可以给另一方发数据,shutdown则可以双方自主选择关闭读端,写端和读写端,此时并没有释放文件描述符,只是对链接做了处理,如果客户端关闭了读端,此时服务端还是可以写,然后客户端还可以读。

        最重要的是四次挥手状态变化,首先主动断开连接的一般是最后一个发应答。

        time_wait状态:客户端主动断开,发出最后一次挥手并没有直接断开连接,而是等待了一段时间,后面会做演示再提及。

        服务端close了才会触发后面的两次挥手。

        之前的三次挥手丢了都没事,各自认为连接还在,可以一直补发,但是第四次丢了就麻烦了,因为主动关闭方发完ACK会等待一段时间,尽可能地把ACK发过去,如果一直没发到给服务端,双方各自等待一会就关闭了呗。

        中间的ack,要不要做应答呢? 没必要,假如左边是客户端,右边是服务端,客户端一发fin断开链接,此时客户端知道自己要断开了,服务端也知道客户端要断开了,就足够了。

        我们来看看time_wait状态测试,用先前写的http代码。

        服务端一直在start函数中创建线程,然后服务端线程读取数据一次就结束,让服务端不close文件描述符。

        此时由于主线程没退出,所以此时即便是次线程退出,文件描述符也不会被关闭

此时我们用先前介绍的netstat命令来查看网络状态。

         然后我们用telnet去链接,注意,这个listen状态的是监听套接字,下面的才是我们通信创建的套接字。

链接状况如下。

        双方已经建立好链接了,从我们后面telnet已经可以发请求可以看出,为什么有两个链接显示? 只是一个链接,将两个方向都显示了一下。因为都在一个云服务器上跑啦。

        然后我们直接把客户端关闭,此时服务端没有close文件描述符状态变化如下。

        终止客户端进程,客户端主动断开链接,所以客户端链接就处于FIN_WAIT2状态,此时服务端已经发了ack了,但是没有调用close,就不会断开连接,也就没有后面的两次挥手,所以服务端状态一直卡在CLOSE_WAIT。但是客户端如果看到服务端既不发数据,也不发fin,过一会自己也就消失了,

        若是此时我们把服务端进程也结束了,相当于调用了close,文件描述符就会被回收,所以会立刻进入LAST_ACK,然后发fin,此时客户端已经关闭了,所以服务端的fin一直到不了客户端,那服务端肯定收不到应答,可能会一直重发FIN,超过一定时间一直没等到应答,也就自己关闭了。上面这个过程绝可能是毫秒级别的,我没在服务端看到LAST_ACK状态。此时客户端链接还没消失,就进入TIME_WAIT状态了。

        所以我们一定要close掉文件描述符,不然会存在大量处于CLOSE_WAIT的链接,这会使得可用链接越来越少,而自主释放的时间至少是两个小时,可调整。LAST_ACK,FIN_WAIT,TIME_WAIT状态是不稳定的,因为用户已经调用了close函数,要关闭连接了,os过一会就会回收了。

        注意,我们现在是客户端先断开连接,状态为FIN_WAIT,后续会变为TIME_WAIT,都是一种等待,过一会就会自己断开链接,

        若是服务端是主动断开连接,链接最后会处于time_wait状态,此时链接还是存在,数据结构管理的对象还没销毁,所以我们再去绑定同一个端口号就会绑定失败(后面会解决)。

        那为什么要有time_wait状态后面提,为什么主动断开的要进入time_wait状态和前面都是一个问题,还有time_wait时长,我们后面解答。一般都是客户端主动关闭,服务器有没有要主动断开场景,然后服务端才会有time_wait状态,如下。

        服务器挂了后,所有的通信都是服务器主动断开,那就服务端会进入time_wait状态,此时服务器挂掉后应该快速重启,可是如何重启? 源端口都被占用了,难道只能等链接消失,如何解决?设置套接字属性,第二参数表示对哪一层协议进行设置,例如对网络层的缓冲区属性设置。

        第三选项是指ip端口全复用,带不带SO_Reusrport都行,然后第四个参数将传一个真传递过去,表示将该属性设置为1,为1就是有这个属性,为0就没有,要传的是监听套接字? 因为我们是拿监听套接字和端口号绑定的。可以理解此时这个端口号是和监听套接字强相关,如果拿通信套接字去设置就会无效。

        然后我们再次启动进程时可以复用端口。

LAST_ACK,FIN_WAIT持续时间不是特别清晰,但可以肯定不会长久存在,而TIME_WAIT时长2msl。为什么TIME_WAIT时间是2msl?

大多回答是说保证两个传输方向上尚未接收报文已经消失,免得被新进程收到,因为报文可能在路上,然后进程重启,就收到了上个进程的报文,对当前进程来说该如何解析,未知,出的错误一定是未知的。

        我一开始还疑惑为什么会有两个传输方向上的报文,因为我觉得既然客户端已经是TIME_WAIT状态了,怎么服务端还会发报文吗,会的,假如最后一个ACK对方没收到,那就会启动超时重传,有意思的这个超时重传的时间不是固定的,一直没收到就逐渐增加等待时间,但是最大就是2MSL,超过就直接断开连接了,所以为了保证我们能收到这个重发的FIN,就直接等2MSL,保证在最长超时重传下也能收到报文。

        虽然客户端发完最后一个ACK,等MSL可以让ACK和服务端之前发的报文消散,但是超时重传最大是2MSL,所以我们必须要保证最坏情况下还可以收到重发的FIN报文,MSL和2MSL就取大的那个了。

所以为什么要等待呢?就是为了保证接收到服务端的FIN数据报文,不然立刻重启后可能会有历史报文给到新连接。同时也是为了尽可能地把最后一次应答发给对方,走正常的断开连接流程,如果不等,那对方异常断开的概率就会增加,就不够可靠了。要更改TIME_WAIT时长一般要调整内核,我试了没起效。

3 滑动窗口

        首次发送消息,如何知道对方的窗口大小? tcp握手就已经做过报文交换,此时就可以知道互相的窗口大小。非常巧妙的是此时并没有任何的数据,就不会出现第一次多发少发的情况。前面说过接收发送缓冲区都是TCP报文的有效载荷,而tcp握手的报文一般是不携带数据的,所以就不会往缓冲区放数据,这种方式称为窗口探测,而流量控制中也有在报文中把窗口大小告诉对方,都是询问对方剩余缓冲区大小。

       所谓的滑动窗口其实是在发送缓冲区的一部分,而且滑动窗口内的数据才是真正要发送的,这个缓冲区内的都是http报文,显然这个滑动窗口大小绝对小于对方的接受能力。(如何确定滑动窗口大小下面提及)

发送缓冲区构成如下图。

        此时我的发送缓冲区分成三部分,左侧是覆盖区,是已经发送了的也已经确认了的,中间是可以发送的窗口期,后面是未发送的数据。接下来说说发送数据的一般情况,首先可以分多个报文将窗口区数据往下层发送,不用等待应答,窗口内的数据可以一直往下发,一旦有应答了,窗口就要往右侧滑动,因为对方已经收到了,所以这部分数据就没有必要保存。

滑动窗口大小和对方的接收能力有关,对方接收能力就是由应答报文中的win窗口大小知道的。滑动窗口的数据就是上层放的,往哪放,就是往覆盖区放,我们再来总结总结滑动窗口的特点。

1 滑动窗口显然只能右滑

 2 滑动窗口显然可以变大变小,如何变化的,就是依据对方响应中的窗口大小调整,那滑动窗口是如何被管理的,就是管理两个下标呗。

3 越界了怎么办,我们只要每次滑动对下标进行取模,就可以保证不越界了,就像环形队列那样。由于我们可以保证应答是按序到达的(如何保证估计和ip层有关),所以我们就直接用应答报文字段中的确认序号来更新滑动窗口。

seq是确认序号,win就是窗口大小。

假设窗口内数据由三个报文发送,某一个丢失了,滑动窗口内应该如何滑动? 

        如果第一个报文只是应答丢了,我们收到了后面的报文应答我们就知道第一个报文没丢,真的丢了的话,服务端应答只会发收到的序号,也就一直发101,因为100-200没收到,我们识别到序号不变,就会用应答给的101序号从头重传,重传的数据在哪,不就在窗口中保存着吗? 所以窗口滑动是随着应答的,免得数据进入覆盖区被覆盖了。如果中间报文丢失就类似第一个报文丢失情况,因为窗口滑动,就变成第一个报文丢失了,同理最后一个报文丢失也是如此。

4  面向字节流

       面向字节流很早之前就出现了,直到现在才能说清楚,假如一个完整http报文是400字节,滑动窗口只有两百,如何发一个完整的http报文,tcp面向字节流的意思就是tcp不会携带一个完整的http报文,对于tcp来说,滑动窗口内就是一个个字节,每次发报文就携带一定量的字节,这就叫面向字节流,拷贝到对方的接收缓冲区后,由上层解析。缓冲区内每个字节天然有下标(相对缓冲区首字节),教科书上:数据报文的起始字节的下标就是tcp报文序号字段

六 拥塞控制

1 快重传

        超时重传要等上不短的时间,那我们就会一直收到重复序号的报文,而且要等上一段时间才可以重传,所以为了提高效率,就有了快重传。

        我们发送缓冲区发出的数据是在对方的接收缓冲区是按顺序排列的,如果中间的200 - 300没收到,后面的报文也会保存下来,此时会一直重发201,我们收到一定量的报文的确认序号都是重复的,此时就启动快重传,会将这个序号后面的报文再发一次。注意:超时重传是兜底的保证机制,因为有可能我们连应答报文都收不到。

2 网络问题

        我们tcp通信不仅可靠性非常优越,它的效率很不错,其中就体现在解决网络问题上,首先肯定是只能解决部分问题,不可能解决所有问题的,例如中间路由器失效,协议能解决个鬼。

在进行通信时我们不仅要考虑对方的接收能力,还要考虑网络的接收能力,网络中有多个发送方和接收方,这些主机都遵守tcp,ip协议,所以一旦网络出现问题,所有主机都会出现丢包重传,那就会导致网络更加拥塞,所以我们的协议应该判断出网络接收能力,在网络拥塞的时候减少报文的发送,如何判断网络的接收能力呢? 拥塞窗口提及。

        不过此时又不能干等,TCP引入了慢启动机制,先发少量报文,如果这些少量都丢了,那就用超时重传机制,因为报文比较少,重传不会加重压力,而且等待时又给了网络缓解的时间。如果没丢,慢启动中,会逐渐增加报文转发,直到某次又出现大量丢包了,就又减少报文转发数。此时我们大致认识了网络通信中的网络问题,并了解到慢启动机制下面我们来认识拥塞窗口,来加深我们对慢启动机制的理解。

3 拥塞窗口

        发生了网络拥塞,发送方要基本得知网络拥塞的严重程度,网络拥塞程度我们用拥塞窗口来描述。注意: 网络的状态是变化的,所以衡量网络拥塞状态 - 拥塞窗口大小应该是变化的。作为主机我们只能一次次探测,时时刻刻获取网络状态。

        接下来我们就说说探测网络健康状态- 慢启动机制介绍一开始先发1个,都没丢,那我们发送方就对拥塞窗口大小*2,也就是发两个,依次类推,我们发送报文数是指数增长的,这种初始慢发报数量的恰好可以给网络缓冲时间。

        可是我们控制发送报文数不是和滑动窗口相关吗,我们怎么一开始发一个呢,别急,先前说滑动窗口是由对方的接收能力来决定,也就是应答报文中的窗口大小,现在我们说了网络,那我们也要考虑网络的接收能力,所以滑动窗口就是这两者的最小值,拥塞窗口大小显然是由发送方维护。网络能接收一个主机发送的报文数,没有出现丢包,那报文数就是拥塞窗口的大小。

        所以我们只要将拥塞窗口大小设为1,就能只发一个报文啦。   

接下来我们根据下图将探测网络状态流程再梳理一下。当我们主机发现通信出现大量丢包的情况,此时就会启动慢机制,这是前因,此时我们的拥塞窗口设为1,逐步探测网络状态,每次发报文只要报文未丢失,我们的拥塞窗口就会逐渐翻倍增加。

        当然以免拥塞窗口增长的太大,我们还设置了一个阈值,之前是翻倍,后面按照线性增加,设计者设计出这个阈值是想让我们更好摸清楚网络的极限状态,让我们最大限度的利用网络资源发送,我举个例子,假设对方接收能力很强大,网络转发有限,如果没有阈值,我们就会很快超过转发极限,而后又从少量报文开始转发,这就有点低效,阈值之后我们就可以增长的不那么快,可以在接近极限下多转发几次,那就可以把更多报文转发出去。

一旦出现网络问题,慢启动下,第一次阈值一般为16,大佬规定的,随后在某个拥塞窗口下又出现网络问题,阈值变为拥塞窗口/2,然后拥塞窗口设为1。

 如果不理解是如何探测的,就结合实际理解,我们是如何得知饭堂多不多人排队的,就是去看看,如果要得到各个时候的排队状况,就要经常去看看。

4 延迟应答

        如何进一步提高发送效率? 如果我们接收方给发送方一个更大的窗口,虽然发送方还要考虑拥塞窗口,但是更大概率发送方会并发发更多的数据。

那如何更新出一个更大的接收窗口? 只能让上层尽快交付,才有可能更新出更大的接受能力,也就是给上层更多时间来进行读取,接收方采用延迟应答,给上层留出更多时间读取,这样接收方的接收能力就更大了。这个和多路转接有点关系,我们现在只要认为给的时间越多,上层就会处理数据。延迟应答有按数量应答和按时间延迟应答。

七 tcp总结

1 可靠性机制总结

        tcp如何保证可靠性的,1 确认应答机制 2 序号和确认序号,3 超时重传(一定时间内没收到应答就会重传,这个时间是要根据距离实时计算的),tcp序号由来在滑动窗口曾提及,可以保证报文按序在缓冲区,4 流量控制和拥塞控制则是为了保证报文能被对方接收 5 链接管理 尽可能保证通信是畅通的才通信,6 校验和。

        提高效率策略如下。滑动窗口如何提高效率呢,这是和其它的设计方案比较的,以前我们认为是从缓冲区取一个报文去发,收到应答后发下一个,现在有了滑动窗口的设计后,一个窗口内的数据都可以发送出去,不用等应答,再发下一段,这使得我们我可以并发发送多个报文,而且应答到来的时候,我窗口就向右滑动,一边发一边等应答来。剩下的提高性能策略就好理解了。

        快重传则是减少等待的时间,延迟应答是为了更新出一个较大的窗口,可以让对方的滑动窗口更大,一次性更新出更多的数据。所以tcp在网络情况好的时候,不一定效率比udp差。

2 粘包问题

        tcp存在粘包问题,因为全部当成一个个字节的数据,需要上层解析出一个完整的应用层报文。例如应用层协议规定一个http包报头上应该有个长度,这样我们上层解析报头读取到长度,就可以读取一个完整报文,就解决了粘包问题。 ip报文中有tcp报文长度,这样就能切割出一个完整的ip报文,而且也能分离tcp报头,这样就只剩下有效载荷(http报文)按顺序放在接收缓冲区中。按照序号来放。

而udp不会存在粘包问题吗?  不会,因为是一个一个报文给上层的,报文之间就有了边界。注意tcp没有有效载荷长度,因为不需要,tcp才不关心有效载荷多长,首先报文不会出错,去掉报头后,剩下的一定是载荷,而分割出一个tcp报文则由ip完成,至于多个载荷如何处理,上层决定。

3 tcp收尾

        我们在关机时,os会提示我们杀掉正在进行的软件,所以和正常关闭进程一样,走四次挥手断开连接,但是如果是网线断了,一方会释放链接,但是对端不知道,只能下次链接发消息才知道,才会有reset报文。

        可是如果客户端不发消息,服务端也会定期检测链接,保活链接,没有应答也会释放,如果网线突然恢复了,然后客户端收到服务端的数据报文,但是客户端认为连接已经没了,所以就会给服务端发reset。这再次说明双方地位是平等的,谁都可以发reset让对方重置。

有时候客户端向服务端进行链接,然后一直不发消息,此时服务端就要维护我们的链接,我们就像是占着资源不使用,tcp是不可以关闭链接的,只能由我们程序员自己根据具体场景断开连接,也就是在应用层进行管理,例子: qq长时间不发消息头像就变灰色。

4 listen第二个参数

        我们先自己搭一个tcp客户端和服务端,并且让服务端处于listen状态后不accept,不对链接做处理,这一点非常关键,要是我们accept了,会对我们的测试有影响,后面提。

        之前backlog传了不大不小的数。

        现在把这个数调小,例如调成1,随后先用两个客户端telnet去链接。如下图,两个链接显示已经建立好了,

        再来一个客户端。

        此时我们发现这个58036的进程认为连接建立好了,状态是established,但是服务端认为没有,而且状态还是SYN_RECV(半链接状态)。可是三次握手的时候,此时服务端不是收到ack就变成established?显然此时服务端并没有处理这个ack,并非是没有accept导致的,和全连接队列有关。

        tcp会在底层维护一个全连接队列和半链接队列,全连接长度是listen的第二个参数+1,进入队列的链接就进入了全连接状态,所以如果队列满了,剩下的客户端来链接就不能进入全连接状态。

        全连接队列意义? 这个队列不能太长,也不能没有,如果没有这些半链接,全连接队列,当我们的服务端还无法处理链接时(也就是没来得及accept),外部又没有等待队列,此时链接只能被销毁,再次三次握手也会浪费时间,所以就有了等待队列,可以保证服务端空闲下来要获取链接处理时,可以直接从等待队列拿了。如果队列太长会让客户端等待太长时间,而且维护链接队列的资源也会增加,不如把这部分用于服务端的处理链接。

        注意:只有变成全连接(状态为established)才会被上层accept带走。还有listen第二个参数+1表示最多维护多少个全连接,不是表示服务端能并发处理的链接数。

        SYN_RECV状态维持一段时间后就会消失,因为此时服务器认为上层压力太大,全连接队列都满了,处理不了新连接了,所以就让新连接维持一段时间就从半链接队列移除了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小何只露尖尖角

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值