JDK 中的 BIO 实现原理分析(二十)

今天对JDK 中的 BIO 实现原理进行分析
一、简介
对比 Linux 上网络编程,我们会发现 JDK Socket 的编程逻辑是一模一样的。实际上也是这样,JDK 网络编程也没有做很多事,主要还是调用了 Linux 相关的函数。唯一的不同是Linux 是面向过程程序, socket 函数返回的是一个句柄, bind listen 都是对这个句柄的操作;而 JDK 是面向对象编程, new ServerSocket() 返回了一个对象,我们可以调用这个
serverSocket 对象的各种方法。JDK 为我们提供了 ServerSocket 类作为服务端套接字的实现,通过它可以让主机监听某个端口而接收其他端的请求,处理完后还可以对请求端做出响应。它的内部真正实现是通过 SocketImpl 类来实现的,它提供了工厂模式,所以如果自己想要其他的实现也可以通过工厂模式来改变的,Socket 类是客户端套接字的实现,和 ServerSocket 几乎差不多。默认的实现类是 SocksSocketImpl ServerSocket Socket  是一个门面模式。
ServerSocket 类:
public
class ServerSocket implements java.io.Closeable {
    /**
     * Various states of this socket.
     */
    private boolean created = false;
    private boolean bound = false;
    private boolean closed = false;
    private Object closeLock = new Object();

    /**
     * The implementation of this Socket.
     */
    private SocketImpl impl;

Socket 类:

public
class Socket implements java.io.Closeable {
    /**
     * Various states of this socket.
     */
    private boolean created = false;
    private boolean bound = false;
    private boolean connected = false;
    private boolean closed = false;
    private Object closeLock = new Object();
    private boolean shutIn = false;
    private boolean shutOut = false;

    /**
     * The implementation of this Socket.
     */
    SocketImpl impl;

 

 

 类的继承图:

实体类图:

 

由于 windows unix-like 系统有差异,而 windows 不同的版本也需要做不同的处理, 所以两类系统的类不尽相同。 SocketImpl 类实现了 SocketOptions 接口,内部的方法基本上都是抽象方法,接着还派生出了一系列的子类,其中 AbstractPlainSocketImpl 是默认套接字的实现的一些抽象,而 PlainSocketImpl 类是一个代理类。 windows 下 PlainSocketImpl 代理 TwoStacksPlainSocketImpl 和 DualStackPlainSocketImpl 两种不同实现。存在两种实现的原因是一个用于处理 Windows Vista 以下的版本,另一个用于处理 Windows Vista 及以上的版本。 unix-like 不存在版本的问题,所以它直接由 PlainSocketImpl 类实现。 这两类操作系统都还存在一个 SocksSocketImpl 类,它其实主要是实现了防火墙安全会 话转换协议,包括 SOCKS V4 V5SOCKS:防火墙安全会话转换协议 (Socks: Protocol for sessions traversal across firewall securely) SOCKS 协议提供一个框架,为在 TCP UDP 域中的客户机/服务器应用程序能更方便安全地使用网络防火墙所提供的服务,并提供一个通用框架来使这些协议安全透明地穿过防火墙 。

 
二、AbstractPlainSocketImpl
这个类是默认的套接字实现方式,属于 Socket 的基本模版,主要为套接字定下了一个大的框架。具体的实现则是交给子类。比如 create 方法:
/**
     * Creates a socket with a boolean that specifies whether this
     * is a stream socket (true) or an unconnected UDP socket (false).
     */
    protected synchronized void create(boolean stream) throws IOException {
        this.stream = stream;
        if (!stream) {
            ResourceManager.beforeUdpCreate();
            // only create the fd after we know we will be able to create the socket
            fd = new FileDescriptor();
            try {
                socketCreate(false);
            } catch (IOException ioe) {
                ResourceManager.afterUdpClose();
                fd = null;
                throw ioe;
            }
        } else {
            fd = new FileDescriptor();
            socketCreate(true);
        }
        if (socket != null)
            socket.setCreated();
        if (serverSocket != null)
            serverSocket.setCreated();
    }

会根据通信类型分 UDP 和 TCP 进行 socket 创建,而具体的创建方法 socketCreate,是个抽象方法

 
abstract void socketCreate(boolean isServer) throws IOException;

 

由具体的子类来实现。其余的 accept connect 等等方法都是一样的套路。
三、ServerSocket
成员变量
 /**
     * Various states of this socket.
     */
    private boolean created = false;//created 表示是否已经创建了 SocketImpl 对象,ServerSocket 需要依赖该对象实现套接字操作。
    private boolean bound = false;//bound 是否已绑定地址和端口。 
    private boolean closed = false;//closed 是否已经关闭套接字。
    private Object closeLock = new Object();//closeLock 关闭套接字时用的锁。

    /**
     * The implementation of this Socket.
     */
    private SocketImpl impl;//impl 真正的套接字实现对象。

