首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java Socket 网络编程完整指南:TCP 通信、多线程服务器、NIO 升级路径

     編輯:Java 相關知識

Socket 是网络通信的端点抽象,Java 提供 java.net.Socket (客户端) 与 ServerSocket (服务端) 实现基于 TCP 的可靠字节流通信。本文系统讲解阻塞 IO (BIO) 基础模型、多线程并发服务器、优雅关闭、半包/粘包处理、NIO 非阻塞升级路径与生产级最佳实践。

一、核心类与建立连接

类角色关键构造/方法
Socket客户端new Socket(host, port)、getInputStream()/getOutputStream()、setSoTimeout(ms)
ServerSocket服务端new ServerSocket(port)、accept() 阻塞等待连接、setReuseAddress(true)
InetSocketAddress地址封装new InetSocketAddress("127.0.0.1", 8080)、绑定特定网卡 IP

最小化客户端


try (Socket socket = new Socket("127.0.0.1", 9091);
     PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
     BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
     Scanner scanner = new Scanner(System.in)) {
    String line;
    while (scanner.hasNextLine() && (line = scanner.nextLine()) != null) {
        out.println(line);
        System.out.println("Server echo: " + in.readLine());
    }
}

最小化服务端 (单线程)


try (ServerSocket server = new ServerSocket(9091)) {
    System.out.println("Server started on 9091");
    while (true) {
        try (Socket client = server.accept();
             BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
             PrintWriter out = new PrintWriter(client.getOutputStream(), true)) {
            String msg;
            while ((msg = in.readLine()) != null) {
                System.out.println("Client: " + msg);
                out.println("Echo: " + msg);
            }
        }
    }
}

二、多线程并发服务器 — 一连接一线程

线程池版 (生产推荐)


public class ThreadPoolEchoServer {
    private static final ExecutorService pool = new ThreadPoolExecutor(
        4, 200, 60L, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(1000),
        new ThreadFactoryBuilder().setNameFormat("echo-worker-%d").build(),
        new ThreadPoolExecutor.CallerRunsPolicy()
    );

    public static void main(String[] args) throws IOException {
        try (ServerSocket server = new ServerSocket(9898)) {
            server.setReuseAddress(true);
            System.out.println("Server started on 9898");
            while (true) {
                Socket client = server.accept();
                pool.execute(new ClientHandler(client));
            }
        } finally {
            pool.shutdown();
        }
    }

    static class ClientHandler implements Runnable {
        private final Socket socket;
        ClientHandler(Socket socket) { this.socket = socket; }

        @Override public void run() {
            try (socket;
                 BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                 PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
                String line;
                while ((line = in.readLine()) != null) {
                    if ("bye".equalsIgnoreCase(line)) break;
                    out.println("Server: " + line);
                }
            } catch (IOException e) {
                log.error("Client error", e);
            }
        }
    }
}

三、TCP 粘包/半包问题与应用层协议设计

TCP 是**字节流**,无消息边界。必须自定义协议分帧:

| 方案 | 格式示例 | 优点 | 缺点 | |------|----------|------|------| | **定长** | 每条 100 字节 | 解析极简 | 浪费带宽、不灵活 | | **分隔符** | 消息内容\n / \r\n | 人类可读、简单 | 消息体不能含分隔符 (需转义) | | **长度前缀 (TLV)** | [4字节长度][消息体] | **通用、高效、二进制友好** | 需处理大端/小端 | | **成熟协议** | HTTP/1.1、HTTP/2、Protobuf + Length、WebSocket | 现成库、扩展性强 | 学习成本略高 |

长度前缀编解码示例 (Netty 风格)


// 发送端
public void send(OutputStream out, byte[] body) throws IOException {
    ByteBuffer header = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN);
    header.putInt(body.length);
    out.write(header.array());
    out.write(body);
    out.flush();
}

