TCP服务简介
UDP 运载的数据是以报文的形式,各个报文在网络中互不相干传输, UDP 每收到一个报文就递交给上层应用,因此如果对于大量数据来说,应用层的重装是非常麻烦的,因为UDP 报文在网络中到达目标主机的顺序是不一样的;
而 TCP 采用数据流的形式传输, 先后发出的数据在网络中虽然也是互不相干的传输,但是这些数据本身携带的信息却是紧密联系的, TCP 协议会给每个传输的字节进行编号,当然啦,两个主机方向上的数据编号是彼此独立的, 在传输的过程中,发送方把数据的起始编号与长度放在 TCP 报文中,在接收方将所有数据按照编号组装起来,然后返回一个确认,当所有数据接收完成后才将数据递交到应用层中。
TCP的特性
连接机制
TCP 是一个面向连接的协议,无论哪一方向另一方发送数据之前,都必须先在双方之间建立一个连接,否则将无法发送数据
确认与重传
一个完整的 TCP 传输必须有数据的交互,接收方在接收到数据之后必须正面进行确认,向发送方报告接收的结果,而发送方在发送数据之后必须等待接收方的确认,同时发送的时候会启动一个定时器,在指定超时时间内没收到确认,发送方就会认为发送失败,然后进行重发操作,这就是重传报文。
缓冲机制
在发送方想要发送数据的时候, 由于应用程序的数据大小、 类型都是不可预估的, 而TCP 协议提供了缓冲机制来处理这些数据, 如在数据量很小的时候, TCP 会将数据存储在一个缓冲空间中, 等到数据量足够大的时候在进行发送数据, 这样子能提供传输的效率并且减少网络中的通信量,而且在数据发送出去的时候并不会立即删除数据,还是让数据保存在缓冲区中,因为发送出去的数据不一定能被接收方正确接收,它需要等待到接收方的确认再将数据删除。同样的,在接收方也需要有同样的缓冲机制,因为在网络中传输的数据报到达的时间是不一样的,而且 TCP 协议还需要把这些数据报组装成完整的数据,然后再递交到应用层中。
全双工通信
在 TCP 连接建立后,那么两个主机就是对等的,任何一个主机都可以向另一个主机发送数据,数据是双向流通的,所以 TCP 协议是一个全双工的协议,这种机制为 TCP 协议传输数据带来很大的方便,一般来说, TCP 协议的确认是通过捎带的方式来实现,即接收方把确认信息放到反向传来的是数据报文中,不必单独为确认信息申请一个报文,捎带机制减少了网络中的通信流量。
流量控制
一条 TCP 连接每一侧的主机都设置了缓冲区域。当该接收方收到数据后,它就将数据放入接收缓冲区,当确认这段数据是正常的时候,就会向发送方返回一个确认。并且向相关的应用层递交该数据,但不一定是数据刚一到达就立即递交。事实上,接收方应用也许正忙于其他任务,甚至要过很长时间后才会去处理这些数据。 这样子如果接收方处理这些数据时相对缓慢,而发送方发送得太多、太快,就会很容易地使接收方的接收缓冲区发生溢出
因此 TCP 提供了流量控制服务(flow-control service)以消除发送方使接收方缓冲区溢出的可能性。流量控制是一个速度匹配服务,即发送方的发送速率与接收方应用程序的读取速率相匹配, TCP 通过让发送方维护一个称为**接收窗口(receive window)**的变量来提供流量控制,它用于给发送方一个指示:接收方还能接收多少数据,接收方会将此窗口值放在 TCP 报文的首部中的窗口字段, 然后传递给发送方,这个窗口的大小是在发送数据的时候动态调整的。
差错控制
除了确认与重传之外, TCP 协议也会采用校验和的方式来检验数据的有效性,主机在接收数据的时候,会将重复的报文丢弃,将乱序的报文重组,发现某段报文丢失了会请求发送方进行重发,因此在 TCP 往上层协议递交的数据是顺序的、无差错的完整数据。
拥塞控制
如果一个主机还是以很大的流量给另一个主机发送数据, 但是其中间的路由器通道很小, 无法承受这样大的数据流量的时候, 就会导致拥塞的发生, 这样子就导致了接收方无法在超时时间内完成接收(接收方此时完全有能力处理大量数据), 而发送方又进行重传,这样子就导致了链路上的更加拥塞, 延迟发送方必须实现一直自适应的机制, 在网络中拥塞的情况下调整自身的发送速度,这种形式对发送方的控制被称为拥塞控制(congestioncontrol), 与前面我们说的流量控制是非常相似的,而且 TCP 协议采取的措施也非常相似,均是限制发送方的发送速度。
端口号的概念
TCP 协议的连接是包括上层应用间的连接,简单来说, TCP 连接是两个不同主机的应用连接,而传输层与上层协议是通过端口号进行识别的,如 IP 协议中以 IP 地址作为识别一样,端口号的取值范围是 0~65535,这些端口标识着上层应用的不同线程,一个主机内可能只有一个 IP 地址,但是可能有多个端口号,每个端口号表示不同的应用线程。
常见的 TCP 协议端口号有 21、 53、 80 等等,更多端口描述具体见表格 13-1,其中 80端口号是我们日常生活中最常见的一个端口号,它也是 HTTP 服务器默认开放的端口。

