Socket / ServerSocket / DatagramSocket 传统网络源码
概述
java.net 的传统套接字 API 是 Java 网络编程的基石:Socket(TCP 客户端)、ServerSocket(TCP 服务端)、DatagramSocket(UDP)、MulticastSocket(组播)。它们都是阻塞式 API——每个操作直接阻塞调用线程直到完成或超时。
实现上采用"接口 + 平台实现"模式:SocketImpl 定义能力,PlainSocketImpl(及 Windows 的 DualStackPlainSocketImpl)落地 native 系统调用,SocketInputStream / SocketOutputStream 包装读写。本文基于 OpenJDK 21 源码拆解这条链路。
核心源码解析
① Socket(String host, int port) 的 TCP 连接
java
public class Socket implements java.io.Closeable, java.io.Serializable {
private SocketImpl impl; // 平台套接字实现
private volatile boolean created;
private InputStream in;
private OutputStream out;
public Socket(String host, int port) throws UnknownHostException, IOException {
this(InetAddress.getAllByName(host)[0], port, (SocketAddress) null, true);
}
private Socket(InetAddress address, int port, SocketAddress localAddr, boolean stream)
throws IOException {
...
impl = new PlainSocketImpl(); // ① 创建平台实现
...
createImpl(stream); // ② 创建底层套接字
...
connect(new InetSocketAddress(address, port), 0); // ③ 连接
}
private void connect(SocketAddress endpoint, int timeout) throws IOException {
...
impl.connect(new InetSocketAddress(address, port), timeout);
}
}PlainSocketImpl:Linux 用socket(PF_INET6, SOCK_STREAM)创建 fd;Windows 的DualStackPlainSocketImpl用socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP)(IPv4/IPv6 双栈)。- native 链路:
socket0()创建 fd →connect0(fd, address, port)发起 TCP 握手(阻塞直到完成或超时)。 localhost解析:InetAddress.getAllByName(host)在构造时即做 DNS 解析(见 url-uri-inet 文档),解析失败抛UnknownHostException。
② Socket.getInputStream() / getOutputStream() 的流创建
java
public InputStream getInputStream() throws IOException {
...
if (in == null) {
in = new SocketInputStream(this); // 输入流包装 impl
}
return in;
}
public OutputStream getOutputStream() throws IOException {
...
if (out == null) {
out = new SocketOutputStream(this); // 输出流包装 impl
}
return out;
}SocketInputStream/SocketOutputStream:分别包装SocketImpl,内部操作最终委托 nativeread()/write()。- 惰性创建:首次调用才创建流对象;
shutdownInput/shutdownOutput可单独关闭半连接。 - 流的读写走
impl的 fd:SocketInputStream.read→impl.getInputStream(AbstractPlainSocketImpl.socketInputStream)→ nativesocketRead0(fd, buf, off, len, timeout)。 - 每次
read返回前还隐含"fd 可用性"检查(socketAvailable0),支持available()探测可读字节数。
③ ServerSocket.accept() 的阻塞等待
java
public class ServerSocket implements java.io.Closeable, java.io.Serializable {
private SocketImpl impl;
public Socket accept() throws IOException {
...
Socket socket = new Socket((SocketImpl) null); // ① 空实现壳
impl.accept(socket); // ② 阻塞接受连接
return socket; // ③ 返回已连接套接字
}
}
// AbstractPlainSocketImpl.accept
protected void accept(SocketImpl s) throws IOException {
...
int newfd = accept0(nativefd, s); // native:accept(2)
...
}- native 阻塞:
accept0调用accept(2)阻塞等待客户端连接;内核完成三次握手后返回新的 fd。 - 返回的
Socket复用平台impl:accept把新 fd 交给空壳 Socket,socket0不再重复创建。 - 超时控制:
setSoTimeout让accept超时抛SocketTimeoutException(内部 native 轮询检查)。 - 每个
accept返回一个独立连接 fd,多个连接由多个线程/线程池各自处理(经典一连接一线程模型)。
④ PlainSocketImpl 的 socketInputStream / socketOutputStream
java
// AbstractPlainSocketImpl
protected InputStream getInputStream() {
if (fd == null) throw new SocketException("Socket is not connected");
if (socketInputStream == null)
socketInputStream = new SocketInputStream(this); // 惰性创建
return socketInputStream;
}
// SocketInputStream.read → SocketRead0
private int socketRead(FileDescriptor fd, byte[] b, int off, int len, int timeout)
throws IOException {
return socketRead0(fd, b, off, len, timeout); // native
}SocketInputStream持有对impl的引用,read()走socketRead0(fd, buf, off, len, timeout):Linux 为read(2)/recv(2),Windows 为recv。- 超时实现:native 侧通过
SO_RCVTIMEO/poll 实现阻塞超时;超时返回 -1 并置错误码,Java 侧抛SocketTimeoutException。 - JDK 新版本中
SocketInputStream的读写也可经由FileChannel(SocketChannel)路径:当 Socket 由SocketChannel.open()创建时,IO 走 NIO 通道而非原生流(见 ⑧)。
⑤ Socket.setSoTimeout(int timeout) 的 SO_TIMEOUT
java
public synchronized void setSoTimeout(int timeout) throws SocketException {
if (isClosed()) throw new SocketException("Socket is closed");
if (timeout < 0) throw new IllegalArgumentException("timeout can't be negative");
getImpl().setOption(SocketOptions.SO_TIMEOUT, timeout);
}
// AbstractPlainSocketImpl
public void setOption(int opt, Object val) throws SocketException {
...
socketSetOption(opt, value); // native
}SO_TIMEOUT语义:设置后read/accept等待的最长时间(毫秒),超时抛SocketTimeoutException——连接本身仍有效,可继续使用。- native 实现:Linux 走
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, ...);JDK 内部socketSetOption是setsockopt的封装。 - 作用范围:只影响
read/accept的阻塞时长,不影响connect(连接超时用Socket.connect(addr, timeout))与write。
⑥ DatagramSocket.send(DatagramPacket p) 的 UDP 发送
java
public class DatagramSocket implements java.io.Closeable {
DatagramSocketImpl impl; // PlainDatagramSocketImpl
public void send(DatagramPacket p) throws IOException {
...
impl.send(p); // 委托数据报实现
}
}
// PlainDatagramSocketImpl.send
protected void send(DatagramPacket p) throws IOException {
...
datagramSocketSend(p, packetPort, connected); // native
}- UDP 无连接:
send直接调用 nativesendto(fd, buf, len, 0, addr, port),不需要对端 accept——这就是sendto语义。 DatagramPacket携带目标地址与端口(p.getAddress()/p.getPort()),每次 send 可发往不同目标(未 connect 时)。connect()后绑定固定对端:send走send(2)省去每次地址参数;receive只收该对端数据报。- 与 TCP 的区别:
send不保证送达、不保证顺序、无流控;大报文(超过 MTU)可能被分片或丢弃(UDP 最大 65507 字节)。
⑦ MulticastSocket.joinGroup(InetAddress group) 的多播加入
java
public class MulticastSocket extends DatagramSocket {
public void joinGroup(InetAddress mcastaddr) throws IOException {
...
getImpl().join(mcastaddr); // 委托 impl.join
}
}
// AbstractPlainDatagramSocketImpl.join
protected void join(InetAddress inetaddr) throws IOException {
...
join(inetaddr, null); // setsockopt(IP_ADD_MEMBERSHIP)
}- native 链路:
join调用setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq),mreq包含组播组地址与本机接口地址——向内核注册"加入该组播组"。 leaveGroup对称地调用IP_DROP_MEMBERSHIP退组。- 加入组播组后:本机监听该组地址的 UDP 报文(
receive接收);发送用setTimeToLive控制报文跳数。 - 组播地址范围
224.0.0.0–239.255.255.255(IPv4 D 类);需要NetworkInterface指定接口时用joinGroup(SocketAddress, NetworkInterface)变体。
⑧ SocketChannel vs Socket
| 维度 | Socket(传统) | SocketChannel(NIO) |
|---|---|---|
| 阻塞性 | 阻塞 | 非阻塞(可配置) |
| 模型 | 一连接一线程 | 一 Selector 多连接 |
| 读写 | InputStream / OutputStream | ByteBuffer 读写 |
getChannel() | 返回 null(普通构造) | 返回对应通道 |
| 创建 | new Socket() | SocketChannel.open() |
Socket.getChannel():普通new Socket()创建的套接字返回null;只有通过SocketChannel.open().socket()获取的Socket才有关联通道。- 内部互通:
SocketChannel.open()内部仍创建SocketImpl(NioSocketImpl),但 IO 走通道的ByteBuffer读写而非传统流——getInputStream返回的是SocketInputStream的通道适配版。 - 从
SocketChannel得到的Socket不能再connect自己的地址(由通道管理连接状态),且流操作与通道操作二选一。 - 选择建议:高并发连接用
SocketChannel+Selector;简单低并发场景用传统Socket更直观。
总结
| 类 | 职责 | 核心 native 链路 |
|---|---|---|
Socket | TCP 客户端 | socket0 → connect0 → socketRead0 / socketWrite0 |
ServerSocket | TCP 服务端 | socket0 → bind0/listen0 → accept0 |
DatagramSocket | UDP 通信 | datagramSocketCreate → datagramSocketSend / datagramSocketReceive |
MulticastSocket | 组播 | join → setsockopt(IP_ADD_MEMBERSHIP) |
传统套接字的本质是"薄 Java 壳 + 厚 native 层":Java 侧管理对象生命周期与配置,真正的协议处理全部由 OS 完成。理解 SocketImpl 的抽象与 PlainSocketImpl 的 native 调用点,就掌握了传统网络 IO 的实现骨架;而 getChannel() 为 null 的语义,则是传统阻塞 API 与 NIO 之间的分水岭。