// 接收端 (需处理半包:循环读满 4 字节头,再读 body)
public byte[] receive(InputStream in) throws IOException {
    byte[] lenBytes = new byte[4];
    readFully(in, lenBytes);
    int len = ByteBuffer.wrap(lenBytes).order(ByteOrder.BIG_ENDIAN).getInt();
    byte[] body = new byte[len];
    readFully(in, body);
    return body;
}
private void readFully(InputStream in, byte[] buf) throws IOException {
    int off = 0;
    while (off < buf.length) {
        int r = in.read(buf, off, buf.length - off);
        if (r == -1) throw new EOFException();
        off += r;
    }
}

四、优雅关闭与异常处理

  1. 半关闭:socket.shutdownOutput() 发送 FIN,对端 read() 返回 -1,但仍可读取残留数据。
  2. 全关闭:socket.close() 释放端口、文件描述符。
  3. try-with-resources:Java 7+ 自动关闭 Socket/流,异常也安全。
  4. 设置超时:socket.setSoTimeout(30_000) 防止 read() 永久阻塞;server.setSoTimeout(1000) 让 accept() 可被中断。

五、NIO 非阻塞升级路径 (Selector + Channel)

BIO 线程模型随连接数线性增长,C10K 问题。NIO 单线程多路复用:

核心组件

  • ServerSocketChannel / SocketChannel:可配置 configureBlocking(false)。
  • Selector:多路复用器,注册 OP_ACCEPT/OP_READ/OP_WRITE。
  • ByteBuffer:直接内存/堆内存,flip()/compact() 读写切换。

NIO 服务器骨架


Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(9091));
ssc.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
    selector.select(); // 阻塞直到有就绪事件
    Set keys = selector.selectedKeys();
    Iterator it = keys.iterator();
    while (it.hasNext()) {
        SelectionKey key = it.next(); it.remove();
        if (key.isAcceptable()) {
            ServerSocketChannel server = (ServerSocketChannel) key.channel();
            SocketChannel client = server.accept();
            client.configureBlocking(false);
            client.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
        } else if (key.isReadable()) {
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer buf = (ByteBuffer) key.attachment();
            int n = client.read(buf);
            if (n == -1) { client.close(); continue; }
            buf.flip();
            // 处理完整消息 (需配合协议解码器处理半包)
            // ...
            buf.compact();
            key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
        } else if (key.isWritable()) {
            // 发送缓冲区数据
            // ...
        }
    }
}

生产建议:直接用 Netty

手写 NIO 易踩坑 (空轮询、半包粘包、内存泄漏、关闭竞态)。Netty 4/5 提供:

  • 事件循环 EventLoopGroup 无锁串行化。
  • 编解码器 LengthFieldBasedFrameDecoder、ProtobufDecoder 即插即用。
  • 内存池 PooledByteBufAllocator 零拷贝。
  • SSL/TLS、HTTP/2、WebSocket 原生支持。

六、绑定特定网卡 IP (多网卡服务器)


// 仅监听 192.168.1.100:9091
InetAddress bindAddr = InetAddress.getByName("192.168.1.100");
ServerSocket server = new ServerSocket();
server.bind(new InetSocketAddress(bindAddr, 9091));
// 或 NIO
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(bindAddr, 9091));

七、避坑清单

坑点后果对策
未设置 SO_REUSEADDR重启服务端口 TIME_WAIT 占用,BindExceptionserver.setReuseAddress(true) (bind 前)
PrintWriter 自动 flush 未开启数据滞留缓冲区,对端收不到new PrintWriter(out, true) 或手动 flush()
readLine() 遇到无换行符数据永久阻塞设置 setSoTimeout、改用长度前缀协议
线程池队列无界突发流量 OOM有界队列 + CallerRunsPolicy
NIO Selector 空轮询 (JDK Bug)CPU 100%升级 JDK / 用 Netty (已修复)

总结

  • BIO + 线程池:连接数 < 1000、业务简单、快速交付首选。
  • 自定义协议 (长度前缀):解决 TCP 粘包/半包,二进制高效。
  • NIO/Netty:高并发 (万级连接)、低延迟、复杂协议栈 (HTTP/WS/私有协议) 必选。
  • 优雅关闭、超时、资源释放:生产系统稳定性基石。

掌握从 BIO 到 Netty 的演进路径,即能根据业务规模选择最合适的网络通信架构。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved