黑马JAVA+AI 加强14-2 网络编程-UDP和TCP通信-线程和线程池优化通信-BS架构原理

1.UDP通信实现

1.1 UDP 通信实现

  • 以下是 UDP 通信的 Java 代码案例,包含客户端和服务端,并附有详细注释:

UDP 服务端代码

java 复制代码
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class UDPServer {
    public static void main(String[] args) throws Exception {
        // 创建服务端 DatagramSocket 对象,指定端口号为 8888
        DatagramSocket serverSocket = new DatagramSocket(8888);
        System.out.println("UDP 服务端已启动,监听端口 8888...");

        // 准备字节数组用于接收数据,长度设为 1024
        byte[] receiveData = new byte[1024];
        // 创建用于接收数据的 DatagramPacket 对象
        DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);

        // 接收客户端发送的数据
        serverSocket.receive(receivePacket);
        // 将接收到的字节数据转换为字符串
        String clientMessage = new String(receivePacket.getData(), 0, receivePacket.getLength());
        // 获取客户端的 IP 地址
        InetAddress clientAddress = receivePacket.getAddress();
        // 获取客户端的端口号
        int clientPort = receivePacket.getPort();
        System.out.println("收到客户端 " + clientAddress + ":" + clientPort + " 的消息:" + clientMessage);

        // 准备要发送给客户端的响应数据
        String response = "服务端已收到你的消息,这是我的回复!";
        byte[] sendData = response.getBytes();
        // 创建要发送的 DatagramPacket 对象,指定客户端的 IP 和端口
        DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, clientAddress, clientPort);
        // 发送响应数据给客户端
        serverSocket.send(sendPacket);
        System.out.println("已向客户端发送响应:" + response);

        // 关闭 DatagramSocket
        serverSocket.close();
    }
}

UDP 客户端代码

java 复制代码
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class UDPClient {
    public static void main(String[] args) throws Exception {
        // 创建客户端 DatagramSocket 对象,系统随机分配端口
        DatagramSocket clientSocket = new DatagramSocket();
        // 服务端的 IP 地址(这里用本地回环地址表示本机服务端)
        InetAddress serverAddress = InetAddress.getByName("localhost");
        // 服务端的端口号,要和服务端指定的一致
        int serverPort = 8888;

        // 准备要发送给服务端的消息
        String message = "你好,UDP 服务端!我是客户端~";
        byte[] sendData = message.getBytes();
        // 创建要发送的 DatagramPacket 对象,指定服务端的 IP 和端口
        DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, serverAddress, serverPort);
        // 发送消息给服务端
        clientSocket.send(sendPacket);
        System.out.println("已向服务端发送消息:" + message);

        // 准备字节数组用于接收服务端的响应,长度设为 1024
        byte[] receiveData = new byte[1024];
        // 创建用于接收响应的 DatagramPacket 对象
        DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
        // 接收服务端的响应
        clientSocket.receive(receivePacket);
        // 将接收到的字节数据转换为字符串
        String serverResponse = new String(receivePacket.getData(), 0, receivePacket.getLength());
        System.out.println("收到服务端的响应:" + serverResponse);

        // 关闭 DatagramSocket
        clientSocket.close();
    }
}

代码注释说明

  • DatagramSocket 类:

    • 服务端通过 new DatagramSocket(8888) 指定端口创建,用于监听客户端请求;客户端通过 new DatagramSocket() 随机分配端口创建,用于发起通信。
    • send(DatagramPacket dp) 方法用于发送数据包,receive(DatagramPacket p) 方法用于接收数据包。
  • DatagramPacket 类:

    • 发送数据时,使用 DatagramPacket(byte[] buf, int length, InetAddress address, int port) 构造器,指定数据字节数组、长度、目标 IP 和端口。
    • 接收数据时,使用 DatagramPacket(byte[] buf, int length) 构造器,指定用于存储数据的字节数组和长度。
  • 通信流程:客户端发送消息给服务端,服务端接收并回复,客户端再接收服务端的回复,完成一次 UDP 通信。

    2.TCP通信

  • 以下是 TCP 通信客户端的完整代码案例,并附带详细注释:

