TCP实现回显服务器及客户端

  • 阿里云国际版折扣https://www.yundadi.com

  • 阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

    目录

    前言

    Socket API

    SeverSocket API

    TCP中的长短连接

    TCP实现回显服务器

    代码实现有详细解释

    TCP实现回显客户端

    代码实现有详细注释

    小结


    前言

        上篇文章介绍了TCP的特点。由于TCP的特点是有连接面向字节流可靠传输等我们就可以想象到TCP的代码和UDP会有一定的差异。TCP和UDP具体使用哪种协议需要根据实际业务需求来选择。

    Socket API

        Socket是客户端Socket或服务端中接收到客户端建立连接accept方法的请求后返回的服务端Socket。

        不管是客户端还是服务端Socket都是双方建立连接后保存两端信息及用来与对方收发数据的。

    Socket构造方法

    注意

        创建一个客户端流套接字Socket并与对应IP的主机上对应端口的进程建立连接。当服务端accept()阻塞时客户端一旦实例出Socket对象就会建立连接。

    Socket方法 

    注意

        获得套接字输入流。如果建立连接服务端调用这个方法就是读取客户端请求。

    注意

        获得套接字输出流。如果建立连接服务端调用这个方法就是往客户端返回响应。

    注意

        连接后获得对方的IP地址。

    SeverSocket API

        ServerSocket 是创建TCP服务端Socket的API。

    ServerSocket构造方法

        创建服务端套接字并绑定端口。这个对象就是用来与客户端建立连接的。

    ServerSocket方法

    注意

        开始监听指定端口创建时绑定的端口有客户端连接后返回一个服务端Socket对象并基于该Socket建立与客户端的连接用来收发数据否则阻塞等待。

     

    注意

        由于在操做系统中Socket被当作文件处理那么就需要释放PCB中文件描述符表中的资源同时断开连接。

    TCP中的长短连接

        短连接每次接收到数据并返回响应后都关闭连接即是短连接。也就是说短连接只能一次收发数据。
        长连接不关闭连接一直保持连接状态双方不停的收发数据即是长连接。也就是说长连接可以多次收发数据。

    注意

        1建立关闭连接耗时很明显短连接需要不断的建立和断开连接而长连接只需要一次。长连接耗时要比短连接短。

        2主动发送请求不同短连接一般是客户端主动向服务端发送请求。长连接客户端可以向服务端主动发送服务端也可以主动向客户端发送。

        3两者使用场景不同短连接一般适用于客户端请求频率不高的场景浏览网页。长连接一般适用于客户端与服务端通信频繁的场景。聊天室

    TCP实现回显服务器

        首先服务器是被动的一方我们必须指定端口。然后通过ServerSocket对象中accept()方法建立连接当返回Socket对象时处理连接并且将响应写回客户端。

        由于不知道客户端什么时候建立连接那么服务器就需要一直等待随时待命。这里使用了死循环的方式但是不会一直循环accept()方法当没有连接时就会阻塞等待。

        这里是本机到本机的数据发送即使用环回ip即可。

     private ServerSocket serverSocket = null;
     public TcpEchoSever(int port) throws IOException {
         serverSocket = new ServerSocket(port);
     }

    注意

       创建ServerSocket对象并且指定端口号。

    Socket clintSocket = serverSocket.accept();

    注意

        accept()方法会阻塞等待。客户端Socket对象一旦实例化就会与服务端建立连接。

     processConnection(clintSocket);

    注意

        这里通过一个方法来处理连接。这样写会有很大的好处。

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

    注意

        我们首先需要获得读和写的流对象。服务器需要接收请求读返回响应写。这里使用的是带有资源的try()这样就会自动关闭流对象。

    Scanner scanner = new Scanner(inputStream);
    String request = scanner.next();

    注意

        这里通过Scanner去从流对象中读取数据。注意这里的next()方法当读到一个换行符/空格/其他空白符结束但最终结果不包含上述空白符。

        因为我们不清楚客户端连接后发送多少次请求因此我们采用死循环的方式读和向客户端响应数据。这里不会一直循环因为scanner当读不到数据就会阻塞。

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

    注意

        这里通过一个函数来处理请求并且返回处理后结果。由于是回显服务器直接返回即可。

      PrintWriter printWriter = new PrintWriter(outputStream);
      printWriter.println(response);
      printWriter.flush();

    注意

        我们为了方便直接写字符串将outputStream转换成PrintWriter。然后将响应写入到网卡并且换行。因为客户端和服务端读数据都是需要空白符结束的所以这里必须有一个空白符。

        由于数据首先会写入缓冲区我们将缓冲区刷新一下保证数据正常写入到文件中网卡

    finally {
          clintSocket.close();
    }

    注意

        和一个客户端建立连接后返回Socket对象使用文件描述表如果并发量大会创建很多对象文件描述符表就有可能满就可能导致无法创建连接。因此需要保证资源得到释放包裹在finally里。

    特别注意

        上述代码只能处理一个客户端。当代码执行到processConnection函数里首先是一个死循环然后还有scanner的阻塞当处理一个连接代码就会一直在这个函数里。没有办法执行到accept()和客户端连接。想要处理下一个客户端的连接就必须断开这个客户端显然这是不合理的。

    解决方案

        使用多线程。当有客户端连接后创建一个线程去处理这个连接主线程代码继续执行就会到accept()方法。要是有多个客户端都可以建立连接并且有独立的线程去处理这些连接这些线程是并发的关系。

        但是存在一个问题如果并发量足够大客户端数量非常多就会创建大量的线程也会存在大量线程的销毁这些就会消耗大量的系统资源。因此使用线程池使用动态变化的线程数量根据并发量来调整线程数量。而且直接使用线程池中的线程代码上就可以实现这样就会减少系统资源的消耗。

    代码实现有详细解释

    public class TcpEchoSever {
        //Tcp协议服务器使用ServerSocket类来建立连接
        private ServerSocket serverSocket = null;
        public TcpEchoSever(int port) throws IOException {
            serverSocket = new ServerSocket(port);
        }
        public void start() throws IOException {
            System.out.println("启动服务器");
            //使用线程池防止客户端数量过多创建销毁大量线程开销太大
            //动态变化的线程池
            ExecutorService threadPool = Executors.newCachedThreadPool();
            while (true) {
                //这里会阻塞直到和客户端建立连接返回Socket对象来和客户端通信
                //客户端构造Socket对象时会指定IP和端口就会建立连接客户端主动连接
                Socket clintSocket = serverSocket.accept();
                threadPool.submit(() -> {
                    try {
                        processConnection(clintSocket);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                });
                //要连接多个客户端需要多线程去处理连接
                //这样才能让主线程继续执行到accept阻塞然后和其他客户端建立连接每个线程是独立的执行流彼此之间是并发的关系
                //如果客户端数量非常大这里就会创建很多线程数量过多对于系统来说也是很大的开销使用线程池
    //            Thread t = new Thread(() -> {
    //                try {
    //                    processConnection(clintSocket);
    //                } catch (IOException e) {
    //                    e.printStackTrace();
    //                }
    //            });
    //            t.start();
            }
        }
    
        private void processConnection(Socket clintSocket) throws IOException {
            System.out.printf("【%s : %d】客户端上线\n", clintSocket.getInetAddress(), clintSocket.getPort());
            //读客户端请求
            //处理请求
            //将结果写回客户端响应
            try(InputStream inputStream = clintSocket.getInputStream();
                OutputStream outputStream = clintSocket.getOutputStream()) {
    
                //流式数据循环读取
                while (true) {
                    Scanner scanner = new Scanner(inputStream);
                    //读取完毕客户端下线
                    if(!scanner.hasNext()) {
                        System.out.printf("【%s : %d】客户端下线\n", clintSocket.getInetAddress(), clintSocket.getPort());
                        break;
                    }
                    //读取请求
                    // 注意!! 此处使用 next 是一直读取到换行符/空格/其他空白符结束, 但是最终返回结果里不包含上述 空白符 .
                    String request = scanner.next();
                    //处理请求
                    String response = process(request);
    
                    //写回客户端处理请求结果响应
                    //为了直接写字符串这里将字节流转换为字符流
                    //也可以将字符串转为字节数组
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    //写入且换行
                    printWriter.println(response);
                    //写入首先是写入了缓冲区这里为了保险就刷新一下缓冲区
                    printWriter.flush();
                    System.out.printf("【%s : %d】请求%s  响应%s\n", clintSocket.getInetAddress(), clintSocket.getPort(),
                            request, response);
                }
    
            }catch (IOException e) {
                e.printStackTrace();
            }finally {
                //和一个客户端建立连接后返回Socket对象使用文件描述表如果并发量大会创建很多对象文件描述符表就有可能满就可能导致无法创建连接
                //因此需要保证资源得到释放包裹在finally里
                clintSocket.close();
            }
        }
        public String process(String request) {
            return request;
        }
    
        public static void main(String[] args) throws IOException {
            TcpEchoSever tcpEchoSever = new TcpEchoSever(8280);
            tcpEchoSever.start();
        }
    }
    

    TCP实现回显客户端

        客户端不需要指定端口号。客户端程序在用户主机上我们如果指定就有可能和其他程序冲突因此让操作系统随机分配一个空闲的端口号。客户端需要明确服务端的ip和端口号这样才能明确哪个主机和哪个进程。

        那么服务端为什么可以指定端口号呢难道就不怕和其他进程端口号冲突吗这里详解请看上篇文章的解释

        首先需要明确客户端的工作流程接收用户输入数据 --> 发送请求 --> 接收响应

    public TcpEchoClint(String severIp, int severPort) throws IOException {
        socket = new Socket(severIp, severPort);
    }

    注意

        创建Socket对象并且指定服务端的ip和端口。当这个对象实例创建完成时同时也就和服务端建立了连接通过这个Socket对象就可以发送和接收数据。

        这里不需要将字符串ip进行转换可以自动转换。

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

    注意

        和服务端一样首先获得输入和输出流。用包含资源的try可以自动关闭释放文件描述符表中的资源。

     System.out.println("请输入请求");
     String request = scanner.next();
     if(request.equals("exit")) {
           System.out.println("bye bye");
           break;
      }

    注意

        让用户从控制台输入数据这里做了一个判断如果输入“exit”就退出客户端break直接跳出循环。

    PrintWriter printWriter = new PrintWriter(outputStream);
    printWriter.println(request);
    printWriter.flush();

    注意

        为了直接发送字符串这里将outputStream转换成PrintWriter。这里在发送时需要换行空白符因为服务端读取的next()方法需要空白符。

        数据首先写入缓冲区为了保证数据写入到文件网卡这里手动刷新一下缓冲区。

    Scanner scanner1 = new Scanner(inputStream);
    String response = scanner1.next();
    System.out.println(response);

    注意

        接收响应通过输入流来读取响应。将接收的响应打印出来。这里的next()方法和上面一致。

    代码实现有详细注释

    public class TcpEchoClint {
        Socket socket = null;
        public TcpEchoClint(String severIp, int severPort) throws IOException {
            //Socket构造方法可以识别点分十进制不需要转换比DatageamPacket方便
            //实例这个对象的同时就会进行连接
            socket = new Socket(severIp, severPort);
        }
        public void start() {
            try(InputStream inputStream = socket.getInputStream();
                OutputStream outputStream = socket.getOutputStream()) {
                Scanner scanner = new Scanner(System.in);
                while (true) {
                    //从控制台读取请求
                    //空白字符结束但不会读空白字符
                    System.out.println("请输入请求");
                    String request = scanner.next();
                    if(request.equals("exit")) {
                        System.out.println("bye bye");
                        break;
                    }
                    //发送请求
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    //需要发送空白符因为scanner需要空白符
                    printWriter.println(request);
                    printWriter.flush();
                    //接收响应
                    Scanner scanner1 = new Scanner(inputStream);
                    String response = scanner1.next();
                    System.out.println(response);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        public static void main(String[] args) throws IOException {
            TcpEchoClint tcpEchoClint = new TcpEchoClint("127.0.0.1", 8280);
            tcpEchoClint.start();
        }
    }
    

    小结

        在写服务端代码时需要考虑高并发的情况。我们需要尽可能节省系统资源的利用。

  • 阿里云国际版折扣https://www.yundadi.com

  • 阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
    标签: 服务器