从 0 到 1 掌握 Java 网络通信:UDP/TCP 实战教程

45 阅读9分钟

从 0 到 1 掌握 Java 网络通信:UDP/TCP 实战教程

在Java开发中,网络通信是核心技能之一,而UDP(用户数据报协议)和TCP(传输控制协议)作为TCP/IP协议族中最常用的两种通信协议,分别适用于不同的业务场景。本文将结合完整代码示例,从协议特点、核心API、实战实现到应用场景,全面拆解UDP与TCP的Java编程实践,帮助开发者快速掌握两种协议的开发技巧。

一、协议核心区别:UDP vs TCP

在开始编码前,我们先明确两种协议的本质差异,这是选择合适通信方式的关键:

对比维度UDPTCP
连接方式无连接面向连接(三次握手建立连接)
可靠性不可靠(发送后不确认,可能丢包、乱序)可靠(重传机制、流量控制、拥塞控制)
数据限制单数据包最大64KB无数据大小限制(基于流传输)
通信效率高(无连接开销,延迟低)中等(连接建立/释放有开销)
适用场景实时通信(直播、游戏、语音)、广播通知可靠传输(文件传输、网页访问、登录认证)

Java为两种协议提供了专门的API支持:UDP依赖DatagramSocketDatagramPacket,TCP依赖SocketServerSocket

二、UDP通信实战:无连接的快速传输

UDP通信的核心是"数据包",发送端直接封装数据、目标地址和端口后发送,接收端被动监听端口并接收数据包,无需提前建立连接。

1. 核心API解析

  • DatagramSocket:用于创建通信端点(客户端/服务端),提供发送(send())和接收(receive())数据包的方法。

    • 客户端构造器:DatagramSocket()(系统随机分配端口)
    • 服务端构造器:DatagramSocket(int port)(绑定固定端口,监听请求)
  • DatagramPacket:封装待发送或接收的数据,包含数据字节数组、长度、目标地址(发送时)等信息。

    • 发送数据包:DatagramPacket(byte[] buf, int length, InetAddress address, int port)
    • 接收数据包:DatagramPacket(byte[] buf, int length)(指定接收缓冲区)

2. 实战1:UDP一发一收(基础版)

适用于单次数据传输场景,如传感器数据上报、简单指令下发。

客户端代码(发送数据)
package com.wmh.demo2udp1;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class UDPClientDemo1 {
    public static void main(String[] args) throws Exception {
        System.out.println("===客户端启动===");
        // 1. 创建发送端通信端点(随机端口)
        DatagramSocket socket = new DatagramSocket();
        
        // 2. 封装发送数据(字节数组形式,指定目标IP和端口)
        byte[] sendData = "是客户端,约你啤酒、龙虾、小烧烤".getBytes();
        DatagramPacket packet = new DatagramPacket(
            sendData, 
            sendData.length, 
            InetAddress.getLocalHost(), // 目标IP(本地测试)
            8080 // 服务端端口
        );
        
        // 3. 发送数据包
        socket.send(packet);
        
        // 4. 关闭资源
        socket.close();
    }
}
服务端代码(接收数据)
package com.wmh.demo2udp1;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class UDPServerDemo2 {
    public static void main(String[] args) throws Exception {
        System.out.println("===服务端启动了===");
        // 1. 创建接收端通信端点(绑定8080端口)
        DatagramSocket socket = new DatagramSocket(8080);
        
        // 2. 创建接收缓冲区(最大64KB,符合UDP协议限制)
        byte[] receiveBuf = new byte[1024 * 64];
        DatagramPacket packet = new DatagramPacket(receiveBuf, receiveBuf.length);
        
        // 3. 阻塞等待接收数据(无数据时会一直等待)
        socket.receive(packet);
        
        // 4. 解析数据(获取有效数据长度,避免缓冲区冗余)
        int dataLength = packet.getLength();
        String receiveData = new String(receiveBuf, 0, dataLength);
        System.out.println("服务端收到了:" + receiveData);
        
        // 5. 获取发送端信息(IP+端口)
        String clientIp = packet.getAddress().getHostAddress();
        int clientPort = packet.getPort();
        System.out.println("发送端IP:" + clientIp + ",端口:" + clientPort);
        
        // 6. 关闭资源
        socket.close();
    }
}

