Java API -> JNI (Java Native Interface) -> 底层操作系统 (如 Linux/BSD 的 Socket API)。

java.net.Socket 类的核心结构
Socket 类是客户端套接字的代表,我们首先来看它的核心字段和构造函数。
核心字段
在 Socket.java 的源代码中,有几个至关重要的字段:
// ... 其他 import 和代码 ...
public class Socket implements java.io.Closeable {
/**
* 各种标志位,用于记录 Socket 的状态,比如是否已连接、是否已绑定、是否已关闭等。
* 这些标志位在多线程环境下通过锁来保护。
*/
private boolean created = false;
private boolean bound = false;
private boolean connected = false;
private boolean closed = false;
private boolean closePending = false;
/**
* 底层文件描述符的包装对象。
* 在 Java 中,一切皆对象,但操作系统层面的网络通信是通过文件描述符来实现的。
* FileDescriptor 就是对这个底层句柄的 Java 封装。
*/
private FileDescriptor fd;
/**
* 输入流,所有从网络中读取的数据都通过这个流来获取。
*/
private SocketInputStream socketInputStream;
/**
* 输出流,所有要发送到网络的数据都通过这个流来写入。
*/
private SocketOutputStream socketOutputStream;
/**
* 对等地址和端口信息。
*/
private InetAddress address;
private int port;
/**
* 本地绑定的地址和端口。
*/
private InetAddress localAddress;
private int localPort;
/**
* 用于同步的锁对象,保护对 Socket 状态和流的并发访问。
*/
private final Object closeLock = new Object();
private boolean shutIn = false;
private boolean shutOut = false;
// ... 更多代码 ...
}
关键点解读:
FileDescriptor fd: 这是连接 Java 世界和操作系统世界的桥梁,真正的网络 I/O 操作最终会通过这个fd来完成。SocketInputStream/SocketOutputStream: 这两个流类是对底层fd的 I/O 操作的封装,当你调用socket.getInputStream()和socket.getOutputStream()时,返回的就是这两个对象,它们内部会调用本地方法来读写数据。- 状态标志 (
connected,closed等): 这些布尔值用于跟踪 Socket 的生命周期状态,确保操作的合法性(不能在一个已关闭的 Socket 上进行读写)。
核心构造函数
Socket 提供了多个构造函数,但最终都会调用一个私有的、受保护的构造函数来完成核心工作。
// 一个常见的构造函数
public Socket(String host, int port) throws UnknownHostException, IOException {
this();
// 解析主机名
InetAddress address = InetAddress.getByName(host);
// 调用 connect 方法
connect(new InetSocketAddress(address, port), 0);
}
// 受保护的构造函数,由其他构造函数或 ServerSocket 使用
protected Socket(SocketImpl impl) throws SocketException {
this.impl = impl; // SocketImpl 是一个抽象类,真正实现是 PlainSocketImpl
this.created = true;
}
关键点解读:
构造函数本身并不直接创建网络连接,它主要负责初始化 Java 对象的状态,真正的连接工作是由 connect() 方法完成的。
连接的建立:connect() 方法
connect() 方法是建立 TCP 连接的核心,我们来看它的简化流程。
public void connect(SocketAddress endpoint, int timeout) throws IOException {
// 1. 参数校验
if (endpoint == null)
throw new IllegalArgumentException("Address can't be null");
if (timeout < 0)
throw new IllegalArgumentException("timeout can't be negative");
// 2. 检查 Socket 是否已连接或关闭
if (isClosed())
throw new SocketException("Socket is closed");
if (!bound && !isClosed())
createSocket(); // 如果尚未创建,则创建底层的 Socket 实现
// 3. 调用 SocketImpl 的 connect 方法
if (timeout == 0) {
impl.connect(endpoint, timeout);
} else {
// 带超时的连接
impl.connect(endpoint, timeout);
}
connected = true; // 更新连接状态
// ... 其他状态更新 ...
}
这里的 impl 是 SocketImpl 类型的对象。SocketImpl 是一个抽象类,它定义了所有与底层操作系统交互的本地方法,Java 提供了一个默认的实现,叫做 PlainSocketImpl(在 sun.nio.ch 包下,这是一个“内部实现”包,不推荐直接使用)。
impl.connect() 的关键在于它调用了本地方法。
在 PlainSocketImpl.java 中,你会看到类似这样的代码:
// sun.nio.ch.PlainSocketImpl
class PlainSocketImpl extends SocketImpl {
// ... 其他代码 ...
// native 关键字表示这是一个本地方法,由 C/C++ 代码实现
private native void connect0(SocketAddress address) throws IOException;
private native void connectWithTimeout0(SocketAddress address, int timeout) throws IOException;
@Override
void connect(SocketAddress address, int timeout) throws IOException {
// ... 参数检查 ...
if (timeout == 0) {
connect0(address);
} else {
connectWithTimeout0(address, timeout);
}
}
}
流程总结:
Socket.connect()被调用。- 它委托给
SocketImpl.connect()。 SocketImpl.connect()调用一个用native关键字修饰的方法(如connect0)。- JVM 在此时会加载一个与操作系统相关的本地库(
net.dllon Windows,libnet.soon Linux)。 - 本地方法
connect0在 C/C++ 代码中执行,调用操作系统的socket()、bind()、connect()系统调用,最终完成 TCP 三次握手,建立连接。
数据的读写:getInputStream() 和 getOutputStream()
连接建立后,就可以进行数据传输了。
getInputStream()
public InputStream getInputStream() throws IOException {
if (isClosed())
throw new SocketException("Socket is closed");
if (!isConnected())
throw new SocketException("Socket is not connected");
if (shutIn)
throw new SocketException("Socket input is shutdown");
return socketInputStream; // 返回一个 SocketInputStream 对象
}
SocketInputStream 内部也定义了本地方法来执行实际的读取操作:
// java.net.SocketInputStream (简化)
class SocketInputStream extends InputStream {
private FileDescriptor fd;
// ... 其他代码 ...
// native 读取方法
private native int socketRead0(FileDescriptor fd, byte[] b, int off, int len,
int timeout) throws IOException;
@Override
public int read(byte[] b, int off, int len) throws IOException {
// ... 参数检查 ...
// 调用本地方法
return socketRead0(fd, b, off, len, 0);
}
}
当调用 inputStream.read(buffer) 时,实际执行路径是:
read() -> socketRead0() -> 操作系统 read() 系统调用 -> 从网卡缓冲区读取数据到用户空间的 Java buffer。
getOutputStream()
同理,getOutputStream() 返回一个 SocketOutputStream,它也通过本地方法 socketWrite0() 来调用操作系统的 write() 系统调用,将数据从 Java buffer 写入到操作系统的发送缓冲区。
服务端:java.net.ServerSocket
ServerSocket 的作用是监听指定端口,等待客户端的连接请求。
核心字段
public class ServerSocket implements java.io.Closeable {
/**
* 底层实现,同样是 SocketImpl。
*/
private SocketImpl impl;
/**
* 是否正在监听。
*/
private boolean bound = false;
/**
* 最大连接队列长度。
*/
private int backlog;
/**
* 绑定的端口。
*/
private int port;
// ... 其他代码 ...
}
核心方法:accept()
accept() 是服务端的核心方法,它会阻塞,直到一个客户端连接到来。
public Socket accept() throws IOException {
if (isClosed())
throw new SocketException("Socket is closed");
if (!isBound())
throw new SocketException("Socket is not bound yet");
// 1. 创建一个新的、未连接的 Socket 对象,用于与客户端通信
Socket s = new Socket((SocketImpl) null);
// 2. 调用底层实现进行 accept
implAccept(s);
// 3. 返回已连接的 Socket
return s;
}
// protected 方法,供子类重写
protected final void implAccept(Socket s) throws IOException {
// ... 参数检查 ...
// 调用 SocketImpl 的 accept 方法
impl.accept(s);
}
这里的 impl.accept() 同样会调用一个本地方法,在 PlainSocketImpl 中:
// sun.nio.ch.PlainSocketImpl
class PlainSocketImpl extends SocketImpl {
// ... 其他代码 ...
// native accept 方法
private native void accept0(SocketImpl s) throws IOException;
@Override
void accept(SocketImpl s) throws IOException {
// ... 参数检查 ...
accept0(s); // 阻塞在这里,等待连接
}
}
accept() 的流程总结:
ServerSocket.accept()被调用,它可能会一直阻塞。- 它创建一个新的
Socket实例s,这个s此时是“未连接”状态。 - 它调用
impl.accept(s),进而调用本地方法accept0(s)。 - 本地方法
accept0()在 C/C++ 层调用操作系统的accept()系统调用,这个调用会一直阻塞,直到有新的 TCP 连接请求到达。 - 当
accept()系统调用返回时,它会返回一个新的文件描述符,代表与客户端的已连接套接字。 - 这个新的文件描述符被设置到传入的
SocketImpl s中。 ServerSocket.accept()方法返回s,s已经是一个与客户端建立好连接的Socket对象,服务端可以通过它与客户端进行后续的通信。
Java Socket 的工作流程图
graph TD
subgraph Java Application Layer
A[应用程序] -- 1. new Socket(host, port) --> B(Socket 对象)
A -- 2. socket.connect() --> B
A -- 3. socket.getInputStream() --> C(SocketInputStream)
A -- 4. inputStream.read() --> C
A -- 5. socket.getOutputStream() --> D(SocketOutputStream)
A -- 6. outputStream.write() --> D
end
subgraph Java Native Layer (JNI)
B -- 7. 调用 native connect0() --> E[本地方法库]
C -- 8. 调用 native socketRead0() --> E
D -- 9. 调用 native socketWrite0() --> E
end
subgraph Operating System Kernel
E -- 10. 调用系统调用 --> F[Socket API]
F -- 11. socket(), bind(), connect(), read(), write() --> G[TCP/IP 协议栈]
G -- 12. 与网卡硬件交互 --> H[物理网络]
end
subgraph Server Side
I[ServerSocket.accept()] -- 13. 调用 native accept0() --> E
E -- 14. 调用系统调用 accept() --> F
end
如何获取源代码?
你可以通过以下方式获取 java.net 包的源代码:
- IDE 内置: 在 IntelliJ IDEA 或 Eclipse 中,当你按住
Ctrl(或Cmd) 键并点击Socket类时,IDE 会直接跳转到其源代码位置(通常是你 JDK 安装目录下的src.zip文件)。 - JDK 安装目录: 在你的 JDK 安装路径下,找到一个名为
src.zip或src.jar的文件,解压它即可。C:\Program Files\Java\jdk-11.0.12\src.zip。 - OpenJDK GitHub: 如果你感兴趣的是 OpenJDK(Java 的开源实现),可以直接在 GitHub 上浏览:
希望这个详细的源代码剖析能帮助你更好地理解 Java Socket 的工作原理!