java 复制代码
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class TCPClient {
    public static void main(String[] args) throws IOException {
        // 1. 创建Socket对象,请求与服务端建立连接
        // host:服务端IP地址(这里用localhost表示本机服务端)
        // port:服务端监听的端口号(需与服务端保持一致,示例用8888)
        Socket socket = new Socket("localhost", 8888);
        System.out.println("客户端Socket创建成功,已与服务端建立连接");

        // 2. 获取字节输出流,用于向服务端发送数据
        OutputStream os = socket.getOutputStream();
        // 发送的消息内容
        String message = "你好,TCP服务端!我是客户端,这是我的消息~";
        // 将字符串转换为字节数组并发送
        os.write(message.getBytes());
        System.out.println("客户端已向服务端发送消息:" + message);

        // 3. 获取字节输入流,用于接收服务端的响应数据
        InputStream is = socket.getInputStream();
        // 准备字节数组存储接收的数据
        byte[] buffer = new byte[1024];
        // 读取服务端响应的数据
        int length = is.read(buffer);
        // 将字节数据转换为字符串
        String response = new String(buffer, 0, length);
        System.out.println("客户端收到服务端的响应:" + response);

        // 4. 关闭资源(顺序:先关闭流,再关闭Socket)
        is.close();
        os.close();
        socket.close();
        System.out.println("客户端资源已关闭");
    }
}
  • 优化上面代码
  • 以下是使用 DataOutputStream 实现的 TCP 客户端代码案例,DataOutputStream 可以更方便地发送各种基本数据类型(如字符串、整数等),无需手动转换字节数组,注释详细且清晰:
