【TCP 协议的相关特性】

报文格式

  • TCP的基本特点:有连接可靠传输面向字节流全双工
  • 除了数据(载荷)外,上面的部分都是报头
  • 这里先快速过一遍当前报头里能看懂的东西⬇
  • 16位源端口号16位目的端口号:和UDP一样,作为一个传输层协议,就需要保存好当前端口号的信息
  • 4位报头长度:不同于UDP报头固定是8个字节,TCP的报头长度是可变长的。选项本部分往上固定长度选项这一块就是可变长的、选项部分可以有,也可以没有。除去选项的报头部分固定20字节

四个bit位表示0-15,但是这里设定的单位是4字节,而不是字节。要在15的基础上再*4、共计60字节

  • 保留(6位)保留位,在UDP协议里,长度受到2个字节的限制,无法进行扩展。但是在TCP里就不一样的,提前准备了几个保留位,后续如果需要用到了再把保留位利用起来(进行扩展)
  • 16位校验和:和UDP的校验和类似,把报头和数据载荷放到一起计算校验和
  • TCP内部有很多机制,上述报头字段都是针对TCP各个机制的支撑属性。想要了解这些报头的含义,就需要了解TCP的其他机制。
  • 6位标志位:上图就是6位标志位,是TCP中非常核心的部分,后面说到相关机制的时候会具体说明

确认应答

  • 可靠传输是TCP "安身立命的本钱",其中用来确保可靠性、最核心的机制就是确认应答
  • 确认应答是什么?
  • 举个例子:
  • 我想和兄弟出去玩,然后我给他发消息
  • 这就是一个简单的确认应答,如果再复杂一点
  • 但是上述的时序过于理想了,实际上在网络传输的过程中、经常会出现后发先至的情况
  • 如果出现了后发先至的情况,这里说话的含义就有了一些问题

为什么网络中会出现"后发先至"的情况,因为一般来说、两个通讯的主机之间不是直连的,而是通过了很多的交换机/路由器这样的网络设备进行转发,这样一来数据包可能就会有很多的路线进行选择,而路线的长度有长有短,如果第一条消息经过的路线不流畅,但是第二条消息经过的路线很流畅就会出现"后发先至"的情况

  • 所以为了解决上述的问题,引入了序号和确认序号。原理很简单、就是对数据进行编号。应答报文里就告诉发送方说,这次应答的是哪个数据
  • 上述是简化的模型,真实的TCP内部的情况要更加的复杂、TCP是面向字节流的,没有这种"一条两条"的概念,TCP的序号和确认序号都是以字节来进行编号的

规则

  • 序号:假设现在有一个要发送的数据包,载荷1000个字节。报头里面的序号只能存一个,所以不可能把每个字节的序号都单独存入报头。又因为这1000个字节明显是连续的,所以序号也一样是连续的,只需要在报头中保存第一个字节的序号

  • 确认序号:应答报文中的确认序号,是按照发送过去的最后一个字节的序号再加1、来进行设定的。假设有两个主机A和B,A发送了1000字节的数据,B收到1到1000字节的数据之后反馈了一个应答报文,应答报文中的确认序号的值就是1001。

  • ⚠️此处确认序号1001意义可以这样理解:1001之前的数据主机B都收到了,接下来A发送给B的数据就应该从1001开始

  • 确认应答中,通过应答报文来反馈给发送方,当前的数据正确收到了。应答报文也叫做ACK(acknowledge)报文

  • 平时这一位是0,如果当前报文是一个应答报文、此时报头中的这一位就为1


超时重传

  • 确认应答补充
  • 如果确认应答一切顺利,通过应答报文就可以告诉发送方,当前数据是否成功收到了
  • 但是网络上可能存在丢包的情况,如果数据丢包了,没到到达对方的主机,对方自然就没有ACK报文了。这个情况下就需要重传