TCP报文段的结构
TCP报文段的封装
如 ICMP 报文一样, TCP 报文段依赖 IP 协议进行发送,因此 TCP 报文段与 ICMP 报文一样,都是封装在 IP 数据报中, IP 数据报封装在以太网帧中,因此 TCP 报文段也是经过了两次的封装,然后发送出去, 其封装具体见图

TCP报文段格式
TCP 报文段如 APR 报文、 IP 数据报一样,也是由首部+数据区域组成, TCP 报文段的首部我们称之为 TCP 首部,其首部内推很丰富,各个字段都有不一样的含义,如果不计算选项字段,一般来说 TCP 首部只有 20 个字节,具体见图

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 报文段都包含源主机和目标主机的端口号,用于寻找发送端和接收端应用线程,这两个值加上 I P 首部中的源 I P 地址和目标 I P 地址就能确定唯一一个 TCP 连接。
序号字段用来标识从 TCP 发送端向 TCP 接收端发送的数据字节流,它的值表示在这个报文段中的第一个数据字节所处位置吗,根据接收到的数据区域长度, 就能计算出报文最后一个数据所处的序号, 因为 TCP 协议会对发送或者接收的数据进行编号(按字节的形式),那么使用序号对每个字节进行计数, 就能很轻易管理这些数据。序号是 32 bit 的无符号整数。
当建立一个新的连接时, TCP 报文段首部的 SYN 标志变 1, 序号字段包含由这个主机随机选择的初始序号 ISN(Initial Sequence Number)。该主机要发送数据的第一个字节序号为 ISN+1,因为 SYN 标志会占用一个序号
既然 TCP 协议给每个传输的字节都了编号, 那么确认序号就包含接收端所期望收到的下一个序号, 因此,确认序号应当是上次已成功收到数据的最后一个字节序号加 1。 当然,只有 ACK 标志为 1 时确认序号字段才有效, TCP 为应用层提供全双工服务,这意味数据能在两个方向上独立地进行传输,因此确认序号通常会与反向数据(即接收端传输给发送端的数据)封装在同一个报文中(即捎带) ,所以连接的每一端都必须保持每个方向上的传输数据序号准确性。
首部长度字段占据 4bit 空间,它指出了 TCP 报文段首部长度,以字节为单位,最大能记录 15*4=60 字节的首部长度,因此, TCP 报文段首部最大长度为 60 字节。 在字段后接下来有 6bit 空间是保留未用的。
此外还有 6bit 空间,是 TCP 报文段首部的标志字段,用于标志一些信息:
URG:首部中的紧急指针字段标志,如果是 1 表示紧急指针字段有效。
ACK:首部中的确认序号字段标志,如果是 1 表示确认序号字段有效。
PSH: 该字段置一表示接收方应该尽快将这个报文段交给应用层。
RST: 重新建立 TCP 连接。
SYN: 用同步序号发起连接。
FIN: 中止连接。
TCP 的流量控制由连接的每一端通过声明的窗口大小来提供,窗口大小为字节数,起始于确认序号字段指明的值,这个值是接收端正期望接收的数据序号,发送方根据窗口大小调整发送数据,以实现流量控制。窗口大小是一个占据 16 bit 空间的字段,因而窗口最大为 65535 字节,当接收方告诉发送方一个大小为 0 的窗口时,将完全阻止发送方的数据发送。
TCP连接
三次握手
第一步:客户端的 TCP 首先向服务器端的 TCP 发送一个特殊的 TCP 报文段。该报文段中不包含应用层数据, 但是在报文段的首部中的 SYN 标志位会被置为 1。因此,这个特殊报文段被称为 SYN 报文段(我们暂且称之为握手请求报文) 。另外,客户会随机地选择一个初始序号(ISN,假设为 A),并将此序号放置于该 SYN 报文段的序号字段中;但SYN 报文段中的 ACK 标志位 0,此时它的确认序号段是无效的。该报文段会被封装在一个IP 数据报中, 然后发送给目标服务器。
第二步:一旦服务器收到了客户端发出的 SYN 报文段,知道客户端要请求握手了,服务器便会从 SYN 报文段中提取对应的信息,为该 TCP 连接分配 TCP 缓存和变量,并向该客户 TCP 发送允许连接的报文段(握手应答报文) 。 这个报文段同样也不包含任何应用层数据, 但是,在报文段的首部却包含 3 个重要的信息。
- SYN 与 ACK 标志都被置为 1。
- 将 TCP 报文段首部的确认序号字段设置为 A+1(这个 A(ISN)是从握手请求报
文中得到) 。 - 服务器随机选择自己的初始序号(ISN,注意此 ISN 是服务器端的 ISN,假设为
B) , 并将其放置到 TCP 报文段首部的序号字段中。
第三步:当客户端收到服务器的握手应答报文后,会将 ACK 标志置位,此时客户端的 TCP 报文段的 ACK 标志被设置为 1,而对于 SYN 标志, 因为连接已经建立了,所以该标志会被置为 0, 同时客户端也要给该 TCP 连接分配缓存和变量,并且客户端还需要返回一个应答报文段,这个报文对服务器的应答报文段作出应答,将 TCP 报文段首部的确认序号字段设置为 B+1,同时也会告知服务器的窗口大小。
在完成握手后,客户端与服务器就建立了连接, 同时双方都得到了彼此的窗口大小,序列号等信息, 在传输 TCP 报文段的时候,每个 TCP 报文段首部的 SYN 标志都会被置 0,因为它只用于发起连接,同步序号。