    /**
     * Are we using an older SocketImpl?
     */
    private boolean oldImpl = false;//oldImpl 是不是使用旧的实现。构造方法






 


 

五类构造函数:
ServerSocket(SocketImpl impl) {
        this.impl = impl;
        impl.setServerSocket(this);
    }

public ServerSocket() throws IOException {
        setImpl();
    }

public ServerSocket(int port) throws IOException {
        this(port, 50, null);
    }

public ServerSocket(int port, int backlog) throws IOException {
        this(port, backlog, null);
    }

//setImpl 方法用于设置实现对象,然后检查端口大小是否正确,检查 backlog 小于 0 就让它等于 50,最后进行端口和地址绑定操作。
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {
        setImpl();
        if (port < 0 || port > 0xFFFF)
            throw new IllegalArgumentException(
                       "Port value out of range: " + port);
        if (backlog < 1)
          backlog = 50;
        try {
            bind(new InetSocketAddress(bindAddr, port), backlog);
        } catch(SecurityException e) {
            close();
            throw e;
        } catch(IOException e) {
            close();
            throw e;
        }
    }

   

这五类构造函数,可以什么参数都不传,也可以传入 SocketImpl、端口、backlog 和地址等。 

构造函数中setImpl 方法,设置套接字实现对象,这里提供了工厂模式可以方便的对接其他的实现,而默认是没有工厂对象的,所以模式的实现为 SocksSocketImpl 对象。
 
 
private void setImpl() {
        if (factory != null) {
            impl = factory.createSocketImpl();
            checkOldImpl();
        } else {
            // No need to do a checkOldImpl() here, we know it's an up to date
            // SocketImpl!
            impl = new SocksSocketImpl();
        }
        if (impl != null)
            impl.setServerSocket(this);
    }

不过 ServerSocket 和 Socket 为何要使用 SocksSocketImpl ?这是很古怪的行为,具体原因没有官方解释,目前比较合理的推测是: 这是从 JDK1.4 保持下来的传统,对代理服务器连接的实验性支持。在实际环境中,JRE 可能已经通过系统属性-DsocksProxyHost -DsocksProxyPort ProxySelector.setDefault()或通过 JRE 安装的默认 ProxySelect 从外部配置为使用 SOCKS 进行代理。但是 PlainSocketImpl 不会参考这些属性或类,所以这些外部配置将被忽略不起作用,而 SocksSocketImpl 则会进行检查。

bind 方法则会直接创建网络连接并监听。
public void bind(SocketAddress endpoint, int backlog) throws IOException {
        if (isClosed())
            throw new SocketException("Socket is closed");
        if (!oldImpl && isBound())
            throw new SocketException("Already bound");
        if (endpoint == null)
            endpoint = new InetSocketAddress(0);
        if (!(endpoint instanceof InetSocketAddress))
            throw new IllegalArgumentException("Unsupported address type");
        InetSocketAddress epoint = (InetSocketAddress) endpoint;
        if (epoint.isUnresolved())
            throw new SocketException("Unresolved address");
        if (backlog < 1)
          backlog = 50;
        try {
            SecurityManager security = System.getSecurityManager();
            if (security != null)
                security.checkListen(epoint.getPort());
            //bind 方法里比较关键的就是两行,
            getImpl().bind(epoint.getAddress(), epoint.getPort());
            getImpl().listen(backlog);
            bound = true;
        } catch(SecurityException e) {
            bound = false;
            throw e;
        } catch(IOException e) {
            bound = false;
            throw e;
        }
    }

点击进入:

SocketImpl getImpl() throws SocketException {
        if (!created)
            createImpl();
        return impl;
    }

getImpl 方法中会创建 SocketImpl

void createImpl() throws SocketException {
        if (impl == null)
            setImpl();
        try {
            impl.create(true);
            created = true;
        } catch (IOException e) {
            throw new SocketException(e.getMessage());
        }
    }

 

 
 

impl.create 方法实际的实现者是 AbstractPlainSocketImpl,在 AbstractPlainSocketImpl 抽象类中 create 方法对 UDP TCP 协议分别做了处理,创建 socket 套接字的代码就一句 socketCreate(true),由具体的实现类完成。当然,在 windows 下我们调试,会进入 windows PlainSocketImpl 代理TwoStacksPlainSocketImpl 和 DualStackPlainSocketImpl。 在 Linux JDK PlainSocketImpl 非常简单,直接调用 JDK 中的 native 方法。并最终调用了操作系统的相关方法。

linux环境的java代码PlainSocketImpl 类(win环境的大家自己看) :

package java.net;

import java.io.IOException;
import java.io.FileDescriptor;
import java.util.Set;
import java.util.HashSet;
import java.util.Collections;
import jdk.net.*;

import static sun.net.ExtendedOptionsImpl.*;

/*
 * On Unix systems we simply delegate to native methods.
 *
 * @author Chris Hegarty
 */

class PlainSocketImpl extends AbstractPlainSocketImpl
{
    static {
        initProto();
    }