3. 实战2:UDP多发多收(交互版)

适用于持续通信场景,如聊天工具、实时监控,通过循环实现多次数据传输。

客户端代码(支持多次输入)
package com.wmh.demo3udp2;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;

public class UDPClientDemo1 {
    public static void main(String[] args) throws Exception {
        System.out.println("===客户端启动===");
        DatagramSocket socket = new DatagramSocket();
        Scanner scanner = new Scanner(System.in);
        
        while (true) {
            // 循环读取用户输入
            System.out.println("请输入要发送的内容(输入exit退出):");
            String content = scanner.nextLine();
            
            // 退出条件判断
            if ("exit".equals(content)) {
                System.out.println("客户端退出了...");
                socket.close();
                break;
            }
            
            // 封装并发送数据
            byte[] sendData = content.getBytes();
            DatagramPacket packet = new DatagramPacket(
                sendData, 
                sendData.length, 
                InetAddress.getLocalHost(), 
                8080
            );
            socket.send(packet);
        }
        scanner.close();
    }
}
服务端代码(持续接收)
package com.wmh.demo3udp2;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class UDPServerDemo2 {
    public static void main(String[] args) throws Exception {
        System.out.println("===服务端启动了===");
        DatagramSocket socket = new DatagramSocket(8080);
        byte[] receiveBuf = new byte[1024 * 64];
        DatagramPacket packet = new DatagramPacket(receiveBuf, receiveBuf.length);
        
        while (true) {
            // 持续阻塞接收数据
            socket.receive(packet);
            
            // 解析数据和发送端信息
            int dataLength = packet.getLength();
            String receiveData = new String(receiveBuf, 0, dataLength);
            String clientIp = packet.getAddress().getHostAddress();
            int clientPort = packet.getPort();
            
            // 打印接收结果
            System.out.println("服务端收到了:" + receiveData);
            System.out.println("发送端IP:" + clientIp + ",端口:" + clientPort);
            System.out.println("---------------------------------------");
        }
    }
}

三、TCP通信实战:可靠的面向连接传输

TCP通信基于"流"传输,需先通过三次握手建立可靠连接,再进行数据交互,适用于对数据完整性要求高的场景。

1. 核心API解析

  • Socket:客户端通信端点,用于与服务端建立连接并传输数据。

    • 构造器:Socket(String host, int port)(指定服务端IP和端口,发起连接)
    • 核心方法:getOutputStream()(获取发送流)、getInputStream()(获取接收流)
  • ServerSocket:服务端监听端点,用于绑定端口、接收客户端连接请求。

    • 构造器:ServerSocket(int port)(绑定固定端口)
    • 核心方法:accept()(阻塞等待客户端连接,返回与该客户端对应的Socket对象)

2. 实战1:TCP一发一收(基础版)

适用于单次可靠数据传输,如文件片段上传、接口请求响应。

客户端代码(发送数据)
package com.wmh.demo4tcp1;
import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.Socket;

