一.关于TCP协议

TCP全称“传输控制协议”,是当今互联网手使用最广泛的传输层协议,因为它在保持通信时也可以保证稳定性,并且对高效性也有一定策略,是目前传输层所使用的非常常见和重要的一种网络协议。

问:为什么TCP叫传输控制协议,控制是体现在哪?

我们首先要知道,我们平常所使用的write无论是向网卡当中写入还是向磁盘当中写入都是先向缓冲区当中写入的,并不是直接写进磁盘或者网卡,具体什么时候向磁盘或者网卡写,这个由操作系统和驱动决定的,这就是一种控制的表现,当然TCP协议除了控制什么时候发,还有一些其它控制的手段,比如发多少,出错了怎么办等等,这完全由TCP自己决定,这就是控制。

数据发送的本质就是拷贝,将我们输入的信息拷贝到发送缓冲区。

缓冲区的本质

缓冲区就是内存空间,操作系统为了管理内存,把整个逻辑分成很多4kb大小空间,为了管理这些内存块创建了结构体struct_page。所以接收/发送缓冲区是由单个/多个struct page以及它们所管理的内存构成的。而打开一个网络套接字就是打开了一个文件描述符,每一个文件描述符都有负责管理它的结构体struct_file,这个对象里面就包含对struct_page的管理结构,这样就实现了每个文件都可以向自己的缓冲区中写入,这里面还存在一个指针,该指针指向磁盘就是像文件写入,该指针指向网卡就是想实现网络传输,这样就可以实现上层不用变,只要切换指针指向的内容就可以实现切换内容写入终点。

二.TCP报头字段解析

16位源端口号,16位目的端口号

  • 与UDP一样,用来确认谁发送的,发送给谁的

16位检验和

  • 依然与UDP一样,确认对端传过来的信息是正确的

4位首部长度

  • 用来实现报头和有效载荷分离,他存储的是报头长度而非整个报文的长度,报文的长度由IP层提供,所以既有整个报文的长度又有报头的大小,所以可以实现报文和有效载荷的分离。当然报头长度并非固定的,因为选项长度并非固定的,但是除选项之外报头长度固定大小20字节,所以4位首部长度减去20字节就是选项大小。
  • 四位首部长度1111,最多表示15,但是在计算四位首部长度的时候有基本单位大小,基本单位大小为4字节,所以报头最大长度为60字节。

16位窗口大小

  • 问1:TCP协议为什么不会造成像UDP那样对端的接收缓冲区满了,还在发送信息,导致丢包?
             因为TCP协议在发送之前知道对端接收缓冲区剩余大小,如果对端已经没有足够的空间大小接收我的消息,发送端就不会发送那么大的消息。
  • 问2:发送端是怎么知道接收端的接收缓冲区剩余大小的?
             这个功能的实现就是靠16位窗口大小实现的,16位窗口大小记录的就是对端接收缓冲区剩余大小。
  • 问3:它们在第一次通信的时候,发送端怎么知道接收端发送缓冲区剩余大小?                                 在第一次通信之前会有三次握手的操作,在这个时候会接收端会告诉发送端接收话缓冲区剩余空间大小。

32位序号和确认序号

  • 序号                                                                                                                                             第一次序号的生成是由双方三次握手来确认的,下一个序号的值为上一个信号加上有效载荷     大小,如发送端和接收端双发在三次握手后商定,序号的初始值为1000,当发送端发送第       一 次消息,序号的值为1000,携带了200字节大小的有效载荷,那么下一次发送的报文所       填写的序号为1200。                                                                                                                   当然双方的起始序号不一定相同。
  • 确认序号                                                                                                                                         确认序号可以保证报文的有序性,以及可以防止一些错误报文的干扰还可以允许少量应答丢失。                                                                                                                                                有序性的实现:当双方商定好起始序号后,比如说发送端的起始序号为1000,那么接收                                   端对该条报文的确认序号为1000,那么当发送第一次消息携带的报文为                                   200有效载荷,那么下一次序号变为1201,接收序号变为1201。                      防止一些非法报文:假如说因为一些网络原因,导致信息发送丢了,因为对端长时间没                                            有收到消息出发了重传机制,致使对端收到了正常消息,序号和                                                确认序号都增长了,但这时丢的报文才发送到,这是确认序号就                                                可以起到保护通信的作用,因为在这时发送过来的序号一定是小于                                            确认序号的,那么就可以直接丢弃该条报文即可,致使不让该条报                                            文影响通信。                                                                                               允许少量确认应答丢失:当发送端把消息发送给接收端,接收端应该给发送端一个确认应                                                答的报文,这个确认应答可以使用捎带应答,也可以在是普通的                                                 应答,二者都可能发生丢失,接收端是允许二者少量丢失的,比                                               如说,分别发送了序号为1000,1201,1402,1603的报文, 如                                               果说1201,1402丢失了,但是收到了1603的确认应答,那么就                                                 可以说明前面的1201,1402,是发送成功了。
  • 问:客户端把序号制成1000,服务器把1000变成1001返回给客户端,这样不行吗,为什么还要搞一个确认序号?                                                                                                                     不行,因为还有捎带应答的情况,这种情况即是对对端的确认,也还携带了信息,那么这种情况就需要两种序号同时存在。