四次挥手
建立一个连接需要三次握手,而终止一个连接要经过 四次挥手(有一些书上也会称为“四次握手” ) , 这由 TCP 的特性造成的,因为 TCP 连接是全双工连接的服务,因此每个方向上的连接必须单独关闭。
“四次挥手” 终止连接示意图具体见图,其具体过程如下:
第一步:客户端发出一个 FIN 报文段主动进行关闭连接,此时报文段的 FIN 标志位为1,假设序号为 C,一般来说 ACK 标志也会被置一,但确认序号字段是无效的。
第二步: 当服务器收到这个 FIN 报文段,它发回一个 ACK 报文段(此报文段是终止连接应答) ,确认序号为收到的序号加 1(C+1), 和 SYN 一样,一个 FIN 将占用一个序号,此时断开客户端->服务器的方向连接。
第三步:服务器会向应用程序请求关闭与这个客户端的连接, 接着服务器就会发送一个 FIN 报文段(这个报文段是服务器向客户端发出,请求终止连接) ,此时假设序号为 D,ACK 标志虽然也为 1,但是确认序号字段是无效的。
第四步: 客户端返回一个 ACK 报文段来确认终止连接的请求, ACK 标志置一, 并将确认序号设置为收到序号加 1(D+1) ,此时断开服务器->客户端的方向连接。

