报文类型
首先tcp的报文可以归纳为以下5种:
- SYN
- Data
- FIN
- Reset
- ACK
其中前三个分别为建立连接、数据传输、断开连接,这三种报文对方接受到一定要ACK确认,这是tcp保证可靠传输依赖的机制。如果对方在超时时间内不确认,发送方会一直重传,直到对方确认或者达到重传上限次数而reset连接。
Reset为重置连接报文,ACK为确认报文,对于这两种报文,对方接收到后不需要ACK确认,发送方也不会重传这两种类型的报文。
Reset和Ack报文为啥不需要确认?
发送Reset报文的一端,在发送完这个报文后,和该tcp session有关的内存结构体瞬间全部释放,无论对方收到或者没有收到,都没有关系了。如果对方也收到Reset报文,也会释放该TCP session相关的内存结构。如果对方没收到Reset报文,可能会继续发送让接收方弹射出Reset的报文,对最后对方一样会收到Reset报文,并最终释放内存。
这里的ACK报文,是指没有携带任何数据的裸ACK报文,对方接受到这样的ACK报文,自然也不需要ACK,否则双方要一直回复ACK导致死循坏。
有同学会说,按照这么说,TCP连接应该是四次消息交互啊:
1.A 发送SYN 报文给B,这是第一次报文交互。
- B发送ACK确认A的SYN报文,这是第二次报文交互
- B发送自己的SYN报文给A,这是第三次报文交互
- A需要ACK确认B的SYN报文,这是第四次报文交互
以上的演绎没有问题,但是报文2、3为何要分开发送呢?增加了延迟不说,同时还白白浪费了网络的带宽,完全可以将报文2、3合并起来,不就是在报文3的ACK状态位的位置置“1”就结了吗
TCP握手握的到底是啥?
TCP作为一种可靠传输控制协议,其核心思想:既要保证数据可靠传输,又要提高传输的效率,而用三次恰恰可以满足以上两方面的需求。
TCP是怎么保证可靠传随的呢? TCP连接的一方A,由操作系统动态随机选取一个32位长的序列号(Initial Sequence Number),假设A的初始化序列号为1000,以该序列号为原点,对自己将要发送的每个字节的数据进行编号,1001,1002...,并将自己的初始序列号ISN告诉B,让B知道什么编号的数据是合法的,什么编号是非法的,同时B还对A的每一个编号的字节数据进行确认,如果A收到B的确认好为2001,意味着字节编号为1001-2000的数据都已经安全到达。 同理B也是类似的操作。
一句话总结,TCP连接握手,握的是通信双方数据原点的序列号!用此核心思想来分析整个握手过程:
四次握手:
1.1 A 发送同步信号SYN + A'sInitial sequence number
1.2 B 确认收到A的同步信号,并记录A's ISN 到本地,命名 B's ACK sequence number
1.3 B发送同步信号SYN + B's Initial sequence number
1.4 A确认收到B的同步信号,并记录B's ISN 到本地,命名 A's ACK sequence number
很显然1.2和1.3 这两个步骤可以合并,只需要三次握手,可以提高连接的速度与效率。
二次握手的过程:
2.1 A 发送同步信号SYN + A'sInitial sequence number
2.2 B发送同步信号SYN + B'sInitial sequence number + B's ACK sequence number
这里有一个问题,A与B就A的初始序列号达成了一致,这里是1000。但是B无法知道A是否已经接收到自己的同步信号,如果这个同步信号丢失了,A和B就B的初始序列号将无法达成一致。于是TCP的设计者将SYN这个同步标志位设计成占用一个字节的编号(FIN标志位也是),既然是一个字节的数据,按照TCP对有数据的TCP segment 必须确认的原则,所以在这里A必须给B一个确认,以确认A已经接收到B的同步信号。(A如果不给呢? B其实也能知道A收到了自己的Syn吧,A重传? 貌似不太合理?)
有童鞋会说,如果A发给B的确认丢了,该如何?A会超时重传这个ACK吗?不会!TCP不会为没有数据的ACK超时重传。那该如何是好?B如果没有收到A的ACK,会超时重传自己的SYN同步信号,一直到收到A的ACK为止。
握手过程的异常情况
- 第一个包,即A发给B的SYN 中途被丢,没有到达B
A会周期性超时重传,直到收到B的确认 - 第二个包,即B发给A的SYN +ACK 中途被丢,没有到达A
B会周期性超时重传,直到收到A的确认 - 第三个包,即A发给B的ACK 中途被丢,没有到达B
A发完ACK,单方面认为TCP为 Established状态,而B显然认为TCP为Active状态:
a. 假定此时双方都没有数据发送,B会周期性超时重传,直到收到A的确认,收到之后B的TCP 连接也为 Established状态,双向可以发包。
b. 假定此时A有数据发送,B收到A的 Data + ACK,自然会切换为established 状态,并接受A的Data。(这里说的有问题,应该是发送不成功,会重传 retransmission)
c. 假定B有数据发送,数据发送不了,会一直周期性超时重传SYN + ACK,直到收到A的确认才可以发送数据。
还有一个就是在谢希仁著《计算机网络》第四版中,讲 “三次握手” 的目的是 “为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误”,这个只能算是表因,并不涉及本质。谢希仁版《计算机网络》中的例子是这样的,“已失效的连接请求报文段” 的产生在这样一种情况下:client 发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达 server。本来这是一个早已失效的报文段。但 server 收到此失效的连接请求报文段后,就误认为是 client 再次发出的一个新的连接请求。于是就向 client 发出确认报文段,同意建立连接。假设不采用 “三次握手”,那么只要 server 发出确认,新的连接就建立了。由于现在 client 并没有发出建立连接的请求,因此不会理睬 server 的确认,也不会向 server 发送数据。但 server 却以为新的运输连接已经建立,并一直等待 client 发来数据。这样,server 的很多资源就白白浪费掉了。采用 “三次握手” 的办法可以防止上述现象发生。例如刚才那种情况,client 不会向 server 的确认发出确认。server 由于收不到确认,就知道 client 并没有要求建立连接。”
拿四次握手来说
1.为什么关闭连接要设计成四次?
2.服务端运行一段时间后,套接字出现了大量的Close_Wait状态,最有可能是什么原因导致的?
3.为什么基于TCP的程序往往都有个应用层的心跳检测机制?
4.服务端的Time_Wait状态再哪个阶段出现?持续多久?为什么要设计这么一个状态?
- 相较于tcp的建立,tcp关闭的时候需要释放两端的资源(发送未完成的数据),因此多了一个对端的FIN的状态("好了,我剩下的数据发送完毕了,可以结束了")
- 服务端收到FIN
- TCP建立链接之后,只是在两端的内核里面维持tcp信息,实际上并没有一个物理的连接的通路,对端这个时候挂了,谁也不知道
- 本端已经发送最后一个ACK后,等待一段时间关闭;一般持续2倍的最大报文生存周期(MSL);因为这时候不确定ack对段是否能够收的到,保持连接的话如果对端没收到这个ack还会重发,另外一点原因是,如果这时候对端还有半路上没到达本端的消息的话,本端关闭时如果有同样端口的服务起来,那就错乱了(当然实际上不可能,因为ack是按照时间累加的),这里做保险
TIME_WAIT状态是怎么产生的
要弄懂TIME_WAIT要从TCP的四次握手的分手协议说起。

