tcp/ip学习笔记--第18章TCP Establishment and termination

来源:互联网 发布:单片机与上位机通信 编辑:程序博客网 时间:2024/06/15 10:34

简介:

TCP是一个基于连接的协议,在开始发送数据前必须建立连接,当连接不再需要的时候需要断开连接。


连接的建立和断开

步骤如下:

1)请求端(通常称为客户)发送一个SYN段指明客户打算连接的服务器的端口,以及初始序号(ISN,在这个例子中为1415531521)。这个SYN段为报文段1。
2)服务器发回包含服务器的初始序号的SYN报文段(报文段2)作为应答。同时,将确认序号设置为客户的ISN加1以对客户的SYN报文段进行确认。一个SYN将占用一个序号。
3)客户必须将确认序号设置为服务器的ISN加1以对服务器的SYN报文段进行确认(报文段3)。

时间线:



关闭:

1.A方发送FIN标志被打开的packet(序列号是另一方发送过来的最大的确认序列号),B方发送ack进行确认。A到B的连接就被关闭了,A不能向B发数据,但是B还可继续向A发数据。B的应用程序这会接收到系统内核函数发送的end of file.

2.当B也不需要向A发送数据的时候,B就向A发送一个FIN标志被打开的包,A收到后发送ack进行确认。然后,A会进入TIME_WAIT状态,B则进入closed状态。

个人关于TCP为什么建立连接握手3次而断开连接又需要发送4个包的理解:

1)如果A要向B通知某件事,那么一般的流程是这样的。A向B发送一个通知,B回复一个确认收到。收到B的回复,A就可以确认B已经收到通知了。

2)回到3次握手上来。由于报文2同时包含确认的内容和SYN报文的内容,所以可以将报文2看做2个报文,其实也可以称为”4次握手",只是将2个报文合二为一了,进行了优化。

3)A要和B建立一个TCP连接,那么A向B发出通知,也就是上面时间系列中的第一个报文,B收到之后,发送ack进行确认。A收到B的确认,就可以确定B已经准备好了。可是B是否知道A是真的希望同自己建立一个TCP连接呢,答案是B并不确认,因为A有可能是误操作导致发送了报文段1。这也是为什么B还需要向A发送一个SYN报文。B在收到SYN报文后需要进行ack确认。其实这就是1)中的说明。A收到B的ack确认后,确认B已经准备好。B收到A的ack确认后,确认B已经准备好。当双方都准备好,一个TCP就成功的建立起来了。


理解了建立连接的过程,要理解断开的过程,相对就比较简单。

A要通知B,我不向你发送数据了,B进行ack确认,A到B方向的连接就相当于断开了。

但是由于TCP连接是全双工模式(两个方向都可以同时发数据),A到B向断开了,B到A并不一定会断开。

当B也不需要向A发送数据的时候,B就通知A,A进行ack确认,B到A方向的连接也就断开了。两个方向的连接都断开了,那么整个连接也就断开了。

也就是因为两个方向连接的断开不一定是同时,所以断开连接的过程必须发送4个包,而不能进行合并优化,像建立连接的过程一样只发送3个包。


建立连接超时

会有各种各样的情况导致连接无法建立起来,另一端Down掉就是一种情况。

如果A发出建立连接的请求并且没有收到回应,那么A会尝试重新发送建立连接请求,直到超过75s左右,放弃建立连接。

值得关注的是重新发起建立连接请求的时间间隔:

第1次重试是5.5s到6s之间,而第2次重试一定是准确的24s,这和计时器的工作原理有关。当系统启动起来以后,会开启一个500ms溢出一次的定时器。然后设置超时的时候会用这个计时器。想设置超时时间为6s,那么你就设置12个时钟周期。但是,这12个周期不是完整的12个周期,第一个周期的时间是随机的0-500ms。第二次超时间,设置48个周期,就会是准备的24s


最大报文段长度  maximum segment size

这是TCP option header中的字段,目的是为了告诉另一端,为了在ip层不产生分片,所允许发送的数据块的最大长度。(至于为什么不希望进行分片,目前并不清楚)

如果A的接口的MTU是1500,那么他就会发送1500-20(TCP头部长度)-20(ip头部长度)=1460。如果B的MTU是576,那么他就会发送536。A收到B发送的536,不会发送大小超过536的报文。B收到A发送的1460,但是他知道自己的MTU是536,所以也不会发送超过536的报文。