java 复制代码
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class TCPDataClient {
    public static void main(String[] args) {
        // 定义服务端IP和端口(本地测试用localhost,端口需与服务端一致)
        String serverIp = "localhost";
        int serverPort = 8888;
        Socket socket = null;
        DataOutputStream dos = null;
        InputStream is = null;

        try {
            // 1. 创建Socket,与服务端建立TCP连接(三次握手在此过程中自动完成)
            socket = new Socket(serverIp, serverPort);
            System.out.println("客户端已与服务端建立连接:" + socket);

            // 2. 获取输出流,并包装为DataOutputStream(方便发送各种数据类型)
            OutputStream os = socket.getOutputStream();
            dos = new DataOutputStream(os); // 包装字节输出流为数据输出流

            // 3. 使用DataOutputStream发送数据(支持多种数据类型,无需手动转字节)
            // 发送字符串(注意:writeUTF会自动处理字符串长度,服务端需用readUTF接收)
            dos.writeUTF("你好,服务端!这是用DataOutputStream发送的字符串~");
            // 发送整数
            dos.writeInt(2024); // 发送年份
            // 发送布尔值
            dos.writeBoolean(true); // 表示"消息发送完成"
            // 强制刷新缓冲区,确保数据立即发送(避免因缓冲区未满导致延迟)
            dos.flush();
            System.out.println("客户端已发送数据(字符串+整数+布尔值)");

            // 4. 接收服务端的响应(简单示例:接收字符串)
            is = socket.getInputStream();
            byte[] buffer = new byte[1024];
            int len = is.read(buffer); // 读取服务端返回的字节
            String response = new String(buffer, 0, len);
            System.out.println("收到服务端响应:" + response);

        } catch (IOException e) {
            e.printStackTrace(); // 捕获连接或IO异常(如服务端未启动、网络中断等)
        } finally {
            // 5. 关闭资源(后打开的先关闭,避免资源泄露)
            try {
                if (is != null) is.close(); // 关闭输入流
                if (dos != null) dos.close(); // 关闭数据输出流(会自动关闭包装的字节流)
                if (socket != null) socket.close(); // 关闭Socket(四次挥手在此过程中自动完成)
                System.out.println("客户端资源已关闭");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 服务端
  • 以下是配合客户端(使用 DataOutputStream)的 TCP 服务端代码案例,服务端通过 DataInputStream 接收客户端发送的多种数据类型,并使用 DataOutputStream 回复,注释详细:
java 复制代码
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPDataServer {
    public static void main(String[] args) {
        // 服务端监听的端口(需与客户端连接的端口一致)
        int port = 8888;
        ServerSocket serverSocket = null;
        Socket socket = null;

        try {
            // 1. 创建ServerSocket,注册端口并启动服务端
            serverSocket = new ServerSocket(port);
            System.out.println("TCP服务端已启动,监听端口:" + port + "(等待客户端连接...)");

            // 2. 阻塞等待客户端连接(accept()会一直等待,直到有客户端连接成功)
            // 连接成功后返回Socket对象,代表与该客户端的通信通道
            socket = serverSocket.accept();
            System.out.println("客户端已连接:" + socket.getInetAddress() + ":" + socket.getPort());

            // 3. 获取输入流,包装为DataInputStream(接收客户端用DataOutputStream发送的数据)
            InputStream is = socket.getInputStream();
            DataInputStream dis = new DataInputStream(is);

            // 4. 接收客户端发送的多种数据类型(需与客户端发送顺序一致)
            // 接收字符串(对应客户端的writeUTF(),必须用readUTF()读取)
            String clientMsg = dis.readUTF();
            // 接收整数(对应客户端的writeInt())
            int year = dis.readInt();
            // 接收布尔值(对应客户端的writeBoolean())
            boolean isCompleted = dis.readBoolean();

            // 打印接收的数据
            System.out.println("收到客户端数据:");
            System.out.println("字符串消息:" + clientMsg);
            System.out.println("整数(年份):" + year);
            System.out.println("布尔值(是否完成):" + isCompleted);

            // 5. 向客户端发送响应(使用DataOutputStream)
            OutputStream os = socket.getOutputStream();
            DataOutputStream dos = new DataOutputStream(os);
            // 发送响应字符串
            dos.writeUTF("服务端已收到所有数据!字符串、整数、布尔值均解析成功~");
            dos.flush(); // 立即发送
            System.out.println("已向客户端发送响应");

        } catch (IOException e) {
            e.printStackTrace(); // 处理连接或IO异常(如端口被占用、客户端断开等)
        } finally {
            // 6. 关闭资源(先关闭客户端Socket,再关闭ServerSocket)
            try {
                if (socket != null) socket.close(); // 关闭与客户端的连接(触发四次挥手)
                if (serverSocket != null) serverSocket.close(); // 关闭服务端
                System.out.println("服务端资源已关闭");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

总结

  • 用 DataOutputStream 和 DataInputStream 进行 TCP 通信时,必须满足 "类型严格匹配、顺序严格一致、数量严格相等" 这三个条件,否则会直接导致数据解析错误或程序崩溃。
    核心规则(一句话总结):
    发送方用 writeXXX() 发什么类型、按什么顺序发、发多少个,接收方就必须用 readXXX() 按相同顺序、相同数量、相同类型接收。
  • 多发多收-不能关服务器

    2.1 如何支持多个客户端的同时通信
  • 原理
    1. 服务端主类(负责接收连接并创建子线程)
java 复制代码
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServer {
    public static void main(String[] args) throws IOException {
        // 1. 创建ServerSocket,绑定端口(如8888)
        ServerSocket serverSocket = new ServerSocket(8888);
        System.out.println("服务端启动,监听端口8888,等待客户端连接...");

        // 2. 循环接收客户端连接(主线程核心逻辑)
        while (true) { 
            // 阻塞等待客户端连接,连接成功后返回Socket对象(与该客户端的通信通道)
            Socket socket = serverSocket.accept(); 
            // 打印客户端信息(IP和端口)
            System.out.println("新客户端连接:" + socket.getInetAddress() + ":" + socket.getPort());

            // 3. 为每个客户端创建独立子线程,负责处理与该客户端的通信
            new ClientHandlerThread(socket).start(); 
        }
    }
}
    1. 子线程类(负责与单个客户端通信)
java 复制代码
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;

// 自定义线程类,处理单个客户端的读写操作
class ClientHandlerThread extends Thread {
    private Socket socket; // 与当前客户端的连接通道

    // 构造方法:传入客户端Socket
    public ClientHandlerThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        DataInputStream dis = null;
        DataOutputStream dos = null;

        try {
            // 1. 获取输入流(读客户端消息)和输出流(向客户端发消息)
            dis = new DataInputStream(socket.getInputStream());
            dos = new DataOutputStream(socket.getOutputStream());

            // 2. 循环与客户端通信(直到客户端断开连接)
            while (true) {
                // 读取客户端发送的消息(这里以字符串为例)
                String clientMsg = dis.readUTF(); 
                System.out.println("收到客户端[" + socket.getPort() + "]的消息:" + clientMsg);

                // 向客户端发送响应
                String response = "服务端已收到:" + clientMsg;
                dos.writeUTF(response);
                dos.flush();
            }

        } catch (IOException e) {
            // 客户端断开连接时会触发异常(如关闭窗口),这里捕获并处理
            System.out.println("客户端[" + socket.getPort() + "]已断开连接");
        } finally {
            // 3. 关闭资源(与该客户端的连接)
            try {
                if (dis != null) dis.close();
                if (dos != null) dos.close();
                if (socket != null) socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
    1. 客户端类(可同时启动多个实例模拟多客户端)
java 复制代码
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;

public class TCPClient {
    public static void main(String[] args) throws IOException {
        // 连接服务端(IP为localhost,端口8888)
        Socket socket = new Socket("localhost", 8888);
        System.out.println("客户端已连接服务端");

        // 获取输入流(读服务端响应)和输出流(向服务端发消息)
        DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
        DataInputStream dis = new DataInputStream(socket.getInputStream());
        Scanner scanner = new Scanner(System.in);

        // 循环发送消息给服务端
        while (true) {
            System.out.print("请输入要发送的消息(输入exit退出):");
            String msg = scanner.nextLine();
            if ("exit".equals(msg)) break;

            // 发送消息
            dos.writeUTF(msg);
            dos.flush();

            // 接收服务端响应
            String response = dis.readUTF();
            System.out.println("服务端响应:" + response);
        }

        // 关闭资源
        scanner.close();
        dis.close();
        dos.close();
        socket.close();
        System.out.println("客户端已断开连接");
    }
}

3.BS架构,客户端就是浏览器,无需开发

  • 开发服务端

3.1以下是模拟 BS 架构中 HTTP 响应格式的服务端代码(用 Java 实现),模拟 Web 服务器向浏览器发送符合 HTTP 协议的响应数据,包含详细注释说明 HTTP 响应格式的每一部分:

  • 模拟 Web 服务器代码(核心:按 HTTP 协议格式返回数据)
java 复制代码
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class SimpleWebServer {
    public static void main(String[] args) throws Exception {
        // 1. 创建服务器Socket,监听8080端口(HTTP默认端口是80,这里用8080方便本地测试)
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("简易Web服务器启动,监听端口8080,等待浏览器连接...");

        while (true) {
            // 2. 等待浏览器(客户端)连接(类似网页访问)
            Socket socket = serverSocket.accept();
            System.out.println("浏览器已连接:" + socket.getInetAddress());

            // 3. 获取输出流,向浏览器发送HTTP响应(必须严格遵循HTTP协议格式)
            OutputStream os = socket.getOutputStream();

            // 4. 构建HTTP响应内容(分三部分:状态行 + 响应头 + 响应正文)
            // 注意:HTTP协议要求每行结尾必须是 "\r\n"(回车换行),且响应头与正文之间必须有一个单独的 "\r\n"

            // 4.1 状态行:协议版本 状态码 状态描述(第一行固定格式)
            String statusLine = "HTTP/1.1 200 OK\r\n"; // HTTP/1.1是协议版本,200是成功状态码,OK是描述

            // 4.2 响应头:键值对格式,说明响应数据的类型、长度等信息(可多个)
            // Content-Type:告诉浏览器响应正文的类型(text/html表示HTML内容,charset=UTF-8确保中文不乱码)
            String contentType = "Content-Type: text/html; charset=UTF-8\r\n";
            // 可选:Content-Length(响应正文的字节长度,浏览器可据此判断数据是否接收完整)
            String contentLength = "Content-Length: 156\r\n"; // 这里156是下面HTML内容的大致字节数

            // 4.3 空行:响应头结束的标志(必须有,否则浏览器无法区分头和正文)
            String emptyLine = "\r\n";

            // 4.4 响应正文:真正展示给用户的内容(这里是HTML代码,浏览器会解析并显示)
            String content = "<html>\n" +
                             "  <head><title>BS架构示例</title></head>\n" +
                             "  <body>\n" +
                             "    <h1 style='color: blue; text-align: center'>欢迎访问我的网页</h1>\n" +
                             "    <p>这是遵循HTTP协议的响应内容</p>\n" +
                             "    <p>当前时间:" + System.currentTimeMillis() + "</p>\n" +
                             "  </body>\n" +
                             "</html>";

            // 5. 拼接完整响应数据,按HTTP格式发送给浏览器
            String response = statusLine + contentType + contentLength + emptyLine + content;
            os.write(response.getBytes("UTF-8")); // 用UTF-8编码发送,确保中文正常显示
            os.flush();

            // 6. 关闭连接(模拟短连接,一次请求后断开)
            socket.close();
            System.out.println("响应已发送,连接关闭");
        }
    }
}
运行结果
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 156

<html>
  <head><title>BS架构示例</title></head>
  <body>
    <h1 style='color: blue; text-align: center'>欢迎访问我的网页</h1>
    <p>这是遵循HTTP协议的响应内容</p>
    <p>当前时间:1620000000000</p>
  </body>
</html>


3.2 用线程池进行优化

  • 在 BS 架构中,使用线程池优化服务端的核心思路是:用线程池管理处理客户端请求的线程,避免频繁创建 / 销毁线程的开销,提高并发处理效率。以下是基于线程池优化的简易 Web 服务器代码,附带详细注释说明优化逻辑:
java 复制代码
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolWebServer {
    public static void main(String[] args) throws Exception {
        // 1. 创建线程池(核心优化点)
        // 这里用固定大小线程池,核心线程数设为3(可根据服务器性能调整)
        // 线程池会复用线程,避免每次连接都新建线程
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        System.out.println("线程池优化的Web服务器启动,线程池大小:3,监听端口8080...");

        // 2. 创建ServerSocket,监听端口
        ServerSocket serverSocket = new ServerSocket(8080);

        while (true) {
            // 3. 主线程接收浏览器连接(不变)
            Socket socket = serverSocket.accept();
            System.out.println("新浏览器连接:" + socket.getInetAddress());

            // 4. 将客户端处理任务提交给线程池(核心优化)
            // 线程池会从池中取空闲线程执行任务,若无空闲线程则放入任务队列等待
            threadPool.execute(new ClientHandler(socket));
        }
    }

    // 处理单个客户端请求的任务类(实现Runnable,供线程池调用)
    static class ClientHandler implements Runnable {
        private Socket socket;

        public ClientHandler(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            OutputStream os = null;
            try {
                // 5. 按HTTP协议格式向浏览器发送响应(逻辑与之前一致)
                os = socket.getOutputStream();

                // 构建HTTP响应(状态行+响应头+空行+正文)
                String statusLine = "HTTP/1.1 200 OK\r\n";
                String contentType = "Content-Type: text/html; charset=UTF-8\r\n";
                String emptyLine = "\r\n";
                String content = "<html>\n" +
                                 "  <body>\n" +
                                 "    <h1>线程池优化的响应</h1>\n" +
                                 "    <p>处理当前请求的线程:" + Thread.currentThread().getName() + "</p>\n" +
                                 "  </body>\n" +
                                 "</html>";

                // 拼接响应并发送
                String response = statusLine + contentType + emptyLine + content;
                os.write(response.getBytes("UTF-8"));
                os.flush();

            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 6. 关闭连接(每个任务独立释放资源)
                try {
                    if (os != null) os.close();
                    if (socket != null) socket.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}


相关推荐
金銀銅鐵6 小时前
[Java] 浅析 Map.of(...) 方法和 Map.ofEntries(...) 方法
java·后端
邪恶紫色秋裤6 小时前
解决IntelliJ IDEA控制台输出中文乱码问题
java·ide·乱码·intellij-idea·报错·中文
摇滚侠6 小时前
Spring Boot3零基础教程,StreamAPI 的基本用法,笔记99
java·spring boot·笔记
代码匠心6 小时前
从零开始学Flink:事件驱动
java·大数据·flink·大数据处理
yours_Gabriel6 小时前
【分布式事务】Seata分布式解决方案
java·分布式·微服务
一只游鱼6 小时前
Springboot+BannerBanner(启动横幅)
java·开发语言·数据库
codingPower6 小时前
升级mybatis-plus导致项目启动报错: net.sf.jsqlparser.statement.select.SelectBody
java·spring boot·maven·mybatis
Mr. zhihao7 小时前
Java 反序列化中的 boolean vs Boolean 陷阱:一个真实的 Bug 修复案例
java·bug·lua
Elieal7 小时前
Spring 框架IOC和AOP
java·数据库·spring