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 是**字节流**,无消息边界。必须自定义协议分帧:
| 方案 | 格式示例 | 优点 | 缺点 | |------|----------|------|------| | **定长** | 每条 100 字节 | 解析极简 | 浪费带宽、不灵活 | | **分隔符** |消息内容\n / \r\n | 人类可读、简单 | 消息体不能含分隔符 (需转义) |
| **长度前缀 (TLV)** | [4字节长度][消息体] | **通用、高效、二进制友好** | 需处理大端/小端 |
| **成熟协议** | HTTP/1.1、HTTP/2、Protobuf + Length、WebSocket | 现成库、扩展性强 | 学习成本略高 |
// 发送端
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;
}
}
socket.shutdownOutput() 发送 FIN,对端 read() 返回 -1,但仍可读取残留数据。socket.close() 释放端口、文件描述符。Socket/流,异常也安全。socket.setSoTimeout(30_000) 防止 read() 永久阻塞;server.setSoTimeout(1000) 让 accept() 可被中断。BIO 线程模型随连接数线性增长,C10K 问题。NIO 单线程多路复用:
ServerSocketChannel / SocketChannel:可配置 configureBlocking(false)。Selector:多路复用器,注册 OP_ACCEPT/OP_READ/OP_WRITE。ByteBuffer:直接内存/堆内存,flip()/compact() 读写切换。
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()) {
// 发送缓冲区数据
// ...
}
}
}
手写 NIO 易踩坑 (空轮询、半包粘包、内存泄漏、关闭竞态)。Netty 4/5 提供:
EventLoopGroup 无锁串行化。LengthFieldBasedFrameDecoder、ProtobufDecoder 即插即用。PooledByteBufAllocator 零拷贝。
// 仅监听 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 占用,BindException | server.setReuseAddress(true) (bind 前) |
| PrintWriter 自动 flush 未开启 | 数据滞留缓冲区,对端收不到 | new PrintWriter(out, true) 或手动 flush() |
| readLine() 遇到无换行符数据 | 永久阻塞 | 设置 setSoTimeout、改用长度前缀协议 |
| 线程池队列无界 | 突发流量 OOM | 有界队列 + CallerRunsPolicy |
| NIO Selector 空轮询 (JDK Bug) | CPU 100% | 升级 JDK / 用 Netty (已修复) |
掌握从 BIO 到 Netty 的演进路径,即能根据业务规模选择最合适的网络通信架构。