6个标记位

问:为什么要有标记位?

我们首先要知道我们发送的TCP报文不光全部是负责信息交互的,有的还是还是申请连接,断开连接,连接异常等相关操作。所以TCP报文是有类型的,不同的类型决定了服务器的操作。那么TCP如何得知收到的报文是什么类型的呢,就是靠的6个标记位。

标志位存在的意义:区分TCP报文类型

SYN标记位:

当报文中的SYN被设置成1时,表明该报文是一个请求建立连接报文,就是三次握手时的发送报文。只有三次握手阶段才会设置SYN正常通信时是不会设置的。

ACK标志位:
告诉对方我已经收到消息了,对于这个信号不需要再次发送ACK表示这个报文收到了,要不然没完没了一直最后一个ACK没有被确认,我方收到ACK的行为是移动滑动窗口(发送缓冲区的一种管理模式,后面会提到)表示该信息对端已经收到了。

FIN标记位:

通知对方本端要关闭了。

PSH标记位:

当对方一直不拿信息导致信息全部阻塞在发送缓冲区中,当对方的接收缓冲区打满了,我方上层write就会把消息阻塞在我方发送缓冲区,我方也快打满了,忍不了了,我方就会给对方发送PSH信号,该报文只有报头,作用是希望对端快速把信息读航上去,对端依然可以不读,我方坚持发送几次PSH信号后直接断开连接。

RST标记位:

要求对方重新连接,这种使用场景为服务端已断开或者压根没连上,你客户端就给我发送消息,服务端一识别发现三次握手都没成功你怎么能给我发送消息,服务端就会给客户端发送RST信号。举个例子,当我三次握手握到第三次的时候,发送端申请第三次握手,当他申请完默认就申请成功了,但是可能失败了,实际没有连接上,而客户端已经认为成功了,已知发送报文到对端接收需要几毫秒,那我客户端在这几毫秒发送消息就会造成我刚才提到的现象。

URG标记位:

  • 使用场景:TCP协议是可靠的,可靠首先就是有序的,但是我们想让有些数据优先处理也就是插队,在TCP规定下插队按理说是不被允许的,但是有些情况必须优先处理,这时候可以设置紧急标志位。
  • 当没有优先数据需要处理时,URG为0,16位紧急指针无效,当URG为1时,16位紧急指针有效,紧急指针记录的是优先处理的数据在报文中的偏移量。
  • 在TCP协议中规定,紧急数据默认只允许是一个字节。
  • URG标记位的缺陷:URG在发送时并没有专门的传输通道,它依然需要在发送缓冲区中排序,因为TCP传输是靠字节流传输,那么就会出现一个问题,你无法高效确定紧急数据会被包含在哪个报文中,除非在将紧急数据发送出去之前,每次发送报文都去遍历一遍有效载荷,那么效率就会低很多,所以TCP采取在将紧急数据发送之前,每次报文都含有URG标记为,那就会出现一种问题,可能该报文就不含紧急数据却含有URG标记为,而且一旦进程卡死,比如说,你想说客户端给服务端发送一个URG报文,服务端一看是含有URG报文就将其加急处理,真的是这样吗?
  • 其实并不是这样的,接收端只是给紧急数据安排了一个单独的存储位置,每次读取可以从那个位置拿到紧急数据,如果想实现上述所说的情况,需要信号的配合,但是上面还说了可能有的报文没有紧急数据,但是含有URG标记为,这种情况也会触发信号,所以不推荐这种做法,URG我个人认为可以用来检查服务器信息使用,比如客户端想查服务端现存线程,进程PID等信息,可以通过URG只发送一个字节大小的数值,然后提前约定好这个数值是想查询什么信息(下面代码有体现,什么位置看不懂可以评论,看到均会回复)。
  • 真正的紧急信息接口我认为可以使用双网络套接字+信号驱动,每个客户端都有两个网络套接字,一个用来正常通信,一个用来通信紧急信息,正常通信的靠epoll监控,紧急信息靠SIGIO信号监控,这个信号的含义是被监视的套接字接收缓冲区和发送缓冲区一旦有信息就会给存在该套接字的进程发送SIGIO信号,只要捕捉该信号就可以实现信号驱动了(下面代码有体现,什么位置看不懂可以评论,看到均会回复)。
  • URG的使用:
  • 与普通write/read的区别就是含有标记位,就是第四个参数,第四个参数如果为0,则是和write一摸一样,如果是MSG_OOB,则代表发送含标记位URG的报文,紧急数据在应用层可以发送字节数量不为1,但是在发到对端的时候只有紧急数据的最后一个位置会被放进为紧急数据提供的位置,前面多余的数据和正常数据一样在接收缓冲区中储存,会被read/recv(标记位为0)按正常读取上去,所以,不推荐一次传递多个紧急数据,没有意义,因为只有最后一个数据被当作紧急数据传递上去了,其余数据还是当作普通数据。
  • 发送紧急数据:int n = send(int sockfd, const void *buf, size_t len,MSG_OOB);
  • 接收紧急数据:int n = recv(int sockfd, const void *buf, size_t len,MSG_OOB);

TCP保证可靠性策略

1.确认应答机制:确认应答是由TCP报头中的序号和确认序号共同实现的。前面序号部分已经详细讲解,在这里就不过多赘述了。

2.超时重传机制:当消息发出后,当一定时间内我方并没有收到对端的确认应答,那么我方就认为该数据已经丢失了,然后我方进行补发。

但是这种情况可能是真丢了,也可能是报文已经发过去了,但是应答丢了,也会触发超时重传现象,如果是后者就会出现收到两个重复报文的情况,那这种情况怎么解决?

其实因为有序号和确认序号的情况,就无所谓了,即使你将报文发送过去,你这条报文的序号小于期望序号,那么这个报文会被直接丢弃,然后给这条报文重新补发一个确认应答报文。

那多久算超时呢?

超时的时间不能设置太长,也不能设置太短,太长当网络状态良好的时候,可能少于当前设置的时间的一半,就已经可以确定丢了,所以设置太长就影响效率,设置太短也不行,可能你当前网络状态并不好,设置太短,将有大量报文都触发超时重传,但是这些可能只是因为网络状态不好造成传输比较慢,并不是丢了。

所以这个时间是动态设置。

3.快重传机制:如果只有超时重传机制,效率还是比较低的,所以产生了快重传,超时重传需要的是时间超过设定时间才会触发重传,而快重传则是超过确认次数就会触发快重传。

举个例子:当发送端给接收端一次发送五条报文,但是第一条报文丢了,其余四条报文正常到了,那么其余四条报文的内容是不会在接收缓冲区的,而是在另一个缓冲区中,这个缓冲区专门存储序号大于当前想接收的报文,这四条报文到了给的确认应答的报文含有的确认序号都是丢失那条报文的序号,这样的确认报文连续收了几次怎满足快重传,这样效率上是高于全部用于超时重传的,所以一般情况下,一次通信连续发送多条报文的情况下,只有最后几条需要使用超时重传的。

我是怎么理解这三条机制可以实现可靠性的:试想一次通信丢失报文的的话可能丢失什么样的报文?

1.丢失携带有效载荷的报文

2.丢失确认应答,无有效载荷的报文

3.丢失确认应答,有有效载荷的报文

第一种丢失,超时重传/快重传就可以解决该报文丢失,第二种因为有确认序号+滑动窗口(后面讲)的存在,可以允许这种报文少量丢失,而不影响通信,第三种报文丢失,1.如果后面有成功的确认应答,那么在结合第二条特性,是不是只丢了该确认应答所携带的有效载荷,那么该内容长时间没有得到应答就会触发超时重传或者快重传,重新封装报文,序号为最新报文的序号将内容发送过去,这样是不是就解决了该报文丢失,2.如果后面没有确认应答了,那就使用超时重传,重新生成应答,那么该应答可能携带当前丢失的有效载荷,也可能该有效载荷没有达到触发超时重传的时间,所以也有可能分成两个报文传过去,也有可能是一个。

综上所述,一次通信的三种丢失情况均有合理的解决办法,而每次通信出问题无非就这三种情况选一种,所以每次通信都是成功的,所以TCP协议是可靠的。

三次握手

上面的SYN和ACK,就是TCP报头字段中的那两个标志位,对于第二次握手,除了应答,服务器也要主动建立连接,所以服务器也要给客户端发送SYN,这就是捎带应答,因为TCP协议中,双方主机地位是对等的,你和我通信,我也可以和你通信,这就是全双工。

问1:为什么是三次握手而不是四次,五次?

1.三次握手可以保证双方都至少有一次发送消息,以及对这个消息的确认.。

2.奇数次可以保证是发送端来完成最后一次发送,也就是确认,一般情况下也就是客户端,这条报文是不确定对端一定可以收到的,试想如果让服务端完成最后一次挥手,那么如果失败了,客户端是一直处于等待完成最后一次挥手的情况,服务端一般情况下也不会主动给客户端发送消息,那么双方就卡死了,直到服务端启动保活机制,给客户端发送消息,这时候才发现握手都没有实现,这样效率时十分低下的,因为有了捎带应答的情况,所以让服务端完成最后一次握手的情况,奇偶次都可能出现。

所以三次是次数最少可以验证双方通信,以及可以保证,尽可能把问题留给发送端,一般发送端都是客户端,接收端是服务端。

三次握手时双方状态变化

  1. 最开始客户端和服务器都处于CLOSED状态,但是服务器为了能够接收客户端发来的连接请求,所以服务器变为了LISTEN监听状态。
  2. 客户端发起三次握手,当客户端发出第一个SYN后,状态变为SYN_SENT\n处于监听状态的服务器收到SYN后,将连接放进内核等待队列中,并向客户端发起第二次握手,发出SYN+ACK后,服务器状态变成SYN_RCVD。
  3. 当客户端收到服务器的第二次握手,紧接着发送三次握手的最后一个ACK,之后客户端状态变为ESTABLISHED。
  4. 服务器收到最后一个SYN后,状态也变为ESTABLISGED。

问1:什么是全连接队列?
为管理完成三次握手所建立的数据结构就是全连接,accept就是从全连接队列中拿取的已经完成三次握手的连接。

问2:listen第二个参数的作用?
它的作用是设置这个全连接的最大长度

问3:listen第二个参数为什么不能太大也不能太小呢?

  • 太长会导致占用公共资源太多,假设你的服务器十分繁忙,导致长时间执行不了accept函数,也就是长时间无法从全连接队列中拿取信息,因为上层繁忙,那上层所拥有的公共资源自然是越多越好,所以这种情况全连接队列就不适合太长,本来上层就忙,内存就不够,所以全连接队列还长时间没办法被拿取,长连接还占用大量内存维持长连接,那么显然是不合适的。
  • 太短也会不太合适,太短就会导致大多数连接为半连接,半连接的节点在半连接中存储时间是比较短的,假如说上层还是比较忙,长时间无法从全连接中拿取节点,大多数为半连接,那么许多半连接因为上层吧从全连接中拿,半连接也就无法成为全连接,那么许多半连接就全被操作系统内核清除掉了,这样对一个服务器的使用感是有着比较大的影响,试想你去链接一个服务器,这个服务器比较忙,你一直处于转圈,因为你处于半连接,不一会你就从转圈页面直接退出了,但是你要处于全连接的话,你不仅能多在转圈的位置持续时间更长,可能一会就被accept拿上去了,你就可以与服务器实现通信了。

问4:什么是半连接?

只完成第二次握手的连接是半连接,当全连接已经满了,那么发送的第三次握手就会被丢弃掉,也会被迫成为半连接。

问5:什么是SYN洪水?

我们前面说了在成为全连接之前,是先成为半连接的,为维持,管理半连接是有半连接队列的存在的,这个队列的长度也是有限的,假如说黑客恶意控制客户端在完成第二次握手后不在发送ACK,也就是不在实现第三次握手,那么就会持续生成半连接,而无全连接,直至半连接打满,因为没有实现三次握手,那么全连接队列也是没有节点的,应用层也是拿不到信息执行的,所以会一直卡在accept的位置,别人想连接该服务器也是连不上的,因为半连接满了,当然半连接是有存活时间限制的,但架不住持续生成半连接与该服务器相连,让这个半连接持续处于满员的状态,这就是SYN洪水。

四次挥手

问1:四次挥手为什么没有使用捎带应答?
1.因为你客户端想断开并不代表服务端想断开。

2.FIN的发送时机,是调用close函数后发送的FIN,你没有办法保证我方调用close函数,对端一定也立马调用close函数,所以无法实现捎带应答成为三次挥手。

注:close是先清理应用层管理该节点的资源,然后发送FIN,这在应用层就是关闭了连接,具体接收对端ACK,以及对端也close后对对端发送ACK这些都由操作系统来执行,不需要应用层操心,应用层的关闭只是应用层不再对该网络套接字接收发送缓冲区进行操作了。,如果对端发现某些数据丢失了,要求我方补发,那么操作系统就做了,不需要应用层管了。

四次挥手时双方状态变化

  1. 在挥手前,客户端和服务器都处于正常通信的ESTABLISHED状态\n客户端最先发起断开连接请求,把FIN标志位设置的报文发给了服务器,然后客户端变为FIN_WAIT_1状态(此时客户端已经没数据需要发给服务器了)。
  2. 服务器收到了FIN,发送ACK应答给客户端,同时状态变为CLOSE_WAIT状态(服务器可能还有数据需要发给客户端)。
  3. 当服务器没有数据再发给客户端时,轮到服务器主动断开连接了,所以服务器主动发送FIN给客户端,之后状态变为LASE_ACK(服务器数据发送完毕,但是可能客户端还没有收到或者全部接收到,因为数据传输也需要时间)。
  4. 客户端收到了第三次挥手,然后向服务器发送最后一个ACK应答,一旦发送最后一次ACK后客户端立马进入TIME_WAIT状态(此时可能还会有数据游离在网络中,需要等一下)。
  5. 服务器收到最后一个ACK报文后,就彻底关闭连接,也就是将该连接的结构体从数据结构中删除,再释放资源,变为CLOSED状态。
  6. 而客户端变为TIME_WAIT状态后,并不会直接关闭连接,而是会等待一个2MSL(Maximum Segment Lifetime,报文最大生存时间),才会进入CLOSED状态

问1:主动断开的一方会变成TIME_WAIT状态,那么为什么要等待一段时间再去关闭套接字?

1.因为主动断开的一方需要等待数据的消散。

2.最后一次ACK发送完后,也不一定成功,万一需要补发,而主动断开的一方却发送完ACK直接CLOSED,那就出问题了。

问2:为什么被动断开的一方不需要等待数据的消散?

因为被动断开的一方将不会在接收到数据了,不会收到数据了自然谈不到要等待数据消散,除了那种丢失很久的数据,这种数据的序号已经被接收了,只是接收的是补发的,这个丢失的第先前发送的,这种数据可能出现在任何时机,但是出现了也无所谓,如果接收方没CLOSED掉的话,那么就由接收方直接丢弃,如果已经CLOSED掉的话,该报文也会正常死亡,所以对结果没有影响,所以自然不需要等待这种数据,这种数据能存活到这步的概率极低。

问3:为什么被动断开的一方将不会在接收到数据了?

因为对被动断开的一方的数据在第二次挥手发出的时候,就已经接收到了所有数据,在后面主动断开的一方也没在向我发送数据。

问4:为什么主动断开的一方就需要有数据等待?

因为被动断开的一方可能在close前发送了一些数据,现存发送缓冲区中,因为close了,那么这些数据的发送就交给操作系统了,操作系统将数据全部发送给主动断开的一方,主动断开的一方全部接收完后会发送最后一次ACK,也就是第四次挥手,那么这些数据就是一次发送成功过去的吗,不存在补发的吗,答案显然不是,也是可能存在补发的,那么这个补发的数据大概率还存在网络之中,所以需要等待这种数据消散。

注:普遍情况下的服务端和客户端一般在调用close函数的时候应该是接收缓冲区和发送缓冲区应该是不存在数据的了,因为客户端先发送数据,这时候清空发送缓冲区,服务端先接受数据,在发送数据,这时候清空接收缓冲区和发送缓冲区,客户端再将服务端发送的数据全部读上去,这时候清空接收缓冲区,然后调用close函数,这时客户端的接收缓冲区和发送缓冲区就是没有数据的,服务端通过read/recv函数知道对端关闭了,所以服务端也调用close,这时候服务端的接收和发送缓冲区也是没有数据的了。

问1:如果是服务器崩溃导致,导致服务器先发送四次挥手,但是服务器崩溃一般都需要立即重启,那么就需要复用当前的端口号,可是这个端口号正在被刚才崩溃的进程在占用,崩溃的进程处于TIME_WAIT状态,这状态需要30~60秒才结束,那是不是说想要重启服务器需要等待30~60秒,这样对于服务器来说显然是不行的,那怎么可以解决这样的情况?

解决办法就是调用setsockopt函数,实现端口复用,如果该端口被正常使用,那么其余进程无法拿取这个端口启动进程,但是这个端口被用在处于TIME_WAIT状态的进程,则可以实现复用。

补充:

MSL是指报文在网络里面最久存活时间,TIME_WAIT的时间一般是二倍MSL。

流量控制

定义:TCP根据接收方的接受能力来决定发送数据的速度,这个机制叫流量控制,前面在解释16位窗口大小时已经详细讲过,在这位置就不再赘述。

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

问1:第三次握手的时候可以携带数据吗?

“握手阶段” 和 “数据传输阶段” 并非严格割裂,只要符合 TCP 协议的封装规则,第三次握手的 ACK 报文里就能附带应用层数据,这也是优化 TCP 连接建立后首包传输延迟的常用手段。

问2:流量控制属于保证可靠性操作还是属于提高效率的操作?

既保证了可靠性,也兼顾了效率性,从可靠性上来讲,可以防止大面积丢包,从效率上来讲大大减少了因对端接收不下导致丢包的概率,进而导致调用超时重传/快重传的次数,进而提高了效率。

滑动窗口

双方主机在TCP通信时,可以一次性发送多条数据,这样可能将发送过去的数据处理后一起给发送回来,这样可以将等待多个响应的时间重叠,进而提高效率。

  1. TCP允许发送方一次性能发送多个报文,然后接收方再一个个应答回去,如果没收到应答就会进行超时重传 --> 已经发出去,暂时没有收到应答的报文,要被TCP暂时保存起来。
  2. 所以发送方就会存在着多个已经发出去但是还没收到应答的报文,它们会被保存到哪里呢?根本不需要保存,因为报文就是在缓冲区里的,所以我们只需要“把缓冲区做一个简单的区域划分”即可。
  3. 把缓冲区当作数组来看的话,只需要有一个数组的下标就可以标记区域了,所以缓冲区可以分为三部分:已发送已确认,已发送未确认,待发送,三个部分。
  4. 对于已发送已确认的部分,这部分可被覆盖,用简单的话说就是这个部分里面的数据可以从缓冲区移除或者设置成无效
  5. 对于已发送未确认,理解为:可以发/已经发,但是就是没有收到应答,我们就把这部分区域叫做“滑动窗口”,是发送缓冲区的一部分。

