回显服务器的制作方法

客户端和服务器

在网络中,主动发起通信的一方是客户端,被动接受的这一方叫做服务器。

  • 客户端给服务器发送的数据是请求(request)
  • 服务器给客户端返回的数据叫响应(response)

客户端和服务器的交互有多种模式

  • 一问一答: 一个请求对应一个响应 。多在网站开发中用到
  • 一问多答:一个请求对应多个相应。主要涉及“下载”的场景中
  • 多问一答:多个请求队对应一个响应 。涉及到“上传”的场景中
  • 多问多答:多个请求对应多个响应。用到“远程控制”和“远程桌面”中

TCP和UDP的特点

要想进行网络编程,需要使用系统的api,本质上是传输层提供的。传输层用到的协议主要是TCP和UDP。这两个协议的差别挺大,所提供的api也有一定的差别。先从本质上看看TCP和UDP的特点有哪些差别。

  • TCP的特点是 有连接,可靠传输,面向字节流,全双工。
  • UDP的特点是 无连接,不可靠传输,面向数据报,全双工。
  1. 有连接/无连接
    举个例子,打电话,需要对方同意后才能打通电话,打电话的过程需要对方确认接听或者不接听,连接的特点就是双方都能认同,而无连接的规则向发微信,自己只管发送,不用在意对方是否同意接收。同理,计算机中的网络连接,就是通信双方,各自保存对方的信息,客户端就有一些数据结构,记录了谁是自己的服务器,服务器也有一些数据结构,记录了谁是自己的客户端。

  2. 可靠传输/不可靠传输
    网络上存在异常情况是非常多的,无论使用什么硬件技术都无法100%保证数据能从A发送到B。此处所说的可靠传输是指尽可能的完成数据传输,即不管数据有没有传输到,A都能清楚的知道。

  3. 面向字节流/面向数据报
    此处提到的字节流和文件中的字节流是一样的。网络中传输数据的基本单位就是字节。
    面向数据报:每次传输的基本单位是一个数据报(有一系列字节构成的)特定的结构。

  4. 全双工/半双工
    一个信道可以双向通信,是全双工。只能单向通信是半双工。

UDP socket api的使用

socket api的中文意思是"网络编程套接字",操作系统中有一类文件叫socket文件,它抽象了网卡这样的硬件设备,而进行网络通信最核心的硬件设备就是网卡,通过网卡发送数据就是写socket文件,通过网卡接收数据就是读socket文件。在UDP中,核心的api有两个类,分别是DatagramSocket和DatagramPacket。下面看看这两个类的使用和注意事项。

DatagramSocket

这个类的作用主要是对soclet文件的读写,也就是借助网卡发送接收数据。接收发送接收数据的单位就是DatagramSocket.。

datagramSocket的构造方法
在这里插入图片描述

DatagramSocket的方法
在这里插入图片描述

DatagramPacket

UDP是面向数据报的,每次发送接收数据的基本单位,就是一个udp数据报,此时表示了一个UDP数据报

DatagramPacket的构造方法
在这里插入图片描述

DatagramPacket方法
在这里插入图片描述

InetSocketAddress API

在这里插入图片描述

做一个简单的回显服务器

UDP版本的回显服务器

回显服务器是客户端发送什么请求,服务器就返回什么响应。做这个服务器的目的是学习UDP socket
api的使用和理解网络编程中客户端和服务器的基本工作流程

服务器的基本逻辑

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

    public class UdpEchoServer {
        private DatagramSocket socket = null;

        public UdpEchoServer(int port) throws SocketException {
            socket = new DatagramSocket(port);
        }

        // 服务器的启动逻辑.
        public void start() throws IOException {
            System.out.println("服务器启动!");
            while (true) {
                // 每次循环, 就是处理一个请求-响应过程.
                // 1. 读取请求并解析
                DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
                socket.receive(requestPacket);
                // 读到的字节数组, 转成 String 方便后续的逻辑处理.
                String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
                // 2. 根据请求计算响应 (对于 回显服务器来说, 这一步啥都不用做)
                String response = process(request);
                // 3. 把响应返回到客户端.
                //    构造一个 DatagramPacket 作为响应对象
                DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,requestPacket.getSocketAddress());
                socket.send(responsePacket);

                // 打印日志
                System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAddress().toString(),
                        requestPacket.getPort(), request, response);
            }
        }

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

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


  1. 首先创建DatagramSocket对象,接下来要使用socket对象来操作网卡。在创建对象的时候需要手动指定端口。在网络编程中,服务器一般需要手动指定端口,而客户端一般不需要。一个主机的一个端口只能被一个进程绑定,而一个进程可以绑定多个端口。

  2. 一个服务器一般都是24时运行的,直接使用while(true)可以不用退出。很多时候要重启一个服务器可以直接杀死进程。

  3. 此处 receive就从网卡能读取到一个 UDP 数据报就被放到了 requestPacket 对象中其中 UDP 数据报的载荷部分就被放到 requestPacket 内置的字节数组中了。另外报头部分,也会被 requestPacket 的其他属性保存。除了 UDP 报头之外,还有其他信息,比如收到的数据源 IP 是啥
    通过 requestPacket 还能知道数据从哪里来的(源 ip 源端口) 在这里插入图片描述

  4. 基于字节数组构造出 String字节数组里面保存的内容也不一定就是二进制数据,也可能是文本数据把文本数据交给 String 来保存,恰到好处~~
    这里得到的长度是 requestPacket 中的有效长度,不一定是 40964096 是最大长度。一定是要使用有效长度来构造这里的 String使用最大长度就会生成一个非常长的 String 后半部分都是空白。在这里插入图片描述

  5. 通过process()方法构造响应,这是一个回显服务器,直接返回请求就可以。

  6. 通过requestPacket.getSocketAddress())获得对应客户端的ip和端口。是把请求的源ip和源端口作为响应的目的ip和目的端口。在这里插入图片描述