    /**
     * Constructs an empty instance.
     */
    PlainSocketImpl() { }

    /**
     * Constructs an instance with the given file descriptor.
     */
    PlainSocketImpl(FileDescriptor fd) {
        this.fd = fd;
    }

    protected <T> void setOption(SocketOption<T> name, T value) throws IOException {
        if (!name.equals(ExtendedSocketOptions.SO_FLOW_SLA)) {
            super.setOption(name, value);
        } else {
            if (isClosedOrPending()) {
                throw new SocketException("Socket closed");
            }
            checkSetOptionPermission(name);
            checkValueType(value, SocketFlow.class);
            setFlowOption(getFileDescriptor(), (SocketFlow)value);
        }
    }

    protected <T> T getOption(SocketOption<T> name) throws IOException {
        if (!name.equals(ExtendedSocketOptions.SO_FLOW_SLA)) {
            return super.getOption(name);
        }
        if (isClosedOrPending()) {
            throw new SocketException("Socket closed");
        }
        checkGetOptionPermission(name);
        SocketFlow flow = SocketFlow.create();
        getFlowOption(getFileDescriptor(), flow);
        return (T)flow;
    }

    protected void socketSetOption(int opt, boolean b, Object val) throws SocketException {
        try {
            socketSetOption0(opt, b, val);
        } catch (SocketException se) {
            if (socket == null || !socket.isConnected())
                throw se;
        }
    }

    native void socketCreate(boolean isServer) throws IOException;

    native void socketConnect(InetAddress address, int port, int timeout)
        throws IOException;

    native void socketBind(InetAddress address, int port)
        throws IOException;

    native void socketListen(int count) throws IOException;

    native void socketAccept(SocketImpl s) throws IOException;

    native int socketAvailable() throws IOException;

    native void socketClose0(boolean useDeferredClose) throws IOException;

    native void socketShutdown(int howto) throws IOException;

    static native void initProto();

    native void socketSetOption0(int cmd, boolean on, Object value)
        throws SocketException;

    native int socketGetOption(int opt, Object iaContainerObj) throws SocketException;

    native void socketSendUrgentData(int data) throws IOException;
}

然后直接调用底层操作系统C语言代码对应socketCreate方法创建socket

/*
 * Class:     java_net_PlainSocketImpl
 * Method:    socketCreate
 * Signature: (ZZ)V */
JNIEXPORT void JNICALL
Java_java_net_PlainSocketImpl_socketCreate(JNIEnv *env, jobject this,
                                           jboolean stream, jboolean isServer) {
    jobject fdObj, ssObj;
    int fd;
    int type = (stream ? SOCK_STREAM : SOCK_DGRAM);
    int domain = ipv6_available() ? AF_INET6 : AF_INET;

    if (socketExceptionCls == NULL) {
        jclass c = (*env)->FindClass(env, "java/net/SocketException");
        CHECK_NULL(c);
        socketExceptionCls = (jclass)(*env)->NewGlobalRef(env, c);
        CHECK_NULL(socketExceptionCls);
    }
    fdObj = (*env)->GetObjectField(env, this, psi_fdID);

    if (fdObj == NULL) {
        (*env)->ThrowNew(env, socketExceptionCls, "null fd object");
        return;
    }

    if ((fd = socket(domain, type, 0)) == -1) {
        /* note: if you run out of fds, you may not be able to load
         * the exception class, and get a NoClassDefFoundError
         * instead.
         */
        NET_ThrowNew(env, errno, "can't create socket");
        return;
    }

    /*
     * If IPv4 is available, disable IPV6_V6ONLY to ensure dual-socket support.
     */
    if (domain == AF_INET6 && ipv4_available()) {
        int arg = 0;
        if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (char*)&arg,
                       sizeof(int)) < 0) {
            NET_ThrowNew(env, errno, "cannot set IPPROTO_IPV6");
            close(fd);
            return;
        }
    }

    /*
     * If this is a server socket then enable SO_REUSEADDR
     * automatically and set to non blocking.
     */
    if (isServer) {
        int arg = 1;
        SET_NONBLOCKING(fd);
        if (NET_SetSockOpt(fd, SOL_SOCKET, SO_REUSEADDR, (char*)&arg,
                       sizeof(arg)) < 0) {
            NET_ThrowNew(env, errno, "cannot set SO_REUSEADDR");
            close(fd);
            return;
        }
    }

    (*env)->SetIntField(env, fdObj, IO_fd_fdID, fd);
}

或者此版本代码: 

 

Socket
实现思路基本和 ServerSocket 一样,不再赘述。

到此,JDK 中的 BIO 实现原理分析完毕,下篇分析Linux 环境下的 IO 复用编程原理,敬请期待!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

寅灯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值