黑马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();
                }
            }
        }
    }
}


相关推荐
xcl09256 小时前
北京24小时自助健身房系统软件开发实战:从架构到部署全流程指南
java·spring boot·架构
一 乐7 小时前
动漫书销售商城|基于springboot + vue动漫书销售商城(源码+数据库+文档)
java·数据库·vue.js·spring boot·毕业设计
卓怡学长8 小时前
w214基于jsp知道特产网
java·intellij-idea
步行cgn8 小时前
Spring 注解使用详解
java·spring
一条小小yu8 小时前
Spring IoC的理解
java·后端·spring
落魄实习生8 小时前
Agent Scope Java 2.x 系列【7】工具使用
java·开发语言·ai
旺仔学长 哈哈9 小时前
springboot钓鱼爱好者交流平台APP设计与实现
java·spring boot·mysql·充电桩管理系统
自强的小白10 小时前
核心功能(Service接口)
java·mybatis
乌暮11 小时前
深入理解 Java 泛型:把「万能盒子」用对、用稳
java·开发语言·后端·学习