为什么会丢包?我们都知道了,网络中传输需要经过很多的交换机/路由器,但是经过的交换机/路由器不可能只给一台主机一次通讯提供服务,如果在某个时刻提供服务过多,机器出现了负载,超出能力范围之外的数据包就会被丢弃

  • TCP的可靠性就是在对抗丢包,期望在丢包的情况下,也尽量把丢包的数据再传过去
  • 当发送方发了个数据之后,接收方需要等待,接收方等待的时间有阈值(上限),如果超过就判断为超时了、数据丢包了,就会把刚才的数据包再传输一次
  • 上面的过程中,是认为没收到ACK就是丢包,其实这样的结论还有点小问题
  • 丢包 不一定是发的数据丢了,也有可能是ACK丢了
  • 对于第一种情况,接收方本身就没有收到数据,此时重传没有任何问题
  • 但是第二种情况,数据已经被B收到了,如果此时再重新传输一次,B就会收到两次
  • TCP对于第二种情况的解决方案
  • TCP socket在内核中存在接收缓冲区,发送方发来的数据,是要先放到接收缓冲区中,然后应用程序调用api才能读到数据,这里的读操作就是读接收缓冲区里的内容
  • 当数据到达接收缓冲区的时候,接收方首先会判定一下,接收缓冲区里是不是已经有这个数据了(或者已经存在过),如果有就不会再次接收

关于防止误判:前面说过序号和确认序号都是按照从小到大的顺序排好的,所以一定是先读序号小的数据再读序号大的数据。比如上次读的最后一个字节的序号是3000,新收到一个数据包的序号是1001,此时这个1001一定是之前已经读过的,就可以这个新的数据包判断为"重复的包"

  • ⚠️超时重传的次数也是有上上限的,不能无限的重传。重传的过程也是一定的策略的
  • 如果重传到一定程度,还没有ACK,就尝试重新连接,如果重置也失败,就直接放弃连接
  • 重传的超时时间阈值也不是固定不变的,随着重传次数的增加而增大,也就是重传的频率越来越低

连接管理

建立连接

  • TCP是有连接的,两台主机之间建立连接的过程都是在操作系统内核中完成的
  • 内核建立连接的过程、就称为三次握手,此处提到的连接都是虚拟抽象的连接,目的是让通讯双方都能保存对方的相关信息

三次握手

  • 发起三次握手客户端主动的一方,第一次交互一定是客户端主动发起的
  • 首先客户端会主动给服务器发送一个SYN(同步报文段)
  • SYN是一个特殊的TCP数据包、没有载荷不携带应用层数据、六个标志位中的第五位(SYN)数值为:1。
  • 虽然SYN不带有应用层载荷,但是会带有IP报头/以太网数据帧帧头,也有TCP报头,还包含客户端自己的端口和IP地址。这些的目的都是为了告诉服务器:"我是xxx,我的IP是xxx,和我建立连接"
  • 然后服务器就会返回一个ACK应答报文,表示:"我收到了"
  • 再返回一个SYN,表示:"我同意你的连接"
  • 然后客户端再回应一个ACK,表示:"好的,我知道我们连接了"
  • 所谓的建立连接的过程,本质上就是通讯双方各自给对方发起一个SYN,然后再各自给对方回应一个ACK
  • 这些环节结束之后,服务器就会保存客户端的信息
  • 上述过程中,虽然画了四次交互,但是实际上中间的两次是可以合并的
  • 因为SYN是六个标志位的第五个,ACK是六个标志位的第二位,两个位置并不冲突,所以完全可以用一个数据包来起到两个作用
  • 了解了三次握手怎么建立连接之后再来思考一下为什么要握手?有什么实际意义呢?
  • 1、三次握手可以针对通讯线路,进行投石问路,初步的确认一下通讯线路是否流程
  • 2、可以验证通讯双方的发送和接收能力是否正常,就像打游戏时好友之间要验麦一样
  • 3、握手过程中会协商一些必要的参数,通讯是两方共同的事情,两边要互相配合。TCP中也有很多参数要进行协商的,往往是以选项部分来体现的