public class ClientDemo1 {
    public static void main(String[] args) throws Exception {
        System.out.println("客户端启动了...");
        // 1. 与服务端建立连接(三次握手)
        Socket socket = new Socket("127.0.0.1", 9999);
        
        // 2. 获取输出流(发送数据)
        OutputStream os = socket.getOutputStream();
        // 包装为数据输出流,支持直接写入基本类型和字符串
        DataOutputStream dos = new DataOutputStream(os);
        
        // 3. 发送数据(先写int,再写字符串)
        dos.writeInt(1); // 发送标识ID
        dos.writeUTF("我想你了,你在哪儿?"); // 发送文本数据
        
        // 4. 关闭资源(四次挥手释放连接)
        socket.close();
    }
}
服务端代码(接收数据)
package com.wmh.demo4tcp1;
import java.io.DataInputStream;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerDemo2 {
    public static void main(String[] args) throws Exception {
        System.out.println("服务端启动了...");
        // 1. 绑定端口,监听客户端连接
        ServerSocket serverSocket = new ServerSocket(9999);
        
        // 2. 阻塞等待客户端连接(返回与该客户端对应的Socket)
        Socket socket = serverSocket.accept();
        
        // 3. 获取输入流(接收数据)
        InputStream is = socket.getInputStream();
        DataInputStream dis = new DataInputStream(is);
        
        // 4. 读取数据(需与发送顺序一致)
        int id = dis.readInt();
        String msg = dis.readUTF();
        
        // 5. 打印结果和客户端信息
        System.out.println("id=" + id + ",收到的客户端msg=:" + msg);
        System.out.println("客户端IP=" + socket.getInetAddress().getHostAddress());
        System.out.println("客户端端口=" + socket.getPort());
        
        // 6. 关闭资源
        serverSocket.close();
        socket.close();
    }
}

3. 实战2:TCP多发多收(交互版)

适用于持续可靠通信,如即时通讯、远程控制。

客户端代码(循环输入)
package com.wmh.demo5tcp2;
import java.io.DataOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

public class ClientDemo1 {
    public static void main(String[] args) throws Exception {
        System.out.println("客户端启动了...");
        Socket socket = new Socket("127.0.0.1", 9999);
        OutputStream os = socket.getOutputStream();
        DataOutputStream dos = new DataOutputStream(os);
        Scanner scanner = new Scanner(System.in);
        
        while (true) {
            System.out.println("请说(输入exit退出):");
            String msg = scanner.nextLine();
            
            if ("exit".equals(msg)) {
                System.out.println("退出成功");
                dos.close();
                socket.close();
                break;
            }
            
            // 发送数据并刷新流(避免数据缓存)
            dos.writeUTF(msg);
            dos.flush();
        }
        scanner.close();
    }
}
服务端代码(循环接收)
package com.wmh.demo5tcp2;
import java.io.DataInputStream;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerDemo2 {
    public static void main(String[] args) throws Exception {
        System.out.println("服务端启动了...");
        ServerSocket serverSocket = new ServerSocket(9999);
        Socket socket = serverSocket.accept();
        InputStream is = socket.getInputStream();
        DataInputStream dis = new DataInputStream(is);
        
        while (true) {
            // 阻塞读取客户端数据
            String msg = dis.readUTF();
            System.out.println("收到的客户端msg=:" + msg);
            System.out.println("客户端IP=" + socket.getInetAddress().getHostAddress());
            System.out.println("客户端端口=" + socket.getPort());
            System.out.println("-------------------------");
        }
    }
}

4. 实战3:TCP多客户端并发处理(进阶版)

上述服务端仅支持单个客户端连接,实际应用中需处理多客户端并发,解决方案是为每个客户端分配独立子线程

服务端核心改造(子线程处理多客户端)
// 子线程类:负责处理单个客户端的通信
package com.wmh.demo6tcp3;
import java.io.DataInputStream;
import java.io.InputStream;
import java.net.Socket;

public class ServerRead extends Thread {
    private Socket socket; // 单个客户端的Socket连接
    
    public ServerRead(Socket socket) {
        this.socket = socket;
    }
    
    @Override
    public void run() {
        try {
            InputStream is = socket.getInputStream();
            DataInputStream dis = new DataInputStream(is);
            
            while (true) {
                String msg = dis.readUTF();
                // 打印当前客户端的消息
                System.out.println("收到客户端[" + socket.getInetAddress().getHostAddress() + "]的msg=:" + msg);
                System.out.println("客户端端口=" + socket.getPort());
                System.out.println("-------------------------");
            }
        } catch (Exception e) {
            System.out.println("客户端[" + socket.getInetAddress().getHostAddress() + "]下线了");
        }
    }
}

// 主服务端:监听连接并分配子线程
package com.wmh.demo6tcp3;
import java.net.ServerSocket;
import java.net.Socket;

