Udp代码

服务器端

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 表示一个 UDP 数据报. 此处传入的字节数组, 就保存 UDP 的载荷部分.
            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. 把响应返回给客户端
            //    根据 response 构造 DatagramPacket, 发送给客户端.
            //    此处不能使用 response.length()
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
                    requestPacket.getSocketAddress());
            //    此处还不能直接发送. UDP 协议自身没有保存对方的信息(不知道发给谁)
            //    需要指定 目的 ip 和 目的端口.
            socket.send(responsePacket);

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

    // 后续如果要写别的服务器, 只修改这个地方就好了.
    // 不要忘记, private 方法不能被重写. 需要改成 public
    public String process(String request) {
        return request;
    }

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

另一个Udp服务器端

本服务器端继承了上一个

import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;

public class UdpDictServer extends UdpEchoServer {
    private HashMap<String, String> dict = new HashMap<>();

    public UdpDictServer(int port) throws SocketException {
        super(port);

        // 初始化词典
        dict.put("小猫", "cat");
        dict.put("小狗", "dog");
        dict.put("小兔子", "rabbit");
        dict.put("小鸭子", "duck");
    }

    @Override
    public String process(String request) {
        // 查字典.
        return dict.getOrDefault(request, "未找到该词条");
    }

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

客户端

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

public class UdpEchoClient {
    private DatagramSocket socket = null;

    // UDP 本身不保存对端的信息, 就自己的代码中保存一下
    private String serverIp;
    private int serverPort;

    // 和服务器不同, 此处的构造方法是要指定访问的服务器的地址.
    public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
        this.serverIp = serverIp;
        this.serverPort = serverPort;
        socket = new DatagramSocket();
    }

    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            // 1. 从控制台读取用户输入的内容.
            System.out.println("请输入要发送的内容:");
            if (!scanner.hasNext()) {
                break;
            }
            String request = scanner.next();
            // 2. 把请求发送给服务器, 需要构造 DatagramPacket 对象.
            //    构造过程中, 不光要构造载荷, 还要设置服务器的 IP 和端口号
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(serverIp), serverPort);
            // 3. 发送数据报
            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();
    }
}

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("启动服务器");

        //这种情况一般不会使用fixedThreadPool,意味着同时处理的客户端连接数目就固定了
        ExecutorService executorService = Executors.newCachedThreadPool();

        while(true){
            // tcp 来说, 需要先处理客户端发来的连接.
            // 通过读写 clientSocket, 和客户端进行通信.
            // 如果没有客户端发起连接, 此时 accept 就会阻塞.

            // 主线程负责进行 accept, 每次 accept 到一个客户端, 就创建一个线程, 由新线程负责处理客户端的请求.
            Socket clientSocket = serverSocket.accept();

            //使用多线程的方式来调整
//            Thread t = new Thread(() -> {
//                processConnection(clientSocket);
//            });
//            t.start();

            //使用线程池来调整
            executorService.submit(() ->{
                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()){

            //针对InputStream和Outputstream再套一层
            Scanner scanner = new Scanner(inputStream);
            PrintWriter writer = new PrintWriter(outputStream);

            while(true){
                //获取请求并解析,可以直接read,也可以借助Scanner来辅助完成
                if(!scanner.hasNext()){
                    System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
                    break;
                }
                String request = scanner.next();
                //根据请求计算相应
                String response = process(request);
                //3.返回响应到客户端
                writer.println(response);
                writer.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);
            }
        }
    }

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

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

