源本科技 | 码上会

Java TCP

2026/03/04
34
0

引言

TCP (Transmission Control Protocol,传输控制协议) 是互联网协议族(TCP/IP)中的核心协议之一。它是一种面向连接可靠基于字节流的传输层通信协议。

  • 面向连接:在数据传输前,必须通过“三次握手”建立连接,确保通信双方都准备好。

  • 可靠性:通过确认机制、重传机制、流量控制和拥塞控制,保证数据无差错、不丢失、不重复且按序到达。

  • 点对点通信:一条 TCP 连接只能有两个端点(一个客户端,一个服务端)。

  • 全双工通信:连接建立后,双方可以同时发送和接收数据。

注意:在 Java 中,只要使用 java.net.Socket 类进行通信,底层默认使用的就是 TCP 协议。如果需要 UDP 协议,则需使用 DatagramSocket


TCP 客户端

客户端是主动发起连接的一方。在 Java 中,通过 java.net.Socket 类来实现。

关键方法

数据流操作

TCP 通信的本质是IO 流的读写。

  • getInputStream(): 获取输入流。用于接收服务端发回的数据。

  • getOutputStream(): 获取输出流。用于发送数据给服务端。

连接管理

  • connect(SocketAddress endpoint): 将未连接的 Socket 连接到远程主机。可配合 Socket 无参构造使用。

  • close(): 关闭 Socket 连接,释放系统资源。务必在 finally 块或使用 try-with-resources 中调用

  • isConnected(): 检查 Socket 是否已成功连接。

信息查询

  • getInetAddress(): 获取远程主机的 IP 地址。

  • getPort(): 获取远程主机的端口号。

  • getLocalAddress(): 获取本地主机的 IP 地址。

  • getLocalPort(): 获取本地主机的端口号(通常由系统随机分配)。

TCP 客户端

以下示例演示了如何连接服务器并发送一条消息。

import java.io.OutputStream;
import java.net.Socket;

public class TCPClient {
    public static void main(String[] args) {
        // 使用 try-with-resources 自动关闭资源,避免资源泄露
        try (Socket socket = new Socket("127.0.0.1", 12345);
             OutputStream outputStream = socket.getOutputStream()) {

            // 要发送的消息
            String message = "Hello, TCP Server!";
            byte[] data = message.getBytes();

            // 发送消息到服务器
            outputStream.write(data);
            // 重要:flush() 确保缓冲区的数据立即发送出去
            outputStream.flush();

            System.out.println("消息已发送:" + message);

        } catch (Exception e) {
            System.err.println("客户端发生错误:" + e.getMessage());
            e.printStackTrace();
        }
    }
}

优化点说明

  1. 使用了 try-with-resources 语法,自动关闭 SocketOutputStream,无需手动调用 close()

  2. 增加了 flush() 调用,确保数据立即发送,防止因缓冲区未满而滞留。

  3. 优化了异常处理信息的输出。


TCP 服务端

服务端是被动等待连接的一方。在 Java 中,通过 java.net.ServerSocket 类来监听端口。

关键方法

连接管理

  • accept(): 阻塞方法。侦听客户端连接请求。当有客户端连接时,返回一个新的 Socket 对象用于与该客户端通信,原 ServerSocket 继续监听其他请求。

  • close(): 关闭 ServerSocket,停止监听端口。

信息查询

  • getInetAddress(): 获取 ServerSocket 绑定的本地 IP 地址。

  • getLocalPort(): 获取 ServerSocket 绑定的本地端口号。

TCP 服务端

以下示例演示了如何启动服务、接受连接并接收客户端消息。

import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServer {
    public static void main(String[] args) {
        int serverPort = 12345;

        // 使用 try-with-resources 确保 ServerSocket 关闭
        try (ServerSocket serverSocket = new ServerSocket(serverPort)) {
            
            System.out.println("服务端已启动,监听端口:" + serverPort);
            System.out.println("等待客户端连接...");

            // accept() 是阻塞的,直到有客户端连接
            try (Socket clientSocket = serverSocket.accept();
                 InputStream inputStream = clientSocket.getInputStream()) {
                
                System.out.println("客户端已连接:" + clientSocket.getInetAddress());

                // 接收数据的缓冲区
                byte[] buffer = new byte[1024];
                int bytesRead;

                // 循环读取数据,直到流结束 (客户端关闭连接)
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    String message = new String(buffer, 0, bytesRead);
                    System.out.println("接收到消息:" + message);
                }
                
                System.out.println("客户端断开连接");
            }

        } catch (Exception e) {
            System.err.println("服务端发生错误:" + e.getMessage());
            e.printStackTrace();
        }
    }
}

常见问题

粘包与拆包问题

TCP 是面向字节流的协议,没有消息边界。

  • 现象:如果客户端连续发送两条短消息 "A" 和 "B",服务端可能会一次性收到 "AB",或者把一条长消息分成多次收到。

  • 解决方案:应用层需要定义协议。常见做法包括:

    • 固定长度:每条消息固定 N 个字节。

    • 特殊分隔符:如使用 \n\r\n 结尾(类似 HTTP 协议)。

    • 长度字段:在消息头包含消息体的长度(例如:前 4 个字节表示后续数据的长度)。

资源管理

  • 必须关闭流:网络资源(Socket、Stream)是有限的。忘记关闭会导致内存泄漏或端口耗尽(Too many open files)。

  • 推荐写法:始终使用 Java 7 引入的 try-with-resources 语句(如上例所示),它会自动调用 close() 方法。

多线程服务端

上面的服务端代码一次只能服务一个客户端。如果要支持多人聊天室或高并发服务,需要修改结构:

// 伪代码示例
while (true) {
    Socket clientSocket = serverSocket.accept(); // 接受连接
    // 为每个客户端启动一个新线程处理
    new Thread(() -> {
        handleClient(clientSocket);
    }).start();
}

端口占用问题

  • 如果运行服务端时报错 Address already in use,说明该端口已被其他程序占用,或者上一次运行的程序未正常关闭(处于 TIME_WAIT 状态)。

  • 解决:更换端口号,或等待操作系统释放端口,或在代码中设置 serverSocket.setReuseAddress(true)