TCP状态
Lwip中定义的TCP状态
TCP 协议根据连接时接收到报文的不同类型,采取相应动作也不同,还要处理各个状态的关系,如当收到握手报文时候、超时的时候、用户主动关闭的时候等都需要不一样的状态去采取不一样的处理。 在 LwIP 中,为了实现 TCP 协议的稳定连接,采用数组的形式定义了 11 种连接时候的状态, 具体见代码清单
static const char *const tcp_state_str[] = {
"CLOSED", //关闭状态(无连接)
"LISTEN", //监听状态
"SYN_SENT", //已发起请求连接(等待确认)
"SYN_RCVD", //已收到请求连接
"ESTABLISHED", //稳定连接状态
"FIN_WAIT_1", //单向请求终止连接状态
"FIN_WAIT_2", //对方已应答请求终止连接
"CLOSE_WAIT", //等待终止连接
"CLOSING", //两端同时关闭
"LAST_ACK", //服务器等待对方接受关闭
"TIME_WAIT" //关闭成功(2MSL 等待状态)
};
- ESTABLISHED 状态: 这个状态是处于稳定连接状态,建立连接的 TCP 协议两端的主机都是处于这个状态,它们相互知道彼此的窗口大小、序列号、最大报文段等信息。
- FIN_WAIT_1 与 FIN_WAIT_2 状态:处于这个状态一般都是单向请求终止连接,然后主机等待对方的回应,而如果对方产生应答,则主机状态转移为FIN_WAIT_2,此时{主机->对方}方向上的 TCP 连接就断开,但是{对方->主机}方向上的连接还是存在的。此处有一个注意的地方: 如果主机处于 FIN_WAIT_2状态, 说明主机已经发出了 FIN 报文段,并且对方也已对它进行确认, 除非主机是在实行半关闭状态,否则将等待对方主机的应用层处理关闭连接,因为对方已经意识到它已收到 FIN 报文段, 它需要主机发一个 FIN 来关闭{对方->主机}方向上的连接。只有当另一端的进程完成这个关闭, 主机这端才会从 FIN_WAIT_2 状态进入 TIME_WAIT 状态。 否则这意味着主机这端可能永远保持这个FIN_WAIT_2 状态, 另一端的主机也将处于 CLOSE_WAIT 状态,并一直保持这个状态直到应用层决定进行关闭。
- TIME_WAIT 状态: TIME_WAIT 状态也称为 2MSL 等待状态。每个具体 TCP 连接的实现必须选择一个 TCP 报文段最大生存时间 MSL(Maximum Segment Lifetime) ,如 IP 数据报中的 TTL 字段,表示报文在网络中生存的时间, 它是任何报文段被丢弃前在网络内的最长时间, 这个时间是有限的, 为什么需要等待呢?我们知道 IP 数据报是不可靠的,而 TCP 报文段是封装在 IP 数据报中, TCP 协议必须保证发出的 ACK 报文段是正确被对方接收, 因此处于该状态的主机必须在这个状态停留最长时间为 2 倍的 MSL,以防最后这个 ACK 丢失,因为 TCP 协议必须保证数据能准确送达目的地。
TCP状态转移

