1、介绍TCP
TCP(Transmission Control Protocol 传输控制协议)是一种面向连接、可靠的、基于字节流的传输层通信协议。
TCP将通过以下方式来提供可靠性传输:
TCP 可靠性传输实现(一)TCP的三次握手和四次挥手;
TCP 可靠性传输实现(二)TCP的重传机制;
https://www.jianshu.com/p/f7f75a0f6384
TCP 可靠性传输实现(三)TCP的流量控制和拥塞机制;
TCP 可靠性传输实现(四)TCP的保活机制;
TCP面向连接意味着两个使用TCP的应用(通常是一个客户端和一个服务器)在彼此交换数据之前必须先建立一个TCP连接。这一过程和打电话相似,先拨号振铃,等待对方接通后说“喂”,然后才说明是谁。在本文将介绍TCP是如何建立连接的,以及当一方通信结束后如何断开连接。
2、TCP的首部
TCP数据被封装在一个IP数据报中,如图:
下图显示TCP首部的数据格式。如果不计任选字段,它通常是20个字节。
每个TCP段都包含源端和目的端的端口号,用于寻找发端和收端应用程序。这两个值加上IP首部中的源端IP地址和目的端IP地址唯一确定一个TCP连接。
一个IP地址和一个端口号也称为一个插口(socket)。插口对(socket pair)(包含客户IP地址、客户端端口号、服务器IP地址和服务器端口号的四元组)可唯一确定互联网络中每个TCP连接的双方。
下面介绍下TCP首部字段:
序列号:用来标识从TCP发端向TCP收端发送的数据字节流,它表示在这个报文段中的第一个数据字节。如果把字节流看做在两个应用程序间的单向流动,则TCP用序号对每个字节进行计数。序号是32bit的无符号数,序号到达2^32-1后又从0开始。序号用来解决网络包乱序问题。接收端根据这个编号进行确认。
确认号:是接收确认端期望收到的下一个序列号。确认号应当是上次已成功收到的数据字节序号加1,只有当标志位中的ACK标志为1时该确认号的字段才有效。主要用来解决丢包问题。
若确认号=N,则表明:到序号N-1为止的所有数据都已正确收到。
首部长度:
TCP 首部总长度由该字段决定。该字段占 4bit,取最大值1111时,也就是十进制的 15,TCP 首部的偏移单位是 4 byte, 那么TCP 首部最长是 15 * 4 = 60 字节。 TCP 首部总长度有20个固定字节,所以该字段最短是 20byte / 4byte = 5,即 0101。首部长度也叫做数据偏移,因为首部长度实际上指示了数据区在报文段的起始偏移值。
保留位:
为将来定义新的用途保留,现在一般置 0。
控制位:TCP首部中有6个标志比特,它们中的多个可同时被设置为1,主要是用于操控TCP的状态机的,依次为URG 、 ACK、 PSH、 RST、 SYN、 FIN。
URG:紧急指针有效,是发送端向接收端发送紧急数据的一种方式。
ACK:该位为1时表示【确认应答号】有效,TCP规定除了最初建立连接时的 SYN 号之外,该位必须设置为1。
PSH:接收方应该尽快将这个报文段交给应用层。
SYN:该位为1时表示希望建立连接,同步序号用来发起一个连接。
RST:该位为1时表示TCP连接出现异常,必须强制断开连接。
FIN:该位为1时表示希望断开连接。
窗口大小:
TCP的流量控制由连接的每一端通过声明的窗口大小来提供。
校验和:
是一个强制性的字段,一定是由发送端计算和存储,并由接收端进行验证。
紧急指针:
当URG标志置1时紧急指针才有效。紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。
选项字段:
选项字段的长度 = TCP 首部总长度 - 20 字节固定长度。 由于 TCP 首部总长度最大为 60字节, 那么选项字段的长度最大为 40 字节;
最常见的可选字段是最长报文大小,又称为MSS(Maximum Segment Size)。每个连接方通常都在通信的第一个报文段(为建立连接而设置SYN标志的那个段)中指明这个选项。它表明本端所能接收的最大长度的报文段。
数据:
数据部分是可选的,在连接建立和终止时,双方交换的报文段仅有TCP首部,如果一方没有数据要发送,也使用没有任何数据的首部来确认收到的数据。
3、TCP连接的建立
第一次握手:客户端发送请求报文将 SYN = J 的初始序列号发送给服务端,发送完之后客户端处于SYN_SENT状态。
第二次握手:服务端收到SYN请求报文后,如果同意连接,会以自己的SYN(服务端) = K 的初始序列号和 ack = SYN(客户端) + 1 报文作为应答,服务端处于SYN_RECEIVE状态。
第三次握手:客户端接收到服务端的SYN+ack, 发送 ack = SYN(服务端) + 1确认包作为应答,客户端转为ESTABLISHED状态。
为什么是三次握手?不是两次、四次?
这个问题需要回到TCP的概念上:
TCP连接是为了得到 保证连接可靠性和流量控制所需维护的某些状态信息,这些信息的组合包括 Socket、序列号、窗口大小。 所以问题就是,为什么三次握手才可以初始化Socket、序列号、窗口大小并建立TCP连接。原因有三:
客户端连续发送多次SYN建立连接的报文,在网络拥堵等情况下出现:
一个【旧的SYN报文】比【最新的SYN】报文更早到了服务端,那么此时,服务端就会回一个SYN + ACK 报文给客户端;
客户端收到后根据自身的上下文,判断这是一个历史连接(序列号过期或超时),那么客户端就会发送 RST 报文给服务端,表示终止这一次的连接;
如果是两次握手连接,就不能判断此次连接是否是历史连接,三次握手可以让客户端准备发送第三次报文时,客户端有足够的上下文来判断当前的连接是否是历史连接。
(2)三次握手才可以同步双方的初始序列号;
TCP的通信双方都必须维护一个【序列号】,序列号是可靠传输的一个关键因素,它的作用:
接收方可以丢弃重复的数据;
接收方可以根据数据包的序列号按序接收;
发送方可以标识哪些数据包是已被对方接收到的;
因此在三次握手中,当客户端方法携带【初始序列号】的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN报文已被服务端成功接收, 那当服务端发送【初始序列号】给客户端的时候,也需要得到客户端的应答回应,这样才能保证双方的初始序列号能被可靠的同步。
四次握手是把 服务端回应客户端的 ACK 请求和服务端发送给服务端的初始序列号(第二步、第三步)合并成了一步,因此就成了【三次握手】。
(3)三次握手才可以避免资源的浪费;
如果只有【两次握手】,当客户端的 SYN 请求连接在网络中阻塞,客户端超时没有收到 ACK 应答报文就会重新发送 SYN,由于没有三次握手,服务端直接就分配资源并建立连接会导致:
建立了多个冗余的无效连接,造成不必要的浪费。
4、TCP连接的断开
第一次挥手:首先进行关闭的一方(即发送第一个 FIN)将执行主动关闭,而另一方(收到这个 FIN)执行被动关闭。客户端发送完 FIN 报文后,就进入了 FIN_WAIT_1 状态;
第二次挥手:服务器收到这个 FIN ,它发回一个确认应答 ACK,ack = M + 1 (即,收到的序号+1) 表示收到了,接着服务端进入CLOSE_WAIT 状态。客户端收到服务端的 ACK 应答报文后,进入 FIN_WAIT_2 状态。
第三次挥手:等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。
第四次挥手:服务端收到了 ACK 应答报文后,就进入了 CLOSE 状态,至此服务端完成了连接的关闭。
为什么挥手需要四次?
关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了,但还是能接收数据。
服务端收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端表示同意关闭连接。
服务端的 ACK 和 FIN 一般都会分开发送,从而比三次握手多了一次。
5、Socket编程
应用在使用 TCP 或 UDP 时,会用到操作系统提供的类库。这种类库一般被称为API(Application Programming Interface 应用编程接口),使用 TCP 或 UDP 通信时,又会广泛使用到套接字(Socket)的API。
那么TCP如何利用Socket编程
服务端和客户端初始化 socket,得到文件描述符;
服务端调用 bind ,将绑定 IP 地址和端口号;
服务端调用 listen ,进行监听;
服务端调用 accept,等待客户端连接;
客户端调用 connect,向服务端的地址和端口发起连接请求;
服务端 accept 返回用于传输的 socket 的文件描述符;
客户端调用 write 写入数据; 服务端调用 read 读取数据;
客户端断开连接时,会调用 close , 服务端 read 读取数据的时候,就会读取到 EOF,待处理完数据后,服务端调用 close 表示连接关闭。
注意:服务端调用 accept 时,连接成功了会返回一个已完成连接的socket,后续用来传输数据。所以,监听的 socket 和 用来传输数据的 socket,是两个 socket,一个是 监听 socket,一个是 已完成连接 socket。
重点讲下三次握手连接请求的实现:
处于 listen 状态的 TCP socket,有两个独立的队列:
SYN 队列(SYN Queue)
存储了收到了 SYN 包的连接,它的职责是回复 SYN + ACK 包,并且在客户端没有收到 ACK包时执行重传。 发送完 SYN + ACK 之后, SYN 队列等待从客户端发出的 Ack 包(三次握手的最后一个包)。当收到 ACK 包时,首先找到对应的 SYN 队列,再在对应的 SYN队列中检查相关的数据是否匹配,如果匹配,则将连接相关的数据从 SYN 队列中移除,创建一个完整的连接(用于传输数据的),并将这个连接加入到 Accept 队列。
Accept 队列(Accept Queue)
存放的是已建立好的连接,等待被应用程序取走。 当进程调用 accept() 时, 将 socket 从队列中取出,传递给上层应用程序。
参考文档:
1、TCP/IP 详解
2、图解tcpip
3、https://mp.weixin.qq.com/s/tH8RFmjrveOmgLvk9hmrkw
4、https://mp.weixin.qq.com/s/5VXhL0dTFcWNyfQ7-7NBEg
5、https://mp.weixin.qq.com/s?src=11×tamp=1592656050&ver=2412&signature=8YnNG5TR2QrxJK4CPHAknP8I4ujYG2voPwedWyH1EJXVSQUKDS5IaHvbMd3S7kpL7GrCaZkQbvgbh5OoX9IZ777abi2e3Ze-Cu1DCoLk9cexKOng3oXRfgt60fOU2gNF&new=1
6、https://blog.csdn.net/mary19920410/article/details/58030147
7、https://blog.csdn.net/Mary19920410/article/details/72857764