客户端

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地址写进来
        socket = new Socket(serverIp,serverPort);
    }

    public void start(){
        Scanner scanner = new Scanner(System.in);

        try(InputStream inputStream = socket.getInputStream()){
            OutputStream outputStream = socket.getOutputStream();
            //方便操作而使用套壳操作
            Scanner scannerNet = new Scanner(inputStream);
            PrintWriter writer = new PrintWriter(outputStream);

            //从控制台读取请求,并发送给服务器
            while(true){
                //1.从控制台获取用户输入
                String request = scanner.next();
                //2.发送给服务器
                writer.println(request);
                // 加上缓冲区操作,才是真正的发送数据
                writer.flush();
                //3.读取服务器返回的响应
                String response = scannerNet.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)

本文章只对Tcp代码进行解释

运行结果

在上面的运行中,有两个客户端和服务器连接,他们的ip地址都是127.0.0.1。端口号分别是55334和55339。服务器的端口号是9090(见代码)。

127.0.0.1是本地回环地址

127.0.0.1 是 localhost,指向本机。当你连接 "127.0.0.1" 时,数据不经过网络,直接在操作系统内部流转

TCP连接的四元组:源IP + 源端口 + 目的IP + 目标端口

服务器的端口是固定的9090(new ServerSocket(9090)),

而客户端的端口是动态分配的( clientSocket.getPort()

即使多个客户端来自同一IP,操作系统也会为每个连接分配不同的客户端端口,从而确保每个连接都是唯一的。

代码中的体现:

Socket clientSocket = serverSocket.accept();
System.out.printf("[%s:%d] 客户端上线!\n", 
    clientSocket.getInetAddress(),  
    clientSocket.getPort());        

服务端的代码解析

初始化阶段

public TcpEchoServer(int port) throws IOException {
    serverSocket = new ServerSocket(port);  // 在指定端口创建监听
}

启动服务器

public void start() throws IOException {
    ExecutorService executorService = Executors.newCachedThreadPool();
    
    while (true) {
        // 阻塞等待客户端连接
        Socket clientSocket = serverSocket.accept();
        
        // 为每个客户端创建新线程处理
        executorService.submit(() -> {
            processConnection(clientSocket);
        });
    }
}

处理客户端连接

private void processConnection(Socket clientSocket) {
    try (InputStream inputStream = clientSocket.getInputStream();
         OutputStream outputStream = clientSocket.getOutputStream()) {
        
        Scanner scanner = new Scanner(inputStream);  // 读取客户端数据
        PrintWriter writer = new PrintWriter(outputStream);  // 向客户端写数据
        
        while (true) {
            if (!scanner.hasNext()) break;  // 检查连接是否断开
            
            String request = scanner.next();    // 读取请求
            String response = process(request); // 处理请求(这里简单回显)
            writer.println(response);           // 发送响应
            writer.flush();                     // 强制刷新缓冲区
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        clientSocket.close();  // 关闭连接
    }
}

客户端代码解析

连接服务器

public TcpEchoClient(String serverIp, int serverPort) throws IOException {
    socket = new Socket(serverIp, serverPort);  // 连接到服务器
}

启动客户端

public void start() {
    try (InputStream inputStream = socket.getInputStream();
         OutputStream outputStream = socket.getOutputStream()) {
        
        Scanner scannerNet = new Scanner(inputStream);  // 读取服务器响应
        PrintWriter writer = new PrintWriter(outputStream);  // 向服务器发送数据
        Scanner scanner = new Scanner(System.in);  // 读取用户输入
        
        while (true) {
            String request = scanner.next();  // 从控制台读取用户输入
            writer.println(request);          // 发送给服务器
            writer.flush();                   // 强制发送
            
            String response = scannerNet.next();  // 读取服务器响应
            System.out.println(response);         // 显示响应
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

完整的通信流程

1. 建立连接

客户端:new Socket("127.0.0.1", 9090)
服务端:serverSocket.accept() 返回新Socket

2. 数据交换循环

用户输入 → 客户端发送 → 服务端接收 → 服务端处理 → 服务端发送 → 客户端接收 → 显示给用户

3.连接终止

客户端断开连接时,scanner.hasNext() 返回 false

服务端关闭对应的 clientSocket

scannerNet.next() 的阻塞行为:

  1. 检查缓冲区:先检查Scanner内部缓冲区是否有数据

  2. 系统调用:如果缓冲区空,发起read()系统调用

  3. 内核等待:操作系统挂起当前线程,等待网络数据

  4. 数据到达:服务器数据到达,操作系统唤醒线程

  5. 数据拷贝:从内核缓冲区拷贝到用户空间

  6. 返回结果:Scanner返回解析后的字符串

针对inputStream再套一层的原因:

1.inputStream是字节流,字节流的操作繁琐

// 如果不套Scanner,需要这样读取:
InputStream inputStream = clientSocket.getInputStream();
byte[] buffer = new byte[1024];
int len = inputStream.read(buffer);  // 读取字节
String request = new String(buffer, 0, len);  // 转换为字符串
// 还要处理分包、粘包等问题

2.使用scanner的优势

Scanner scanner = new Scanner(inputStream);
String request = scanner.next();  // 自动按空格/换行分割

Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