第18章 TCP连接的建立与终止
18.11 TCP 服务器的设计
我们在1 . 8节说过大多数的T C P服务器进程是并发的。当一个新的连接请求到达服务器时,服务器接受这个请求,并调用一个新进程来处理这个新的客户请求。不同的操作系统使用不同的技术来调用新的服务器进程。在 U n i x系统下,常用的技术是使用 f o r k函数来创建新的进程。
如果系统支持,也可使用轻型进程,即线程( t h r e a d)。我们感兴趣的是 T C P与若干并发服务器的交互作用。需要回答下面的问题:当一个服务器进程接受一来自客户进程的服务请求时是如何处理端口的?如果多个连接请求几乎同时到达会发生什么情况?
18.11.1 TCP服务器端口号
通过观察任何一个 T C P服务器,我们能了解 T C P如何处理端口号。我们使用 n e t s t a t命令来观察 Te l n e t服务器。下面是在没有 Te l n e t连接时的显示(只留下显示 Te l n e t服务器的行)。
- a标志将显示网络中的所有主机端,而不仅仅是处于 E S TA B L I S H E D的主机端。- n标志将以点分十进制的形式显示 I P地址,而不是通过 D N S将地址转化为主机名,同时还要求显示端口号(例如为 2 3)而不是服务名称(如 Te l n e t)。-f inet选项则仅要求显示使用 T C P或U D P的主机。
显示的本地地址为 2 3,星号通常又称为通配符。这表示传入的连接请求(即 S Y N)将被任何一个本地接口所接收。如果该主机是多接口主机,我们将制定其中的一个 I P地址为本地I P地址,并且只接收来自这个接口的连接(在本节后面我们将看到这样的例子)。本地端口为2 3,这是Te l n e t的熟知端口号。
远端地址显示为 * . *,表示还不知道远端 I P地址和端口号,因为该端还处于 L I S T E N状态,正等待连接请求的到达。现在我们在主机 s l i p(1 4 0 . 2 5 2 . 1 3 . 6 5)启动一个Te l n e t客户程序来连接这个 Te l n e t服务器。以下是n e t s t a t程序的输出行:
端口为2 3的第1行表示处于E S TABLISHED 状态的连接。另外还显示了这个连接的本地 I P地址、本地端口号、远端 I P地址和远端端口号。 本地I P地址为该连接请求到达的接口(以太网接口,1 4 0 . 2 5 2 . 1 3 . 3 3)。
处于L I S T E N状态的服务器进程仍然存在。这个服务器进程是当前 Te l n e t服务器用于接收其他的连接请求。当传入的连接请求到达并被接收时,系统内核中的 T C P模块就创建一个处于E S TA B L I S H E D状态的进程。另外,注意处于 E S TA B L I S H E D状态的连接的端口不会变化:也是2 3,与处于L I S T E N状态的进程相同。
现在我们在主机s l i p上启动另一个Te l n e t客户进程,并仍与这个 Te l n e t服务器进行连接。以下是n e t s t a t程序的输出行:
现在我们有两条从相同主机到相同服务器的处于 E S TA B L I S H E D的连接。它们的本地端口号均为2 3。由于它们的远端端口号不同,这不会造成冲突。因为每个 Te l n e t客户进程要使用一个外设端口,并且这个外设端口会选择为主机( s l i p)当前未曾使用的端口,因此它们的端口号肯定不同。
这个例子再次重申 T C P使用由本地地址和远端地址组成的 4元组:目的I P地址、目的端口号、源I P地址和源端口号来处理传入的多个连接请求。 T C P仅通过目的端口号无法确定那个进程接收了一个连接请求。另外,在三个使用端口 2 3的进程中,只有处于 L I S T E N的进程能够接收新的连接请求。处于 E S TA B L I S H E D的进程将不能接收 S Y N报文段,而处于L I S T E N的进 程将不能接收数据报文段。
下面我们从主机s o l a r i s上启动第3个Te l n e t客户进程,这个主机通过 S L I P链路与主机s u n相连,而不是以太网接口。
现在第一个 E S TA B L I S H E D连接的本地 I P地址对应多地址主机 s u n中的 S L I P链路接口地址(1 4 0 . 2 5 2 . 1 . 2 9)。
18.11.2 限定的本地IP地址
我们来看看当服务器不能任选其本地 I P地址而必须使用特定的 I P地址时的情况。如果我们为s o c k程序指明一个 I P地址(或主机名),并将它作为服务器,那么该 I P地址就成为处于L I S T E N服务器的本地I P地址。例如
代码语言:javascript复制sun % sock -s 140.252.1.29 8888
使这个服务器程序的连接仅局限于来自 S L I P接口(1 4 0 . 2 5 2 . 1 . 2 9)。n e t s t a t的显示说明了这一点:
代码语言:javascript复制Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp 0 0 140.252.1.29.8888 *.* LISTEN
如果我们从主机s o l a r i s通过S L I P链路与这个服务器相连接,它将正常工作。
代码语言:javascript复制Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp 0 0 140.252.1.29.8888 140.252.1.32.34614 ESTABLISHED
tcp 0 0 140.252.1.29.8888 *.* LISTEN
但如果我们试图从以太网( 1 4 0 . 2 5 2 . 1 3)中的主机与这个服务器进行连接,连接请求将被T C P模块拒绝。如果使用t c p d u m p来观察这一切,对连接请求 S Y N的响应是一个如图1 8 - 2 1所示的R S T。
这个连接请求将不会到达服务器的应用程序,因为它根据应用程序中指定的本地 I P地址被内核中的T C P模块拒绝。
18.11.3 限定的远端IP地址
在11 . 1 2节,我们知道U D P服务器通常在指定 I P本地地址和本地端口外,还能指定远端 I P地址和远端端口。RFC 793中显示的接口函数允许一个服务器在执行被动打开时,可指明远端插口(等待一个特定的客户执行主动打开),也可不指明远端插口(等待任何客户)。
遗憾的是,大多数A P I都不支持这么做。服务器必须不指明远端插口,而等待连接请求的到来,然后检查客户端的I P地址和端口号。
图1 8 - 2 2总结了T C P服务器进行连接时三种类型的地址绑定。在三种情况中, l p o r t是服务器的熟知端口,而l o c a l I P必须是一个本地接口的I P地址。表中行的顺序正是 T C P模块在收到一个连接请求时确定本地地址的顺序。最常使用的绑定(第 1行,如果支持的话)将最先尝试,最不常用的(最后一行两端的 I P地址都没有制定)将最后尝试。
18.11.4 呼入连接请求队列
一个并发服务器调用一个新的进程来处理每个客户请求,因此处于被动连接请求的服务器应该始终准备处理下一个呼入的连接请求。那正是使用并发服务器的根本原因。但仍有可能出现当服务器在创建一个新的进程时,或操作系统正忙于处理优先级更高的进程时,到达多个连接请求。当服务器正处于忙时, T C P是如何处理这些呼入的连接请求?在伯克利的T C P实现中采用以下规则:
- 正等待连接请求的一端有一个固定长度的连接队列,该队列中的连接已被 T C P接受(即三次握手已经完成),但还没有被应用层所接受。注意区分T C P接受一个连接是将其放入这个队列,而应用层接受连接是将其从该队列中移出。
- 应用层将指明该队列的最大长度,这个值通常称为积压值 ( b a c k l o g )。它的取值范围是 0 ~ 5之间的整数,包括0和5(大多数的应用程序都将这个值说明为 5)。
- 当一个连接请求(即S Y N)到达时,T C P使用一个算法,根据当前连接队列中的连接数来确定是否接收这个连接。我们期望应用层说明的积压值为这一端点所能允许接受连接的最大数目,但情况不是那么简单。图1 8 - 2 3显示了积压值与传统的伯克利系统和 S o l a r i s
2 . 2所能允许的最大接受连接数之间的关系。注意,积压值说明的是 T C P监听的端点已被T C P接受而等待应用层接受的最大连接数。这个积压值对系统所允许的最大连接数,或者并发服务器所能并发处理的客户数,并无影响。在这个图中,S o l a r i s系统规定的值正如我们所期望的。而传统的B S D系统,将这个
值(由于某些原因)设置为积压值乘3除以2,再加1。
- 如果对于新的连接请求,该 T C P监听的端点的连接队列中还有空间(基于图 1 8 - 2 3),T C P模块将对S Y N进行确认并完成连接的建立。但应用层只有在三次握手中的第三个报文段收到后才会知道这个新连接时。另外,当客户进程的主动打开成功但服务器的应用层还不知道这个新的连接时,它可能会认为服务器进程已经准备好接收数据了(如果发生这种情况,服务器的 T C P仅将接收的数据放入缓冲队列 )。
- 如果对于新的连接请求,连接队列中已没有空间, T C P将不理会收到的 S Y N。也不发回任何报文段(即不发回 R S T)。如果应用层不能及时接受已被 T C P接受的连接,这些连接可能占满整个连接队列,客户的主动打开最终将超时。通过s o c k程序能了解这种情况。我们调用它,并使用新的选项( - O)。让它在创建一个新的服务器进程后而没有接受任何连接请求之前暂停下来。如果在它暂停期间又调用了多个客户进程,它将导致接受连接队列被填满,通过 t c p d u m p能够看到这一切。
bsdi % sock -s -v -q1 -O30 5555
- q 1选项将服务器端的积压值置 1。在这种情况下,传统的 B S D系统中的队列允许接受两个连接请求(图 1 8 - 2 3)。- O 3 0选项使程序在接受任何客户连接之前暂停 3 0秒。在这3 0秒内,我们可启动其他客户进程来填充这个队列。在主机 s u n上启动4个客户进程。
图1 8 - 2 4显示了t c p d u m p的输出,首先是第 1个客户进程的第 1个S Y N(省略窗口大小和M S S声明。当T C P连接建立时,将客户进程的端口号用粗体标出)。
端口为1 0 9 0的第一个客户连接请求被 T C P接受(报文段1 ~ 3)。端口为1 0 9 1的第2个客户连接请求也被 T C P接受(报文段 4 ~ 6)。而服务器的应用仍处于休眠状态,还未接受任何连接。目前的一切工作都由内核中的 T C P模块完成。另外,两个客户进程已经成功地完成了它们的主动打开,因为它们建立连接的三次握手已经完成。
我们接着在报文段 7(端口1 0 9 2)和报文段8(端口1 0 9 3)启动第3和第4个客户进程。由于服务器的连接队列已满, T C P将不理会两个S Y N。这两个客户进程在报文段 9, 10, 11, 12, 15重发它们的S Y N。第4个客户进程的第 3个S Y N重传被接受了,因为服务器程序的 3 0秒休眠结束后,它将已接受的两个连接从队列中移出,使连接队列变空(服务器程序接收连接的时间是2 8 . 1 9,小于3 0的原因在于启动服务器程序后它需要几秒的时间来启动第 1个客户进程(报文段1,显示的就是启动时间))。第3个客户进程的第4个S Y N重传这时将被接受(报文段1 5 ~ 1 7)。
服务器程序先接受第4个客户连接(端口1 0 9 3)的原因是服务器程序 3 0秒休眠与客户程序重传之间的定时交互作用。
我们期望接收连接队列按先进先出顺序传递给应用层。如 T C P接受了端口为1 0 9 0和1 0 9 1的连接,我们希望应用层先接受端口为 1 0 9 0的连接,然后再接受端口为1 0 9 1的连接。但许多伯克利的T C P实现都出现按后进先出的传递顺序,这个错误已存在了多年。产商最近已开始改正这个错误,但在如SunOS 4.13等系统中仍存在这个问题。当队列已满时,T C P将不理会传入的S Y N,也不发回R S T作为应答,因为这是一个软错误,而不是一个硬错误。通常队列已满是由于应用程序或操作系统忙造成的,这样可防止应用程序对传入的连接进行服务。这个条件在一个很短的时间内可以改变。但如果服务器的 T C P以系统复位作为响应,客户进程的主动打开将被废弃(如果服务器程序没有启动我们就会遇到)。
由于不应答S Y N,服务器程序迫使客户 T C P随后重传S Y N,以等待连接队列有空间接受新的连接。
这个例子中有一个巧妙之处,这在大多 T C P / I P的具体实现中都能见到,就是如果服务器的连接队列未满时, T C P将接受传入的连接请求(即 S Y N),但并不让应用层了解该连接源于何处(即不告知源 I P地址和源端口)。这不是T C P所要求的,而只是共同的实现技术(如伯克利源代码通常都这么做)。如果一个A P I如T L I(见1 . 1 5节)向应用程序提供了解连接请求的到 来的方法,并允许应用程序选择是否接受连接。当应用程序假定被告知连接请求已经到来时,T C P的三次握手已经结束!其他运输层的实现可能将连接请求的到达与接受分开(如 O S I的运输层),但T C P不是这样。
Solaris 2.2 提供了一个选项使 T C P只有在应用程序说可以接受( t c p _ e a g e r _l i s t e n e r s见E . 4),才允许接受传入的连接请求。
这种行为也意味着 T C P服务器无法使客户进程的主动打开失效。当一个新的客户连接传递给服务器的应用程序时, T C P的三次握手就结束了,客户的主动打开已经完全成功。如果服务器的应用程序此时看到客户的 I P地址和端口号,并决定是否为该客户进行服务,服务器所能做的就是关闭连接(发送 F I N),或者复位连接(发送 R S T)。无论哪种情况,客户进程都认为一切正常,因为它的主动打开已经完成,并且已经向服务器程序发送过请求。