断开连接

  • 连接本质上就是让通讯双方保存对方的信息,这些信息一旦太多了,就需要用到数据结构来储存。所以断开连接的本质就是把对方的数据从自己这边的数据结构里删除掉,双方共同断开连接的过程我们就称为四次挥手

四次挥手

  • 此处的四次挥手,其实就类似谈恋爱里的和平分手双方都认可断开。但是对于单方面断开的情况,四次挥手不一定适用,也就是说不一定断开连接就是四次挥手
  • 四次挥手的第一步就是有一端发起结束报文段FIN(六位标志位的第六位)、不一定非得是客户端先发FIN(三次握手一定是客户端先发起),也有可能是服务器先发FIN,这里以客户端先发为例
  • 和三次握手差不多一样的流程,双方各自发给对方FIN,然后再各自返回ACK
  • 四次挥手,并不能像三次握手一样,百分之百的把中间两次交互、合二为一,有的时候能合并,有的时候不能合并
  • 在四次挥手的过程中,双方发起的FIN是在应用程序(socket)触发的,ACK是内核发送的

关于应用层怎么调用并关闭:在Java中有一个叫套接字Socket的东西,这个代码里面的Socket就负责管理网卡层面的网络、包括前面的建立连接的三次挥手,这里的四次挥手

  • 如果在实际通讯过程中,第一个ACK和第二个FIN时间间隔比较长,此时就无法进行合并,于是就分成两次来传输
  • ⚠️如果时间间隔很小,是有可能合并在一起的,后面会提到TCP的延迟应答捎带应答机制

TCP状态转换

  • TCP状态线程状态是类似的概念,状态都是描述某个实体现在正在干什么
  • LISTEN:该状态表示服务器这边、创建好了serverSocket,并且完成了端口的绑定,等待有客户端连接
  • ESTABLISHED:该状态表示客户端和服务器的连接已经建立完毕(完成三次握手)
  • CLOSE_WAIT:该状态表示接下来代码中需要调用close(socket.close())来主动发起FIN,当此端收到对端的FIN之后就会进入这个状态,该状态属于一种阻塞状态(在四次挥手中会出现)
  • TIME_WAIT:该状态表示本端给对方发起FIN之后,对端返回FIN了。TIME_WAIT存在的意义主要是防止最后一个ACK丢包。当客户端主动发起四次挥手断开连接,客户端发送了一个FIN,服务器返回了一个ACK和一个FIN,此时如果服务器没有收到最后一个ACK,就会重传FIN,当到达设置的限制时间,服务器还没有重传FIN,就会判断为客户端发的ACK已经到达服务器
  • 谁主动断开连接谁进入TIME_WAIT,谁被动断开连接谁进入CLOSE_WAIT
  • 正常情况下,CLOSE_WAIT不太容易观察到,代码中会比较快速的执行到socket.close()关闭socket操作,这个时候CLOSE_WAIT就变成了LAST_ACK
  • 如果发现,服务器或者客户端上出现大量的CLOSE_WAIT意味着很有可能是代码出现了问题(无法执行到close关闭)
  • 这里我们写一个TCP回响服务器和客户端
