文章链接:https://codemouse.online/archives/2020-06-30202243
产生了五个状态,分别是: 服务器上LISTEN,SYN_RCVD,ESTABLISHED 客户端上CLOSE,SYN_SENT,ESTABLISHED
TCB(tcp control block):伴随每一个连接的生命周期,存储着连接的状态等信息。
accept函数的作用:从全连接队列中取出一个节点,并对此分配一个fd。
listen()函数的第二个参数backlog的作用:限制全连接队列和半连接队列的节点数。 backlog>=全连接队列节点数+半连接队列节点数
在第一次挥手的时候,被动接受方收到fin,第二次挥手的ack没有发送的时候(协议栈已经准备好,但没有发送的时候),这时候被动方立马调用close函数,第二次的ack和第三次的fin就会合并到一起发送。
用来保证第四次挥手的成功抵达,如果在对方等待时候内ack没有抵达,那么第三次挥手就会重发,而time_wait状态就是等待来响应这个重发。
被动方调用close这个函数不正常。
在每接收到一个包之后,会等待200ms,如果在这期间有新包到了,会重置这个200ms的定时器,继续等待,直到超时的时候,会回复一个ack,ack包里面包含最完整序列的号值,比如4号包以前的,包括4号包都收到了,那么就会回复一个4。每一个tcp的io都有一个定时器。
tcp的效率低,实时性不够高。延迟ack也是为了增加tcp的传输效率。 以下是udp的使用场景:
udp的下载操作。不做拥塞控制,不管网络多烂都往里面塞,如果有问题,直接重发。比如ack回复4,那么4号之后的包全部重发。upd的实时性。比如玩王者荣耀打团战的时候,要知道谁先打的谁,保证实时性。但是会牺牲传输效率,保证实时性。试探性发送,试探出一次性应该发多少包。
它有一个拥塞控制门限值,默认是16,在16之前,增长是指数增长,大于等于这个数之后,就呈现线性增长。直到这个包收不到之后,直接砍一半,然后继续线性增长。
慢启动计算出来的这个值也就说滑动窗口的大小。
滑动窗口的大小是一直在改变的,是动态的。 滑动窗口就说允许发送的数据,将这些数据都往网络上发送,然后通过ack延迟的方法,得到一个返回值,这个返回值也就是允许发送的包序号的起点,用来设置允许发送的包开始的位置,然后将滑动窗口内的数据报包发送出去。
这与如何保证tcp包的顺序性和延迟ack相呼应。
也就等待多久回ack。 一次往返的时间rtt =0.9 * old_rtt + 0.1 * new_rtt 通过这个公式得到定时器的超时时间。
分包原因: 当数据大小大于一帧容纳的大小会分包。
分包解决方案 由于分包了,同步操作的时候可以通过循环接收的方式获取数据 https://codemouse.online/archives/2020-03-13202900
黏包原因: 数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,那么他本身会启用Nagle算法(可配置是否启用)对较小的数据包进行合并(基于此,TCP的网络延迟要UDP的高些)然后再发送(超时或者包大小足够)。
或者由于数据长时间未接收,全都堵塞在tcp缓存中。(断点调试的时候常出现)
黏包解决方案 可以设置一个特殊的结尾符号。设置自定义协议,设置本次包长,先接收包头得到长度,通过循环接收的方式,接收指定长度。此协议的定义常与柔性数组配合。 https://codemouse.online/archives/2020-02-24-164632定死自己每次的包长。网线断开它不像直接close,它会有挥手的过程。在服务器看来,如果它不于客户端进行通信的话,可能就不知道客户端已经断开了,这时候就会变成一个死链接,但是服务器又不知道。
解决: 设置TCP套接字保持存活选项SO_KEEPALIVE,也就是系统帮忙做心跳,每隔一段时间系统会检测客户端是否存活。在应用层自己做一个心跳包。参考此博客: https://www.cnblogs.com/chinaops/p/9303041.html
client向server发起连接请求,server接到请求,然后双方建立连接。client向server发送消息,server回应client,然后一次读写就完成了,这时候双方任何一个都可以发起close操作,不过一般都是client先发起close操作。由于一般的server不会回复完client后立即关闭连接的,防止client还没收到数据就收到close的fin数据包,recv的返回值直接为0,当然不排除有特殊的情况。从上面的描述看,短连接一般只会在client/server间传递一次读写操作。
长连接的情况,client向server发起连接,server接受client连接,双方建立连接。Client与server完成一次读写之后,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。
首先说一下TCP/IP详解上讲到的TCP保活功能,保活功能主要为服务器应用提供,服务器应用希望知道客户主机是否崩溃,从而可以代表客户使用资源。如果客户已经消失,使得服务器上保留一个半开放的连接,而服务器又在等待来自客户端的数据,则服务器将应远等待客户端的数据,保活功能就是试图在服务器端检测到这种半开放的连接。
如果一个给定的连接在两小时内没有任何的动作,则服务器就向客户发一个探测报文段,客户主机必须处于以下4个状态之一:
客户主机依然正常运行,并从服务器可达。客户的TCP响应正常,而服务器也知道对方是正常的,服务器在两小时后将保活定时器复位。 客户主机已经崩溃,并且关闭或者正在重新启动。在任何一种情况下,客户的TCP都没有响应。服务端将不能收到对探测的响应,并在75秒后超时。服务器总共发送10个这样的探测 ,每个间隔75秒。如果服务器没有收到一个响应,它就认为客户主机已经关闭并终止连接。 客户主机崩溃并已经重新启动。服务器将收到一个对其保活探测的响应,这个响应是一个复位,使得服务器终止这个连接。 客户机正常运行,但是服务器不可达,这种情况与2类似,TCP能发现的就是没有收到探查的响应。 从上面可以看出,TCP保活功能主要为探测长连接的存活状况,不过这里存在一个问题,存活功能的探测周期太长,还有就是它只是探测TCP连接的存活,属于比较斯文的做法,遇到恶意的连接时,保活功能就不够使了。
在长连接的应用场景下,client端一般不会主动关闭它们之间的连接,Client与server之间的连接如果一直不关闭的话,会存在一个问题,随着客户端连接越来越多,server早晚有扛不住的时候,这时候server端需要采取一些策略,如关闭一些长时间没有读写事件发生的连接,这样可以避免一些恶意连接导致server端服务受损;如果条件再允许就可以以客户端机器为颗粒度,限制每个客户端的最大长连接数,这样可以完全避免某个蛋疼的客户端连累后端服务。
长连接和短连接的产生在于client和server采取的关闭策略,具体的应用场景采用具体的策略,没有十全十美的选择,只有合适的选择。