《TCP/IP详解卷3:TCP事务协议、HTTP、NNTP和UNIX域协议》 —1.3 TCP上的客户-服务器

举报
华章计算机 发表于 2019/11/19 20:46:17 2019/11/19
【摘要】 本节书摘来自华章计算机《TCP/IP详解卷3:TCP事务协议、HTTP、NNTP和UNIX域协议》一书中第1章,第1.3节,作者是[美]W. 理查德·史蒂文斯(W.Richard Stevens) ,胡谷雨 吴礼发 等译 谢希仁 校。

1.3   TCP上的客户-服务器

下一个例子是TCP上的客户-服务器事务应用。图1-5给出了客户程序。

image.png

图1-5   TCP事务的客户程序

1. 创建TCP插口和连接到服务器

10-17   调用socket函数创建一个TCP插口,然后在Internet插口地址结构中填入服务器的IP地址和端口号。对connect函数的调用启动TCP的三次握手过程,在客户和服务器之间建立起连接。卷1的第18章给出了TCP连接建立和释放过程中交换分组的详细情况。

2. 发送请求和半关闭连接

19-22   客户的请求是用write函数发给服务器的。之后客户调用shutdown函数(函数的第2个参数为1)关闭连接的一半,即数据流从客户向服务器的方向。这就告知服务器客户的数据已经发完了:从客户端向服务器传递了一个文件结束的通知。这时有一个设置了FIN标志的TCP报文段发给服务器。客户此时仍然能够从连接中读取数据—只关闭了一个方向的数据流。这就叫作TCP的半关闭(half-close)。卷1的18.5节给出了有关细节。

3. 读取应答

23-24   读取应答是由函数read_stream完成的,如图1-6所示。由于TCP是一个面向字节流的协议,没有任何形式的记录定界符,因而从服务器端TCP传回的应答可能会包含在多个TCP报文段中。这也就可能会需要多次调用read函数才能传递给客户进程。而且我们知道,当服务器发送完应答后就会关闭连接,使得TCP向客户端发送一个带FIN的报文段,在read函数中返回一个文件结束标志(返回值为0)。为了处理这些细节问题,在read_stream函数中不断调用read函数直到接收缓存满或者read函数返回一个文件结束标志。read_stream函数的返回值就是读取到的字节数。

image.png

图1-6   read_stream函数

还有一些别的方法可以在类似TCP这样的流协议中给记录定界。许多Internet应用程序(FTP、SMTP、HTTP和NNTP)使用回车和换行符来标记记录的结束。其他一些应用程序(DNS、RPC)则在每个记录的前面加上一个定长的记录长度字段。在我们的例子中,利用了TCP的文件结束标志(FIN),因为在每次事务中客户只向服务器发送一个请求,而服务器也只发回一个应答。FTP也在其数据连接中采用这项技术,告知对方文件已经结束。

图1-7给出的是TCP的服务器程序。

image.png

图1-7 TCP事务的服务器程序

image.png

图1-7   (续)

4. 创建监听用TCP插口

8-17   用于创建一个TCP插口,并将服务器的知名端口绑定到该插口上。与UDP服务器一样,TCP服务器也将通配符作为其IP地址。调用listen函数将新创建的插口作为监听插口,用于等待客户端发起的连接。listen函数的第二个参数规定了允许的最大挂起连接数,内核要为该插口将这些连接进行排队处理。

SOMAXCONN在头文件<sys/socket.h>中定义。其数值过去一直都取5,但现在有一些比较新的系统将其定为10。对于一些很繁忙的服务器(例如Web服务器),已经发现需要取更大的值,比如256或1024。在14.5节中我们还将对此问题进行更多的讨论。

5. 接受连接和处理请求

18-28    服务器进程调用accept函数后就进入阻塞状态,直到有客户进程调用connect函数而建立起一个连接。函数accept返回一个新的插口描述符sockfd,代表与客户和服务器之间所建立的连接。服务器调用函数read_stream读取客户的请求(图1-6),再调用write函数向客户发送应答。

