文档章节

网络编程学习——TCP(一)

thanatos_y
 thanatos_y
发布于 2016/04/07 09:15
字数 6005
阅读 32
收藏 0

一 传输控制协议——TCP

 1.1 TCP简介

  TCP在RFC 793中详细描述,然后由RFC 1323、RFC 2581、RFC 2988和RFC 3390加以更新。TCP客户先与某个给定服务器建立一个连接(connection),再跨该连接与那个服务器交换数据,然后终止这个连接。

  其次,TCP还提供了可靠性(reliability)。当TCP向另一端发送数据时,它要求对端返回一个确认。如果没有受到确认,TCP就自动重传数据并等待更长时间。在数次重传失败之后,TCP才放弃,如此尝试发送数据上所花的总时间一般为4~10分钟。

  TCP含有用于动态估算客户和服务器之间往返时间(round-trip time,RTT)的算法,以便它知道等待一个确认需要多少时间。

  TCP通过给其中每个字节关联一个序列号对所发送的数据进行排序(sequencing)。分节是TCP传递给IP的数据单元。如果这些分节非顺序到达,接收端TCP将先根据它们的序列号重新排序,再把结果数据传递给接收应用。如果接收端TCP接收到来自对端的重复数据(譬如说对端认为一个分节已丢失并因此重传,而这个分节并没有真正丢失,只是网络通信过于拥挤),它可以(根据序列号)判定数据是重复的,从而丢弃重复数据。

  再次,TCP提供流量控制(flow control)。TCP总是告知对端在任何时刻它一次能够从对端接收多少字节的数据,这称为通告窗口(advertised window)。在任何时刻,该窗口指出接收缓冲区中当前可用的空间量,从而确保发送端发送的数据不会使接收缓冲区溢出。该窗口动态变化:当接收到来自发送端的数据时,窗口大小就减小,但是接收端应用从缓冲区中读取数据时,窗口大小就增大。通告窗口大小减小到0是有可能的:当TCP对应某个套接字的接收缓冲区已满,导致它必须等待应用从该缓冲区读取数据时,方能从对端再接收数据。

  最后,TCP连接是全双工的(full-duplex)。这意味着在一个给定的连接上应用可以在任何时刻在进出两个方向上既发送数据又接收数据。因此,TCP必须为每个数据流方向跟踪诸如序列号和通告窗口大小等状态信息。建立一个全双工连接后,需要的话可以把它转化成一个单工连接。

 

 1.2 TCP连接的建立和终止

   1.2.1 三路握手

  建立一个TCP连接时会发生下述情形。

  1. 服务器必须准备好接收外来的连接。这通常通过调用socket、bind和listen这3个函数来完成,我们称之为被动打开(passive open)。

  2. 客户通过调用connect发起主动打开(active open)。这导致客户TCP发送一个SYN(同步)分节,它告诉服务器将在(待建立的)连接中发送的数据的初始序列号。通常SYN分节不携带数据,其所在IP数据报只含一个IP首部、一个TCP首部及可能有的TCP选项。

  3. 服务器必须确认(ACK)客户的SYN,同时自己也得发送一个SYN分节,它含有服务器将在同一连接中发送的数据的初始序列号。服务器在单个分节中发送SYN和对客户SYN的ACK(确认)。

  4. 客户必须确认服务器的SYN。

  这个交换至少需要3个分组,因此称之为TCP的三路握手(three-way handshake)。图1-1展示了所交换的3个分节。

