目录
TCP问题好难,对于这种问题,一方面是应用场景少,实践的不够多,比如调试网络问题的机会;另一方面是工作的时候调试问题总是得过且过,没有深入研究,这个可能还有其他外界因素的干扰,也不允许深究下去。
工作中遇到的问题主要是一些异常的tcp状态,比如一些tcp错误码,如delayAckLost
等,还有就是一些异常的网络连接,比如大量close wait以及time wait等。另外对于网络编程来说,比如我们在日常工作中,一般只会写http服务,那这个时候我们应该注意什么网络问题?
三次握手
关于三次握手,发现维基百科-TCP说的很清楚,这里直接复制一下吧。首先是服务端创建一个server,并调用LISTEN
方法进行listen,服务端执行了listen函数后,就在服务器上起两个队列:
- SYN队列:存放二次握手的结果。队列长度由listen函数的参数backlog指定。
- ACCEPT队列:存放完成了三次握手的结果。队列长度由listen函数的参数backlog指定。
三次握手的流程如下:
- 客户端(通过执行
connect
函数,对于golang来说,是通过执行net.Dial
函数)向服务器端发送一个SYN
包,请求一个主动打开。该包携带客户端为这个连接请求而设定的随机数A作为消息序列号。(报文的序号主要用来保证报文的顺序) - 服务器端收到一个合法的SYN包后,把该包放入SYN队列中;回送一个SYN/ACK。ACK的确认码应为A+1,SYN/ACK包本身携带一个随机产生的序号B。
- 客户端收到SYN/ACK包后,发送一个ACK包,该包的序号被设定为A+1,而ACK的确认码则为B+1。然后客户端的connect函数成功返回。当服务器端收到这个ACK包的时候,把请求帧从SYN队列中移出,放至ACCEPT队列中;这时accept函数如果处于阻塞状态,可以被唤醒,从ACCEPT队列中取出ACK包,重新创建一个新的用于双向通信的sockfd,并返回。
上面有几个问题需要注意一下,一个是服务端调用listen开始监听,然后客户端调用connect进行连接,connect成功返回就表示tcp连接已经成功建立了,下面就可以发送数据了。还有就是SYN
这个标志位:表示是一个请求建立的报文,或者对请求建立进行回复的报文。
四次挥手
连接断开的时候,使用四次挥手,过程稍微复杂一些,如下,连接主动断开的一方,首先发送FIN
报文,被动断开的一方,对FIN报文确认之后,进入半关闭
状态,这时被动关闭的一方还可以进行数据发送,等被动关闭的一方处理完成之后,其发送FIN
报文,主动关闭的一方收到对方发的FIN报文之后,对这个报文进行确认,并经过TIME_WAIT
之后,关闭连接。
维基百科对图中几个状态也进行了说明:
- 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
的原因:
- 让被动关闭的一方正常结束,这个主要是预防对FIN报文ack丢失的情况,如果ack丢了,而主动关闭的一方立即进入CLOSED状态,那么在FIN发送者没有收到对FIN的ACK之后进行重发。因为对端已经把连接关了,所以被动关闭的一方会收到
RST
报文,从而出现错误。如果存在TIME_WAIT状态,这个时候,只需要对FIN重新ACK一次就好了。 - 另一个原因跟报文迷路有关。是为了让旧连接的TCP报文全部自然消失。假设一个tcp连接在断开后,进行了重连,重连连接的四元组(ip地址,端口号,tcp报头关心的是端口号),如果没有
TIME_WAIT
状态,之前断开的连接的迷失的报文在到达目的地后就会引发错误。
TIME_WAIT
的危害:主要是在TIME_WAIT
阶段,还要保持对端口的占用,过多会导致无法开启新连接。
TIME_WAIT
优化:复用TIME_WAIT
状态的连接,net.ipv4.tcp_tw_reuse
,对该控制变量的解释为:从协议角度理解,如果是安全可控的,可以复用TIME_WAIT的套接字为新的连接使用,那么什么是协议角度理解的安全可控呢?主要有两点(这两点没看懂):
- 只适用于连接发起方(C/S模型中的客户端)
- 对应的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使用滑动窗口协议
实现流量控制。接收方在接收窗口
域指出还可接收的字节数量。发送方在没有新的确认包的情况下至多发送接收窗口
允许的字节数量。接收方可修改接收窗口的值。
这部分先不细讲了,暂时还没搞清楚细节,也总是忘记,记一下大概思想。对于发送端,要维护下面一个窗口(来自刘超《趣谈网络协议》)。
对于接收端,维护的窗口如下: 其中:
- MaxRcvBuffer:最大缓存的量;
- LastByteRead: 之后是已经接收了,但是还没被应用层读取的;
- NextByteExpected: 是第一部分和第二部分的分界线。
此外 tcp 的拥塞控制也挺重要,后面关注一下。