总结

  • 上述代码中,可以看到,UDP 是无连接的通信 UDP socket 自身不保存对端的IP 和端口.而是在每个数据报中有一个~.另外代码中也没有“建立连接”"接受连接”操作
  • 不可靠传输,代码中体现不到的.
  • 面向数据报,send和receive 都是以 DatagramPacket 为单位
  • 全双工:一个 socket 既可以发送又可以接收

客户端的基本逻辑

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIp;
    private int serverPort;

 // 此处 ip 使用的字符串, 点分十进制风格. "192.168.2.100"
    public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
        //请求的目的IP和目的端口
        this.serverIp = serverIp;
        this.serverPort = serverPort;
        socket = new DatagramSocket();
    }

    public void start() throws IOException {
        System.out.println("客户端启动");
        Scanner scanner = new Scanner(System.in);
        while (true) {
            // 要做四个事情
            System.out.print("-> "); // 表示提示用户接下来要输入内容.
            // 2. 从控制台读取要发送的请求数据.
            if (!scanner.hasNext()) {
                break;
            }
            String request = scanner.next();
            // 3. 构造请求并发送.
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(serverIp), serverPort);
            socket.send(requestPacket);
            // 4. 读取服务器的响应.
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            // 5. 把响应显示到控制台上.
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            System.out.println(response);
        }
    }

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

三种DatagramSocket对象的构造方法
在这里插入图片描述
网络通信的基本流程

  1. 服务器启动.启动之后,立即进入 while 循,执行到 receive,进入阻塞,此时没有任何客户端发来请求呢。
  2. 客户端启动.启动之后,立即进入 while 循环,执行到 hasNext 这里进入阻塞,此时用户没有在控制台输入任何内容
  3. 用户在客户端的控制台中输入字符串,按下回车此时 hasNext 阻塞解除,next 会返回刚才输入的内容基于用户输入的内容,构造出一个 DatagramPacket 对象,并进行 send 。send执行完毕之后,继续执行到
    reeive 操作,等待服务器返回的响应数据此时服务器还没返回响应呢,这里也会阻塞)
  4. 服务器收到请求之后,就会从 receive 的阻塞中返回返回之后,就会根据读到的 DataqramPacket 对象,构造 String request, 通过 process 方法构造一个 String response再根据 response 构造一个
    DatagramPacket表示响应对象, 再通过 send 来进行发送给客户端。执行这个过程中,客户端也始终在阻塞等待
  5. 客户端从 receive 中返回执行.就能够得到服务器返回的响应并且打印倒控制台上于此同时,服务器进入下一次循环,也要进入到第二次的 receive 阳塞等待下个请求了

TCP版本的回显服务器

tcp中的长短连接
TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:

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

