13.TCP-bite

TCP:有连接,可靠传输,面向字节流,全双工

有连接:得先建立连接再交换数据
可靠传输:发送方知道接收方有没有收到数据(不是对方100%能收到,不是安全传输)
面向字节流:以字节为单位进行传输. (非常类似于文件操作中的字节流)
全双工:一条链路,双向通信
半双工:一条链路,单向通信

TCP版本的Socket

两个核心类:
ServerSocket(专门给TCP服务器用的)

Socket(既给服务器用也给客户端用)

通信的两端都要有Socket(也可以叫“套接字”),是两台机器间通信的端点。网络通信其实就是Socket间的通信。Socket可以分为:
TCP:
流套接字(stream socket):使用TCP提供可依赖的字节流服务
服务器端: ServerSocket:此类实现TCP服务器套接字。服务器套接字等待请求通过网络传入。
客户端: Socket:此类实现客户端套接字(也可以就叫“套接字”)。套接字是两台机器间通信的端点。
UDP:
数据报套接字(datagram socket):使用UDP提供“尽力而为”的数据报服务
DatagramSocket:此类表示用来发送和接收UDP数据报包的套接字。

TCP反转字符串服务

服务器端:

public static void main(String[] args) throws IOException {
        //开启一个TCP协议的服务,监听端口号为9999(监听客户端)
        ServerSocket serverSocket = new ServerSocket(9999);
        //与客户端建立连接,阻塞式方法,没有客户端连接时会一直等待.
        while(true){
        	//每一个线程对应一个连接
            Socket accept = serverSocket.accept();
            new Thread(()->{
                func(accept);
            }).start();
        }

    }

    public static void func(Socket accept){
        try {
            /*------------------------------------------------*/
            //读取客户端发送来的数据,获取客户端发送到服务器的流
            InputStream acceptInputStream = accept.getInputStream();
            int len = 0;
            byte[] bytes = new byte[1024];
            StringBuilder stringBuilder = new StringBuilder();
            while ((len = acceptInputStream.read(bytes)) != -1 ){
                stringBuilder.append(new String(bytes,0,len));
            }
            System.out.println("客户端请求" + stringBuilder);
            /*------------------------------------------------------------------------------*/
            //获取能传输数据到客户端的流
            OutputStream acceptOutputStream = accept.getOutputStream();
            //编写客户端响应,将字符串反转后送回
            StringBuilder response = stringBuilder.reverse();
            //发送数据
            acceptOutputStream.write(response.toString().getBytes(StandardCharsets.UTF_8));
            accept.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

客户端:

public static void main(String[] args) throws IOException {
        System.out.println(InetAddress.getLocalHost());
        Socket clientSocket = new Socket(InetAddress.getLocalHost().getHostAddress(),9999);
        
        //发送数据
        OutputStream socketOutputStream = clientSocket.getOutputStream();
        socketOutputStream.write("lalala".getBytes());
        //会在流末尾写入一个“流的末尾”标记,对方才能读到-1,否则对方的读取方法会一致阻塞,
        clientSocket.shutdownOutput();

        //接收数据
        InputStream socketInputStream = clientSocket.getInputStream();
        int len = 0;
        byte[] bytes = new byte[1024];
        StringBuilder stringBuilder = new StringBuilder();
        while ((len = socketInputStream.read(bytes)) != -1 ){
            stringBuilder.append(new String(bytes,0,len));
        }
        System.out.println("服务器响应" + stringBuilder);
        clientSocket.close();
    }

TCP版的回显服务

服务器端:

public class TcpThreadEchoServer {
    // 但是在 Java socket 中是体现不出来 "监听" 的含义的~~
    // 之所以这么叫, 其实是 操作系统原生的 API 里有一个操作叫做 listen
    // private ServerSocket listenSocket = null;
    private ServerSocket serverSocket = null;
	//构造方法指定的端口,也是表示自己绑定哪个端口,对于TCP的Socket来说
    public TcpThreadEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            // 由于 TCP 是有连接的, 不能一上来就读数据, 而要先建立连接. (接电话)
            // accept 就是在 "接电话", 接电话的前提是, 有人给你打了~~, 如果当前没有客户端尝试建立连接, 此处的 accept 就会阻塞.
            // accept 返回了 一个 socket 对象, 称为 clientSocket. 后续和客户端之间的沟通, 都是通过 clientSocket 来完成的.
            // 进一步讲, serverSocket 就干了一件事, 接电话~~
            Socket clientSocket = serverSocket.accept();
            // [改进方法] 在这个地方, 每次 accept 成功, 都创建一个新的线程, 由新线程负责执行这个 processConnection 方法~
            Thread t = new Thread(() -> {
                processConnection(clientSocket);
            });
            t.start();
        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端建立连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
        // 接下来来处理请求和响应
        // 这里的针对 TCP socket 的读写就和文件读写是一模一样的!!
        try (InputStream inputStream = clientSocket.getInputStream()) {
            try (OutputStream outputStream = clientSocket.getOutputStream()) {
                // 循环的处理每个请求, 分别返回响应
                Scanner scanner = new Scanner(inputStream);
                while (true) {
                    // 1. 读取请求
                    if (!scanner.hasNext()) {
                        System.out.printf("[%s:%d] 客户端断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                        break;
                    }
                    // 此处用 Scanner 更方便. 如果不用 Scanner 就用原生的 InputStream 的 read 也是可以的
                    String request = scanner.next();
                    // 2. 根据请求, 计算响应
                    String response = process(request);
                    // 3. 把这个响应返回给客户端
                    // 为了方便起见, 可以使用 PrintWriter 把 OutputStream 包裹一下
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(response);
                    // 刷新缓冲区, 如果没有这个刷新, 可能客户端就不能第一时间看到响应结果.
                    printWriter.flush();

                    System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress().toString(),
                            clientSocket.getPort(), request, response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 此处要记得来个关闭操作.
            //这个是每个连接有一个的,数目很多.连接断开,也就不再需要了,每次都得保证处理完的连接都给进行释放.
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

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

    public static void main(String[] args) throws IOException {
        TcpThreadEchoServer server = new TcpThreadEchoServer(9090);
        server.start();
    }
}

客户端:
当上面的代码,第一次accept结束之后,就会进入processConnection在processConnection 又会有一个循环,如果processConnection里面的循环不结束,processConnection就无法执行完成如果无法执行完成,就导致外层循环无法进入下一轮,也就无法第二次调用accept,也就不能接收第二个客户端的连接了.

当客户端new Socket成功的时候,其实在操作系统内核层面,已经建立好连接了.(TCP三次握手)但是应用程序没有接通这个连接.

即:您所拨打的用户正在通话中,请稍后再拨.sorry…

解决:
多线程:每个客户端连接上来后分配一个线程

public class TcpEchoClient {
    // 用普通的 socket 即可, 不用 ServerSocket 了
    // 此处也不用手动给客户端指定端口号, 让系统自由分配.
    private Socket socket = null;

    public TcpEchoClient(String serverIP, int serverPort) throws IOException {
        // 其实这里是可以给的. 但是这里给了之后, 含义是不同的. ~~
        // 这里传入的 ip 和 端口号 的含义表示的不是自己绑定, 而是表示和这个 ip 端口建立连接!!
        // 调用这个构造方法, 就会和服务器建立连接 (打电话拨号了)
        socket = new Socket(serverIP, serverPort);
    }

    public void start() {
        System.out.println("和服务器连接成功!");
        Scanner scanner = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream()) {
            try (OutputStream outputStream = socket.getOutputStream()) {
                while (true) {
                    // 要做的事情, 仍然是四个步骤
                    // 1. 从控制台读取字符串
                    System.out.print("-> ");
                    String request = scanner.next();
                    // 2. 根据读取的字符串, 构造请求, 把请求发给服务器
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(request);
                    printWriter.flush(); // 如果不刷新, 可能服务器无法及时看到数据.
                    // 3. 从服务器读取响应, 并解析
                    Scanner respScanner = new Scanner(inputStream);
                    String response = respScanner.next();
                    // 4. 把结果显示到控制台上.
                    System.out.printf("req: %s, resp: %s\n", request, response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
        client.start();
    }
}

传输层的TCP协议

在这里插入图片描述

16位源端口号:操作系统给客户端自动分配的端口

16位目的端口号:服务器的端口

32位序号:TCP的针对消息的序号,还有说法,并不是按"消息条数"来进行编号的.而是按照字节来编号

32位确认序号:确认序号表示当前这个应答报文是针对哪个消息进行的确认应答

4位首部长度:4个bit位,0- 15,表示TCP报头的长度,TCP的报头是变长的,不像UDP就是固定8字,此处的单位是4字节,例如, 1111 OxF,此时意思是报头长度就是15* 4 => 60

保留位(6位):保留位的意思,就是现在还不用,但是保不齐后面要用(为了未来的升级,留点空间)

6位标志位:
URG:紧急指针是否有效
ACK:确认号是否有效
PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走
RST:对方要求重新建立连接;我们把携带RST标识的称为复位报文段
SYN:请求建立连接;我们把携带SYN标识的称为同步报文段
FIN:通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段

SYN - 创建一个连接

FIN - 终结一个连接

ACK - 确认接收到的数据

16位窗口大小:一次批量发送数据的多少

16位校验和:发送端填充,CRC校验。接收端校验不通过,则认为数据有问题。此处的检验和不
光包含TCP首部,也包含TCP数据部分。

16位紧急指针:标识哪部分数据是紧急数据

选项:实际上,TCP首部40字节选项中还包含了一个窗口扩大因子M,实际窗口大小是窗口字段的值左移M位

TCP中保证可靠传输的一些机制:1.确认应答

关键就是接收方收到消息之后,给发送方,返回一个应答报文(ACK, acknowledge),表示自己已经收到了
32位序号:TCP的针对消息的序号,还有说法,并不是按"消息条数"来进行编号的.而是按照字节来编号
32位确认序号:确认序号表示当前这个应答报文是针对哪个消息进行的确认应答

2.超时重传

发送方没有收到接收方返回的ACK:
1.接收方没收到
2.接收方收到了,但发送方没有收到ACK
对于发送方来说,我无法区分是哪种原因导致的没有收到ACK,我就直接往坏了想,我就认为是对方压根没收到,
重新再发一次(触发超时重传)(不是立即就发),需要等待一段时间.
但是这样可能会导致接收方收到重复的消息,对消息进行重复操作

TCP内部就会有一个去重操作,接收方收到的数据会先放到操作系统内核的"接收缓冲区”中.接收缓冲区可以视为是一个内存空间,并且也可以视为是一个阻塞队列,收到新的数据,TCP就会根据序号,来检查看这个数据是不是在接收缓冲区中已经存在了.如果不存在,就放进去,如果存在,直接丢弃.保证应用程序调用socket api拿到的这个数据一定是不重复的!!!

应用程序感知不到超时重传的过程的!!!

超时重传不会一直进行,每次重传的时间间隔也不一样

3.连接管理:三次握手,四次挥手

三次握手:
客户端是主动发起连接请求的一方,客户端先发送一个SYN同步报文段给服务器,服务器将ACK应答报文和SYN同步报文一次封装后发给客户端.客户端再返回一个ACK应答报文.

发送的ACK和SYN都是操作系统内核负责进行的

在这里插入图片描述

三次握手的作用:投石问路,检查一下当前这个网络的情况是否满足可靠传输的基本条件
三次握手其实也是在检测通信双方,发送能力和接收能力是否都正常:
即确认对方和己方是否有接收和发送信息的能力

具体确认过程:
当客户端发送syn,服务器接收到syn,此时服务器可以确认:客户端具有发送能力,服务器具有接收能力.

当服务器接发syn+ack,客户端接收到syn+ack,此时客户端通过服务器发来的ack可以确定:客户端具有发送能力,服务器具有接收能力,又通过服务器发送的syn可以确定,服务器具有发送能力,客户端具有接收能力.此时客户端已经确定双方都具有发送和接收能力

当客户端发送ack,服务器接收到ack,此时,服务器可以确定客户端具有接收能力,服务器具有发送能力.这时,服务器可以确定双方都具有发送和接收能力.

两个状态:
LISTEN:表示服务器启动成功,端口绑定成功,随时可以有客户端来建立连接.(手机开机,信号良好,随时可以有人给我打电话)
ESTABLISHED:表示客户端已经连接成功,随时可以进行通信了.(有人给我打电话,我接听了,接下来就可以说话了)

三次握手,就让客户端和服务器之间建立好了连接,其实建立好连接之后,操作系统内核中,就需要使用一定的数据结构来保存连接相关的信息!!保存的信息其实最重要就是前面说的"五元组"(源IP,源端口,目的IP,目的端口,TCP),这些信息是需要占用系统资源的,当断开连接时,需要释放资源,销毁连接信息.

四次挥手:
双方各自向对方发送了FIN(结束报文段)请求,并且各自给对方一个ACK确认报文
四次挥手,可能是客户端主动发起,也可能是服务器主动发起.

在这里插入图片描述

具体细节:
当B收到A发送的FIN后,B的操作系统内核会立即返回ACK,B给A发送的FIN是用户代码负责的,只有用户代码中调用了socket.close()方法后才会触发FIN,这取决于用户代码咋写的.如果B发送ACK和FIN的时间差比较大,就不能合并.

表面上看起来,A发送完ACK之后,就没有A的啥事了,按理说,A就应该销毁连接,释放资源了但是并没有直接释放,而是会进入TIME_WAIT状态等待一段时间,一段时间之后,再来释放连接,等这一会,就是怕最后一个ACK丢包!!!
如果最后一个ACK丢包了就意味着B过一会就会重传FIN.

如果最后一个ACK丢了B就收不到ACK了.
B是无法区分是FIN丢了,还是ACK丢了
于是B就假设是FIN丢了,于是就重传FIN(超时重传)

两个重要状态:
CLOSE_WAIT:四次挥手挥了两次之后出现的状态.这个状态就是在等待代码中调用socket.close方法,来进行后续的挥手过程,正常情况下,一个服务器上不应该存在大量的CLOSE_WAIT.如果存在,说明大概率是代码bug , close 没有被执行到

TIME_WAIT:谁主动发起FIN,谁就进入TIME_WAIT.起到的效果,就是给最后一次ACK提供重传机会
表面上看起来,A发送完ACK之后,就没有A的啥事了,按理说,A就应该销毁连接,释放资源了,但是并没有直接释放,而是会进入TIME_WAIT状态等待一段时间,一段时间之后,再来释放连接,等这一会,就是怕最后一个ACK丢包!!!如果最后一个ACK丢包了就意味着B过一会就会重传FIN.

4.滑动窗口

在这里插入图片描述

由于确认应答机制的存在,导致了当前每次执行一次发送操作,都需要等待上个ACK的到达大量的时间都花在等ACK上了.

引入滑动窗口,本质就是在"批量的发送数据"一次发一波数据,然后一起等一波ACK
例如:

在这里插入图片描述

这里是一次发送4组数据,

一份等待时间,等待了多份ACK .就把等到多份ACK的时间压缩成一份了,此时,一次批量发送的的数据N称为窗口大小,"滑动”的意思是,并不用把N组数据的ACK都等到了,才继续往下发送而是**收到一个ACK,就继续往下发送一组(例如当1001这个ACK到达后就立即发送下一组数据(4001-5000)).**看起来像是在滑动一样.

窗口大小越大,可以认为就是传输速度就越快,同一份时间内等待的ACK就更多,总的等待ACK的时间就少了

滑动窗口下丢包的两种情况:
1.ACK丢了,数据没丢,:没事不必处理,当2001的ACK丢了,但是后续的5001的ACK收到了,也就代表前面的数据包都收到了.
2.数据丢了:
如图式情况:
在这里插入图片描述

由于1001 - 2000这个数据丢了,所以B就再反复索要1001这个数据,即使A给B已经往后发了,这个时候仍然是在索要1001,当索要若干次之后,A就明白了,就触发了重传.

在在A重传1001-2000之前,B的接收缓冲区如图:
在这里插入图片描述

当A重传了1001-2000之后,B的接收缓冲区,就把缺口给补上了,后续的2001-7000这些数据都是已经传输过了的,这些数据就不必再重传.接下来B就向A索要7001开始的数据.

这里的重传只是需要把丢了的那一块数据给重传了即可.其他已经到了的数据就不必再重传了.整体的重传效率还是比较高的.(快重传)

5.流量控制

滑动窗口,窗口越大,传输速率就越高,但还得考虑接收方处理数据的能力.发送方发的太快,接收方处理不过来了.接收方就会把新收到的包给丢了,发送方还得再重传.

流量控制的关键,就是得能够衡量接收方的处理速度,此处就直接使用接收方接收缓冲区的剩余空间大小,来衡量当前的处理能力.

接收方通过ACK报文来告知发送方,剩余空间的大小

如果剩余空间比较大,就认为B的处理能力是比较强.就可以让A发的快点.(增大窗口大小)
如果剩余空间比较小,就认为B的处理能力是比较弱,就可以让A发的慢点.(减小窗口大小):当窗口大小为0时,发送方不再发送数据,但是发送方会定期发送一个,探测报文,不传输实际数据,只为了触发ACK,获知当前的剩余空间大小.

6.拥塞控制

拥塞控制衡量的是,发送方到接收方,这整个链路之间,拥堵情况(处理能力)
A能够发多快,不光取决于B的处理能力,也取决于中间链路的处理能力~
A和B之间的中间节点,有多少个?不知道,难以对这些设备衡量
拥塞控制的处理方案,就是通过"实验"的方式,逐渐调整发送速度,找到一个比较合适的值(范围)

7.延时应答

这个操作就是在有限的情况下,又尽可能的提高了一点传输速度,让窗口更大一些.
就好比一个又进水又出水的水池,每次注入一波水,就会询问当前水池里剩余空间有多少,采取的策略就是不立即回答,而是稍微晚一会回答,迟一点回答,意味着在这个延时间里,就会出更多的水.这样可以一次注入更多的水,而不是分批次注入少量的水.

8.捎带应答

客户端和服务器之间的通信,有以下几种模型:
例如:
当发送方向接收方发送数据后,接收方的内核会立即返回ACK,再返回响应,但由于捎带应答,ACK不一定立即返回,而是将ACK和响应数据合并一次返回,提升了传输效率

1.一问一答.客户端发一个请求,服务器返回一个对应的响应用,浏览器上网,打开网页,主要就是这种模型
2多问一答.上传文件
3.一问多答.下载文件
4.多问多答.直播~~,串流…

9.面向字节流:粘包问题

当发送方向接收方发送多份数据(aaa,bbb,ccc)时,这些数据报到达B之后,就会进行分用,分用意味着就把TCP数据进行解析了.取出其中的应用层数据放到接收缓冲区中,以备应用程序来取,但是B的应用程序就需要通过read方法来从接收缓冲区中取数据出来,因为TCP是面向字节流的.

但是,B取的时候就是取的若干个字节,如果没有额外的限制,其实就很难进行区分了.归根到底,是没有明确包之间的边界.

解决方案:关键就是要在应用层协议这里,加入包之间的边界(粘包问题,说是在TCP这里讲的,实际上这是个应用层的问题)例如,约定每个包以 ; 结尾

10.TCP的异常处理

1.进程终止
在进程毫无防备的情况下,偷袭他,突然结束进程

TCP连接,是通过 socket来建立的.socket 本质上是进程打开的一个文件.文件其实就存在于进程的PCB里面有个文件描述符表每次打开一个文件(包括socket),都在文件描述符表里,增加一项.每次关闭一个文件,都在文件描述符表里,进行删除一项.

如果直接杀死进程, PCB也就没了.里面的文件描述符表也就没了,此处的文件相当于’自动关闭’了.这个过程其实和手动调用socket.close()一样,都会触发4次挥手.

2.机器关机:
按照操作系统约定的正常流程关机,正常流程的关机,会让操作系统杀死所有进程.即:和上述的进程终止的过程是一样的

3.机器掉电/网线断开

1)如果接收方断电
意味着发送方发送的数据不再有ACK了.进入超时重传逻辑,重传几次之后,发送方认为这个连接已经出现严重故障了.尝试重新建立连接,重连失败之后,就会放弃连接(发送方主动释放曾经和B相关的连接信息)
2)如果发送方断电
B就不知道当前是A挂了,还是A休息会再继续,B就会时不时的给A发送一个小的报文(不带有实际的数据,只是为了触发ACK),通过探测报文(心跳包),发现A不再返回ACK了,因此B就认为A出现了问题~

问UDP实现可靠传输:套用TCP机制即可

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值