java 复制代码
package TcpEcho;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TcpEchoServer {
    private ServerSocket socket = null;

    public TcpEchoServer(int port) throws IOException {
        socket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("Server is starting");
        ExecutorService pool = Executors.newCachedThreadPool();//线程池
        while (true) {
            Socket clientSocket = socket.accept();//阻塞等待client的连接,如果client链接之后,返回client的sock->clientSocket
            //就像一个负责拉客
            //一群人负责处理客人
            pool.submit(new Runnable() {//充分利用,避免频繁的创建销毁线程
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            });

        }
    }

    private void processConnection(Socket clientSocket) throws IOException {

        System.out.printf("[%s:%d] Client is online now!\n",clientSocket.getInetAddress(),clientSocket.getPort());

        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()) {

            while (true) {
                Scanner scanner = new Scanner(inputStream);//clientSocket的inputStream就是client上的请求request,这里用clientSocket的InputStream来获取
                if(!scanner.hasNext()) {
                    System.out.printf("[%s:%d] Client is offline now!",clientSocket.getInetAddress(),clientSocket.getPort());
                    break;
                }
                String request = scanner.next();//这里scanner等效于inputStream,使用空白(换行)作为分割符号,读取到client一方输入的请求

                String response = process(request);//业务处理

                PrintWriter writer = new PrintWriter(outputStream);//clientSocket上的outputStream需要PrintWriter来承载,和前面的Scanner类似

                writer.println(response);;//writer就代表了clientSocket的outputStream,意思就是通过outputStream把处理之后的响应response写回到clientSocket上,让client方收到
                writer.flush();//处理内存缓冲区,避免出现卡住的现象
                System.out.printf("Log:[%s:%d],request:%s,response:%s\n",clientSocket.getInetAddress(),
                        clientSocket.getPort(),request,response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            clientSocket.close();
        }
    }

    public String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(4096);
        server.start();
    }
}
java 复制代码
package TcpEcho;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

public class TcpEchoClient {
    private Socket socket = null;

    public TcpEchoClient(String serverIp,int serverPort) throws IOException {
        socket = new Socket(serverIp,serverPort);
    }

    public void start() {
        System.out.println("client is starting");
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()){
            Scanner scannerConsole = new Scanner(System.in);//读取Console上的数据,也就是读取client上的输入请求

            PrintWriter writer = new PrintWriter(outputStream);//通过PrintWriter来承载socket上的outputStream,用来向server发送(写)数据(request)

            Scanner scannerNet = new Scanner(inputStream);//以scanner为载体,用inputStream来接收server一方处理之后的数据
            while (true) {
                System.out.println("-->");
                if(!scannerConsole.hasNext()) {
                    break;
                }
                String request = scannerConsole.next();//读client一方的输入request
                writer.println(request);//向server一方写入request
                writer.flush();//处理内存缓冲区

                String response = scannerNet.next();//通过inputStream读取server处理过的request-》response


                System.out.println(response);
            }

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient("127.0.0.1",4096);
        client.start();
    }
}
  • 先启动服务器,再启动客户端
  • 再去Windows命令行里输入netstat -ano | findstr 4096(此处的4096为端口号,这里我给的是4096)
  • 这样就可以观察到当前TCP的状态了,在Windows系统里不好观察到TIME_WAIT和CLOSE_WAIT这两个状态

滑动窗口

  • 确认应答的机制下,每次发送方收到一个ACK之后才会发下一个数据,这就导致了需要花费大量的时间成本。所以我们希望在尽可能保证可靠传输的前提下,能尽量让效率高一点,时间成本小一点
  • 滑动窗口的提出就是为了解决这个问题,滑动窗口可以在保证可靠性的基础上,提高效率(这里的提高效率不是增加速度,是降低损失)
  • 引入滑动窗口之前,我们每发一个数据,都要等待ACK,才能再发下一条
  • 引入了滑动窗口之后 ,我们发一个数据,不再等待ACK,直接继续再发一个,连续发了一定量的数据之后,统一等一波ACK,一并处理。这样一来,原本发一条等一次,现在发n条等一次,就减少了总的等待时间
  • 现在可以知道为什么称作滑动窗口
  • 图中红色的框我们就形象的称为窗口,把不等待能够批量传输的数据量称为窗口大小,在这个窗口内的数据都会被批量发送到对端服务器,然后只要一收到ACK,窗口就往后面"滑动"
  • ⚠️但是请注意,这里我画的两个图只是理想化的,因为批量发了三个数据,就会对应三个ACK,此时三个ACK也不一定是同时到达的,而是有先有后。所以实际上不会像上面两个图一样,完全等到所有ACK回来,再批量发三组数据。而是等一个ACK回来就往后发一组