对比以上长短连接,两者区别如下:

  • 建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,长连接效率更高。
  • 主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发。
  • 两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于 客户端与服务端通信频繁的场景,如聊天室,实时游戏等服务器的处理逻辑。
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 serverSocket = null;

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

   public void start() throws IOException {
       System.out.println("服务器启动!");
       ExecutorService pool = Executors.newCachedThreadPool();
       while (true) {
           // 通过 accept 方法来 "接听电话", 然后才能进行通信
           Socket clientSocket = serverSocket.accept();
//            Thread t = new Thread(() -> {
//                processConnection(clientSocket);
//            });
//            t.start();

           pool.submit(new Runnable() {
               @Override
               public void run() {
                   processConnection(clientSocket);
               }
           });
       }
   }

   // 通过这个方法来处理一次连接. 连接建立的过程中就会涉及到多次的请求响应交互.
   private void processConnection(Socket clientSocket) {
       System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
       // 循环的读取客户端的请求并返回响应.
       try (InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()) {
           while (true) {
               Scanner scanner = new Scanner(inputStream);
               if (!scanner.hasNext()) {
                   // 读取完毕. 客户端断开连接, 就会产生读取完毕.
                   System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
                   break;
               }
               // 1. 读取请求并解析. 这里注意隐藏的约定. next 读的时候要读到空白符才会结束.
               //    因此就要求客户端发来的请求必须带有空白符结尾. 比如 \n 或者空格.
               String request = scanner.next();
               // 2. 根据请求计算响应
               String response = process(request);
               // 3. 把响应返回给客户端
               //    通过这种方式可以写回, 但是这种方式不方便给返回的响应中添加 \n
               // outputStream.write(response.getBytes(), 0, response.getBytes().length);
               //    也可以给 outputStream 套上一层, 完成更方便的写入.
               PrintWriter printWriter = new PrintWriter(outputStream);
               printWriter.println(response);
               printWriter.flush();

               System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress(), clientSocket.getPort(),
                       request, response);
           }
       } catch (IOException e) {
           throw new RuntimeException(e);
       } finally {
           try {
               clientSocket.close();
           } catch (IOException e) {
               throw new RuntimeException(e);
           }
       }
   }

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

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

在这里插入图片描述
在这里插入图片描述

  • 在服务器代码中,ServerSocket是创建服务端Socket的api。Socket是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket.
    不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
  • 当客户端发送请求的时候,内核就会发起建立连接的请求,服务器的内核会配合客户端的工作来建立连接,而内核的连接不是决定性的,还需要应用程序把这个连接接受,通过accept方法来接受连接。accept方法方法是会阻塞等待的,当没有客户端发起请求的时候此时就会阻塞。
  • 上面的操作也表现出Tcp是有连接的。

在这里插入图片描述

  • Tcp是面向字节流的。这里的字节流和文件中的字节流完全一致。使用和文件操作一样的类和方法来针对Tcp Socket的读和写。
  • InputStream是往网卡上读数据,OutputStream是往网卡上写数据。

客户端的处理逻辑

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 {
        // 此处可以把这里的 ip 和 port 直接传给 socket 对象.
        // 由于 tcp 是有连接的. 因此 socket 里面就会保存好这俩信息.
        // 因此此处 TcpEchoClient 类就不必保存.
        socket = new Socket(serverIp, serverPort);
    }

    public void start() {
        System.out.println("客户端启动!");
        try (InputStream inputStream = socket.getInputStream()) {
            try (OutputStream outputStream = socket.getOutputStream()) {
                Scanner scannerConsole = new Scanner(System.in);
                Scanner scannerNetwork = new Scanner(inputStream);
                PrintWriter writer = new PrintWriter(outputStream);
                while (true) {
                    // 这里的流程和 UDP 的客户端类似.
                    // 1. 从控制台读取输入的字符串
                    System.out.print("-> ");
                    if (!scannerConsole.hasNext()) {
                        break;
                    }
                    String request = scannerConsole.next();
                    // 2. 把请求发给服务器. 这里需要使用 println 来发送. 为了让发送的请求末尾带有 \n
                    //    这里是和服务器的 scanner.next 呼应的.
                    writer.println(request);
                    // 通过这个 flush 主动刷新缓冲区, 确保数据真的发出去了.
                    writer.flush();
                    // 3. 从服务器读取响应. 这里也是和服务器返回响应的逻辑对应.
                    String response = scannerNetwork.next();
                    // 4. 把响应显示出来
                    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", 9090);
        client.start();
    }
}

在TCP回显服务器中,需要注意下面几种情况

  1. 在上面代码中,PrintWriter中内置了缓冲区,IO操作都是很低效的,为了让低效的操作少一些,会引入一个缓冲区,先把写入网卡的数据王道缓冲区中,等达到一定的数量在一次发送出去,但如果发送的数据太少,缓冲区没有满,可能导致数据发送不出去,使用flush方法可以冲刷缓冲区,确保每条消息都能发送出去。

在这里插入图片描述

  1. ServerSocket在整个程序中,只有唯一一个对象,并且这个对象的生命周期伴随着整个程序,这个对象无法提前关闭,只有程序结束,随着进程的销毁一起结束。而clientSocket是每个客户端一个,随着客户端越来越多,如果不释放可能会占满文件描述符表。需要使用close方法关闭。

在这里插入图片描述

  1. 解决多个客户端向一个服务器发送请求的问题

在这里插入图片描述

上面的问题核心思路就是使用多线程,单个线程无法及给客户端提供服务,又能快速调用第二次accept,使用多线程,主线程就负责执行accept,其他线程就负责给客户端提供服务。如果客户端比较多就会频繁的创建销毁线程,就可以使用线程池解决频繁创建销毁线程的问题。

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值