杰瑞科技汇

Java Socket 源代码如何实现网络通信?

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

Java Socket 源代码如何实现网络通信?-图1
(图片来源网络,侵删)

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;
    // ... 更多代码 ...
}

关键点解读:

  1. FileDescriptor fd: 这是连接 Java 世界和操作系统世界的桥梁,真正的网络 I/O 操作最终会通过这个 fd 来完成。
  2. SocketInputStream / SocketOutputStream: 这两个流类是对底层 fd 的 I/O 操作的封装,当你调用 socket.getInputStream()socket.getOutputStream() 时,返回的就是这两个对象,它们内部会调用本地方法来读写数据。
  3. 状态标志 (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; // 更新连接状态
    // ... 其他状态更新 ...
}

这里的 implSocketImpl 类型的对象。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);
        }
    }
}

流程总结:

  1. Socket.connect() 被调用。
  2. 它委托给 SocketImpl.connect()
  3. SocketImpl.connect() 调用一个用 native 关键字修饰的方法(如 connect0)。
  4. JVM 在此时会加载一个与操作系统相关的本地库(net.dll on Windows, libnet.so on Linux)。
  5. 本地方法 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() 的流程总结:

  1. ServerSocket.accept() 被调用,它可能会一直阻塞。
  2. 它创建一个新的 Socket 实例 s,这个 s 此时是“未连接”状态。
  3. 它调用 impl.accept(s),进而调用本地方法 accept0(s)
  4. 本地方法 accept0() 在 C/C++ 层调用操作系统的 accept() 系统调用,这个调用会一直阻塞,直到有新的 TCP 连接请求到达。
  5. accept() 系统调用返回时,它会返回一个新的文件描述符,代表与客户端的已连接套接字。
  6. 这个新的文件描述符被设置到传入的 SocketImpl s 中。
  7. ServerSocket.accept() 方法返回 ss 已经是一个与客户端建立好连接的 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 包的源代码:

  1. IDE 内置: 在 IntelliJ IDEA 或 Eclipse 中,当你按住 Ctrl (或 Cmd) 键并点击 Socket 类时,IDE 会直接跳转到其源代码位置(通常是你 JDK 安装目录下的 src.zip 文件)。
  2. JDK 安装目录: 在你的 JDK 安装路径下,找到一个名为 src.zipsrc.jar 的文件,解压它即可。C:\Program Files\Java\jdk-11.0.12\src.zip
  3. OpenJDK GitHub: 如果你感兴趣的是 OpenJDK(Java 的开源实现),可以直接在 GitHub 上浏览:

希望这个详细的源代码剖析能帮助你更好地理解 Java Socket 的工作原理!

分享:
扫描分享到社交APP
上一篇
下一篇