• JavaEE——网络编程(TCP流编程)


    一、解释什么是 TCP 流套接字编程

    在上一篇文章中,我向大家介绍了有关UDP套接字方面的相关编程。
    详见: JavaEE——网络编程(UDP套接字编程)

    这篇文章,同样会通过一个简单的回显服务器的形式来解释什么是 TCP流套接字编程。

    首先我们要知道的是 TCP 提供的两个主要 API。
    这里的 API 主要是两个类,如下:

    1. ServerSocket 类
      专门给服务器使用的 Socket 对象
      在这里插入图片描述
      其中包含的 SeverSocket 方法:
      在这里插入图片描述
    2. Socket 类
      既会给客户端使用,也会给服务器使用。
      在这里插入图片描述
      相关方法
      在这里插入图片描述
      我们在前面已经知道,TCP 传输是面向字节流的,所以,TCP 不需要一个类来表示 “TCP 数据报”。
      TCP 不是以数据报为单位进行传输的,是以字节流的方式,流式传输
      这里的流式传输与 IO 文件操作 十分相似。

    二、代码实现TCP流套接字创建客户端服务器

    注:这里只是单纯的解释其中的核心代码,整体代码的逻辑会在后面统一展示

    1. 实现回显服务器

    • 首先创建出一个服务器流套接字
        private ServerSocket serverSocket = null;
    
        //构造方法实现创建新的 socket 对象
        //这里的 TcpEchoSever 是类名
        public TcpEchoSever(int port) throws IOException {
            //将端口号传递进来
            serverSocket = new ServerSocket(port);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这里就使用了,创建一个服务器端流套接字 Socket,并指定到端口。
    ServerSocket(int port)

    • 实现服务器的启动方法
        public void start() throws IOException {
            System.out.println("启动服务器");
            while(true){
                //这个 clientSever 是和具体的 客户端进行交流
                Socket clientSocket = serverSocket.accept();
                processConnection(clientSocket);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这里的 accept() 方法是 “接受连接” ,前提是得有一个客户端连接。
    当客户端在构造 Socket 对象时,就会指定服务器的 IP 和 端口。如果没有客户端来这里连接,此时就会在这里产生阻塞。

    • 实现 processConnection 方法与客户端进行交流

    这里与客户端交流大致分为下面的几步操作

    1. 读取客户端的请求
                    //将输入的流元素传入到 scanner 中
                    Scanner scanner = new Scanner(inputStream);
                    //判断输入流元素是否读取结束
                    if(!scanner.hasNext()){
                        //没有下个数据,就说明读完。(即就是说明客户端关闭了连接)
                        System.out.printf("[%s:%d] 客户端关闭!\n", clientSocket.getInetAddress().toString(),clientSocket.getPort());
                        break;
                    }
                    //如果没有结束,就将元素读取到对应的 String 类型的变量中
                    //注!! 这里使用的 next 关键字是一直读取到换行符/空格/其他空白字符结束,但是最终返回的内容中,没有 其中的 空白符等。
                    String request = scanner.next();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    简单分析代码

    1. 这里将输入的流变换成 inputStream 让客户端的数据直接被读取进来。
    2. 这里通过 hasNext() 方法获取字节元素直至最后
    1. 通过请求计算响应
    String response = process(request);
    
    • 1

    实现响应代码

    // 因为是回显服务器,所以直接返回元素即可
        public String process(String request) {
            return request;
        }
    
    • 1
    • 2
    • 3
    • 4
    1. 返回计算后的请求结果
     PrintWriter printWriter = new PrintWriter(outputStream);
                    // 此处使用 println 进行写入,让结果中带有 /n 换行,方便对端进行接受解析
     printWriter.println(request);
                    // 使用 flush 刷新缓冲区,保证当前写入的信息确实发送出去
     printWriter.flush();
     System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),
                       request,response);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    1. 这里的 outputStream 的作用是返回当前套接字的输出流。
    2. 需要注意的是这里使用 PrintWriter 是将这里的流进行转换。
      主要是因为 OutputStream 自身的方法不能够写入字符串,需要使用上面的方法进行转换
    3. 这里的 printWriter.println(request) 就是将处理响应后的 request 元素写回到网卡中,即就是返回到客户端。

    要注意理解此处这两个关键字之间的关系和用法。为了更好的理解,我下面举个例子:

    以打电话为例。
    假设此时我正在办公室里把电话拿在手上接听电话,此时,突然同事给了我一摞文件需要我签字,此时外放又不方便,于是,我拿出了一个耳机带上来接听,同时进行签字。

    这里的手上接听电话,就类似于这里的 outputStream。但是此时又不方便使用。
    而这里的戴上耳机接听电话,就类似于此处的 PrintWriter
    虽然方式方法不同,但是都达到了目的。这里的两个方法也是如此。

    到这里,这个回显服务器就基本完成了。

    (1)服务器对客户端响应的问题分析解决

    虽然上面的代码已经实现了对客户端信息的接受,但是其中存在着一个重要问题,如图:
    在这里插入图片描述
    上述画红线的代码是我们实现对客户端响应的核心代码。
    但是我们要知道,一个服务器绝对不是给一个客户端服务的。 但是代码写到这里每次只能处理一个客户端的请求,很显然这不符合我们最基本的需求。
    对此,处理的方式也很简单,要处理多个客户端,多线程是一个很好的解决办法。

    对代码简单修改:

        public void start() throws IOException {
            System.out.println("启动服务器");
            while(true){
                //这个 clientSever 是和具体的 客户端进行交流
                Socket clientSocket = serverSocket.accept();
                Thread t = new Thread(()->{
                  processConnection(clientSocket);
               });
            }
       
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    如上,使用多线程包裹了处理客户端信息的方法。虽然解决了问题,但是任然存在缺点。
    这里如果有许多客户端频繁建立连接,此时就需要频繁的创建 / 销毁线程。此时的开销就比较繁重。对此,使用线程池是一个很好的办法。

    对代码的最终修改

            //此处使用 CachedThreadPool,和 使用 FixedThreadPool 都不太合适(线程数不应该有固定的。。)
            ExecutorService threadPool = Executors.newCachedThreadPool();
            while(true){
                //这个 clientSever 是和具体的 客户端进行交流
                Socket clientSocket = serverSocket.accept();
                // 使用线程池来解决问题
                threadPool.submit(()->{
                    processConnection(clientSocket);
                });
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这样就更一步优化了代码。

    (2) 回显服务器代码整体罗列

    import java.io.*;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.Scanner;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class TcpEchoSever {
        private ServerSocket serverSocket = null;
    
        //构造方法实现创建新的 socket 对象
        public TcpEchoSever(int port) throws IOException {
            //将端口号传递进来
            serverSocket = new ServerSocket(port);
        }
    
        public void start() throws IOException {
            System.out.println("启动服务器");
            //创建线程池
            //此处使用 CachedThreadPool,和 使用 FixedThreadPool 都不太合适(线程数不应该有固定的。。)
            ExecutorService threadPool = Executors.newCachedThreadPool();
            while(true){
                //这个 clientSever 是和具体的 客户端进行交流
                Socket clientSocket = serverSocket.accept();
    //            //在这里对创建方式进行改变,使用多线程的方式
    //            // 此处使用多线程的方式对代码进行优化,会出现多次的创建删除线程的操作,此时开销会比较大
    //            Thread t = new Thread(()->{
    //                processConnection(clientSocket);
    //            });
    //            t.start();
    
                // 使用线程池来解决问题
                threadPool.submit(()->{
                    processConnection(clientSocket);
                });
            }
        }
    
        //使用下面的方法实现和客户端的交流
        //这里一个连接实现一个交互,但是要注意的是,这里可能会有多次的交流
        private void processConnection(Socket clientSocket){
            //先打印一个客户端开启时的响应,打印一下当前的端口号和IP地址
            System.out.printf("[%s:%d] 客户端开启\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
            //基于上述的 Socket 对象实现与客户端进行交流
    
            //对输入输出方法进行异常抓取
            try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()){
                //由于要处理多个响应,所以这里使用循环执行
                while(true){
                    //1. 读取请求
                    //将输入的流元素传入到 scanner 中
                    Scanner scanner = new Scanner(inputStream);
                    //判断输入流元素是否读取结束
                    if(!scanner.hasNext()){
                        //没有下个数据,就说明读完。(即就是说明客户端关闭了连接)
                        System.out.printf("[%s:%d] 客户端关闭!\n", clientSocket.getInetAddress().toString(),clientSocket.getPort());
                        break;
                    }
                    //如果没有结束,就将元素读取到对应的 String 类型的变量中
                    //注!! 这里使用的 next 关键字是一直读取到换行符/空格/其他空白字符结束,但是最终返回的内容中,没有 其中的 空白符等。
                    String request = scanner.next();
                    //2. 通过请求计算响应
                    String response = process(request);
                    //3,返回计算后的请求结果
                    //      OutputStream 中没有 write String 这样的功能,可以将 String 中的字节数组拿出来进行写入
                    //      也可以使用字符流进行交换
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    // 此处使用 println 进行写入,让结果中带有 /n 换行,方便对端进行接受解析
                    printWriter.println(request);
                    // 使用 flush 刷新缓冲区,保证当前写入的信息确实发送出去
                    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();
                }
            }
        }
    
        //根据请求计算响应
        public String process(String request) {
            return request;
        }
    
        public static void main(String[] args) throws IOException {
            //创建服务器并给定端口号
            TcpEchoSever tcpEchoSever = new TcpEchoSever(9090);
            tcpEchoSever.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101

    2. 实现回显客户端

    1. 实现客户端核心代码初步准备

    有关 TCP 客户端的配置和 UDP 客户端的配置十分相似。都需要两个关键信息:
    服务器 IP 和 服务器 端口

    除此之外,我们在前面的第一板块描述过。对于客户端 使用的是Socket 关键字以及其内部的方法
    所以,代码如下

        //对于客户端要使用 Socket 来创建客户端
        private Socket socket = null;
    
        //使用构造方法实现客户端
        public TcpEchoClient(String severIP, int severPart) throws IOException {
            // Socket 构造方法,能够识别点分十进制的 IP 地址,比 DatagramPacket 使用更方便
            // new 这个对象的同时,就会进行 TCP 连接操作
            socket = new Socket(severIP,severPart);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    1. 实现客户端核心代码

    对于客户端代码,需要分为下面三部分:

    • 先从键盘上获取用户的输入请求
      这里的代码比较简单,如下:
           System.out.println("> ");
           String request = scanner.next();
                  if(request.equals("exit")){
                        System.out.println("good bye");
                        break;
                    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 将读取到的元素发送给服务器
      这里需要传输的元素仍然是一个字符串,同样,这里的传输也需要使用到 OutputStream 来将信息传输。
      呢么这里的问题就和前面服务器将信息返回给客户端的问题相同。 对此,这里也需要使用 PrintWrite 方法修饰。

    代码如下:

           // 2. 把读到的内容构造成请求,发送回服务器。
           PrintWriter printWriter = new PrintWriter(outputStream);
           printWriter.println(request);
           //此处加上一个 flush 确保数据已经发送
           printWriter.flush();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 将从服务器返回的数据流接受并分析到客户端
           Scanner respScanner = new Scanner(inputStream);
           String response = respScanner.next();
    
    • 1
    • 2
    • 打印返回结果
      System.out.println(response);
    
    • 1

    (1) 回显客户端整体代码罗列

    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 {
        //对于客户端要使用 Socket 来创建客户端
        private Socket socket = null;
    
        //使用构造方法实现客户端
        public TcpEchoClient(String severIP, int severPart) throws IOException {
            // Socket 构造方法,能够识别点分十进制的 IP 地址,比 DatagramPacket 使用更方便
            // new 这个对象的同时,就会进行 TCP 连接操作
            socket = new Socket(severIP,severPart);
        }
    
        public void start(){
            System.out.println("客户端启动");
            Scanner scanner = new Scanner(System.in);
            try(InputStream inputStream = socket.getInputStream();
                OutputStream outputStream = socket.getOutputStream()){
                while(true){
                    // 1. 先从键盘上读取用户输入的请求
                    System.out.println("> ");
                    String request = scanner.next();
                    if(request.equals("exit")){
                        System.out.println("good bye");
                        break;
                    }
                    // 实现发送数据
                    // 2. 把读到的内容构造成请求,发送回服务器。
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(request);
                    //此处加上一个 flush 确保数据已经发送
                    printWriter.flush();
                    // 3. 读取服务器的响应
                    // 使用这个 scanner 进行读取数据
                    Scanner respScanner = new Scanner(inputStream);
                    String response = respScanner.next();
                    // 4. 将响应内容显示到页面上
                    System.out.println(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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    (2) 对代码中整体存在的小问题分析

    到此,对应的客户端和服务器之间的代码都已经实现完毕,在最后,这里还需要在说明一个问题,如下图:
    在这里插入图片描述
    在上图的代码中,客户端和服务器都是用的是 println 对数据进行发送。
    我们知道,println 会在发送的数据后面加上 \n 换行。

    问题:
    呢么,这里不使用 println 而是使用 print(不带换行) 呢么这个代码是否可以正常运行?

    其实答案很明确,就是不可以
    TCP 协议是面向字节流的协议,对于读的一方,一次读多少字节都可以。但是,对于接收方,这次需要读多少字节是不明确的

    所以,针对上面的问题,就需要在数据传输中进行明确地约定。在此处的代码中,隐性约定就是使用 \n 来作为当前代码请求和响应的分割约定。

    在这里插入图片描述
    如上图所示,将双方的情况调转过来也是相同的。

    三、总结与运行结果展示

    1. 总结
    • 简单分析 TCP 流套接字的客户端服务器之间的响应过程
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述

    • 横向对比 TCP 和 UDP 两个版本之间的代码

    在这里插入图片描述

    • TCP 中客户端和服务器交流图解
      在这里插入图片描述
    1. 运行结果展示

    首先启动客户端和服务器
    在这里插入图片描述
    创建两个客户端并行运行
    在这里插入图片描述
    服务器端的反馈响应
    在这里插入图片描述

  • 相关阅读:
    MySql模糊查询大全
    【大体思路】rv1126 跑通 yolov5
    CSRF(跨站请求伪造)攻击演示
    基于物联网技术的校园智慧消防管理平台-Susie 周
    【C++】类和对象(下)
    关于A level真题的获取(持续更新中)
    微服务系统设计——数据模型与系统架构设计
    Java内部类
    路由基础+静态路由
    单商户商城系统功能拆解36—分销应用—分销商
  • 原文地址:https://blog.csdn.net/qq_62905847/article/details/129479081