当然,也并不就是说,有了MSS,就一定不会发生分片,因为两端接口的MTU,并不代表整个路径的MTU。

一般来说,MSS越大越好,因为大的数据块能够更多的分摊IP和TCP头部所带来的开销。


TCP半关闭

什么是半关闭前面已经记录。

半关闭的应用场景:

A向B发送数据,A发送完毕后,B对数据进行处理,然后返回结果。

那么A就可以方便的在数据发送完成之后,关闭A到B方向的连接。B就可以由此知道A方的数据已经发送完毕。而不需要A和B进行协商,怎么来表示数据的结束。


2MSL wait

MSL:maximum segment lifetime ,即报文最大存活时间。在IP报文中,由TTL来标识IP报文的存活时间,所以我们知道IP报文的存活时间一定是有限的。

各种TCP的实现都会为MSL选择一个值,这个值一般是30s,1min,或2min。当主动发起连接关闭的一方发送最后一个ack确认后,必须进行TIME_WAIT状态,并保持两个MSL。这样做的目的是防止最后一个ack丢失,然后另一端会重发最后一个FIN。(如果另一边没有收到这个确认就会一直处于LAST_ACK状态而无法close,轻则导致资源浪费甚至程序无法正常结束。)

任何在处于2MSL wait 状态下到达的报文都会被丢弃。

RFC要求,处于2MSL wait 状态的套接字(由本地ip,端口,远端ip,端口唯一决定),是不能再次建立连接的,这样要求的原因应该是为了防止无法区分之前的连接可能存在的报文。

但是一般的TCP实现进行了更为严格的限制——处于2MSL wait的端口不能重新被占用。这对于客户端来说一般没有什么影响,应该客户端每次都用不同的临时端口。但是如果是服务端主动关闭的连接,那么服务端会进入2MSL wait 状态。这将导致服务端在2MSL的时间内不能重新启动。

幸运的是,一般的实现都提供了SO_REUSEADDR选项,设置此选项后,可以继续使用此端口。

如在java代码中,可以这么写:

ServerSocket serverSocket = new ServerSocket();serverSocket.setReuseAddress(true);serverSocket.bind(new InetSocketAddress(10228));

但是,一般的实现, 在2MSL wait 状态时,同一个套接字是确实不能复用的。


安静时间的概念  quiet time concept

如果主机出现故障 崩溃了,那么重启之后不会受到2MSL wait 状态的约束。如果再次建立与崩溃之前相同的套接字,则有可能会接收到崩溃之前的连接发送的报文段。并且不论怎么设置初始序列号都有可能发生这种情况(不明白为什么),所以说主机在重新启动之后最好在2MSL之后再建立TCP连接。

但是,一般的实现都没有遵守这个要求,因为一般来说,主机重启的时间会超过2MSL


复位报文段 reset Segments

一般来说,如果收到一个对指定socket连接来说并不正确的报文,就会返回复位报文。(原文:a segment arrives that doesnot appear correct for the referenced conection)

到不存在的端口的连接请求
如果收到一个报文,发现目的端口并没有被任何程序占用,那么就会回发复位报文。值得注意的是,通过我编写java代码的经验发现,如果是建立连接的时候收到复位报文,程序抛出的错误是:connection refused.而如果连接已经建立好,在发送消息的过程中另一端因为程序被关闭而返回了复位报文的情况,程序抛出的错误是:connection reset.

异常终止一个连接  aborting a connection

TCP实现一般都会提供 套接字选项(SO_KUBGER)来异常终止一个连接。这会提供两个特性:1.正在排除的数据会被丢弃并立即发送一个复位报文。2.收到复位报文的另一端可以分辨出来另一方异常关闭了一个连接而不是正常关闭。

收到复位报文的一端不会发任何确认信息。


检测半打开连接 detecting half-open connections

当一端主机crash掉,如果另一端也一直没有发消息,那么就会一直不知道另一端已经关闭了,如果一直不检测,会导致很多处于半打开状态的连接,造成对资源和端口的浪费。TCP的keepAlive选项可以解决这个问题(后面的章节讲)

