一、协议简介
1、TCP的必要性
在很多情 况下,应用程序期望传输层能提供可靠的服务,在上层应用想把大量的顺序数据(例如一个文件)发送到另一台主机时,使用 UDP 会显得非常不方便。 在接收端,所有数据将按照编号被顺序组织起来,当所有数据接收成功后,TCP 才把数据递交给应用层。应用层不必担心报文的乱序、重复、丢失等题,TCP 采用正面确认以及重传等机制保证数据流能全部正确到达。
2、TCP的特性
TCP 是一个很复杂的机制,它规定了两台主机间传送的报文的格式,以及为保证报文能正确到达目的地而采取的一系列措施,同时也包括了必要的差错与控制机制。
3、连接的定义
TCP 中引入了端点的概念,端点定义为一对整数,标识为(IP 地址,端口号),例如端口(192.168.1.1,80)就代表了主机 192.168.1.1 上的端口 80。 同一台主机上的两个应用可以使用两个不同的端点建立连接并进行数据交互,利用 TCP/IP 进行数据交互是主机上实现进程间通信、数据共享的常用方法。
连接 1:(192.168.1.37,80)和(192.168.1.1,7675)
连接 2:(192.168.1.37,80)和(192.168.1.37,9546)
TCP 中也使用端口号的概念,端口号是 0 到 65535 之间的整数,端口号可以分为熟知端口号和短暂端口号,常见的 TCP 熟知端口定义如表 131 所示。
4、数据流编号
TCP 把一个连接中的所有数据字节都进行了编号,当然两个方向上的编号是彼此独立的,编号的初始值由发送数据的一方随机选取,编号的取值范围在0~(2的32方-1),比如发送方选择的起始编号为 20,且将要发送的数据长度为 800 字节,那么字节编号将覆盖 20 到 819 的范围。
例如,将上述的 800 字节放在三个报文段中来发送,前两个报文段各装载了300 字节的数据,最后一个报文装载了 200 字节的数据,则三个报文段携带的数据情况如下:
报文段 1::起始序号:20,数据长度:300,序号范围:20~319
报文段 2::起始序号:320,数据长度:300,序号范围:320~619
报文段 3::起始序号:620,数据长度:200,序号范围:620~819
接收方通过确认的机制来告诉发送方数据的接收状况,这是通过向发送方返回一个确认号来完成的,确认号标识出自己期望接收到的下一个字节的编号 ,例如在上面的例子中,如果接收方接收到了报文段 1,则它返回给发送方的确认号为 320,表示自己期望收到数据编号为 320 的数据 。还有一种情况,如果接收方只收到报文段 1 和 3,那么它返回的确认号仍然是 320,确认号始终表示接收方期望的下一字节数据,尽管可能已有更高编号的数据被收到。
5、滑动窗口
在数据发送端,所有数据流按照顺序被组织在发送缓存中,什么时候将发送数据以及发送的多少是由滑动窗口决定的,使用窗口滑动的概念可以达到很好的流量控制效果和拥塞控制效果。 接收方为了接收数据,也必须在接收缓存上维护一个接收窗口,接收方需要将数据填入缓冲区,对数据进行顺序组织等操作,并向发送方通告自己的接收窗口大小 。
最后需要指出的是,TCP 是全双工的通信,两个方向上的数据传送是独立的,任何一方既可以作为发送端同时也会成为接收端,因此,任何一端都将为每个 TCP 连接维护两个窗口,一个用于数据接收,一个用于数据发送。这样,在一条完整的 TCP 连接上,应该同时存在四个窗口 。
如图所示,窗口内的数据又可以分为两部分,第一是编号 5~10 的 已发送但是未被接收方确认的数据,第二是编号为 11~13 的可发送但未发送的数据(这些数据可能是因为不能组成一个合适长度的报文,因此未被发送)。若此时发送方接收到值为 6 的确认号,则窗口会向右滑动,5 和 6 被排除在窗口之外,而 14~15 将被包括在窗口内,成为可发送的数据。
二、TCP报文
1、报文格式
TCP 报文段由 TCP 首部和 TCP 数据区组成,如图 132 所示,首部区域包含了连接建立与断开、数据确认、窗口大小通告、数据发送相关的所有标志和控制信息。首部的大小为 20~60 字节,在没有任何选项的情况下,首部大小为 20 字节,与不含选项字段的 IP 报首部大小相同。
32 位序号字段标识了从 TCP 发送端到 TCP 接收端的数据字节编号,它的值为当前报文段中的第一个数据的字节序号。在接收方,先计算出数据区数据的长度,然后使用首部中的序号字段,就能计算出报文最后一字节数据的序号。
32 位确认序号只有 ACK 标志为 1 时才有效,它包含了本机所期望收到的下一个数据序号。确认序号 = 序号 + 数据长度。
4 位首部长度指出了 TCP 首部的长度,以 4 字节为单位。 TCP 最多有 60 字节的首部,如果没有任何选项字段,首部长度应该为 5(20 字节)。
接下来的 6bit 保留字段暂时未用,为将来保留。
6 个标志位的意义如表 132 所示,后续会对各个字段进行详细的介绍。
窗口大小字段是 16bit 的,所以通告窗口的最大值为 65535 字节。窗口字段是实现流量控制的关键字段,当接收方向发送方通知一个大小为 0 的窗口时,将完全阻止发送方的数据发送 。
16 位的紧急指针只有当紧急标志位 URG 置位时才有效,此时报文中包含了紧急数据,紧急数据始终放在报文段数据开始的地方,而紧急指针定义出了紧急数据在数据区中的结束处,用这个值加上序号字段值就得到了最后一个紧急数据的序号。
2、TCP选项
每条 TCP 选项由 3 部分组成:1 字节的选项类型+1 字节的选项总长度+选项数据。如图 133所示,它展示出了常见的两种 TCP 选项的格式定义,它们分别是最大报文段长度选项和窗口扩大因子选项。(最大为40字节)
最大报文段长度(MSS) :用于向对方指明自己所能接受的最大报文段 ,每一方都不应该发送超过对方指定 MSS 大小的报文段,由于选项中最大报文段长度是 16 位的,所以 MSS 的值在 0 到 65535 之间,如果一方没有向另一方指明自己的 MSS,则表明它将使用默认最大报文段长度 536。
窗口扩大因子:通告的窗口大小=首部中窗口大小字段值×2的A次方
3、紧急数据
应用程序在向接收方发送了大量的数据后,发现由于自身的问题,使得这些数据中存在错误,此时它期望向接收方发送一个命令,告诉接收方:这些数据是错误的,你不要处理,否则会给连接带来致命的危害。这个命令不能通过数据流的形式被发送,假若这样,接收端只有处理完了所有数据才会去读取这个命令。
使用 URG 位置 1 的报文段,这种类型的报文段将告诉接收方:这里面的数据是紧急的,你可以直接读取,不必把它们放在接收缓冲里面。在包含紧急数据的报文段中,紧急数据总是放在数据区域的起始处,且报文首部中的紧急指针指明了紧急数据的最后一个字节。
4、强迫数据交互
发送方应用程序向 TCP传递数据时,请求推送(push)操作,这时,TCP 协议不会等待发送缓冲区被填满,而是直接将报文发送出去。同时,被推送出去的报文首部中推送位(PSH)将被置 1,接收端在收到这类的报文时,会 尽快将数据递交给应用程序,而不必缓冲更多的数据再递交。
5、报文首部数据结构
协议栈中 TCP 部分的代码量很大,源代码中的 tcp.h、tcp.c、tcp_in.c、tcp_out.c 四个文件包含了 TCP 协议实现的全部数据结构和函数。其中 tcp.c 文件包含了与 TCP 编程、TCP 定时器相关的函数,tcp_in.c 包含了 TCP 报文段输入处理相关的函数、tcp_out.c 包含了 TCP 报文段输出处理相关的函数,而 tcp.h 包含了所有的宏、结构体的定义。在 LwIP 中是怎样来描述一个 TCP 首部的,数据结构名字叫做 tcp_hdr。
//定义 TCP 首部结构体 PACK_STRUCT_BEGIN struct tcp_hdr { PACK_STRUCT_FIELD(u16_t src); //源端口 PACK_STRUCT_FIELD(u16_t dest); //目的端口 PACK_STRUCT_FIELD(u32_t seqno); //序号 PACK_STRUCT_FIELD(u32_t ackno); //确认序号 PACK_STRUCT_FIELD(u16_t _hdrlen_rsvd_flags); //首部长度+保留位+标志位 PACK_STRUCT_FIELD(u16_t wnd); //窗口大小 PACK_STRUCT_FIELD(u16_t chksum); //校验和 PACK_STRUCT_FIELD(u16_t urgp); //紧急指针 } PACK_STRUCT_STRUCT; PACK_STRUCT_END
三、TCP连接
1、建立连接
握手过程由客户端程序首先发起,整个过程要经历下面三个步骤:
(1)客户端发送一个 SYN 标志置 1 的 TCP 报文段,报文段首部指明自己的端口号及期望连接的服务器端口号。同时在报文段中,客户端需要通告自己的初始序号 ISN(这里假设为 A)。除此之外,这个报文中还可以携带一些选项字段。
(2)当服务器接收到该报文并解析后,返回一个 SYN 和 ACK 标志置 1 的报文段作为应答。ACK 为 1 表示该报文段包含了有效的确认号,这个值应该是客户端初始序列号加 1(即 A+1)。另一方面,SYN 标志表明服务器响应连接,并在回应报文中包含服务器自身选定的初始序号 ISN(这里假设为 B)。
(3)当客户端接收到服务器的 SYN 应答报文后,会再次产生 ACK 置位的报文段,该报文段包含了对服务器 SYN 报文段的有效确认号,该值为服务器发送的 ISN 加 1(即 B+1),同时,该报文段中还包含了有效的窗口大小,用来向服务器指明客户端的接收窗口大小。
当服务器接收到(3)中的这个 ACK 报文后,服务器和客户端之间的连接就建立起来了,它们双方都获得彼此的窗口大小、初始序列号、最大报文段等信息。
注意:当建立一个新连接时,握手报文首部中的 SYN 标志置 1,此时,序号字段包含由发送方随机选择的初始序号 ISN(Initial Sequence Number)。建立连接的报文(SYN)将占用一个数据编号,因此发送方随后将要发送数据的第一个字节序号为 ISN+1。
2、断开连接
断开连接的四次握手过程如下:
(1)首先,当客户端应用程序主动执行关闭操作时,客户端会向服务器发送一个 FIN 标志置1 的报文段,用来关闭从客户端到服务器的数据传送,假设该报文段序号字段值为 C。
(2)当服务器收到这个 FIN 报文段后,它返回一个 ACK 报文,确认序号为收到的序号加 1(即 C+1)。和 SYN 一样,一个 FIN 将占用一个序号。当客户端接收到这个 ACK 后,从客户端到服务器方向的连接就断开了。
(3)服务器 TCP 向其上层应用程序通告客户端的断开操作,这会导致服务器程序关闭它的连接,同样,此时一个 FIN 标志置 1 的报文段将被发送到客户端,假设序号为 D。
(4)客户端也会返回一个 ACK 报文段(D+1)作为响应,以完成该方向连接的断开操作。 <