基于UDP和TCP实现回显服务器

目录

[一. UDP 回显服务器](#一. UDP 回显服务器)

[1. UDP Echo Server](#1. UDP Echo Server)

[2. UDP Echo Client](#2. UDP Echo Client)

[二. TCP 回显服务器](#二. TCP 回显服务器)

[1. TCP Echo Server](#1. TCP Echo Server)

[2. TCP Echo Client](#2. TCP Echo Client)


回显服务器 (Echo Server) 就是客户端发送什么样的请求, 服务器就返回什么样的响应, 没有任何的计算和处理逻辑.

一. UDP 回显服务器

1. UDP Echo Server

下面实现服务器.

java 复制代码
public class UdpEchoSever {
    private DatagramSocket socket = null;
    public UdpEchoSever(int port) throws SocketException { 
        socket = new DatagramSocket(port); // 创建一个DatagramSocket对象,并绑定一个端口号.
    }
    
}

(1) 这里声明的SocketException是IOException的子类, 是网络编程中常见的异常.

(2) UdpEchoSever的构造方法方法中, 在调用DatagramSocket的构造方法时, jvm就会调用系统API, 完成 "端口号 - 进程" 的绑定.

(3) 同一时刻, 一个端口号只能绑定一个进程; 而一个进程可以绑定多个端口号.

java 复制代码
public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            // 此处通过一个"死循环"来不停地处理客户端的请求.
            // 1. 读取客户端的请求并解析.
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(requestPacket);
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());

            // 2. 根据请求, 计算响应. (回显服务器, 响应==请求)
            String response = process(request);

            // 3. 把响应写回到客户端.
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
                    requestPacket.getSocketAddress()); 
            socket.send(responsePacket);

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

接下来我们通过start()方法编写服务器的核心流程

1. 读取客户端请求并解析

(1) 服务器的主要工作, 就是不停地处理客户端发来的请求. 所以需要写一个 while(true) 死循环来不停地处理客户端发来的请求.

(2) 这里的 receive 方法: 一调用receive方法, 就会就从网卡上读取数据, 但是此时网卡上不一定有数据, 如果网卡上有数据, receive立即返回获取到的数据; 如果网卡上没数据, receive就会阻塞等待, 一直等待到获取到数据为止. 此处receive中的的参数也是"输出型参数", 从网卡中获取到的数据会存到requestPacket里面.

(3) receive接收到的数据是二进制数据, 为了方便后续处理, 我们把它转成字符串类型的数据.

2. 根据请求, 计算响应

因为我们这里实现的是回显服务器, 所以响应 == 请求.

3. 把相应写回客户端

由于我们为了方便处理吧字节数组转成了字符串, 所以在往回发的时候需要再基于字符串构造出字节数组. 并且, 由于UDP是无连接的, 所以通信双方不包含对端信息, 所以在往回发的时候, 我们还要带上客户端信息. 客户端信息可以从请求中拿到. getSocketAddress()这个方法就会返回客户端的IP和端口号.

4. 打印日志

那么这样的话服务器端的代码就完成了.

java 复制代码
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

public class UdpEchoSever {
    private DatagramSocket socket = null;
    public UdpEchoSever(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

    // 通过start()方法启动服务器核心流程
    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            // 此处通过一个"死循环"来不停地处理客户端的请求.
            // 1. 读取客户端的请求并解析.
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(requestPacket);
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());

            // 2. 根据请求, 计算响应. (回显服务器, 响应==请求)
            String response = process(request);

            // 3. 把响应写回到客户端.
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
                    requestPacket.getSocketAddress());
            socket.send(responsePacket);

            // 4. 打印日志
            System.out.printf("[%s:%d] req=%s, resp=%s\n", requestPacket.getAddress(), requestPacket.getPort(),
                    request, response);
        }
    }
    private String process(String request) {
        return request;
    }


}

此处还有一个小问题, 这里我们创建了Socket对象, 使用完成之后应该关闭资源啊, 但是我们的代码里并没有写close() --> 主要是因为这里Socket的生命周期是跟随进程的, 进程退出, Socket资源自然也就关闭了.

2. UDP Echo Client

java 复制代码
import java.io.IOException;
import java.net.*;
import java.util.Scanner;

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String severIP;
    private int severPort;

    public UdpEchoClient(String severIP, int severPort) throws SocketException {
        socket = new DatagramSocket(); // 服务器创建Socket对象, 一定要指定端口号 (服务器必须是指定了端口号, 客户端发起的时候, 才能找到服务器),
        // 但是客户端这里最好不要指定端口号 因为我们不知道客户端那个端口繁忙, 那个端口空闲, 所以我们手动指定, 让系统去指定一个最合适的端口.
        this.severIP = severIP;
        this.severPort = severPort;
    }

    public void start() throws IOException {
        System.out.println("启动客户端");
        Scanner scanner = new Scanner(System.in);
        while (true) {
            // 1.从控制台读取用户输入
            System.out.println("-> ");
            String request = scanner.next();

            // 2. 构造出一个UDP请求数据报, 发送给服务器
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(this.severIP), this.severPort);
            socket.send(requestPacket);

            // 3. 从服务器读取到响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);

            // 4. 把响应打印到控制台上.
            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", 9071);
        // 这里的127.0.0.1是特殊的IP地址, 是"环回IP" 这个IP代表"本机", 如果客户端和服务器在一个主机上, 就使用这个IP
        client.start();
    }
}

