tcp 协议的连接与断开

目录

TCP问题好难,对于这种问题,一方面是应用场景少,实践的不够多,比如调试网络问题的机会;另一方面是工作的时候调试问题总是得过且过,没有深入研究,这个可能还有其他外界因素的干扰,也不允许深究下去。

工作中遇到的问题主要是一些异常的tcp状态,比如一些tcp错误码,如delayAckLost等,还有就是一些异常的网络连接,比如大量close wait以及time wait等。另外对于网络编程来说,比如我们在日常工作中,一般只会写http服务,那这个时候我们应该注意什么网络问题?

三次握手

关于三次握手,发现维基百科-TCP说的很清楚,这里直接复制一下吧。首先是服务端创建一个server,并调用LISTEN方法进行listen,服务端执行了listen函数后,就在服务器上起两个队列:

  • SYN队列:存放二次握手的结果。队列长度由listen函数的参数backlog指定。
  • ACCEPT队列:存放完成了三次握手的结果。队列长度由listen函数的参数backlog指定。

三次握手的流程如下:

  1. 客户端(通过执行connect函数,对于golang来说,是通过执行net.Dial函数)向服务器端发送一个SYN包,请求一个主动打开。该包携带客户端为这个连接请求而设定的随机数A作为消息序列号。(报文的序号主要用来保证报文的顺序)
  2. 服务器端收到一个合法的SYN包后,把该包放入SYN队列中;回送一个SYN/ACK。ACK的确认码应为A+1,SYN/ACK包本身携带一个随机产生的序号B
  3. 客户端收到SYN/ACK包后,发送一个ACK包,该包的序号被设定为A+1,而ACK的确认码则为B+1。然后客户端的connect函数成功返回。当服务器端收到这个ACK包的时候,把请求帧从SYN队列中移出,放至ACCEPT队列中;这时accept函数如果处于阻塞状态,可以被唤醒,从ACCEPT队列中取出ACK包,重新创建一个新的用于双向通信的sockfd,并返回。

上面有几个问题需要注意一下,一个是服务端调用listen开始监听,然后客户端调用connect进行连接,connect成功返回就表示tcp连接已经成功建立了,下面就可以发送数据了。还有就是SYN这个标志位:表示是一个请求建立的报文,或者对请求建立进行回复的报文。 java-javascript

四次挥手

连接断开的时候,使用四次挥手,过程稍微复杂一些,如下,连接主动断开的一方,首先发送FIN报文,被动断开的一方,对FIN报文确认之后,进入半关闭状态,这时被动关闭的一方还可以进行数据发送,等被动关闭的一方处理完成之后,其发送FIN报文,主动关闭的一方收到对方发的FIN报文之后,对这个报文进行确认,并经过TIME_WAIT之后,关闭连接。 java-javascript 维基百科对图中几个状态也进行了说明:

  • FIN_WAIT_1: 主动关闭的一方调用close函数发出FIN请求包,表示本方的数据全部结束,等待TCP连接的另一端的ACK确认或者FIN&ACK请求包。
  • CLOSE-WAIT:被动关闭端接到FIN后,就发出ACK以回应FIN请求,并进入等待本地用户的连接终止请求的半关闭状态。这时可以发送数据,但不再接收数据。
  • FIN_WAIT_2: 主动关闭端在FIN_WAIT_1状态下收到ACK确认包,进入等待远程TCP的连接终止请求的半关闭状态。这时可以接收数据,但不能再发送数据。
  • TIME-WAIT:主动关闭端接收到FIN后,就发送ACK包,等待足够时间以确保被动关闭端收到了终止请求的确认包。

TIME_WAIT等待的时长是2MSL,MSL是 Maximum Segment Lifetime,报文最大生存时间,【Linux系统里有一个硬编码的字段TCP_TIME_WAIT_LEN,其值为60秒,也就是说,Linux系统停留在TIME_WAIT的时间为固定的60秒,Linux 对于 MSL 的定义就是hardcode 30s————网络编程实战】。另外为什么是2个MSL,不是3个或者4个呢?这里主要是考虑一去一回两组报文的生存时间,在一个连接的4元组中,一去一回报文的四元组都是相同的,所以需要两个MSL。

TIME_WAIT的原因:

  1. 让被动关闭的一方正常结束,这个主要是预防对FIN报文ack丢失的情况,如果ack丢了,而主动关闭的一方立即进入CLOSED状态,那么在FIN发送者没有收到对FIN的ACK之后进行重发。因为对端已经把连接关了,所以被动关闭的一方会收到RST报文,从而出现错误。如果存在TIME_WAIT状态,这个时候,只需要对FIN重新ACK一次就好了。
  2. 另一个原因跟报文迷路有关。是为了让旧连接的TCP报文全部自然消失。假设一个tcp连接在断开后,进行了重连,重连连接的四元组(ip地址,端口号,tcp报头关心的是端口号),如果没有TIME_WAIT状态,之前断开的连接的迷失的报文在到达目的地后就会引发错误。

TIME_WAIT的危害:主要是在TIME_WAIT阶段,还要保持对端口的占用,过多会导致无法开启新连接。

TIME_WAIT优化:复用TIME_WAIT状态的连接,net.ipv4.tcp_tw_reuse,对该控制变量的解释为:从协议角度理解,如果是安全可控的,可以复用TIME_WAIT的套接字为新的连接使用,那么什么是协议角度理解的安全可控呢?主要有两点(这两点没看懂):

  1. 只适用于连接发起方(C/S模型中的客户端)
  2. 对应的TIME_WAIT状态的连接创建时间超过1秒才可以被复用。 使用net.ipv4.tcp_tw_reuse,还有一个前提net.ipv4.tcp_timestamps=1要打开。

另外在之前工作中,发现过节点有很多CLOSE_WAIT的连接,这个状态是被动关闭的一方,在收到FIN报文后,自己处理完后再去发送FIN,出现大量CLOSE_WAIT有可能是被动关闭的一方,没有主动关闭连接。没有主动关闭的原因可能有:1)被动关闭方负载比较高,来不及关闭连接,比如http server 负载很高,响应很慢,对于这个 case 我们可以通过在server端 调用 sleep 来模拟。2)server 可能忘记关闭连接了。

tcp 流量控制

流量控制用来避免主机分组发送得过快而使接收方来不及完全收下,一般由接收方通告给发送方进行调控。TCP使用滑动窗口协议实现流量控制。接收方在接收窗口域指出还可接收的字节数量。发送方在没有新的确认包的情况下至多发送接收窗口允许的字节数量。接收方可修改接收窗口的值。

这部分先不细讲了,暂时还没搞清楚细节,也总是忘记,记一下大概思想。对于发送端,要维护下面一个窗口(来自刘超《趣谈网络协议》)。 java-javascript

对于接收端,维护的窗口如下: java-javascript 其中:

  • MaxRcvBuffer:最大缓存的量;
  • LastByteRead: 之后是已经接收了,但是还没被应用层读取的;
  • NextByteExpected: 是第一部分和第二部分的分界线。

此外 tcp 的拥塞控制也挺重要,后面关注一下。

浅谈CLOSE_WAIT