【Java】TCP网络编程:从可靠传输到Socket实战

【Java】TCP网络编程:从可靠传输到Socket实战

Java 中的 TCP 网络编程是后端开发最基础、最重要的技能之一。它基于 TCP/IP 协议栈传输层 TCP,提供面向连接、可靠、有序、流量控制、拥塞控制的字节流传输。

本文从 TCP 的可靠传输核心机制讲起,一步步带你理解为什么 TCP 可靠,然后通过 Java Socket 实战代码,逐步实现从简单 echo 到多线程聊天室的完整过程。

1. TCP 为什么是“可靠传输”?核心机制一览

TCP 之所以被称为可靠传输协议,靠的是以下几大机制:

机制作用实现方式
三次握手建立可靠连接,确保双方都能收发数据SYN → SYN-ACK → ACK
序列号 + 确认应答保证数据有序到达,丢失/乱序可重传每个字节都有 seq,接收方回复 ack
超时重传发送方超时未收到 ACK 则重传RTO(重传超时时间)动态计算
滑动窗口实现流量控制,避免快发慢收接收窗口(advertised window)
拥塞控制避免网络拥塞(慢启动、拥塞避免、快速重传、快速恢复)拥塞窗口(cwnd)动态调整
校验和检测数据是否损坏TCP 头部 + 数据部分的 checksum
四次挥手安全关闭连接,确保双方数据都已确认FIN → ACK → FIN → ACK

三次握手简图(建立连接):

客户端               服务端
  |   SYN (seq=x)     |
  |------------------>|
  |   SYN-ACK (seq=y, ack=x+1) |
  |<------------------|
  |   ACK (ack=y+1)   |
  |------------------>|
     连接建立(全双工)

四次挥手简图(关闭连接):

一方                  另一方
  |   FIN (seq=m)     |
  |------------------>|
  |   ACK (ack=m+1)   |
  |<------------------|
  |   (可能继续发数据)|
  |   FIN (seq=n)     |
  |<------------------|
  |   ACK (ack=n+1)   |
  |------------------>|
     连接完全关闭

Java 层面:你不需要手动实现这些机制。
当你调用 new Socket(host, port) 时,操作系统内核的 TCP 协议栈自动完成三次握手;
调用 socket.close() 时自动发起四次挥手。

2. Java TCP 编程核心类

类名作用常用构造方法 / 方法
ServerSocket服务端监听套接字new ServerSocket(port)
accept()
Socket客户端 / 已连接的客户端套接字new Socket(host, port)
getInputStream()
getOutputStream()
InputStream从 Socket 读字节流BufferedReader / DataInputStream
OutputStream向 Socket 写字节流PrintWriter / DataOutputStream

注意:Socket 的流是全双工的,可以同时读写。

3. 实战一:最简单的单线程 Echo 服务器 & 客户端

服务器端(EchoServer.java)

import java.io.*;
import java.net.*;

public class EchoServer {
    public static void main(String[] args) {
        int port = 8888;
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("Echo Server 启动,监听端口:" + port);

            while (true) {
                // 阻塞等待客户端连接(三次握手在这里完成)
                Socket clientSocket = serverSocket.accept();
                System.out.println("客户端连接成功:" + clientSocket.getInetAddress());

                // 获取输入输出流
                try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                     PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {

                    String line;
                    while ((line = in.readLine()) != null) {
                        System.out.println("收到:" + line);
                        out.println("Echo: " + line);  // 回显
                        out.flush();
                    }
                } catch (IOException e) {
                    System.out.println("客户端断开:" + e.getMessage());
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端(EchoClient.java)

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

public class EchoClient {
    public static void main(String[] args) {
        String host = "localhost";
        int port = 8888;

        try (Socket socket = new Socket(host, port);
             PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
             BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
             Scanner scanner = new Scanner(System.in)) {

            System.out.println("已连接到服务器 " + host + ":" + port);
            System.out.println("输入消息(输入 quit 退出):");

            String userInput;
            while (!(userInput = scanner.nextLine()).equals("quit")) {
                out.println(userInput);
                out.flush();

                String response = in.readLine();
                System.out.println("服务器回复:" + response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

运行顺序:先运行 EchoServer,再运行多个 EchoClient 窗口测试。

4. 实战二:多线程版本聊天室(支持多个客户端)

服务器端(ChatServer.java)

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

public class ChatServer {
    private static final int PORT = 9999;
    private static final List<ClientHandler> clients = new ArrayList<>();

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(PORT);
        System.out.println("聊天室服务器启动,端口:" + PORT);

        while (true) {
            Socket clientSocket = serverSocket.accept();
            System.out.println("新用户加入:" + clientSocket.getInetAddress());

            ClientHandler handler = new ClientHandler(clientSocket);
            synchronized (clients) {
                clients.add(handler);
            }
            new Thread(handler).start();
        }
    }

    // 广播消息给所有客户端
    public static void broadcast(String message, ClientHandler sender) {
        synchronized (clients) {
            for (ClientHandler client : clients) {
                if (client != sender) {
                    client.sendMessage(message);
                }
            }
        }
    }

    // 移除断开客户端
    public static void removeClient(ClientHandler client) {
        synchronized (clients) {
            clients.remove(client);
        }
    }

    static class ClientHandler implements Runnable {
        private final Socket socket;
        private PrintWriter out;
        private String username;

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

        @Override
        public void run() {
            try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                 PrintWriter writer = new PrintWriter(socket.getOutputStream(), true)) {

                this.out = writer;

                // 读取用户名
                username = in.readLine();
                System.out.println(username + " 加入聊天室");
                broadcast(username + " 加入了聊天室", this);

                String message;
                while ((message = in.readLine()) != null) {
                    if ("quit".equalsIgnoreCase(message)) {
                        break;
                    }
                    String formatted = username + " : " + message;
                    System.out.println(formatted);
                    broadcast(formatted, this);
                }
            } catch (IOException e) {
                // 客户端异常断开
            } finally {
                System.out.println(username + " 离开聊天室");
                broadcast(username + " 离开了聊天室", this);
                ChatServer.removeClient(this);
                try {
                    socket.close();
                } catch (IOException ignored) {}
            }
        }

        public void sendMessage(String msg) {
            if (out != null) {
                out.println(msg);
                out.flush();
            }
        }
    }
}

客户端(ChatClient.java)与上面类似,修改为:

  • 先输入用户名
  • 循环发送消息,支持 quit 退出

5. 常见问题与最佳实践

  • 粘包 / 半包:TCP 是字节流,无边界。解决方法:
  • 定长协议
  • 特殊分隔符(如 \n
  • 长度前缀(最推荐)
  • 优雅关闭socket.shutdownOutput() → 半关闭;再读到 -1 再完全关闭
  • 心跳机制:定期发送 ping/pong 检测连接是否存活
  • 线程模型
  • 单线程 → 简单场景
  • 线程池 + NIO → 高并发(Netty 更优)
  • 异常处理SocketExceptionIOException 要仔细捕获
  • 资源释放:用 try-with-resources 自动关闭流和 socket

总结:一句话记住 TCP + Socket

TCP 提供了可靠的管道,Java 的 Socket 只是这根管道的两端把手。你只需要关心“怎么读写数据”,内核帮你完成了三次握手、序列号、重传、拥塞控制等所有复杂工作。

接下来想深入哪个方向?

  • 解决粘包半包的长度前缀协议实战
  • 使用线程池优化多客户端
  • 引入 NIO / Netty 的对比
  • 实现心跳 + 重连机制

告诉我,我可以继续带你写更完整的代码!

文章已创建 4391

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部