注意这里使用到了一个特殊的IP地址: "127.0.0.1" 这个IP地址叫做"回环IP", 代表"本机", 如果客户端可服务器都在同一个主机上, 就使用这个IP.

下面我们来看一下运行结果:

没有任何问题~

服务器和客户端交互的过程大致如下:

(1) 启动服务器, 服务器等待请求, 如果没有请求发来, 就一直阻塞.

(2) 启动客户端, 在客户端输入内容, 发送请求. (客户端发送完请求之后进入receive等待服务器返回响应.)

(3) 服务器收到请求, 并对请求做出响应. 服务器往客户端返回响应.

(4) 客户端收到响应, 交互结束. 进入下一轮交互.

二. TCP 回显服务器

1. TCP Echo Server

java 复制代码
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;

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

    public void start() throws IOException {
        System.out.println("启动服务器");
        while (true) {
            Socket clientSocket = serverSocket.accept();
            // 每次服务器调用一次accept就会产生一个新的socket对象, 来和客户端进行"一对一服务"
            // TCP建立连接的过程是由操作系统完成的, 代码不能直接感知到 ~
            // 已经完成建立连接的操作了, 才能进行accept. accept相当于是针对内核中已经建立好的连接做一个"确认"动作.
            processConnection(clientSocket);
        }
    }

    private void processConnection(Socket clientSocket) {
        // 先打印客户端信息
        System.out.printf("[%s:%d] 客户端上线!", clientSocket.getInetAddress(), clientSocket.getPort());
        // 获取到Socket中持有的流对象.
        //TCP是全双工的通信, 一个Socket对象, 既可以读, 也可以写 ~
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()){
            // 使用Scanner包装一下inputStream
            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true) {
                // 1. 读取请求并解析

                if (!scanner.hasNext()) {
                    // 如果scanner中无法读取出数据, 说明客户端关闭了连接, 导致服务器这里读到了末尾 ~
                    break;
                }
                String request = scanner.next();

                // 2. 根据请求计算响应
                String response = process(request);

                // 3. 把响应写回给客户端
                //outputStream.write(response.getBytes());
                printWriter.println(response); //这里使用println是为了在数据末尾能够加上一个换行.

                // 4. 打印日志
                System.out.printf("[%s:%d] req=%s; resp=%s\n", clientSocket.getInetAddress(), clientSocket.getPort(),
                        request, response);
            }
        } catch(IOException e) {
            e.printStackTrace();
        }
        System.out.printf("[%s:%d] 客户端下线\n", clientSocket.getInetAddress(),clientSocket.getPort());
    }

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

2. TCP Echo Client

java 复制代码
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("客户端启动");

        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            Scanner scanner = new Scanner(inputStream);
            Scanner scannerIn = new Scanner(System.in);
            PrintWriter printWriter = new PrintWriter(outputStream);

            while (true) {
                // 1. 从控制台读取数据
                System.out.print("-> ");
                String request = scannerIn.next();
                // 2. 把请求发送给服务器
                printWriter.println(request);
                printWriter.flush(); // 刷新缓冲区
                // 3. 从服务器读取响应
                if (!scanner.hasNext()) {
                    break;
                }
                String response = scanner.next();
                // 4. 打印响应结果
                System.out.println(response);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
相关推荐
mghio25 分钟前
Dubbo 中的集群容错
java·微服务·dubbo
咖啡教室5 小时前
java日常开发笔记和开发问题记录
java
咖啡教室5 小时前
java练习项目记录笔记
java
鱼樱前端6 小时前
maven的基础安装和使用--mac/window版本
java·后端
RainbowSea6 小时前
6. RabbitMQ 死信队列的详细操作编写
java·消息队列·rabbitmq
RainbowSea7 小时前
5. RabbitMQ 消息队列中 Exchanges(交换机) 的详细说明
java·消息队列·rabbitmq
李少兄8 小时前
Unirest:优雅的Java HTTP客户端库
java·开发语言·http
此木|西贝8 小时前
【设计模式】原型模式
java·设计模式·原型模式
可乐加.糖9 小时前
一篇关于Netty相关的梳理总结
java·后端·网络协议·netty·信息与通信