滑动窗口出现丢包

  • 滑动窗口出现丢包、分为两种情况(这里先把窗口的大小限制到1批(1000字节)、方便演示):
  • 1、接收方数据收到了,返回的ACK丢了:
  • 像这种情况,不用做任何处理,对于可靠性没有任何影响,不需要重传
  • 我们回忆一下ACK报文中确认序号含义,图中第一条的1001序号意思就是1001之前的数据都收到了,后续即使2001丢了,但是2001后的3001没丢,3001就代表3001之前的数据都收到了
  • 举个例子:这就好比在外面搭讪一个陌生人一样,但是这个陌生人说"我都有孩子了",这里的这句话意思就是TA都结婚了,也就包括了谈恋爱这步
  • 2、发送方发出的数据丢了:
  • 当接收方收到的数据跳过了某个片段,就重复返回三遍缺失之前的确认序号,然后发送方收到确认序号之后判断为丢包、进行重传
  • 解决这个问题的关键要点还是上个例子说的,此处三次返回的应答报文(确认序号)是1001,而不是3001,因为确认序号就代表了这个序号之前的数据已经收到了,3001意思就是3001之前的所有数据都收到了,但是这里显然不是,1001-2000的数据丢了
  • 在上述重传的过程中,整体的效率是很高的,这里的重传具有针对性,丢了哪个就重传哪个,已经收到的数据绝对不需要重新再发送,整体的效率没有额外的损失,这种重传叫做:快速重传

确认应答超时重传滑动窗口快速重传,这些概念并不冲突,而且是同时存在的。滑动窗口中也有确认应答,区别就在于把等待的策略,变成了批量的。如果当前数据量很少,窗口甚至无法填满,就会退化为确认应答

如果当前传输的过程是滑动窗口,那就按照快速重传来保证可靠性,判断标准就是:是否存多个ACK索要同一个数据

如果当前传输的过程不是滑动窗口,就是按照之前的超时重传来保证可靠性,判断标准就是,是否到达超时时间还没ACK到达


流量控制

  • 通过滑动窗口可以提高传输效率,但是也衍生出一个问题:窗口大小应该为多少、才能最有效率?
  • TCP的原则就是在保证可靠性的前提下,任何提升效率的行为都不应该影响到可靠性
  • 当接收方的接收缓冲区 满了的时候,就会产生丢包。所以就和生产者消费者模型一样,要让缓冲区处于一个临界状态,让发送方的发送速度和接收方的处理速度,能够步调一致
  • 流量控制就是让接收方反过来影响发送方的速度
  • 通过报头中的这个字段来给发送方反馈发送速度,这个字段在普通报文中无意义,在ACK报文中才有意义。通过这个大小反馈给发送方、接下来发送的窗口大小应该设置为多少才合适
  • 此处的算法其实也简单,接收方就会按照自己接收缓冲区剩余空间的大小,作为ack中的窗口大小的数值,下一步发送方就会根据这个数值来调整自己的窗口大小,当接收方的接收缓冲区满了,发送方就应该停止发送,然后发送方再周期性地发送窗口探测包(探测包并不带有载荷)来查看窗口大小判断是否恢复发送

注意这里的16位并不意味着窗口的大小就64K,TCP报头的选项中,还包含了一个参数,叫做窗口扩展因子,实际上真实要设置的窗口大小,是16位窗口大小*2^窗口扩展因子