滑动窗口的大小就是发送方不用等待接收方的ACK应答,一次最多可以发送的数据。

问1:为什么不把发送缓冲区的数据一次全部发送出去,按理说这样的IO吞吐量是最大的,效率自然也是最高的?

因为一次数据量发送数据量太多,增大在数据链路层数据碰撞的概率。

问2:滑动窗口一直向右移会不会出现越界的情况?
不会,滑动窗口采用的时类似环形算法,结尾的下一个位置是开头,所以永远不会有越界的情况。

延迟应答

就是收到数据后不立即对其进行应答而是等一会在对其进行应答,原因也很简单,这样有概率可以提高IO吞吐量,进而可以提高效率。

举个例子:有两个服务器,一个服务器采用了延迟应答,一个服务器没有采用延迟应答,现有两个客户端分别向服务器里面发送信息,且都是发送字节量为5000大小的报文,无延迟应答机制的是收到后立马返回,那么应答报文所应答的缓冲区大小为5000,有延迟应答机制的选择等一会在对其进行应答,那么在这个等一会的时间内,应用层将数据从接收缓冲区给读上去了,那么有延迟应答的应答报文所携带的16位缓冲区大小为10000,那么这两个服务器在发送数据无延迟应答机制的发送的报文有效载荷大小为5000,有延迟应答机制的发送的有效载荷大小为10000,无延迟应答机制想发送10000就需要再跑一趟,也就是说,有延迟应答机制的服务器有概率只需要多等一会则能少跑一趟,那么只要等待时间小于多跑一趟的时间,效率就是提高了。

拥塞控制

如果双方出现了大面积丢包问题,TCP判断网络出现了问题,网络拥塞了。

造成用色的原因:

  • 硬件设备出现了问题。
  • 数据量太大引起的阻塞。

对于大面积丢包我们不能采取超时重传,原因是:

  • 如果造成阻塞的原因是硬件出现了问题,那么重传也没什么意义,结果一样是发送不出去
  • 如果是数据量太大更不可以重传了,都已经因为数据量太大造成了阻塞,这时在重传,那不就造成了数据量更大了,阻塞就变得更加严重了。

TCP面对网络阻塞引入了慢启动机制。

问1:什么是慢启动机制?

慢启动采用的信息发送数量就是指数级增长,开始的时候发送少量报文,如果说都ok,说明网络状态是健康的,后期指数级增长就比较快了,目的是为了恢复正常网络通信。

问2:实际机器发送数据会一直指数级增长吗?
当然不会,TCP引入了一个阈值,当超过这个阈值的时候就会采用线性增长,同时线性增长也不会一直增长,当拥塞窗口大于剩余数据大小或者滑动窗口大小就不会增长了,当拥塞窗口一直是最小的,那么就会一直线性增长,当增长到一定之后就再次触发网络拥塞,那么就会重新触发慢启动机制,但是这次阈值是上次触发网络拥塞值得一半(像这种具体除以几,都是通过实验得来的,在这里除以2得效果最好)。

补充:一次发送的数据量大小,要考虑数据量大小,滑动窗口大小,拥塞窗口大小,三者取小,才是一次真正发送的数据大小。

TCP提高传输效率的策略有哪些?

滑动窗口,快重传,延迟应答,捎带应答,拥塞控制

面向字节流/面向数据报

面向字节流:写100个报文每个报文携带一字节数据,在读的时候不必拘泥于写,写了多少次,完全可能出现1次将其读上来。

面向数据报:写一百个报文每个报文携带一字节数据,在读的时候必须读100次才可以将数据全部读上来。

数据报粘包问题

就是应用层从TCP缓冲区中读取数据,可能读的数据是半个报文,也可能是一个报文,也可能是多个报文,也可能是多个半报文,这就是粘包。

这种问题的解决方案就是定协议,比如说:

  1. 采用定长报文
  2. 采用特殊字符进行分割
  3. 子描述字段+定长报文,参考UDP
  4. 自描述字段—+特殊字符,参考TCP

本质都是根据报头提取报头,效载荷长度,进行报头和有效载荷的分离。

核心思想就是明确报头和有效载荷的边界。

TCP异常对应的处理情况

  • 进程异常终止:和正常终止处理方式一样,在操作系统层面上关掉一个正常的进程和关掉一个异常的进程没什么区别。
  • 机器关机/重启:和正常的处理方式一样
  • 台式机断电/网线断开:这种就与前两种有一些区别了,你断网了,根本无法传递信息,更别提四次挥手了,所以一般都是客户端单方面释放资源,服务端还以为和客户端是连接的,服务端中有个机制是保活机制,就是服务端发现客户端长时间也不给我发送信息,服务端就会给客户端发送保活信号,发送几次后发现对端无响应就会断开与这个客户端的连接。