这是一个反复循环的服务器:把当前的客户请求处理完毕后才又调用accept去接受另一个客户的连接。并发服务器可以并行地处理多个客户请求(即同时处理)。在Unix的主机上实现并发服务器的常用技术是:在accept函数返回后,调用Unix的fork函数创建一个子进程,由子进程处理客户的请求,父进程则紧接着又调用accept去接受别的客户连接。实现并发服务器的另一项技术是为每个新建立的连接创建一个线程(叫作轻量进程)。为了避免那些与网络无关的进程控制函数把例子搞复杂,我们只给出了反复循环的服务器。参考书[Stevens 1992]的第4章讨论比较了循环服务器和并发服务器。

还有第三个选择是采用预分支服务器。即服务器启动时连续调用fork函数数次,并让每个子进程都在同一个监听插口描述符上调用accept函数。这种办法节省了为每个客户的连接请求临时创建子进程的时间开销,这对于繁忙的服务器来说,是很大的节省。有些HTTP服务器就采用了这项技术。

图1-8给出了TCP上客户-服务器事务的时序图。我们首先注意到,与图1-4中UDP上的事务相比,网络上交换的分组数增加了:TCP上事务的分组数是9,而UDP上的则是2。采用TCP后,客户端测量的事务时间是不少于2 × RTT + SPT。通常,中间三个从客户到服务器的报文段(对服务器SYN的ACK、请求以及客户的FIN)是紧密相连的;后面两个从服务器到客户的报文段(服务器的应答和FIN)也是紧密相连的。这使实际事务时间比从图1-8中看到的更接近2×RTT + SPT。

image.png

本例中多出来的一个RTT源于TCP连接建立的时间开销:图1-8中前两个报文段所花的时间。如果TCP可以把建连和发送客户数据以及客户FIN(图中客户端发出的前四个报文段)合起来,再把服务器的应答和FIN合起来,事务时间就又可以回到RTT + SPT了,这与UDP的一样。事实上,这就是T/TCP中采用的基本技巧。

6. TCP的TIME_WAIT状态

TCP要求,首先发出FIN的一端(我们的例子中是客户),在通信双方都完全关闭连接之后,仍然要保持在TIME_WAIT状态直至两倍的报文段最大生存时间(MSL)。MSL的建议值是120秒,也即处于TIME_WATE状态要达到4分钟。当连接处于TIME_WAIT状态时,同一连接(即客户IP地址和端口号,以及服务器IP地址和端口号这4个值相同)不能重复打开(我们在第4章中还要更多地讨论TIME_WAIT状态)。

许多基于伯克利代码的TCP实现,在TIME_WAIT状态的保持时间仅仅为60秒,而不是RFC 1122 [Braden 1989]中指定的240秒。在本书的所有计算中,我们还是假定正确的等待周期为240秒。

在我们的例子中,客户端首先发出FIN,这称为主动关闭,因而TIME_WAIT状态出现在客户端。在这个状态延续期内,TCP要为这个已经关闭的连接保留一定的状态信息,以便能正确处理那些在网络中延迟一段时间、在连接关闭之后到达的报文段。同样,如果最后一个ACK丢失了,服务器将重传FIN,使客户端重传最后的ACK。

其他一些应用程序,特别是WWW中的HTTP,要求客户程序发送一个专门的命令来指示已经将请求发送完毕(而不是像我们的客户程序那样采用半关闭连接的办法);接着服务器就发回应答,紧接着就是服务器的FIN。然后客户程序再发出FIN。这样做与前面所述的不同之处在于,现在的TIME_WAIT状态出现在服务器端而不是客户端。对许多客户访问的繁忙服务器来说,需要保留的状态信息会占用服务器的大量内存。因此,当设计一个事务性客户-服务器应用程序时,让连接的哪一端关闭后进入TIME_WAIT状态值得仔细斟酌。我们还将看到,T/TCP可以让TIME_WAIT状态的延续时间从240秒减少到大约12秒。

7. 减少TCP中的报文段数