拥塞控制

  • 拥塞控制限制发送方发送数据的速率,区别是流量控制是站在接收方的角度来制约发送方速率的

  • 在网络传输的过程中,两个主机之间往往是通过很多中间设备来进行连接的,如果当前接收方处理速度很快,但是中间的通讯路径出现了问题,比如设备超负载了,这样一来就算发送的速度再快也没有用。就和木桶效应一样、一个木桶能装多少水,取决于桶上最短的木板

  • 拥塞控制就是这个道理,如果按照某个窗口大小发送了数据之后,出现了丢包,就视为中间路径存在拥堵,然后就减小窗口大小。如果没出现丢包,就视为中间路径不存在拥堵,就增大窗口大小

  • 由于网络环境复杂的,拥塞控制这样的方案就很好的适应了当前环境。我们不能预测中间设备什么时候拥堵什么时候不拥堵,这是随机的,所以按照上述策略,就可以让发送速率动态调整

  • 拥塞控制和流量控制谁说的算呢?

  • 总的原则是,流量控制和拥塞控制,谁产生的窗口大小更小,谁就说得算

  • 拥塞控制试窗口的具体流程:

  • 1、先是要慢启动,刚开始传输的数据,速率是比较小的,采用的窗口大小也比就较小。此时、网络的拥堵情况是未知的

  • 2、如果上述传输的数据,没有出现丢包,说明网络还是流畅的,就要增大窗口大小(指数增长)

  • 3、指数增长不会一直保持,当窗口大小达到了阈值之后,就会变成线性增长(新版

  • 4、线性增长也不是一直保持的,当积累了一段时间之后,传输的速率可能太快了,此时还是会引起丢包的,一旦出现丢包,就会把窗口大小重置回较小的值。回到最初的慢启动过程(又要重新指数增长),并且会根据丢包前到达的顶峰大小,重新设置线性增长的阈值

  • 值得注意的是这张图是教材上的经典版,该版本在到达顶峰的阈值之后,会先回到慢启动的状态,然后再指数增长,再线性增长。但在新版已经不会按照中图中画红圈的部分执行了


延时应答

  • 延时应答也是基于滑动窗口,但是要尽可能的再提高点效率
  • 结合滑动窗口以及流量控制,能够通过延时应答ACK的方式,把反馈的窗口大小放大一点
  • 当接收方收到数据之后,不会立即返回ACK,而是稍等一下再返回ACK,等的这一会,相当于给接收方的应用程序、腾出更多时间,来消费这里的数据
  • 按照滑动窗口的方式来传输数据,如前面说的,在滑动窗口的条件下·,如果ACK丢了,没有任何影响,此时延时应答可以按照ACK丢了来处理数据。正常情况下每个数据都有ACK,此时就可以每隔几个数据再返回一个ACK。但是这个过程不仅仅是何数据的数量有关,还和时间有关,如果延迟的时间到达了一定程度,即使数据个数还没够,也会返回ACK

捎带应答

  • 基于延时应答引入的机制,也能够提升传输效率
  • 捎带应答就是尽可能的把能合并的数据包进行合并,从而起到提高效率的效果
  • 这里拿服务器客户端通讯中最简单的一问一答模式来举例
  • 这里很像四次挥手区别在于,这里通讯的数据、请求(request)、响应(response)都是带有载荷的
  • 正常情况下,第一个ACK和response之间是有一定时间间隔的,此时就要分两个包来发送,但是有了延时应答,ACK的应答时间可能会往后推迟,ACK延时的这段时间里,响应数据刚好准备好了,此时就可以把ACK和应答的响应数据合并成一个TCP数据报
  • 如果服务器和客户端之间是长连接,要进行若干次请求(收到响应之后、后续还要继续发送请求),此时第二个ACK就可以和下一个请求合并在一起
  • 在捎带应答的加持下,后续每次传输的请求和响应,都有可能触发捎带应答,都可能把接下来要传输的数据和上次的ACK合二为一

面向字节流

  • 此处重点讨论面向字节流中的一个非常重要的问题:粘包问题(TCP载荷中的应用数据包)
  • 粘包问题的本质就是、没有消息边界
  • 举个例子:发送方向接收方发了以下数据"aaa"、"bbb"、"ccc",当接收方的接收缓冲区去读取的时候有可能读出"a"、"aa"、"aaab"等这样的结果,这里的问题就是:究竟哪种读法,读出来的才是完整的应用层数据包呢?解决问题的关键、就是"明确包之间的边界",方法也很简单,两种方案都可行
  • 1、通过特殊符号,作为分隔符,遇到分隔符,就视为一个包结束了。可以使用任意字符作为分隔符,但是要确保这个符号不会在正式的数据中存在
  • 2、指定出包的长度,比如在包开始的位置上,加上一个特殊的位置来表示整个数据的长度

异常情况

  • 考虑丢包更严重的情况,或是网络出故障的情况:
  • 1、其中有一方出现了进程崩溃:进程无论是正常结束,还是异常崩溃,都会触发回收文件资源、关闭文件的效果,会触发四次挥手。TCP连接的生命周期,可以比进程更长一些,虽然进程已经退出了,但是TCP连接还在,仍然可以继续进行四次挥手
  • 2、其中有一方出现了关机(正常的):当有一个主机正常流程地触发关机,就会先强制终止所有进程,终止进程自然就会触发四次挥手,但是此时四次挥手不一定能挥玩,因为系统马上就要关闭了,如果挥得快,能够顺利挥完,此时,对端和本端都能正确的删除掉保存的连接信息。如果挥得不快,至少能把第一个FIN发给对端,至少告诉对端,本端要结束了,对端收到FIN之后就会进行前面提到的一些列判断,然后再释放连接信息
  • 3、其中一方出现了断电(突然性的):如果直接断电,机器瞬间关机,此时肯定来不及发送FIN。如果断电的是接收方,发送方就会突然发现,没有ACK了,就要重传,重传几次之后,还是不行,TCP就会尝试复位连接,相当于清除原来的TCP中的各种临时数据,重新开始。此时就需要用到复位报文段 RST(reset),重置了还不行,就单方面放弃连接

补充剩下两个标志位

URG:和TCP带外数据有关,TCP中有些特殊的数据包,携带一些特殊功能的数据

PSH:催促对方快点发消息

  • 如果断电的是发送方,这个情况下,接收方需要区分出,发送方是挂了,还是暂时没发。如果接收方在一段时间之后,没有收到对方的消息,就会触发心跳包来询问对方的情况(此处的心跳包也是不携带应用层数据的特殊数据包),对端如果没心跳了,此时本端也会尝试复位并且单方面释放连接
  • 4、网线断开:这个情况本质上就是情况3中两边都断电的情况结合在一起了
相关推荐
JavaLearnerZGQ1 小时前
Spring Boot 流式响应接口核心组件解析
java·spring boot·后端
山岚的运维笔记1 小时前
SQL Server笔记 -- 第80章:分页
java·数据库·笔记·sql·microsoft·sqlserver
wenzhangli72 小时前
OoderAgent AI 能力分发与自动化协作框架白皮书(V0.7.3 )
网络·去中心化·p2p
REDcker2 小时前
RTP、RTCP 与 SRTP 协议详解
网络·音视频·webrtc·实时音视频·rtp·rtcp
开开心心就好2 小时前
文字转语音无字数限,对接微软接口比付费爽
java·linux·开发语言·人工智能·pdf·语音识别
海兰2 小时前
Elasticsearch 9.x Java 异步客户端
java·elasticsearch·jenkins
马猴烧酒.2 小时前
【JAVA算法|hot100】哈希类型题目详解笔记
java·笔记
毕设源码-邱学长2 小时前
【开题答辩全过程】以 果蔬销售管理系统为例,包含答辩的问题和答案
java
wangjialelele2 小时前
万字整理计算机网络知识点
linux·c语言·网络·c++·计算机网络·php