#define MAX_EVENTS 1024
//业务监听端口
#define BUSINESS_PORT 8888
//紧急监听端口
#define URG_CONTROL_PORT 8889
#define BUF_SIZE 1024

std::unordered_map<int,int> hash_fd;

typedef enum
{
    CONN_TYPE_BUSINESS,
    CONN_TYPE_URG_CONTROL
}ConnType;
type struct
{
    int fd;
    ConnType type;
}EpollDate;
//ev.data.ptr = epoll_data; // 存储自定义数据

//供URG查询使用
typedef struct 
{
    pid_t server_pid;//服务端进程ID
    int conn_count;//当前业务连接数
    time_t start_time;//服务端启动时间
}ServerStatus;
ServerStatus g_server_status;
//创建对URG查询的响应
void handle_urgent_commend(unsigned char cmd,int fd)
{
    char response[BUF_SIZE] = {0};
    time_t now_time = time(NULL);
    switch(cmd)
    {
        case 1:
            snprintf(response,sizeof(response),"[URG指令响应]服务端进程ID:%d",(int)g_server_status.server_pid);
            break;
        case 2:
             snprintf(response,sizeof(response),"[URG指令响应]当前业务连接数:%d",(int)g_server_status.conn_count);
             break;
        case 3:
              snprintf(response,sizeof(response),"[URG指令响应]服务端运行时间:%ld 秒",now_time - g_server_status.start_time);
              break;
        default:
               snprintf(response, sizeof(response), "【URG指令响应】无效指令:%d", cmd);
               break;          

    }
    send(fd,response,strlen(response),0);
    printf("已处理URG指令 %d,响应:%s\n", cmd, response);
}
//创建TCP监听套接字
int create_tcp_listen_socket(uint16_t port)
{
    int listen_fd = socket(AF_INET,AF_STREAM,0);
    struct sockaddr_in localaddr;
    localaddr.family = AF_INET;
    localaddr.sin_port = htons(port);
    localaddr.sin_addr.s_addr = INADDR_ANY;
    socket_t len = sizeof(localaddr);
    if(bind(listen_fd,&localaddr,len) < 0)
    {
        close(listen_fd);
        return -1;
    }
    if(listen(listen_fd,10) < 0)
    {
        close(listen_fd);
        return -1;
    }
    return listen_fd;
}

//创建非阻塞
void non_block(int fd)
{
    int flags = fcntl(fd,FGETFL,0);
    if(flags < 0)
    {
        perror("fcntl F_GETFL failed");
        return -1;
    }
    if(fcntl(fd,F_SETFL,flags | O_NONBLOCK) < 0)
    {
        perror("fcntl F_GETFL failed");
        return -1;
    }
}
//向epoll对象中添加节点
bool epoll_add(int epoll_fd,int fd,int events)
{
    struct epoll_event ev;
    ev.events = events;
    ev.data.fd = fd;
    return epoll_ctl(epoll_fd,EPOLL_CTL_ADD,&ev);
}

//SIGIO信号处理函数:将紧急套接字的信息读取出来,并进行一定处理
void handler_sigio(int signum)
{
    //该类型可以配合fcntl拿到是哪个进程的套接字触发的信号
    struct f_owner_ex owner_info;
    //是进程类型
    owner_info.type = F_OWNER_PID;
    owner_info.pid = getpid();
    //拿取触发信号的套接字
    int tarig_fd = fcntl(0,F_GETOWN_EX,&owner_info);
    if(tarig_fd < 0)
    {
        printf("SIGIO:获取触发信号的套接字失败,错误码:%d\n", errno);
        return;
    }
    char buffer[BUF_SIZE] = {0};
    size_t read_len = recv(tarig_fd,buffer,sizeof(buffer)-1,0);
    if(read_len > 0)
    {
        //这里没有设置对紧急报文的处理,只是把收到的信息发送回给客户端了
        send(tarig_fd,....)
    }
    else if(read_len == 0)
    {
        close(tarig_fd);
    }
    else
    {
        //发生错误,直接关闭该套接字
        close(tarig_fd);
    }
}
//注册SIGIO信号处理函数(仅针对紧急紧急套接字)
int register_sigio_handler()
{
    struct sigaction sa_io;
    memset(&sa_io,0,sizeof(sa_io));
    sa_io.handler(handler_sigio);
    //屏蔽当前信号,避免同一信号重复调用
    sigemptyset(&sa_io.sa_mask);
    //将该处理函数以及屏蔽信号设置进block,handler表
    if(sigaction(SIGIO,&sa_io,nullptr) < 0)
    {
        perror("注册SIGIO信号失败");
        return -1;
    }
    return 0;
}
//为紧急套接字启动事件IO
int enable_urgent_socket_sig_driven(int urgent_conn_fd)
{
    //让内核知道哪个进程的哪个套接字触发了IO需要给其发送SIGIO信号
    if(fcntl(urgent_conn_fd,F_SETOWN,getpid()) < 0)
    {
        perror("设置紧急套接字失败");
        return -1;
    }
    int flags = fcntl(urgent_conn_fd,F_GETFL,0);
    if(flags < 0)
    {
        perror("获取紧急套接字失败");
        return -1;
    }
    if(fcntl(urgent_conn_fd,F_SETFL,flags | O_ASYNC) < 0)
    {
        perror("启动紧急套接字信号驱动失败");
        return -1;
    }
}