虚线:表示服务器的状态转移。
实线:表示客户端的状态转移。
图中所有“关闭”、“打开”都是应用程序主动处理。
图中所有的“超时”都是内核超时处理。
三次握手过程
图 (7):服务器的应用程序主动使服务器进入监听状态,等待客户端的连接请求。
图**(1):首先客户端的应用程序会主动发起连接,发送 SNY 报文段给服务器,在发送之后就进入 SYN_SENT 状态等待服务器的 SNY ACK 报文段进行确认,如果在指定超时时间内服务器不进行应答确认,那么客户端将关闭连接。
图(8):处于监听状态的服务器收到客户端的连接请求(SNY 报文段),那么服务器就返回一个 SNY ACK 报文段应答客户端的响应,并且服务器进入 SYN_RCVD 状态。
图(2):如果客户端收到了服务器的 SNY ACK 报文段,那么就进入ESTABLISHED 稳定连接状态,并向服务器发送一个 ACK 报文段。
图(9)**:服务器收到来自客户端的 ACK 报文段,表示连接成功,进入ESTABLISHED 稳定连接状态,这正是我们建立连接的三次握手过程。
四次挥手过程
图**(3)**:一般来说,都是客户端主动发送一个 FIN 报文段来终止连接,此时客户端从 ESTABLISHED 稳定连接状态转移为 FIN_WAIT_1 状态,并且等待来自服务器的应答确认。
图 (10): 服务器收到 FIN 报文段,知道客户端请求终止连接,那么将返回一个ACK 报文段到客户端确认终止连接,并且服务器状态由稳定状态转移为 CLOSE_WAIT 等待终止连接状态。
图 (4): 客户端收到确认报文段后,进入 FIN_WAIT_2 状态, 等待来自服务器的主动请求终止连接, 此时{客户端->服务器}方向上的连接已经断开。
图 (11): 一般来说,当客户端终止了连接之后,服务器也会终止{服务器->客户端}方向上的连接,因此服务器的原因程序会主动关闭该方向上的连接,发送一个 FIN 报文段给客户端。
图 (5): 处于 FIN_WAIT_2 的客户端收到 FIN 报文段后,发送一个 ACK 报文段给服务器。
图 (12): 服务器收到 ACK 报文段,就直接关闭,此时{服务器->客户端}方向上的连接已经终止,进入 CLOSED 状态。
图 (6): 客户端还会等待 2MSL,以防 ACK 报文段没被服务器收到,这就是四次挥手的全部过程。
注意: 对于图 **(13)(14)(15)**的这些状态都是一些比较特殊的状态。
TCP中的数据结构
1 #define IP_PCB \
2 /* 本地 ip 地址与远端 IP 地址 */ \
3 ip_addr_t local_ip; \
4 ip_addr_t remote_ip; \
5 /* 绑定 netif 索引 */ \
6 u8_t netif_idx; \
7 /* 套接字选项 */ \
8 u8_t so_options; \
9 /* 服务类型 */ \
10 u8_t tos; \
11 /* 生存时间 */ \
12 u8_t ttl \
13 /* 链路层地址解析提示 */ \
14 IP_PCB_NETIFHINT
15
16
17 #define TCP_PCB_COMMON(type) \
18 type *next; /* 指向链表中的下一个控制块 */ \
19 void *callback_arg; \
20 TCP_PCB_EXTARGS \
21 enum tcp_state state; /* TCP 状态 */ \
22 u8_t prio; \
23 /* 本地主机端口号 */ \
24 u16_t local_port
25
26
27 /** TCP 协议控制块 */
28 struct tcp_pcb
29 {
30 IP_PCB;
31 /** 协议特定的 PCB 成员 */
32 TCP_PCB_COMMON(struct tcp_pcb);
33
34 /* 远端端口号 */
35 u16_t remote_port;
36
37 tcpflags_t flags;
38 #define TF_ACK_DELAY 0x01U /* 延迟发送 ACK */
39 #define TF_ACK_NOW 0x02U /* 立即发送 ACK. */
40 #define TF_INFR 0x04U /* 在快速恢复。 */
41 #define TF_CLOSEPEND 0x08U /* 关闭挂起 */

本文介绍了TCP服务,对比UDP,阐述其面向连接、确认重传等特性。讲解端口号概念、报文段结构,详细说明三次握手和四次挥手的连接与断开过程。还介绍了Lwip中TCP状态转移、窗口概念,以及报文段的缓冲队列、发送和接收处理。
最低0.47元/天 解锁文章
4125

被折叠的 条评论
为什么被折叠?