值得注意的是,主机crash和程序crash是两个不同的概念。如果主机突然crash,比如说断电,那么主机任何事情也做不了,肯定不会发复位报文。但是如果是程序crash,操作系统可能会发送复位报文。我在eclipse上用java写socket程序进行了测试,如果如果突然关闭一端,操作系统确实是会发复位报文给另一端的。但是,如果程序中并没有读取操作的话,程序不会收到操作系统的通知。


同时打开 simultaneous open

理论上存在这种可能,但是实际情况中很少会出现这种情况,因为现在一般的软件都设计成服务器-客户端的模式,都是客户端主动去连接服务器,服务器不会主动连接客户端。而且就算两端都需要主动连接,刚好同一时刻连接的概率也实在是小得不行。但是TCP作为一个完善的协议,肯定各种情况都要考虑周全。

TCP对于同时打开的这种情况,希望最终还是只建立一个连接。所以设计是这样的:


对照正常情况来看:


正常情况下,A做为主动发起建立连接请求的一方B,当A发送SYN报文之后,就等待另一端B的SYN+ack报文。在同时打开的情况,A发出SYN之后,居然只收到了B的SYN报文,但是没ack。这个时候,A就知道自己中奖了,他妈的,是同时打开的情况。这个时候,A就不像正常情况下那样,发送一个只包含ack内容的报文就完了。A会发送一个SYN+ack的报文。

同理B也是一样。

但是,考虑一种极端情况,如果A发往B的“SYN J,ack K+1”报文比“SYN J”先到,主机B会如何处理呢。网上搜索了一番,没有找到任何相关的讨论,鉴于这个问题实在是太偏,我也就不花时间继续深究了,以后如果遇到再来研究吧。


同时关闭   simultaneous close

作为主动关闭的一方,在发送了FIN J之后,正常情况下将要收到ack,然后进入FIN_WAIT2状态。但是如果在收到ack前收到了另一端发来的FIN报文,就明白是同时关闭的情况了,然后进入CLOSING的状态,等收到ack之后进入TIME_WAIT状态。两端都会进入TIME_WAIT状态,为什么呢?还是和正常关闭时主动关闭的一方要等待一样的原因:应对ack在发送的过程中丢失了的情况,进行超时重传。

同时关闭,不存在同时打开时那种极端的情况。如果“ack K+1”报文比“FIN J”报文先到,那么这一端的感受就跟正常的主动关闭一样,虽然另一端检测到的是同时关闭,而且对结果也没有任何影响。都会进行TIME_WAIT状态。


TCP选项

就是简单的:结束选项,无操作选项(用来填充,因为TCP头部长度的单位是4字节为单位进行衡量的),MSS选项,window sacle factor选项(不知道作用),timestamp选项。


TCP服务器设计

端口和ip

不管建立多少个连接,服务器端端口都一样的。而客户端的端口一般会随机分配。

如果一个主机有多个网卡,可以选择监听所有网卡的某个端口,或者指定某个特定网卡的端口。一般来说,默认的都是监听所有网卡的端口。(不知道是否有监听指定某几个网卡端口的办法?但是好像一般也没有这种场景)

限制远端ip

一般的实现都无法提供这样的功能。需要程序在连接建立后进行过滤。

接入的连接请求队列

在服务端,连接建立过程是这样的:tcp实现收到连接请求,自动进行应答,建立连接。然后将这个连接放入一个队列中,等待程序来取,然后进行相应的处理。这个队列的长度是有限并且固定的,应用程序一般可以通过backlog这个值来指定。有新的连接请求来到,会检查队列是否已经满了,如果是的话,就不会对连接请求做出响应,注意是不响应,而不是发送错误报文。这样做的原因是,希望客户端会进行超时重传,而在客户端重传的这段时间,应用程序有希望将队列中的连接拿走进行处理,而如果发送错误报文,一户般来说,客户端将会放弃这次连接。

需要注意的是,backlog值和最大连接数的区别。这是两个完全不同的概念。

另一个需要注意的细节:当队列还有空间的时候,大多数TCP/ip实现会自动与客户端建立连接,根本就没有机会来通知应用程序这个连接来自何处,是否可以同其建立连接。所以说,TCP的上层一定还需要进行其他保护和过滤措施,而不是依赖于TCP。