上面这个图片展示了TCP从连接建立到连接释放的过程中,客户端和服务端的状态变化图。如果只看连接释放阶段,四次握手
- 客户端先发送FIN,进入FIN_WAIT1状态
- 服务端收到FIN,发送ACK,进入CLOSE_WAIT状态,客户端收到这个ACK,进入FIN_WAIT2状态
- 服务端发送FIN,进入LAST_ACK状态
- 客户端收到FIN,发送ACK,进入TIME_WAIT状态,服务端收到ACK,进入CLOSE状态
- 客户端TIME_WAIT持续2倍MSL时长,在linux体系中大概是60s,转换成CLOSE状态
当然在这个例子和上面的图片中,使用客户端和服务端来描述是不准确的,TCP主动断开连接的一方可能是客户端,也可能是服务端。所以使用主动断开的一方,和被动断开的一方替换上面的图可能更为贴切。
不管怎么说,TIME_WAIT的状态就是主动断开的一方,发送完最后一次ACK之后进入的状态。并且持续时间还挺长的。
能不能发送完ACK之后不进入TIME_WAIT就直接进入CLOSE状态呢?不行的,这个是为了TCP协议的可靠性,由于网络原因,ACK可能会发送失败,那么这个时候,被动一方会主动重新发送一次FIN,这个时候如果主动方在TIME_WAIT状态,则还会再发送一次ACK,从而保证可靠性。那么从这个解释来说,2MSL的时长设定是可以理解的,MSL是报文最大生存时间,如果重新发送,一个FIN+一个ACK,再加上不定期的延迟时间,大致是在2MSL的范围。
根据第三版《UNIX网络编程 卷1》,TIME_WAIT状态的主要目的有两个:
- 优雅的关闭TCP连接,也就是尽量保证被动关闭的一端收到它自己发出去的FIN报文的ACK确认报文;
- 处理延迟的重复报文,这主要是为了避免前后两个使用相同四元组的连接中的前一个连接的报文干扰后一个连接。
如果只考虑上述第一个目标,则TIME_WAIT状态需要持续的时间应该参考对端的RTO(重传超时时间)以及MSL(报文在网络中的最大生存时间)来计算而不是仅仅按MSL来计算,因为只要对端没有收到针对FIN报文的ACK,就会一直持续重传FIN报文直到重传超时,所以最能实现完美关闭连接的时长计算方式应该是从对端发送第一个FIN报文开始计时到它最后一次重传FIN报文这段时长加上MSL,但这个计算方式过于保守,只有在所有的ACK报文都丢失的情况下才需要这么长的时间,另外,第一个目标虽然重要,但并不十分关键,因为既然已经到了关闭连接的最后一步,说明在这个TCP连接上的所有用户数据已经完成可靠传输,所以要不要完美的关闭这个连接其实已经不是那么关键了。
因此,(我猜)RFC标准的制定者才决定以网络丢包不太严重为前提条件,然后根据第二个目标来计算TIME_WAIT状态应该持续的时长。
再来看一下《UNIX网络编程》在描述为什么需要TIME_WAIT状态时的一段话:
Since the duration of the TIME_WAIT state is twice the MSL, this allows MSL seconds for packet in one direction to be lost, and another MSL seconds for the reply to be lost. By enforcing this rule, we are guaranteed that when we successfully establish a TCP connecton, all old duplicates from previous incarnations of the connection have expired in the network.
这段文字说明了TIME_WAIT状态持续2MSL的时间可以让一个TCP连接的两端发出的报文都从网络中消失,从而保证下一个使用了相同四元组的tcp连接不会被上一个连接的报文所干扰。
如何理解TIME_WAIT状态持续2MSL的时间就可以让一个TCP连接的两端发出的报文都从网络中消失呢?
我们假设A发送了ACK报文后过了一段时间t之后B才收到该ACK,则有 0 < t <= MSL。因为A并不知道它发送出去的ACK要多久对方才能收到,所以A至少要维持MSL时长的TIME_WAIT状态才能保证它的ACK从网络中消失。同时处于LAST_ACK状态的B因为收到了ACK,所以它直接就进入了CLOSED状态,而不会向网络发送任何报文。所以晃眼一看,A只需要等待1个MSL就够了,但仔细想一下其实1个MSL是不行的,因为在B收到ACK前的一刹那,B可能因为没收到ACK而重传了一个FIN报文,这个FIN报文要从网络中消失最多还需要一个MSL时长,所以A还需要多等一个MSL。(如果A重传的报文B没有接收到,A继续重传,是不是还是无法保证A重传的报文消失?? 这种说法牵强, 怎么能保证fin报文都消失,所以只是为了保证之前的data报文全部消失 ?)
综上所述,TIME_WAIT至少需要持续2MSL时长,这2个MSL中的第一个MSL是为了等自己发出去的最后一个ACK从网络中消失,而第二MSL是为了等在对端收到ACK之前的一刹那可能重传的FIN报文从网络中消失。另外,虽然说维持TIME_WAIT状态一段时间有2个目的,但这段时间具体应该多长主要是为了达成上述第二个目的而设计的。
TIME_WAIT状态存在的意义,为什么不是直接_CLOSED?
客户端收到服务器的FIN报文之后,并不直接进入CLOSED状态,而是TIME_WAIT状态。客户端会在此状态等等2MSL的时长之后,才会彻底关闭。(MSL是Maximum Segment Life,报文段最大生存时间,一般为2分钟。
)
TIME_WAIT状态存在的原因有两点:
可靠的终止TCP连接
如果 B 没收到 A 发送来的确认报文,那么就会重新发送连接释放请求报文,A 等待一段时间就是为了处理这种情况的发生
保证让迟到的TCP报文段有足够的时间被识别并丢弃
在Linux系统中,一个TCP端口不能被同时打开多次。当一个TCP连接处于TIME_WAIT状态时,我们无法使用此接口来建立新连接。如果不存在此状态,则可以建立一个和刚关闭的连接具有相同IP和端口的连接,也就是原来连接的化身。此化身可以收到属于原来连接的在网络中滞留迟到的报文段,这显然不应该存在,故需要TIME_WAIT状态。
另外,因为MSL是一个TCP报文的最大生存时间,所以2MSL的时间可以保证双向的数据都发送完毕(可收到Fin对方就不会发数据了吧 1MSL就够了?),迟到的报文都已消失(被中转路由器丢弃)。所以2MSL时间之后新的连接可以绝对安全的建立,这就是TIME_WAIT状态需要持续2MSL的原因。
但是,有时候我们希望避免TIME_WAIT状态,因为当程序退出后,我们希望能立即重启它,因为此状态的存在,我们是无法立即重启的。
对于客户端而言,我们一般不需要担心此问题。因为TCP连接中,客户端通常使用的是系统自动分配的临时端口号来建立连接,这个端口号是随机的,所以一般不会和上次的重复。
但是对于服务端而言,如果是服务器主动关闭连接然后异常终止,因为服务器提供服务的总是同一个知名端口号,则会出现不能立即重启的情况。我们可以通过socket选项的SO_REUSEADDR来强制进程立即使用处于TIME_WAIT状态的连接占用的端口.
服务端TIME_WAIT过多
一个朋友问到,自己用go写了一个简单的HTTP服务端程序,为什么压测的时候服务端会出现一段时间的TIME_WAIT超高的情况,导致压测的效果不好呢?
那么这个感觉就有点奇怪了,HTTP服务并没有依赖外部mysql或者redis等服务,就是一个简单的Hello world,而TIME_WAIT的是主动断开方才会出现的,所以主动断开方是服务端?
答案是是的。在HTTP1.1协议中,有个 Connection 头,Connection有两个值,close和keep-alive,这个头就相当于客户端告诉服务端,服务端你执行完成请求之后,是关闭连接还是保持连接,保持连接就意味着在保持连接期间,只能由客户端主动断开连接。还有一个keep-alive的头,设置的值就代表了服务端保持连接保持多久。
HTTP1.0默认的Connection值为close,那么就意味着关闭请求的一方几乎都会是由服务端这边发起的。那么这个服务端产生TIME_WAIT过多的情况就很正常了。
解决方法
按照HTTP协议的头,我们在压测程序发出的HTTP协议头里面加上connection:keep-alive当然能解决这个问题。
还有的方法就是系统参数调优
sysctl net.ipv4.tcp_tw_reuse=1
sysctl net.ipv4.tcp_tw_recycle=1
sysctl net.ipv4.tcp_timestamps=1
tcp_tw_reuse
这个参数作用是当新的连接进来的时候,可以复用处于TIME_WAIT的socket。默认值是0。
tcp_tw_recycle和tcp_timestamps
默认TIME_WAIT的超时时间是2倍的MSL,但是MSL一般会设置的非常长。如果tcp_timestamps是关闭的,开启tcp_tw_recycle是没用的。但是一般情况下tcp_timestamps是默认开启的,所以直接开启就有用了。
Let's assume there is a client that makes a lot of short-living connections to a server.
If the client closes the connection, there will be many ports in TIME_WAIT state on the client side. Since the client runs out of local ports, it becomes impossible to make a new connection attempt quickly.
If the server closes the connection, I will see many TIME_WAITs on the server side. However, does this do any harm? The client (or other clients) can keep making connection attempts since it never runs out of local ports, and the number of TIME_WAIT state will increase on the server side. What happens eventually? Does something bad happen? (slowdown, crash, dropped connections, etc.)
Please note that my question is not "What is the purpose of TIME_WAIT?" but "What happens if there are so many TIME_WAIT states on the server?" I already know what happens when a connection is closed in TCP/IP and why TIME_WAIT state is required. I'm not trying to trouble-shoot it but just want to know what is the potential issue with it.
Each socket in TIME_WAIT consumes some memory in the kernel, usually somewhat less than an ESTABLISHED socket yet still significant. A sufficiently large number could exhaust kernel memory, or at least degrade performance because that memory could be used for other purposes. TIME_WAIT sockets do not hold open file descriptors (assuming they have been closed properly), so you should not need to worry about a "too many open files" error.
The socket also ties up that particular src/dst IP address and port so it cannot be reused for the duration of the TIME_WAIT interval. (This is the intended purpose of the TIME_WAIT state.) Tying up the port is not usually an issue unless you need to reconnect a with the same port pair. Most often one side will use an ephemeral port, with only one side anchored to a well known port. However, a very large number of TIME_WAIT sockets can exhaust the ephemeral port space if you are repeatedly and frequently connecting between the same two IP addresses. Note this only affects this particular IP address pair, and will not affect establishment of connections with other hosts.
13
Each connection is identified by a tuple (server IP, server port, client IP, client port). Crucially, the TIME_WAIT connections (whether they are on the server side or on the client side) each occupy one of these tuples.
With the TIME_WAITs on the client side, it's easy to see why you can't make any more connections - you have no more local ports. However, the same issue applies on the server side - once it has 64k connections in TIME_WAIT state for a single client, it can't accept any more connections from that client, because it has no way to tell the difference between the old connection and the new connection - both connections are identified by the same tuple. The server should just send back RSTs to new connection attempts from that client in this case.
模仿聊天:
server: nc -l 9090
client: nc server-2 9090
但nc复用了端口 复现不了server端的timewait
Looking to one of the netcat source code netcat.c in the method
local_listen
How can I make nc disallow other programs to use its same port? I'd like it to listen on port 8888 and make sure that's the only program that will be listening on that port.
So far I've been able to do what I want by introducing socat along nc like this:
socat TCP-LISTEN:8888,fork TCP:localhost:4444
nc -lv 4444
here is no config option for turning that option off :
server:
socat - tcp-listen:12345client:
socat - tcp:server-2:12345
socat没有复用端口