文章目录
- 前言
- 第1章 概述
- 分层
- TCP/IP的分层
- 域名系统
- 分用
- 客户-服务器模型
- 端口号
- IP:网际协议
- 引言
- IP首部
- Ping程序
- 引言
- UDP:用户数据报协议
- 引言
- UDP检验和
- IP分片
- 最大UDP数据报长度
- UDP服务器的设计
- TCP:传输控制协议
- TCP的服务
- TCP通过下列方式来提供可靠性:
- TCP的首部
- TCP连接的建立与终止
- 引言
- 连接的连接与终止
- 三次握手
- 四次挥手
- 连接建立超时
- TCP的半关闭
- TCP状态变迁图
- 2MSL等待状态
- TCP服务器设计
- 小结
- TCP的超时与重传
- 拥塞避免算法
- 快速重传与快速恢复算法
- 重新分组
- TCP的保活定时器
前言
本文参考资料均来自我的男神:Richard Stevens
Stevens 99年因病去世(享年48岁,1951年生),身后给我们留下了七本书,按时间逆序是:
代码语言:javascript复制UNIX Network Programming, Volume 2, Second Edition: Interprocess Communications, Prentice Hall, 1999.
UNIX Network Programming, Volume 1, Second Edition: Networking APIs: Sockets and XTI, Prentice Hall, 1998.
TCP/IP Illustrated, Volume 3: TCP for Transactions, HTTP, NNTP,and the UNIX Domain Protocols, Addison-Wesley, 1996.
TCP/IP Illustrated, Volume 2: The Implementation, Addison-Wesley, 1995.
TCP/IP Illustrated, Volume 1: The Protocols, Addison-Wesley, 1994.
Advanced Programming in the UNIX Environment, Addison-Wesley, 1992.
UNIX Network Programming, Prentice Hall, 1990.
翻译成我们熟悉的语言分别是:
代码语言:javascript复制UNP ( UNIX Network Programming, Prentice Hall, 1990.)
UNP卷一 ( UNIX Network Programming, Volume 1, Second Edition: Networking APIs: Sockets and XTI, Prentice Hall, 1998.)
UNP卷二 ( UNIX Network Programming, Volume 2, Second Edition: Interprocess Communications, Prentice Hall, 1999.)
TCP/IP卷一( TCP/IP Illustrated, Volume 1: The Protocols, Addison-Wesley, 1994.)
TCP/IP卷二( TCP/IP Illustrated, Volume 2: The Implementation, Addison-Wesley, 1995.)
TCP/IP卷三( TCP/IP Illustrated, Volume 3: TCP for Transactions, HTTP, NNTP,and the UNIX Domain Protocols, Addison-Wesley, 1996.)
APUE( Advanced Programming in the UNIX Environment, Addison-Wesley, 1992.)
第1章 概述
很多不同的厂家生产各种型号的计算机,它们运行完全不同的操作系统,但 TCP/IP协议族允许它们互相进行通信。这一点很让人感到吃惊,因为它的作用已远远超出了起初的设想。
分层
网络协议通常分不同层次进行开发,每一层分别负责不同的通信功能。一个协议族,比如 TCP/IP,是一组不同层次上的多个协议的组合。 TCP/IP通常被认为是一个四层协议系统,如图 1 - 1所示
TCP为两台主机提供高可靠性的数据通信。它所做的工作包括把应用程序交给它的数据分成合适的小块交给下面的网络层,确认接收到的分组,设置发送最后确认分组的超时时钟等。由于运输层提供了高可靠性的端到端的通信,因此应用层可以忽略所有这些细节。 而另一方面,UDP则为应用层提供一种非常简单的服务。它只是把称作数据报的分组从一台主机发送到另一台主机,但并不保证该数据报能到达另一端。任何必需的可靠性必须由应用层来提供。 这两种运输层协议分别在不同的应用程序中有不同的用途,这一点将在后面看到。
在TCP/IP协议族中,网络层IP提供的是一种不可靠的服务。也就是说,它只是尽可能快地把分组从源结点送到目的结点,但是并不提供任何可靠性保证。而另一方面,TCP在不可靠的IP层上提供了一个可靠的运输层。为了提供这种可靠的服务, TCP采用了超时重传、发送和接收端到端的确认分组(三次握手)等机制。由此可见,运输层和网络层分别负责不同的功能。
TCP/IP的分层
TCP和UDP是两种最为著名的运输层协议,二者都使用 IP作为网络层协议。 但是与TCP不同的是,UDP是不可靠的,它不能保证数据报能安全无误地到达最终目的。
域名系统
尽管通过IP地址可以识别主机上的网络接口,进而访问主机,但是人们最喜欢使用的还是主机名。在 TCP/IP领域中,域名系统(DNS)是一个分布的数据库,由它来提供 I P地址和主机名之间的映射信息。 现在,我们必须理解,任何应用程序都可以调用一个标准的库函数来查看给定名字的主机的IP地址。类似地,系统还提供一个逆函数—给定主机的IP地址,查看它所对应的主机名。 大多数使用主机名作为参数的应用程序也可以把 IP地址作为参数。
1.6 封装 当应用程序用 TCP传送数据时,数据被送入协议栈中,然后逐个通过每一层直到被当作一串比特流送入网络。其中每一层对收到的数据都要增加一些首部信息(有时还要增加尾部信息),该过程如图 1 - 7所示。
分用
当目的主机收到一个以太网数据帧时,数据就开始从协议栈中由底向上升,同时去掉各层协议加上的报文首部。每层协议盒都要去检查报文首部中的协议标识,以确定接收数据的上层协议。这个过程称作分用(Demultiplexing),图1 - 8显示了该过程是如何发生的。
这些分层协议盒并不都是完美的。
客户-服务器模型
大部分网络应用程序在编写时都假设一端是客户,另一端是服务器,其目的是为了让服务器为客户提供一些特定的服务。 可以将这种服务分为两种类型:重复型或并发型。重复型服务器通过以下步骤进行交互:
代码语言:javascript复制I1. 等待一个客户请求的到来。
I2. 处理客户请求。
I3. 发送响应给发送请求的客户。
I4. 返回I1步。
重复型服务器主要的问题发生在 I2状态。在这个时候,它不能为其他客户机提供服务。
相应地,并发型服务器采用以下步骤:
代码语言:javascript复制C1. 等待一个客户请求的到来。
C2. 启动一个新的服务器来处理这个客户的请求。在这期间可能生成一个新的进程、任务或线程,并依赖底层操作系统的支持。
这个步骤如何进行取决于操作系统。生成的新服务器对客户的全部请求进行处理。处理结束后,终止这个新服务器。
C3. 返回C 1步。
并发服务器的优点在于它是利用生成其他服务器的方法来处理客户的请求。也就是说,每个客户都有它自己对应的服务器。如果操作系统允许多任务,那么就可以同时为多个客户服务。
一般来说, TCP服务器是并发的,而 UDP服务器是重复的,但也存在一些例外。
端口号
TCP和UDP采用16 bit的端口号来识别应用程序。那么这些端口号是如何选择的呢? 服务器一般都是通过知名端口号来识别的。例如,对于每个TCP/IP实现来说,FTP服务器的TCP端口号都是21,每个Telnet服务器的TCP端口号都是23,每个TFTP (简单文件传送协议)服务器的UDP端口号都是69。任何TCP/IP实现所提供的服务都用知名的1~1023之间的端口号。这些知名端口号由Internet号分配机构(Internet Assigned Numbers Authority, IANA)来管理。
到1992年为止,知名端口号介于1~255之间。256~1023之间的端口号通常都是由Unix系统占用,以提供一些特定的Unix服务—也就是说,提供一些只有Unix系统才有的、而其他操作系统可能不提供的服务。现在IANA管理1~1023之间所有的端口号。
客户端通常对它所使用的端口号并不关心,只需保证该端口号在本机上是唯一的就可以了。客户端口号又称作临时端口号(即存在时间很短暂)。这是因为它通常只是在用户运行该客户程序时才存在,而服务器则只要主机开着的,其服务就运行。
大多数TCP/IP实现给临时端口分配1024~5000之间的端口号。大于 5000的端口号是为其他服务器预留的(Internet上并不常用的服务)。我们可以在后面看见许多这样的给临时端口分
IP:网际协议
引言
IP是TCP/IP协议族中最为核心的协议。所有的TCP、UDP、ICMP及IGMP数据都以IP数据报格式传输。许多刚开始接触 TCP/IP的人对IP提供不可靠、无连接的数据报传送服务感到很奇怪。
不可靠的意思是它不能保证 I P数据报能成功地到达目的地。 IP仅提供最好的传输服务。如果发生某种错误时,如某个路由器暂时用完了缓冲区, IP有一个简单的错误处理算法:丢弃该数据报,然后发送ICMP消息报给信源端。任何要求的可靠性必须由上层来提供(如TCP)。
无连接这个术语的意思是 IP并不维护任何关于后续数据报的状态信息。 每个数据报的处理是相互独立的。这也说明,IP数据报可以不按发送顺序接收。如果一信源向相同的信宿发送两个连续的数据报(先是 A,然后是B),每个数据报都是独立地进行路由选择,可能选择不同的路线,因此 B可能在A到达之前先到达。
IP首部
IP数据报的格式如图3 - 1所示。普通的IP首部长为20个字节,除非含有选项字段。
分析图3 - 1中的首部。最高位在左边,记为 0 bit;最低位在右边,记为31 bit。 4个字节的32 bit值以下面的次序传输:首先是 0~7 bit,其次8~15 bit,然后16~23 bit,最后是24~31 bit。这种传输次序称作big endian字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。以其他形式存储二进制整数的机器,如little endian格式,则必须在传输数据之前把首部转换成网络字节序。
目前的协议版本号是4,因此IP有时也称作IPv4。
尽管可以传送一个长达 65535字节的IP数据报,但是大多数的链路层都会对它进行分片。而且,主机也要求不能接收超过 576字节的数据报。由于 TCP把用户数据分成若干片,因此一般来说这个限制不会影响TCP。 事实上现在大多数的实现(特别是那些支持网络文件系统 NFS的实现)允许超过 8192字节的IP数据报。
Ping程序
引言
“ping”这个名字源于声纳定位操作。 ping程序由Mike Muuss编写,目的是为了测试另一台主机是否可达。该程序发送一份 ICMP回显请求报文给主机,并等待返回 ICMP回显应答
一般来说,如果不能 ping到某台主机,那么就不能 Telnet或者FTP到那台主机。反过来如果不能Telnet到某台主机,那么通常可以用 ping程序来确定问题出在哪里。ping程序还能测出到这台主机的往返时间,以表明该主机离我们有“多远”。
UDP:用户数据报协议
引言
UDP是一个简单的面向数据报的运输层协议:进程的每个输出操作都正好产生一个 UDP数据报,并组装成一份待发送的 IP数据报。 这与面向流字符的协议不同,如 TCP,应用程序产生的全体数据与真正发送的单个 IP数据报可能没有什么联系。
尽管相互独立,如果TCP和UDP同时提供某种知名服务,两个协议通常选择相同的端口号。这纯粹是为了使用方便,而不是协议本身的要求。 UDP长度字段指的是UDP首部和UDP数据的字节长度。该字段的最小值为 8字节(发送一份0字节的 UDP数据报是OK)。这个 UDP长度是有冗余的。 IP数据报长度指的是数据报全长,因此UDP数据报长度是全长减去 IP首部的长度(该值在首部长度字段中指定所示)。
UDP检验和
UDP检验和覆盖UDP首部和UDP数据。回想IP首部的检验和,它只覆盖 IP的首部—并不覆盖IP数据报中的任何数据。 UDP和TCP在首部中都有覆盖它们首部和数据的检验和。 UDP的检验和是可选的,而TCP的检验和是必需的。
如果发送端没有计算检验和而接收端检测到检验和有差错,那么 UDP数据报就要被悄悄地丢弃。不产生任何差错报文(当 IP层检测到IP首部检验和有差错时也这样做)。 UDP检验和是一个端到端的检验和。它由发送端计算,然后由接收端验证。其目的是为了发现UDP首部和数据在发送端到接收端之间发生的任何改动。
尽管UDP检验和是可选的,但是它们应该总是在用。在 80年代,一些计算机产商在默认条件下关闭UDP检验和的功能,以提高使用UDP协议的NFS(Network File System)的速度。在单个局域网中这可能是可以接受的,但是在数据报通过路由器时,通过对链路层数据帧进行循环冗余检验(如以太网或令牌环数据帧)可以检测到大多数的差错,导致传输失败。不管相信与否,路由器中也存在软件和硬件差错,以致于修改数据报中的数据。如果关闭端到端的UDP检验和功能,那么这些差错在UDP数据报中就不能被检测出来。另外,一些数据链路层协议(如SLIP)没有任何形式的数据链路检验和。Host Requirements RFC声明,UDP检验和选项在默认条件下是打开的。它还声明,如果发送端已经计算了检验和,那么接收端必须检验接收到的检验和(如接收到检验和不为0)。但是,许多系统没有遵守这一点,只是在出口检验和选项被打开时才验证接收到的检验和。
IP分片
物理网络层一般要限制每次发送数据帧的最大长度。任何时候IP层接收到一份要发送的 IP数据报时,它要判断向本地哪个接口发送数据(选路),并查询该接口获得其MTU。IP把MTU与数据报长度进行比较,如果需要则进行分片。分片可以发生在原始发送端主机上,也可以发生在中间路由器上。把一份IP数据报分片以后,只有到达目的地才进行重新组装(这里的重新组装与其他网络协议不同,它们要求在下一站就进行进行重新组装,而不是在最终的目的地)。重新组装由目的端的 IP层来完成,其目的是使分片和重新组装过程对运输层( TCP和UDP)是透明的,除了某些可能的越级操作外。已经分片过的数据报有可能会再次进行分片(可能不止一次)。
尽管IP分片过程看起来是透明的,但有一点让人不想使用它:即使只丢失一片数据也要重传整个数据报。为什么会发生这种情况呢?因为 IP层本身没有超时重传的机制——由更高层来负责超时和重传(TCP有超时和重传机制,但UDP没有。一些UDP应用程序本身也执行超时和重传)。当来自TCP报文段的某一片丢失后,TCP在超时后会重发整个TCP报文段,该报文段对应于一份IP数据报。没有办法只重传数据报中的一个数据报片。事实上,如果对数据报分片的是中间路由器,而不是起始端系统,那么起始端系统就无法知道数据报是如何被分片的。就这个原因,经常要避免分片。
使用UDP很容易导致IP分片(在后面我们将看到, TCP试图避免分片,但对于应用程序来说几乎不可能强迫 TCP发送一个需要进行分片的长报文段)。
最大UDP数据报长度
理论上,IP数据报的最大长度是65535字节,这是由IP首部16比特总长度字段所限制的。去除 20字节的IP首部和8个字节的UDP首部,UDP数据报中用户数据的最长长度为65507字节。但是,大多数实现所提供的长度比这个最大值小。
我们将遇到两个限制因素。第一,应用程序可能会受到其程序接口的限制。 socket API提供了一个可供应用程序调用的函数,以设置接收和发送缓存的长度。对于 UDP socket,这个长度与应用程序可以读写的最大UDP数据报的长度直接相关。现在的大部分系统都默认提供了可读写大于 8192字节的UDP数据报(使用这个默认值是因为 8192是NFS读写用户数据数的默认值)。
第二个限制来自于TCP/IP的内核实现。可能存在一些实现特性(或差错),使IP数据报长度小于65535字节。
UDP服务器的设计
来自客户的是 UDP数据报。IP首部包含源端和目的端 IP地址,UDP首部包含了源端和目的端的UDP端口号。当一个应用程序接收到 UDP数据报时,操作系统必须告诉它是谁发送了这份消息,即源IP地址和端口号。 这个特性允许一个交互 UDP服务器对多个客户进行处理。给每个发送请求的客户发回应答。
一些应用程序需要知道数据报是发送给谁的,即目的 IP地址。例如,Host Requirements RFC规定,TFTP服务器必须忽略接收到的发往广播地址的数据报。 这要求操作系统从接收到的 UDP数据报中将目的 IP地址交给应用程序。不幸的是,并非所有的实现都提供这个功能。 socket API以IP_RECVDSTADDR socket选项提供了这个功能。
大多数UDP服务器是交互服务器。这意味着,单个服务器进程对单个UDP端口上(服务器上的名知端口)的所有客户请求进行处理。 通常程序所使用的每个UDP端口都与一个有限大小的输入队列相联系。这意味着,来自不同客户的差不多同时到达的请求将由UDP自动排队。接收到的UDP数据报以其接收顺序交给应用程序(在应用程序要求交送下一个数据报时)。
然而,排队溢出造成内核中的UDP模块丢弃数据报的可能性是存在的。
TCP:传输控制协议
TCP的服务
尽管TCP和U D P都使用相同的网络层(I P),TCP却向应用层提供与U D P完全不同的服务。 TCP提供一种面向连接的、可靠的字节流服务。 面向连接意味着两个使用TCP的应用(通常是一个客户和一个服务器)在彼此交换数据之前必须先建立一个TCP连接。这一过程与打电话很相似,先拨号振铃,等待对方摘机说 “喂”,然后才说明是谁。 在一个TCP连接中,仅有两方进行彼此通信。广播和多播不能用于TCP。
TCP通过下列方式来提供可靠性:
• 应用数据被分割成TCP认为最适合发送的数据块。这和 UDP完全不同,应用程序产生的数据报长度将保持不变。由TCP传递给I P的信息单位称为报文段或段(segment) • 当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。 • 当TCP收到发自TCP连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒。 • TCP将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP将丢弃这个报文段和不确认收到此报文段(希望发端超时并重发)。 • 既然TCP报文段作为IP数据报来传输,而 IP数据报的到达可能会失序,因此 TCP报文段的到达也可能会失序。如果必要, TCP将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层。 • 既然IP数据报会发生重复,TCP的接收端必须丢弃重复的数据。 •TCP还能提供流量控制。TCP连接的每一方都有固定大小的缓冲空间。 TCP的接收端只允许另一端发送接收端缓冲区所能接纳的数据。这将防止较快主机致使较慢主机的缓冲区溢出。
两个应用程序通过TCP连接交换8 bit字节构成的字节流。TCP不在字节流中插入记录标识符。我们将这称为字节流服务( byte stream service)。如果一方的应用程序先传 10字节,又传20字节,再传50字节,连接的另一方将无法了解发方每次发送了多少字节。收方可以分 4次接收这80个字节,每次接收 20字节。一端将字节流放到TCP连接上,同样的字节流将出现在TCP连接的另一端。 另外,TCP对字节流的内容不作任何解释。 TCP不知道传输的数据字节流是二进制数据,还是ASCII字符、EBCDIC字符或者其他类型数据。对字节流的解释由 TCP连接双方的应用层解释。
TCP的首部
TCP数据被封装在一个IP数据报中,
每个TCP段都包含源端和目的端的端口号,用于寻找发端和收端应用进程。这两个值加上IP首部中的源端IP地址和目的端IP地址唯一确定一个TCP连接。
TCP连接的建立与终止
引言
TCP是一个面向连接的协议。无论哪一方向另一方发送数据之前,都必须先在双方之间建立一条连接。
连接的连接与终止
三次握手
为了建立一条TCP连接:
代码语言:javascript复制1) 请求端(通常称为客户)发送一个 SYN段指明客户打算连接的服务器的端口,以及初始序号(ISN,在这个例子中为1415531521)。这个SYN段为报文段1。
2) 服务器发回包含服务器的初始序号的 SYN报文段(报文段2)作为应答。同时,将确认序号设置为客户的ISN加1以对客户的SYN报文段进行确认。一个SYN将占用一个序号。
3) 客户必须将确认序号设置为服务器的 ISN加1以对服务器的SYN报文段进行确认(报文段3)。
这三个报文段完成连接的建立。这个过程也称为三次握手( three-way handshake)
发送第一个SYN的一端将执行主动打开( active open)。接收这个SYN并发回下一个SYN的另一端执行被动打开(passive open)
当一端为建立连接而发送它的 SYN时,它为连接选择一个初始序号。 ISN随时间而变化,因此每个连接都将具有不同的 ISN。
四次挥手
建立一个连接需要三次握手,而终止一个连接要经过 4次握手。这由TCP的半关闭(half-close)造成的。既然一个TCP连接是全双工(即数据在两个方向上能同时传递),因此每个方向必须单独地进行关闭。 这原则就是当一方完成它的数据发送任务后就能发送一个 FIN来终止这个方向连接。当一端收到一个FIN,它必须通知应用层另一端已经终止了那个方向的数据传送。发送FIN通常是应用层进行关闭的结果。 收到一个FIN只意味着在这一方向上没有数据流动。一个 TCP连接在收到一个 FIN后仍能发送数据。而这对利用半关闭的应用来说是可能的,尽管在实际应用中只有很少的 TCP应用程序这样做。
图 18-3中的报文段4发起终止连接,它由Telnet客户端关闭连接时发出。它将导致TCP客户端发送一个FIN,用来关闭从客户到服务器的数据传送。当服务器收到这个 FIN,它发回一个ACK,确认序号为收到的序号加 1(报文段5)。和SYN一样,一个FIN将占用一个序号。同时 TCP服务器还向应用程序(即丢弃服务器)传送一个文件结束符。接着这个服务器程序就关闭它的连接,导致它的 TCP端发送一个FIN(报文段6),客户必须发回一个确认,并将确认序号设置为收到序号加1(报文段7)。
图18-4显示了终止一个连接的典型握手顺序。我们省略了序号。在这个图中,发送FIN将导致应用程序关闭它们的连接,这些FIN的ACK是由TCP软件自动产生的。
连接建立超时
有很多情况导致无法建立连接。一种情况是服务器主机没有处于正常状态。
TCP的半关闭
TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。这就是所谓的半关闭。 虽然很少软件用,反正我是被坑过。
为了使用这个特性,编程接口必须为应用程序提供一种方式来说明“我已经完成了数据传送,因此发送一个文件结束( FIN)给另一端,但我还想接收另一端发来的数据,直到它给我发来文件结束(FIN)”。 如果应用程序不调用 close而调用shutdown,且第2个参数值为1,则插口的API支持半关闭。然而,大多数的应用程序通过调用close终止两个方向的连接。
我现在知道当时是为什么被坑惨了,一定要手动close()!!! 虽然我很快就反应过来要close(),但是原因我是今天才知道。
图18-10显示了一个半关闭的典型例子。让左方的客户端开始半关闭,当然也可以由另一端开始。开始的两个报文段和图18-4是相同的:初始端发出的 FIN,接着是另一端对这个 FIN的ACK报文段。但后面就和图18-4不同,因为接收半关闭的一方仍能发送数据。我们只显示一个数据报文段和一个ACK报文段,但可能发送了许多数据报文段。当收到半关闭的一端在完成它的数据传送后,将发送一个FIN关闭这个方向的连接,这将传送一个文件结束符给发起这个半关闭的应用进程。当对第二个 FIN进行确认后,这个连接便彻底关闭了。
TCP状态变迁图
2MSL等待状态
TIME_WAIT状态也称为2MSL等待状态。每个具体 TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime)。它是任何报文段被丢弃前在网络内的最长时间。我们知道这个时间是有限的,因为 TCP报文段以IP数据报在网络内传输,而IP数据报则有限制其生存时间的TTL字段。 RFC 793 [Postel 1981c] 指出MSL为2分钟。然而,实现中的常用值是30秒,1分钟,或2分钟。 在实际应用中,对 IP数据报TTL的限制是基于跳数,而不是定时器。
对一个具体实现所给定的 MSL值,处理的原则是:当 TCP执行一个主动关闭,并发回最后一个ACK,该连接必须在 TIME_WAIT状态停留的时间为 2倍的MSL。这样可让TCP再次发送最后的ACK以防这个ACK丢失(另一端超时并重发最后的 FIN)。
这种2MSL等待的另一个结果是这个 TCP连接在2MSL等待期间,定义这个连接的插口(客户的IP地址和端口号,服务器的 IP地址和端口号)不能再被使用。这个连接只能在 2MSL结束后才能再被使用。
遗憾的是,大多数 TCP实现(如伯克利版)强加了更为严格的限制。在 2MSL等待期间,插口中使用的本地端口在默认情况下不能再被使用。 某些实现和API提供了一种避开这个限制的方法。使用插口API时,可说明其中的SO_REUSEADDR选项。它将让调用者对处于2MSL等待的本地端口进行赋值,但我们将看到TCP原则上仍将避免使用仍处于2MSL连接中的端口。
在连接处于2MSL等待时,任何迟到的报文段将被丢弃。因为处于 2MSL等待的、由该插口对(socket pair)定义的连接在这段时间内不能被再用,因此当要建立一个有效的连接时,来自该连接的一个较早替身( incarnation)的迟到报文段作为新连接的一部分不可能不被曲解(一个连接由一个插口对来定义。一个连接的新的实例( instance)称为该连接的替身)。我们说图18-13中客户执行主动关闭并进入 TIME_WAIT是正常的。服务器通常执行被动关闭,不会进入TIME_WAIT状态。这暗示如果我们终止一个客户程序,并立即重新启动这个客户程序,则这个新客户程序将不能重用相同的本地端口。这不会带来什么问题,因为客户使用本地端口,而并不关心这个端口号是什么。 然而,对于服务器,情况就有所不同,因为服务器使用熟知端口。如果我们终止一个已经建立连接的服务器程序,并试图立即重新启动这个服务器程序,服务器程序将不能把它的这个熟知端口赋值给它的端点,因为那个端口是处于 2MSL连接的一部分。在重新启动服务器程序前,它需要在1 ~ 4分钟。
TCP服务器设计
找我入门啊
代码语言:javascript复制Socket/Epoll/Pthread/设计模式,你必须拥有
小结
两个进程在使用 TCP交换数据之前,它们之间必须建立一条连接。完成后,要关闭这个连接。本章已经详细介绍了如何使用三次握手来建立连接以及使用 4个报文段来关闭连接。
弄清TCP操作的关键在于它的状态变迁图。
一个TCP连接由一个4元组唯一确定:本地 IP地址、本地端口号、远端 IP地址和远端端口号。无论何时关闭一个连接,一端必须保持这个连接,我们看到 TIME_WAIT状态将处理这个问题。处理的原则是执行主动打开的一端在进入这个状态时要保持的时间为 TCP实现中规定的MSL值的两倍。
TCP的超时与重传
TCP提供可靠的运输层。它使用的方法之一就是确认从另一端收到的数据。但数据和确认都有可能会丢失。TCP通过在发送时设置一个定时器来解决这种问题。如果当定时器溢出时还没有收到确认,它就重传该数据。对任何实现而言,关键之处就在于超时和重传的策略,即怎样决定超时间隔和如何确定重传的频率。
对每个连接,T C P管理4个不同的定时器。
代码语言:javascript复制1) 重传定时器使用于当希望收到另一端的确认。
2) 坚持(persist)定时器使窗口大小信息保持不断流动,即使另一端关闭了其接收窗口。
3) 保活(keepalive)定时器可检测到一个空闲连接的另一端何时崩溃或重启。
4) 2MSL定时器测量一个连接处于TIME_WAIT状态的时间。
拥塞避免算法
慢启动算法是在一个连接上发起数据流的方法,但有时我们会达到中间路由器的极限,此时分组将被丢弃。拥塞避免算法是一种处理丢失分组的方法。 该算法假定由于分组受到损坏引起的丢失是非常少的(远小于 1 %),因此分组丢失就意味着在源主机和目的主机之间的某处网络上发生了拥塞。有两种分组丢失的指示:发生超时和接收到重复的确认。
拥塞避免算法和慢启动算法是两个目的不同、独立的算法。但是当拥塞发生时,我们希望降低分组进入网络的传输速率,于是可以调用慢启动来作到这一点。在实际中这两个算法通常在一起实现。
拥塞避免算法和慢启动算法需要对每个连接维持两个变量:一个拥塞窗口 cwnd和一个慢启动门限ssthresh。这样得到的算法的工作过程如下:
- 对一个给定的连接,初始化cwnd为1个报文段,ssthresh为65535个字节。
- TCP输出例程的输出不能超过cwnd和接收方通告窗口的大小。拥塞避免是发送方使用的流量控制,而通告窗口则是接收方进行的流量控制。前者是发送方感受到的网络拥塞的估计,而后者则与接收方在该连接上的可用缓存大小有关。
- 当拥塞发生时(超时或收到重复确认),ssthresh被设置为当前窗口大小的一半(cwnd和接收方通告窗口大小的最小值,但最少为 2个报文段)。此外,如果是超时引起了拥塞,则cwnd被设置为1个报文段(这就是慢启动)。
- 当新的数据被对方确认时,就增加cwnd,但增加的方法依赖于我们是否正在进行慢启动或拥塞避免。如果cwnd小于或等于ssthresh,则正在进行慢启动,否则正在进行拥塞避免。 慢启动一直持续到我们回到当拥塞发生时所处位置的半时候才停止,然后转为执行拥塞避免。 慢启动算法初始设置 cwnd为1个报文段,此后每收到一个确认就加 1。
那样,这会使窗口按指数方式增长:发送 1个报文段,然后是2个,接着是4个……。 拥塞避免算法要求每次收到一个确认时将 cwnd增加1/cwnd。与慢启动的指数增加比起来,这是一种加性增长(additive increase)。我们希望在一个往返时间内最多为 cwnd增加1个报文段(不管在这个 RTT中收到了多少个 ACK),然而慢启动将根据这个往返时间中所收到的确认的个数增加cwnd。
快速重传与快速恢复算法
由于我们不知道一个重复的 ACK是由一个丢失的报文段引起的,还是由于仅仅出现了几个报文段的重新排序,因此我们等待少量重复的ACK到来。假如这只是一些报文段的重新排序,则在重新排序的报文段被处理并产生一个新的 ACK之前,只可能产生1 ~ 2个重复的ACK。 如果一连串收到 3个或3个以上的重复ACK,就非常可能是一个报文段丢失了。于是我们就重传丢失的数据报文段,而无需等待超时定时器溢出。这就是快速重传算法。
接下来执行的不是慢启动算法而是拥塞避免算法。这就是快速恢复算法。
这个算法通常按如下过程进行实现:
- 当收到第3个重复的ACK时,将ssthresh设置为当前拥塞窗口 cwnd的一半。重传丢失的报文段。设置cwnd为ssthresh加上3倍的报文段大小。
- 每次收到另一个重复的 ACK时,cwnd增加1个报文段大小并发送 1个分组(如果新的cwnd允许发送)。
- 当下一个确认新数据的ACK到达时,设置cwnd为ssthresh(在第1步中设置的值)。这个ACK应该是在进行重传后的一个往返时间内对步骤 1中重传的确认。另外,这个ACK也应该是对丢失的分组和收到的第 1个重复的ACK之间的所有中间报文段的确认。这一步采用的是拥塞避免,因为当分组丢失时我们将当前的速率减半。
重新分组
当TCP超时并重传时,它不一定要重传同样的报文段。相反, TCP允许进行重新分组而发送一个较大的报文段,这将有助于提高性能(当然,这个较大的报文段不能够超过接收方声明的MSS)。在协议中这是允许的,因为 TCP是使用字节序号而不是报文段序号来进行识别它所要发送的数据和进行确认。
TCP的保活定时器
许多TCP/IP的初学者会很惊奇地发现可以没有任何数据流通过一个空闲的TCP连接。也就是说,如果TCP连接的双方都没有向对方发送数据,则在两个TCP模块之间不交换任何信息。例如,没有可以在其他网络协议中发现的轮询。这意味着我们可以启动一个客户与服务器建立一个连接,然后离去数小时、数天、数个星期或者数月,而连接依然保持。中间路由器可以崩溃和重启,电话线可以被挂断再连通,但是只要两端的主机没有被重启,则连接依然保持建立。
这意味着两个应用进程—客户进程或服务器进程—都没有使用应用级的定时器来检测非活动状态,而这种非活动状态可以导致应用进程中的任何一个终止其活动。 然而,许多时候一个服务器希望知道客户主机是否崩溃并关机或者崩溃又重新启动。许多实现提供的保活定时器可以提供这种能力。
保活并不是TCP规范中的一部分。Host Requirements RFC提供了3个不使用保活定时器的理由:
- 在出现短暂差错的情况下,这可能会使一个非常好的连接释放掉;
- 它们耗费不必要的带宽;
- 在按分组计费的情况下会在互联网上花掉更多的钱。
然而,许多实现提供了保活定时器。
保活定时器是一个有争论的功能。许多人认为如果需要,这个功能不应该在 TCP中提供,而应该由应用程序来完成。这是应当认真对待的一些问题之一,因为在这个论题上有些人表达出了很大的热情。
在连接两个端系统的网络出现临时故障的时候,保活选项会引起一个实际上很好的连接终止。例如,如果在一个中间路由器崩溃并重新启动时发送保活探查,那么 TCP会认为客户的主机已经崩溃,而实际上所发生的并非如此。 保活功能主要是为服务器应用程序提供的。服务器应用程序希望知道客户主机是否崩溃,从而可以代表客户使用资源。许多版本的Rlogin和Telnet服务器默认使用这个选项。
一个说明现在需要使用保活功能的常见例子是当个人计算机用户使用TCP/IP向一个使用Telnet的主机注册时。如果在一天结束时,他们仅仅关闭了电源而没有注销,那么便会留下一个半开放的连接。如果客户已经消失了,使得在服务器上留下一个半开放连接,而服务器又在等待来自客户的数据,则服务器将永远等待下去。保活功能就是试图在服务器端检测到这种半开放的连接。
在这个描述中,我们称使用保活选项的一端为服务器,而另一端则为客户。并没有什么使客户不能使用这个选项,但通常都是服务器设置这个功能。如果双方都特别需要了解对方是否已经消失,则双方都可以使用这个选项。
如果一个给定的连接在两个小时之内没有任何动作,则服务器就向客户发送一个探查报文段。 客户主机必须处于以下 4个状态之一:
代码语言:javascript复制1) 客户主机依然正常运行,并从服务器可达。客户的TCP响应正常,而服务器也知道对方是正常工作的。服务器在两小时以后将保活定时器复位。
如果在两小时定时器到时间之前有应用程序的通信量通过此连接,则定时器在交换数据后的未来2小时再复位。
2) 客户主机已经崩溃,并且关闭或者正在重新启动。在任何一种情况下,客户的TCP都没有响应。服务器将不能够收到对探查的响应,并在 75秒后超时。
服务器总共发送10个这样的探查,每个间隔75秒。如果服务器没有收到一个响应,它就认为客户主机已经关闭并终止连接。
3) 客户主机崩溃并已经重新启动。这时服务器将收到一个对其保活探查的响应,但是这个响应是一个复位,使得服务器终止这个连接。
4) 客户主机正常运行,但是从服务器不可达。这与状态2相同,因为TCP不能够区分状态
4与状态2之间的区别,它所能发现的就是没有收到探查的响应。
服务器不用关注客户主机被关闭和重新启动的情况(这指的是一个操作员的关闭,而不是主机崩溃)。当系统被操作员关闭时,所有的应用进程也被终止(也就是客户进程),这会使客户的TCP在连接上发出一个FIN。接收到FIN将使服务器的TCP向服务器进程报告文件结束,使服务器可以检测到这个情况。
在第1种情况下,服务器的应用程序没有感觉到保活探查的发生。 TCP层负责一切。这个过程对应用程序都是透明的,直至第 2、3或4种情况发生。在这三种情况下,服务器应用程序将收到来自它的TCP的差错报告(通常服务器已经向网络发出了读操作请求,然后等待来自客户的数据。如果保活功能返回一个差错,则该差错将作为读操作的返回值返回给服务器)。
在第2种情况下,差错是诸如“连接超时”之类的信息,而在第 3种情况则为“连接被对方复位”。第4种情况看起来像是连接超时,也可根据是否收到与连接有关的 ICMP差错来返回其他的差错。
一个被人们不断讨论的关于保活选项的问题就是两个小时的空闲时间是否可以改变。通常他们希望该数值可以小得多,处在分钟的数量级。这个值通常可以改变,但是保活间隔时间是系统级的变量,因此改变它会影响到所有使用该功能的用户。
到这儿吧