像图1-9所示的那样,把数据和控制报文段合并起来可以减少图1-8中所示的TCP报文段数。请注意,这里的第一个报文段中包含有SYN、数据和FIN,而不像图1-8中那样仅仅是SYN。类似地,服务器的应答和服务器的FIN也可以合并。虽然这样的分组序列也符合TCP的规定,但是作者无法在应用程序中利用现有的插口API使TCP产生这样的报文段序列(因此才在图1-9中客户端产生第一个报文段时和服务器端产生最后一个报文段时标上问号);而且据作者所知,也没有哪一个应用程序确实生成了这样的报文段序列。

值得一提的是,尽管我们把报文段的数目由9减少到了5,但客户端观测的事务依然是2×RTT + SPT。这是因为TCP中规定,服务器端的TCP在三次握手结束之前不能向服务器进程提交数据(卷2的27.9节说明了TCP是如何在连接建立之前将到达的数据进行排队缓存的)。加上这种限制的原因是服务器必须确信来自客户的SYN是“新的”,即不是以前某次连接的SYN在网络中延迟一段时间后到达服务器端的。确认过程是这样的:服务器对客户发送的SYN发送确认,再发出自己的SYN,然后等待客户对该SYN的确认。当三次握手完成之后,通信双方就都知道对方的SYN是新的。由于在三次握手结束之前服务器无法开始处理客户的请求,故分组数的减少并没有缩短客户端测得的事务时间。

image.png

下面这段话引自RFC 1185 [Jacobson, Braden, and Zhang 1990]的附录:“注意:使连接能够尽快重复利用是早期TCP开发的重要目标。之所以有这样的要求是因为当时人们希望TCP既是应用层事务协议的基础,同时也是面向连接协议的基础。当时讨论中甚至把既包含SYN和FIN比特,同时又包含数据的报文段叫作‘圣诞树’报文段和‘Kamikaze(敢死队)’报文段。但这种热情很快就被泼了冷水,因为人们发现,三次SYN握手和FIN握手意味着一次数据交换至少需要5个分组。而且,TIME_WAIT状态的延续说明同一个连接不可能马上再次打开。于是,再没有人在这个领域做进一步的研究,尽管现在的某些应用程序(比如简单邮件传输协议(SMTP))经常会产生很短的会话。人们一般都可以采用为每个连接选用不同的端口对的办法来避开重用问题”。

RFC 1379 [Braden 1992b]中写道:“这些‘Kamikaze(敢死队)’报文段不是作为一种支持的服务来提供,而是主要用来搞垮其他实验性的TCP!”

作为一个实验,作者编写了一个测试程序,这个程序把SYN与数据和FIN在一个报文段中发出去,即图1-9中的第一个报文段。该报文段发给8个不同版本Unix的标准echo服务器(卷1的1.12节),再用Tcpdump观察所交换的数据。其中的7个(4.4BSD、AIX 3.2.2、BSD/OS 2.0、HP-UX 9.01、IRIX System V.3、SunOS 4.1.3和System V Release 4.0)都能正确处理该报文段,另外一个(Solaris 2.4)则把随SYN一起传送的数据扔掉,迫使客户程序重传数据。

那7个系统中的报文段序列与图1-9所描绘的不尽相同。当三次握手结束后,服务器立刻就对客户的数据和FIN发出确认。另外,由于echo服务器无法把数据和FIN***在一起(图1-9中的第四个报文段)发送,结果是发了两个报文段而不只是一个:应答和紧接其后的FIN。因此,报文段的总数是7而不是图1-9中所示的5。我们在3.7节中会进一步讨论与非T/TCP实现的兼容性问题,并给出一些Tcpdump的输出结果。

许多从伯克利演变而来的系统中,服务器无法处理接收到的报文段中只有SYN、FIN,而没有数据、ACK的情况。这个bug使得新创建的插口保持在CLOSE_WAIT状态直到主机重新启动。但这却是一个合法的T/TCP报文段:客户建立起了一个连接,没有发送任何数据,然后就关闭连接。


【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。