PseudoTcp - 建立UDP之上的TCP(1):连接和关闭
mail:lihe21327 [at] gmail [dot] com
最近阅读了Libjingle的PseudoTcp.LibJingle很是下功夫做P2P了,在UDP之上做了可靠的传输协议PseudoTcp.
了解PseudoTcp之前,我们需要了解一些TCP的特性。
根据《TCP/IP详解》卷1,可以总结如下:
1.TCP是面相连接的,他需要3次握手和4次终止过程。
2.TCP支持Nangle算法和经受时延的确认来控制报文段数目。
3.TCP含有滑动窗口来控制接收方的流量。
4.TCP支持超时与重传。
5.TCP支持拥塞避免算法。
6.TCP具有坚持定时器和保活定时器
7.TCP要支持路径MTU发现、长肥管道、时间戳选项。
那我们一起剖析一下PseudoTcp实现了上面哪些功能。
PseudoTcp(以后简称PTCP吧)的格式:
通过结构Segment 定义此报文头部:
struct Segment {
uint32 conv, seq, ack;
uint8 flags;
uint16 wnd;
const char * data;
uint32 len;
uint32 tsval, tsecr;
};
各个字段的含义如下:
A)Conversation Number : 流水号,是用来标识此次连接。即TCP里所谓的本地IP:本地端口-远程IP:远程端口,4组合为一个流水号。因为PTCP是UDP之上的(当然也可以是其他协议之上),如果socket没有绑定到本地端口,可能获取的不是需要的数据。如果获取的Conv Number不一样,接收方会发送RST(不过PTCP里已经注释了此段代码)。此外,PTCP并不关心他的传输层是有一个连接还是多个连接,她只关心CONV Number是否一致。
B)Seq Number:32位序号,即此数据表示的序列,不一定从0开始
C)Ack Number:32确认序列号。确认已经获取到的数据序列加1,即下一个需要接受的序列号。
D)Control:现未使用
E)URG:紧急指针,1bit
F)ACK:确认序列号有效,1bit
G)PSH:接收方尽可能将这个报文送给应用层,1bit
H)RST:重置连接
I)FIN:表示发送完所有数据,断开连接。
J)Window:窗口大小
K)TimeStamp Sending:本端发送包时间(采用以本端的时间计算方式)
L)TimeStamp Receiving:对方最近接收包时间(采用以对方的时间计算方式)
M)Data:数据
注:上面的E-I的含义,在实现上完全不同。下面会提到。
PTCP的状态:
TCP_LISTEN:监听
TCP_SYN_SENT:SYN包已经发送
TCP_SYN_RECEIVED:已经接收SYN包
TCP_ESTABLISHED:已经建立连接
TCP_CLOSED:已经关闭连接
PTCP的状态转移相对TCP来说简单多了,TCP如下:
3路握手:
TCP建议连接时需要来回总共有3个TCP包来做握手,即
A)SYN[A]:
B)ACK[B],SYN[A+1]
C)ACK[B+1]
PTCP握手过程如下:
当开始时两端都处于TCP_LISTEN状态。
当C端发送SYN包到S端时,C端处于TCP_SYN_SENT状态
当S端处于TCP_LISTEN时收到SYN包,S端转为TCP_SYN_RECEIVED
当S端处于TCP_SYN_RECEIVED时,发送ACK时状态不变
当C端处于TCP_SYN_SENT时,收到ACK,则转为TCP_ESTABLISHED
当S端处于TCP_SYN_RECEIVED,收到非控制包时转为TCP_ESTABLISHED
这里解释一下控制包:上面PTCP协议头结构里的第13个字节处(即URG,ACK等在的字节)其实只取3个值之一:
0:数据包
0x02:CTL包,当握手时使用。
0x04:RST包。现在发此段包的代码被注释掉。
所以控制包,指的是握手时才会发送,握手完之后都属于数据包。
可见PTCP的握手过程和TCP的握手过程有微小的差异。当C端转为TCP_ESTABLISHED后,等到有数据才会发送给S端(而不是立即),S端直到只有等到有数据的包时,才把状态改为TCP_ESTABLISHED。而TCP是,如果没有数据会立即发送,S端只要收到ACK就改为ESTABLISHED状态。
连接建立时超时:
当C端发送完SYN包之后,一直没有响应时,没过3S,C端会发送一个SYN请求。直到发送30次之后,还没有收到回包,则停止发送并关闭连接。即等待时间为3*30=90S,而大多数TCP实现的超时时间为75S。
最大报文段长度(MSS):
TCP默认MSS为536,即取MTU为576( X.25 Networks),包括20个字节的IP头和20个字节的TCP头。
对于PTCP,默认MTU取为65536,即UDP容纳的最大长度,那么MSS取值为65536-116。
116的计算来自:
PACKET_OVERHEAD = HEADER_SIZE + UDP_HEADER_SIZE + IP_HEADER_SIZE + JINGLE_HEADER_SIZE
JINGLE_HEADER_SIZE用于Relay包,具体需要了解STUN协议和TURN协议。
MTU的发现完全由调用方来决定,PTCP只提供了接口来更新MTU。
在Libjingle里,对于win32,枚举下面数组PACKET_MAXIMUMS,然后通过WinPing来发现此次PTCP连接的MTU。如果没有获取到MTU,默认取值为1280(此时MSS为1280-116=1164)。
为什么MTU默认取值为1280呢,有什么数据依据呢?
// Standard MTUs
const uint16 PACKET_MAXIMUMS[] = {
65535, // Theoretical maximum, Hyperchannel
32000, // Nothing
17914, // 16Mb IBM Token Ring
8166, // IEEE 802.4
//4464, // IEEE 802.5 (4Mb max)
4352, // FDDI
//2048, // Wideband Network
2002, // IEEE 802.5 (4Mb recommended)
//1536, // Expermental Ethernet Networks
//1500, // Ethernet, Point-to-Point (default)
1492, // IEEE 802.3
1006, // SLIP, ARPANET
//576, // X.25 Networks
//544, // DEC IP Portal
//512, // NETBIOS
508, // IEEE 802/Source-Rt Bridge, ARCNET
296, // Point-to-Point (low delay)
//68, // Official minimum
0, // End of list marker
};
PTCP的关闭。
TCP的关闭时由4步骤完成。
1. FIN[A]
2. ACK[A+1]
3. FIN[B]
4. ACK[B+1]
然而,有时候可以做到3步,即上面的2,3步可以合成在一个TCP包里发送。对于上面只完成前两步的状态成为半关闭状态,此时发送FIN[A]的端表示自己不再有多余的数据要发送,但还能接收数据。
当调用PTCP的Close方法时,此端丢弃对方发过来的数据,只做应答,即只发送对方发来数据的ACK。并且等到此方数据都发送完,需关闭整个连接。以此看来,PTCP没有半关闭状态,并且PTCP也只是用来支持P2P用的,不需要半关闭状态。
2MSL等待状态
MSL是指一个数据包在网络上存在的最长时间。而2MSL是指当主动关闭方发送被动关闭方发送的FIN对应的ACK时,如果这个ACK被丢失了,则被动关闭方超时重发最后的FIN,此时主动关闭方再次发ACK,当主动关闭方发送第一个FIN对应的ACK到,拿到最后的FIN之间的时间段最长为2MSL。那为什么主动关闭方处于2MSL等待状态呢?是因为,如果主动关闭方发送了第一个FIN对应的ACK之后,放弃了此连接,那么下一个新建的连接有可能复用此连接(即同一个插口对),此时新建的连接有可能因为上一个丢失的ACK,而收到重发的FIN,导致连接被关闭。
然而PTCP不存在半关闭的概念,故2MSL等待状态也随之没有。此外,PTCP是用来做P2P的,两者之间的连接时双方协商定义的,并且PTCP在头部给予了Conversation number的概念,以便协商中防止产生同一个连接的产生。
复位报文段
当TCP存在如下情况时会产生复位报文段。
A.当服务器没有开启指定的连接端口时,对于UDP来说产生端口不可达,而TCP产生RST报文
B.当一端产生异常终止时,会发送RST报文。即当设置SO_LINGER套接口选项时,close套接口会产生RST报文。
C.检测到半打开连接,当接收方异常终止重启后接收对方在旧的连接上传过来的数据时,会发送RST报文。
对于PTCP来说,现在没有一个地方会发送RST报文(之前有过的被注释了,当收到不是当前的CONV时会发送RST),但如果一旦收到了RST报文,则立即关闭此连接。
同时打开
TCP的同时打开情景是如下:当C用端口7777连接S的端口8888,同时S用端口8888连接C的端口7777,此时包的顺序如下:
1) SYN[A]
2) SYN[B]
3) ACK[A+1]
4) ACK[B+1]
显然上面的握手从3次变为4次。
PTCP的同时打开,也类似如上,由4个包来完成握手。
1) C端发送SYN时,状态变为TCP_SYN_SENT
2) 同时S端发送SYN,S和C的状态此时都为TCP_SYN_SENT
3) C,S同时向对方(可以不是同时)发送ACK,此时C,S状态都变为TCP_ESTABLISHED。
同时关闭
TCP是支持C,S同时关闭的。
1)C,S同时发送FIN,状态变为FIN_WAIT_1
2)C,S同时收到FIN,并发送ACK,状态变为CLOSING
3)C,S同时收到ACK,两个状态都变为TIME_WAIT
对于PTCP,没有像TCP,不存在FIN包,显然对关闭状态的维护不是很完美。也同样,看不到同时关闭的情形,这些交给底层传输层(UDP)等之类来完成,由调用方来维护状态。
为什么PTCP没有提供FIN报文以及对应的状态呢?
TCP选项
TCP保留40个字节传输其他选项,主要有窗口扩大因子,时间戳选项,MSS长度等。
PTCP也通过一种方式来增加其他选项,如MSS和窗口扩大因子。当传输的是控制包且有数据内容时,如果第一个字节为CTL_CONNECT,则会调用方法parseOptions来解析是否含有MSS,窗口扩大因子等等选项。这些选项的实现细节后续会提及(时间戳选项直接在报文头里有,固这个选项很重要,后续会提到此选项的作用)。