图1-1 TCP的三路握手

  图1-1给出的客户初始序列号为J,服务器的初始序列号为K。ACK中的确认号是发送这个ACK的一端所期待的下一个序列号。因为SYN占据一个字节的序列号空间,所以每一个SYN的ACK中的确认号就是该SYN的初始序列号加1。类似地,每一个FIN(表示结束)的ACK中的确认号为该FIN的序列号加1。

 

   1.2.2 TCP选项

  每个SYN可以含有多个TCP选项。

  • MSS选项。发送SYN的TCP一端使用本选项通告对端它的最大分节大小(maximum segment size)即MSS,也就是它在本连接的每个TCP分节中愿意接收的最大数据量。发送端TCP使用接收端的MSS值作为发送分节的最大大小。TCP_MAXSEG套接字选项提取和设置这个TCP选项。

  • 窗口规模选项。TCP连接任何一端能够通告对端的最大窗口大小是65535,因为在TCP首部中相应的字段占16位。然而当今因特网上业已普及的高速网络连接或长延迟路径(卫星链路)要求有更大的窗口以获得尽可能大的吞吐量。这个新选项指定TCP首部中的通告窗口必须扩大(即左移)的位数(0~14),因此所提供的最大窗口接近1GB(65535x2^14)。在一个TCP连接上使用窗口规模的前提是它的两个端系统必须支持这个选项。SO_RCVBUF套接字选项影响这个TCP选项。

  • 时间戳选项。这个选项对于高速网络连接是必需的,它可以防止失而复现的分组可能造成的数据破坏。它是一个较新的选项,也以类似于窗口规模选项的方式协商处理。作为网络编程人员,我们无需考虑这个选项。

  TCP的大多数实现都支持这些常用选项。后两个在RFC 1323中说明。既然高宽带或长延迟的网络被称为“长胖管道”(long fat pipe),这两个选项也称为“长胖管道选项”。

   1.2.3 TCP连接终止

  TCP建立一个连接需3个分节,终止一个连接需4个分节。

  1. 某个进程首先调用close,我们称为该端执行主动关闭(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。

  2. 接收到这个FIN的对端执行被动关闭(passive close)。这个FIN由TCP确认。它的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程(放在已排队等候该应用进程接收的任何其他数据之后),因为FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。

  3. 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。

  4. 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。

  既然每个方向都需要一个FIN和一个ACK,因此通常需要4个分节。我们使用限定词“通常”是因为:某些情形下步骤1的FIN随数据一起发送;另外,步骤2和步骤3发送的分节都出自执行被动关闭那一端。图1-2展示了这些分组。

图1-2 TCP连接关闭时的分组交换

  类似SYN,一个FIN也占据一个字节的序列号空间。因此每个FIN的ACK确认号就是这个FIN的序列号加1。在步骤2与步骤3之间,从执行被动关闭一端到执行主动关闭一端流动数据是可能的。这称为半关闭(half-close)。

  当套接字被关闭时,其所在端TCP各自发送一个FIN。我们在图中指出,这是由应用进程调用close而发生的,不过需认识到,当一个Unix进程无论自愿地(调用exit或从main函数返回)还是非自愿地(收到一个终止本进程的信号)终止时,所有打开的描述符都被关闭,这也导致仍然打开的任何TCP连接上也发出一个FIN。

  图1-2展示了客户执行主动关闭的情形,不过我们指出,无论是客户还是服务器,任何一端都可以执行主动关闭。通常情况是客户执行主动关闭,但是某些协议(譬如HTTP/1.0)却由服务器执行主动关闭。

   1.2.4 TCP状态转换图

  TCP涉及连接建立和连接终止的操作可以用状态转换图(state transition diagram)来说明,如图1-3所示。

图1-3 TCP状态转换图

  TCP为一个连接定义了11种状态,并且TCP规则规定如何基于当前状态及在该状态下所接收的分节从一个状态转换到另一个状态。举例来说,当某个应用进程在CLOSED状态下执行主动打开时,TCP将发送一个SYN,且新的状态是SYN_SENT。如果这个TCP接着接收到一个带ACK的SYN,它将发送一个ACK,且新的状态是ESTABLISHED。这个最终状态是绝大多数数据传送发生的状态。

  自ESTABLISHED状态引出的两个箭头处理连接的终止。如果某个应用进程在接收到一个FIN之前调用close(主动关闭),那就转换到FIN_WAIT_1状态。但如果某个应用进程在ESTABLISHED状态期间接收到一个FIN(被动关闭),那就转换到CLOSE_WAIT状态。

  我们用粗实线表示通常的客户状态转换,用粗虚线表示通常的服务器状态转换。图中还注明存在两个我们未曾讨论的转换:一个为同时打开(simultaneous open),发生在两端几乎同时发送SYN并且这两个SYN在网络中交错的情形下,另一个为同时关闭(simultaneous close),发生在两端几乎同时发送FIN的情形下。

  展示状态转换图的原因之一是给出11种TCP状态的名称。这些状态可使用netstat显示,它是一个在调试客户/服务器应用时很有用的工具。

   1.2.5 观察分组

  图1-4展示一个完整的TCP连接所发生的实际分组交换情况,包括连接建立、数据传送和连接终止3个阶段。图中还展示了每个端点所经历的TCP状态。

  本例中的客户通知一个值为536的MSS(表明该客户只实现了最小重组缓冲区大小),服务器通告一个值为1460的MSS(以太网上IPv4的典型值)。不同方向上MSS值不相同不成问题。

图1-4 TCP连接的分组交换

  一旦建立一个连接,客户就构成一个请求并发送给服务器。这里我们假设该请求适合单个TCP分节(即请求大小小于服务器通告的值为1460字节的MSS)。服务器处理该请求并发送一个应答,我们假设该应答也适合单个分节(本例小于536字节)。图中使用粗箭头表示这两个数据分节。注意,服务器对客户请求的确认是伴随其应答发送的。这种做法称为捎带(piggybacking),它通常在服务器处理请求并产生应答的时间少于200ms时发生。如果服务器耗用更长时间,譬如1s,那么我们将看到显示确认后是应答。

  图中随后展示的是终止连接的4个分节。图2-5中值得注意的是,如果该连接的整个目的仅仅是发送一个单分节的请求和接收一个单分节的应答,那么使用TCP有8个分节的开销。如果改用UDP,那么只需交换两个分组:一个承载请求,一个承载应答。然而从TCP切换到UDP将丧失TCP提供给应用进程的全部可靠性,迫使可靠服务的一大堆细节从传输层(TCP)转移到UDP应用进程。TCP提供的另一个重要特性即拥赛控制也必须由UDP应用进程来处理。尽管如此,我们仍然需要直到许多网络应用是使用UDP构建的,因为它们需要交换的数据量较少,而UDP避免了TCP连接建立和终止所需的开销。

 1.3 TIME_WAIT状态

  在图1-3中我们看到执行主动关闭的那端经历了这个状态。该断点停留在这个状态的持续时间是最长分节生命期(maximum segment lifetime,MSL)的两倍,有时候称为2MSL。

  任何TCP实现都必须为MSL选择一个值。RFC 1122的建议值是2分钟,传统上是30秒。这意味着TIME_WAIT状态的持续时间在1分治到4分钟之间。MSL是任何IP数据报能够在因特网中存活的最长时间。我们直到这个时间是有限的,因为每个数据报含有一个称为跳限(hop limit)的8位字段,它的最大值为255。尽管这是一个跳数限制不是真正的时间限制,我们仍然假设:具有最大跳数(255)的分组在网络中存在的时间不可能超过MSL秒。

  分组在网络中“迷途”通常是路由异常的结果。某个路由器崩溃或某两个路由器之间的某个链路断开时,路由协议需花数秒钟到数分钟的时间才能稳定并找出另一条通路。在这段时间内有可能发生路由循环(路由器A把分组给路由器B,而B再把它们发送回A),我们关心的分组可能就此陷入这样的循环。假设迷途的分组是一个TCP分节,在它迷途期间,发送端TCP超时并重传该分组,而重传的分组却通过某条候选路径到达最终目的地。然而不久后(自迷途的分组开始其旅途起最多MSL秒以内)路由循环修复,早先迷失在这个循环中的分组最终也被送到目的地。这个原来的分组称为迷途的重复分组(lost duplicate)或者漫游的重复分组(wandering duplicate)。TCP必须正确处理这些重复的分组。

  TIME_WAIT状态有两个存在的理由;

 (1) 可靠地实现TCP全双工连接的终止。

 (2) 允许老的重复分节在网络中消逝。

  第一个理由可以通过查看图1-4并假设最终的ACK丢失了来解释。服务器将重新发送它的最终那个FIN,因此客户必须维护状态信息,以允许它重新发送最终那个ACK。要是客户不维护状态信息,它将响应以一个RST(另外一种类型的TCP分节),该分节被服务器解释成一个错误。如果TCP打算执行所有必要的工作以彻底终止某个连接上两个方向的数据流(即全双工关闭),那么它必须正确处理连接终止序列4个分节中任何一个分节丢失的情况。本例子也说明了为什么执行主动关闭的那一端是处于TIME_WAIT状态的那一端:因为可能不得不重传那个ACK的就是那一端。

  为理解存在TIME_WAIT状态的第二个理由,我们假设早12.106.32.254的1500端口和206.168.112.219的21端口之间有一个TCp连接。我们关闭这个连接,过一段时间后在相同的IP地址和端口之间建立令另一个连接。后一个连接称为前一个连接的化身(incarnation),因为它们的IP地址和端口号都相同。TCP必须防止来自某个连接的老的重复分组在该连接已终止后再现,从而被误解成属于同一连接的某个新的化身。为了做到这一点,TCP将不给出于TIME_WAIT状态的连接发起新的化身。既然TIME_WAIT状态的持续时间是MSL的2倍,这就足以让某个方向上的分组最多存活MSL秒即被丢弃,另一个方向上的应答最多存活MSL秒也被丢弃。通过建立这个规则,我们就能保证每成功建立一个TCP连接时,来自连接先前化身的老的重复分组都已在网络中消逝了。

 

 1.4 TCP端口号与并发服务器

  并发服务器中主服务器循环通过派生一个子进程来处理每个新的连接。如果一个子进程继续使用服务器众所周知的端口来服务一个长时间的请求,那将发生什么?让我们来看如下典型的序列。首先,在主机freebsd上启动服务器,该主机是多宿的,其IP地址为12.106.32.254和192.168.42.1。服务器在它众所周知的端口(本例为21)上执行被动打开,从而开始等待客户的请求,如图1-5所示。

图1-5 TCP服务器在端口21上执行被动打开

  我们使用记号(*:21,*:*)指出服务器的套接字对。服务器在任意本地接口(第一个星号)的端口21上等待连接请求。外地IP地址和外地端口都没有指定,我们用“*,*”来表示。我们称它为监听套接字(listening socket)。

  这里指定本地IP地址的星号称为通配(wildcard)符。如果运行服务器的主机是多宿的(如本例),服务器可以指定它只能接收到达某个特定本地接口的外来连接。这里要么选一个接口要么选任意接口。服务器不能指定一个包含多个地址的清单。通配的本地地址表示“任意”这个选择。通配地址通过在调用bind之前把套接字地址结构中的IP地址字段设置成INADDR_ANY来指定。

  稍后在IP地址为206.168.112.219的主机上启动第一个客户,它对服务器的IP地址之一12.106.32.254执行主动打开。我们假设本例中客户主机的TCP为此选择的临时端口为1500,如图1-6所示。图中在该客户的下方标出了它的套接字对。

图1-6 客户对服务器的连接请求

  当服务器接收并接受这个客户的连接时,它fork一个自身的副本,让子进程来处理该客户的请求,如图1-7所示。

图1-7 并发服务器让子进程处理客户

  至此,我们必须在服务器主机上区分监听套接字和已连接套接字(connected socket)。注意已连接套接字使用与监听套接字相同的本地端口(21)。还要注意在多宿服务器主机上,连接一旦建立,已连接套接字的本地地址(12.106.32.254)随机填入。

  下一步我们假设在客户主机上另有一个客户请求连接到同一个服务器。客户主机的TCP为这个新客户的套接字分配一个未使用的临时端口,譬如说1501,如图1-8所示。服务器上这两个连接是有区别的:第一个连接的套接字对和第二个连接的套接字对不一样,因为客户的TCP给第二个连接选择了一个未使用的端口(1501)。

图1-8 第二个客户与同一服务器的连接

  通过本例应注意,TCP无法仅仅通过查看目的端口号来分离外来的分节到不同的端点。它必须查看套接字对的所有4个元素才能确定由哪个端点接收某个到达的分节。图1-8中对于同一个本地端口(21)存在3个套接字。如果一个分节来自206.168.112.219端口1501,目的地为12.106.32.254端口21,它就被递送给第二个子进程。所有目的端口为21的其他TCP分节都被递送给拥有监听套接字的最初那个服务器(父进程)。

 1.5 TCP输出

  图1-9展示了某个应用进程写数据到一个TCP套接字中时发生的步骤。

  每一个TCP套接字有一个发送缓冲区,我们可以使用SO_SNDBUF套接字选项来更改缓冲区的大小。当某个应用进程调用write时,内核从该应用进程的缓冲区中复制所有数据所写套接字的发送缓冲区。如果该套接字的发送缓冲区容不下该应用进程的所有数据(或是应用进程的缓冲区大于套接字的发送缓冲区,或是套接字的发送缓冲区中已有其他数据),该应用进程将被投入睡眠。这里假设该套接字是阻塞的,它是通常的默认设置。内核将不从write系统调用返回,直到应用进程缓冲区中的所有数据都复制到套接字发送缓冲区。因此,从写一个TCP套接字的write调用成功返回仅仅表示我们可以重新使用原来的应用进程缓冲区,并不表明对端的TCP或应用进程已接收数据。

图1-9 应用进程写TCP套接字时涉及的步骤和缓冲区

  这一端的TCP提取套接字发送缓冲区中的数据并把它发送给对端TCP,其过程基于TCP数据传送的所有规则。对端TCP必须确认收到的数据,伴随来自对端的ACK的不断到达,本端TCP至此才能从套接字发送缓冲区中丢弃已确认的数据。TCP必须为己发送的数据保留一个副本,直到它被对端确认为止。

  本端TCP以MSS大小的或更小的块把数据传递给IP,同时给每个数据块安上一个TCP首部以构成TCP分节,其中MSS或是由对端通告的值,或是536(若对端末发送一个MSS选项)。(536是IPv4最小重组缓冲区字节数576减去IPv4首部字节数20和TCP首部字节数20的结果)。IP给每个TCP分节安上一个IP首部以构成IP数据报,并照其目的IP地址查找路由表项以确定外出接口,然后把数据报传递给相应的数据链路。IP可能在把数据报传递给数据链路之前将其分片,不过我们已经谈到过MSS选项的目的之一就是试图避免分片,较新的实现还使用了路径MTU发现功能。每个数据链路都有一个输出队列,如果该队列已满,那么新到的分组将被丢弃,并沿协议栈向上返回一个错误:从数据链路到IP,再从IP打破TCP。TCP将注意到这个错误,并在以后某个时刻重传相应分节。应用进程并不知道这种暂时的情况。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

© 著作权归作者所有

共有 人打赏支持
thanatos_y
粉丝 8
博文 112
码字总数 315059
作品 0
成都
程序员
私信 提问
0-Linux 网络编程修炼指南——内功心法

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/q1007729991/article/details/69091877 学习交流群: Linux 学习交流群 610441700 说明:本系列文章并不能取代...

--Allen--
2017/04/04
0
0
脑残式网络编程入门(六):什么是公网IP和内网IP?NAT转换又是什么鬼?

本文引用了“帅地”发表于公众号苦逼的码农的技术分享。 1、引言 搞网络通信应用开发的程序员,可能会经常听到外网IP(即互联网IP地址)和内网IP(即局域网IP地址),但他们的区别是什么?又...

JackJiang2011
2018/11/20
0
0
我的网络开发之旅——TCP/IP协议分析

之前在当地的一期技术沙龙做了一个《网络开发那些事》的技术分享,讲述了自己职业生涯从事的与网络相关的开发工作。在接触这类开发之前一直在从事业务系统或者单机系统的开发,说真的那时感觉...

yaocoder
2014/09/16
0
0
IM开发者的零基础通信技术入门(二):通信交换技术的百年发展史(下)

【来源申明】本文原文来自:微信公众号“鲜枣课堂”,官方网站:xzclass.com,原题为:《通信交换的百年沧桑(下)》,本文引用时已征得原作者同意。为了更好的内容呈现,收录时内容有稍许调...

JackJiang2011
04/02
0
0
IM开发者的零基础通信技术入门(二):通信交换技术的百年发展史(下)

1、系列文章引言 1.1 适合谁来阅读? 本系列文章尽量使用最浅显易懂的文字、图片来组织内容,力求通信技术零基础的人群也能看懂。但个人建议,至少稍微了解过网络通信方面的知识后再看,会更...

首席大胸器
04/02
0
0

没有更多内容

加载失败,请刷新页面

加载更多

OSChina 周一乱弹 —— 加油,还有11个小时就下班了

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @_全村的希望 :吴亦凡把大碗面正儿八经做成单曲了,你别说,还挺好听 《大碗宽面》- 吴亦凡 手机党少年们想听歌,请使劲儿戳(这里) @tom_t...

小小编辑
26分钟前
53
7
C++ vector和list的区别

1.vector数据结构 vector和数组类似,拥有一段连续的内存空间,并且起始地址不变。 因此能高效的进行随机存取,时间复杂度为o(1); 但因为内存空间是连续的,所以在进行插入和删除操作时,会造...

shzwork
今天
6
0
Spring之invokeBeanFactoryPostProcessors详解

Spring的refresh的invokeBeanFactoryPostProcessors,就是调用所有注册的、原始的BeanFactoryPostProcessor。 相关源码 public static void invokeBeanFactoryPostProcessors(Configu......

cregu
昨天
5
0
ibmcom/db2express-c_docker官方使用文档

(DEPRECIATED) Please check DB2 Developer-C Edition for the replacement. What is IBM DB2 Express-C ? ``IBM DB2 Express-C``` is the no-charge community edition of DB2 server, a si......

BG2KNT
昨天
4
0
Ubuntu 18.04.2 LTS nvidia-docker2 : 依赖: docker-ce (= 5:18.09.0~3-0~ubuntu-bionic)

平台:Ubuntu 18.04.2 LTS nvidia-docker2 版本:2.0.3 错误描述:在安装nvidia-docker2的时候报dpkg依赖错误 nvidia-docker2 : 依赖: docker-ce (= 5:18.09.0~3-0~ubuntu-bionic) 先看一下依......

Pulsar-V
昨天
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部