bool bussiness_listen_accept(int listen_fd,int epoll_fd,int bevents)
{
    int new_fd = accept(listen_fd,nullptr,nullptr);
    if(new_fd < 0)
        return false;
    //将该节点设置为非阻塞
    non_block(new_fd);
    //将该节点添加在监控下
    epoll_add(epoll_fd,new_fd,bevents);
}
bool urg_control_listen_accept(int listen_fd,int epoll_fd,int uevents)
{
    int new_fd = accept(listen_fd,nullptr,nullptr);
    if(new_fd < 0)
        return false;
    //将该节点设置为非阻塞
    non_block(new_fd);
    //设置该节点满足SIGIO信号触发条件时,对该进程发送SIGIO信号
    enable_urgent_socket_sig_driven(new_fd);
    //靠信号监视,而不靠epoll监视,要不然既有epoll监视,又有SIGIO信号监视可能会出现问题
}
//普通事件读的缓冲区,用来储存读取信息不完整,无法交给上层HTTP层
//正常这个位置应该是个自定义的上下文结构,但是这里主要目的不是写一个服务器,而是认识一些接口,所以就从简了
//应该每一个被监视的节点都应该有两个缓冲区:接收缓冲区,发送缓冲区
char buffer[BUF_SIZE] = {0};
bool recv_handler(int fd)
{
    while(true)
    {
        int n = recv(fd,buffer,sizeof(buffer)-1,0);
        if(n > 0)
        {
            //把信息交给协议层进行处理
            Http(&buffer);
            //如果发送缓冲区有内容,则就启动该事件的写监控
            ...
        }
        else if(n == 0)
        {
            close(fd);
        }
        else
        {
            //如果将内容读没了,则退出,因为是ET模式,所以一次必须要把数据全部读取掉
            if(errno == EAGAIN || error == EWOULDBLOCK)
                break;
            close(fd);
        }
    }
}
int main()
{
    //设置触发SIGIO信号执行的handler方法
    register_sigio_handler();
    //初始化服务端全局状态
    g_server_status.server_pid = getpid();
    g_server_status.conn_count = 0;
    g_server_status.start_time = time(NULL);
    int id = 0;
    //创建监听套接字
    int business_listen_fd = create_listen_socket(BUSINESS_PORT);
    int urg_control_listen_fd = create_listen_socket(URG_CONTROL_PORT);
    hash_fd.insert(make_pair(id++,business_listen_fd));
    hash_fd.insert(make_pair(id++,urg_control_listen_fd));
    //创建epoll对象
    int epoll_fd = epoll_create(1);
    //将监控节点添加进epoll对象进行监控
    if(epoll_add(epoll_fd,business_listen_fd,EVENTIN) == false)
        exit(-1);
    if(epoll_add(epoll_fd,urg_control_listen_fd,EVENTIN) == false)
        exit(-1);
    char buffer[BUF_SIZE] = {0};
    struct epoll_event events[MAX_EVENTS];
    while(true)
    {
        int n = epoll_wait(epoll_fd,&events,MAX_EVENTS,-1);
        if(n < 0)
        {
            perror("epoll_wait failed");
            break;
        }
        for(int i = 0;i < n;i++)
        {
            int fd = events[i].data.fd;
            int event = events[i].events;
            if(event & EPOLLIN)
            {
                if(fd == business_listen_fd)
                {
                    bussiness_listen_accept(business_listen_fd,epoll_fd,EVENTIN|EVENTET);
                }
                else if(fd == urg_control_listen_fd)
                {
                    urg_control_listen_accept(urg_control_listen_fd,epoll_fd,EVENTIN|EVENTET)
                }
                else
                {
                    //普通节点触发读事件
                    recv_handler(fd);
                }
            }
            if(event & EPOLLPRI)
            {
                //有可能设置了URG标记位,但是紧急数据并未发送过来呢,所以这是对节点设置非阻塞的意义
                //紧急数据大小为1字节
                char buffer;
                int urecv_len = recv(fd,buffer,1,MSG_OOB);
                if(n < 0)
                {
                    if(errno != EAGAIN || errno != EWULDBLOCK)
                        close(fd);
                        
                }
            }
            if(event & EPOLLOUT)
            {
                //将发送缓冲区的内容发送出去,如果将数据全部发送出去,则关闭对写事件的监控
            }
            if(event & EPOLLERR)
            {
                close(fd);
            }
        }
    }
    
}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