public class ServerDemo2 {
    public static void main(String[] args) throws Exception {
        System.out.println("服务端启动了...");
        ServerSocket serverSocket = new ServerSocket(9999);
        
        while (true) {
            // 持续接收客户端连接
            Socket socket = serverSocket.accept();
            System.out.println("一个客户端上线了:" + socket.getInetAddress().getHostAddress());
            
            // 为每个客户端分配独立子线程处理
            new ServerRead(socket).start();
        }
    }
}

5. 实战4:TCP线程池优化

多线程方案在高并发时会创建大量线程,导致系统资源耗尽。通过线程池复用线程,可提升系统稳定性和性能。

线程池优化版服务端
// 任务类:实现Runnable接口,封装客户端通信逻辑
package com.wmh.demo7tcp4;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;

public class ServerReaderRunnable implements Runnable {
    private Socket socket;
    
    public ServerReaderRunnable(Socket socket) {
        this.socket = socket;
    }
    
    @Override
    public void run() {
        try {
            // 响应HTTP格式数据(模拟BS架构)
            OutputStream os = socket.getOutputStream();
            PrintStream ps = new PrintStream(os);
            
            // 必须遵循HTTP协议格式,否则浏览器无法识别
            ps.println("HTTP/1.1 200 OK");
            ps.println("Content-Type:text/html;charset=utf-8");
            ps.println(); // 空行分隔响应头和响应体
            ps.println("<html><body><h1 style='color:red'>欢迎访问TCP服务端</h1></body></html>");
            
            ps.close();
            socket.close();
        } catch (Exception e) {
            System.out.println("客户端[" + socket.getInetAddress().getHostAddress() + "]下线了");
        }
    }
}

// 线程池服务端
package com.wmh.demo7tcp4;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;

public class ServerDemo {
    public static void main(String[] args) throws Exception {
        System.out.println("服务端启动了...");
        ServerSocket serverSocket = new ServerSocket(8080);
        
        // 创建线程池(核心3线程,最大10线程,队列容量100)
        ExecutorService threadPool = new ThreadPoolExecutor(
            3, 
            10, 
            10, 
            TimeUnit.SECONDS, 
            new ArrayBlockingQueue<>(100),
            new ThreadPoolExecutor.AbortPolicy() // 队列满时拒绝新任务
        );
        
        while (true) {
            Socket socket = serverSocket.accept();
            System.out.println("一个客户端上线了:" + socket.getInetAddress().getHostAddress());
            
            // 将客户端通信任务提交给线程池
            threadPool.execute(new ServerReaderRunnable(socket));
        }
    }
}

四、关键注意事项与最佳实践

  1. 资源关闭:UDP的DatagramSocket、TCP的SocketServerSocket需手动关闭,避免端口占用。
  2. 流刷新:TCP的输出流需调用flush(),确保数据即时发送(避免缓存)。
  3. 数据边界:UDP数据包天然带边界(一次发送对应一次接收),TCP是流传输,需自定义边界(如固定长度、分隔符)。
  4. 异常处理:网络通信可能出现断连、超时等异常,需捕获并处理,避免程序崩溃。
  5. 端口选择:避免使用0-1023的系统端口,选择1024-65535之间的端口。

五、总结

  • UDP:无连接、高效率、不可靠,适合实时性要求高的场景(直播、游戏、物联网数据上报)。
  • TCP:面向连接、可靠传输、支持流传输,适合对数据完整性要求高的场景(文件传输、接口通信、网页服务)。
  • 高并发场景下,TCP服务端需使用线程池优化,避免线程爆炸;UDP需注意数据包大小限制(64KB)和数据校验。

通过本文的代码示例和解析,相信你已掌握Java中UDP与TCP通信的核心实现。实际开发中,需根据业务场景选择合适的协议,并结合线程池、异常处理等机制,打造高效稳定的网络应用。

提示:在实际部署中,建议使用NIO(非阻塞IO)或Netty等高性能网络框架替代原生Socket编程,以获得更好的性能和可维护性。