首页 > 其他 > 详细

NIO 源码分析(02) 从 BIO 至 NIO

时间:2019-07-06 13:40:22      阅读:98      评论:0      收藏:0      [点我收藏+]

NIO 源码分析(02) 从 BIO 至 NIO

一、BIO 最简使用姿势

(1) JDK BIO 启动服务典型场景

// 1. 绑定端口
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress((InetAddress) null, PROT), BACKLOG);

while (true) {
    // 2. 获取客户端请求的Socket,没有请求就阻塞
    Socket socket = serverSocket.accept();
    // 3. 开启一个线程执行客户端的任务
    new Thread(new ServerHandler(socket)).start();
}

// 绑定端口,开启服务
public void bind(SocketAddress endpoint, int backlog) throws IOException {
    getImpl().bind(epoint.getAddress(), epoint.getPort());
    getImpl().listen(backlog);
}

ok,代码已经完成!!!下面我们和 Linux 下的网络编程进行对比。

(2) Linux BIO 启动服务典型场景

int listenfd = socket(AF_INET, SOCK_STREAM, 0);
bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
listen (listenfd, BACKLOG);

socklen_t cliaddr_len = sizeof(client_addr);
int clientfd = accept(listenfd, (struct sockaddr*)&client_addr, &cliaddr_len);

对比 Linux 上网络编程,我们会发现 JDK Socket 的编程逻辑是一模一样的。实时上也是这样,JDK 网络编程也没有做很多事,主要还是调用了 Linux 相关的函数。唯一的不同是 Linux 是面向过程程序,socket 函数返回的是一个句柄,bind 和 listen 都是对这个句柄的操作;而 JDK 是面向对象编程,new ServerSocket() 返回了一个对象,我们可以调用这个 serverSocket 对象的各种方法。

Linux NIO 系列(02) 阻塞式 IO 网络编程

二、ServerSocket 源码分析

JDK 为我们提供了 ServerSocket 类作为服务端套接字的实现,通过它可以让主机监听某个端口而接收其他端的请求,处理完后还可以对请求端做出响应。它的内部真正实现是通过 SocketImpl 类来实现的,它提供了工厂模式,所以如果自己想要其他的实现也可以通过工厂模式来改变的。默认的实现类是 SocksSocketImpl。

2.1 相关类图

前面说到 ServerSocket 类真正的实现是通过 SocketImpl 类,于是可以看到它使用了 SocketImpl 类,但由于 windows 和 unix-like 系统有差异,而 windows 不同的版本也需要做不同的处理,所以两类系统的类不尽相同。

技术分享图片

说明:

  1. SocketImpl 类实现了 SocketOptions 接口,接着还派生出了一系列的子类,其中 AbstractPlainSocketImpl 是原始套接字的实现的一些抽象,而 PlainSocketImpl 类是一个代理类。

  2. windows 下 PlainSocketImpl 代理 TwoStacksPlainSocketImpl 和 DualStackPlainSocketImpl 两种不同实现。存在两种实现的原因是一个用于处理 Windows Vista 以下的版本,另一个用于处理 Windows Vista 及以上的版本。

  3. unix-like 不存在版本的问题,所以它直接由 PlainSocketImpl 类实现。

  4. 这两类操作系统都还存在一个 SocksSocketImpl 类,它其实主要是实现了防火墙安全会话转换协议,包括 SOCKS V4 和 V5 。

根据上面可以看到其实对于不同系统就是需要做差异处理,基本都是大同小异,下面涉及到套接字实现均以 Windows Vista 及以上的版本为例进行分析,即 DualStackPlainSocketImpl。

2.2 主要属性

private boolean created = false;
private boolean bound = false;
private boolean closed = false;
private Object closeLock = new Object();
private SocketImpl impl;
private boolean oldImpl = false;
  • created 表示是否已经创建了 SocketImpl 对象,ServerSocket 需要依赖该对象实现套接字操作。
  • bound 是否已绑定地址和端口。
  • closed 是否已经关闭套接字。
  • closeLock 关闭套接字时用的锁。
  • impl 真正的套接字实现对象。
  • oldImpl 是不是使用旧的实现。

2.3 主要方法

2.3.1 构造函数

有五类构造函数,可以什么参数都不传,也可以传入 SocketImpl、端口、backlog 和地址等。主要看一下最后一个构造函数,setImpl 方法用于设置实现对象,然后检查端口大小是否正确,检查 backlog 小于 0 就让它等于 50,最后进行端口和地址绑定操作。

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);
}
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;
    }
}    

总结: 在 new ServerSocket() 时会通过 setImpl 方法创建一个 SocketImpl 的实现类,以 window 下 DualStackPlainSocketImpl 为例。时序图如下:

技术分享图片

2.3.2 setImpl 方法

设置套接字实现对象,这里提供了工厂模式可以方便的对接其他的实现,而默认是没有工厂对象的,所以模式的实现为 SocksSocketImpl 对象。

private void setImpl() {
    if (factory != null) {
        impl = factory.createSocketImpl();
        checkOldImpl();
    } else {
        impl = new SocksSocketImpl();
    }
    if (impl != null)
        impl.setServerSocket(this);
}

createImpl方法
该方法用于创建套接字实现对象,如果实现对象为空则先调用
setImpl
方法设置一下,接着调用套接字实现对象的
create
方法创建套接字。

void createImpl() throws SocketException {
if (impl == null)
setImpl();
try {
impl.create(true);
created = true;
} catch (IOException e) {
throw new SocketException(e.getMessage());
}
}
create
方法干了些啥?它的实现逻辑在 AbstractPlainSocketImpl 类中,这里会传入一个 boolean 类型的 stream 变量,这里其实用来标识是 udp 还是 tcp 协议,stream 即是流,tcp是基于连接的,自然存在流的抽象。而 udp 是非连接的非流的。

两类连接是通过 boolean 类型来标识的,true 为 tcp,false 为 udp,再通过 socketCreate 方法传入到本地实现中,在此之前两者都会创建 FileDescriptor 对象作为套接字的引用,FileDescriptor 为文件描述符,可以用来描述文件、套接字和资源等。另外,udp 协议时还会通过
ResourceManager.beforeUdpCreate()
来统计虚拟机 udp 套接字数量,超过指定最大值则会抛出异常,默认值为25。最后将套接字的 created 标识设为 true,对应 Java 中抽象的客户端套接字 Socket 对象和服务端套接字 ServerSocket 对象。

protected synchronized void create(boolean stream) throws IOException {
this.stream = stream;
if (!stream) {
ResourceManager.beforeUdpCreate();
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();
}
往下看上面调用的
socketCreate
方法的逻辑,判断文件描述符不能为空,再调用本地
socket0
方法,最后将得到的句柄关联到文件描述符对象上。

void socketCreate(boolean stream) throws IOException {
if (fd == null)
throw new SocketException("Socket closed");

    int newfd = socket0(stream, false /*v6 Only*/);

    fdAccess.set(fd, newfd);
}

static native int socket0(boolean stream, boolean v6Only) throws IOException;
接着看本地方法
socket0
的实现,逻辑为:

  1. 通过调用
    NET_Socket
    函数创建套接字句柄,其中通过 Winsock 库的
    socket
    函数创建句柄,并且通过
    SetHandleInformation
    函数设置句柄的继承标志。这里可以看到根据 stream 标识对应的类别为
    SOCK_STREAM

    SOCK_DGRAM
    。如果句柄是无效的则抛出 create 异常。
  2. 然后通过
    setsockopt
    函数设置套接字的选项值,如果发生错误则抛出 create 异常。
  3. 最后再次通过
    SetHandleInformation
    设置句柄的继承标志,返回句柄。

JNIEXPORT jint JNICALL Java_java_net_DualStackPlainSocketImpl_socket0
(JNIEnv env, jclass clazz, jboolean stream, jboolean v6Only /unused*/) {
int fd, rv, opt=0;

fd = NET_Socket(AF_INET6, (stream ? SOCK_STREAM : SOCK_DGRAM), 0);
if (fd == INVALID_SOCKET) {
    NET_ThrowNew(env, WSAGetLastError(), "create");
    return -1;
}

rv = setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (char *) &opt, sizeof(opt));
if (rv == SOCKET_ERROR) {
    NET_ThrowNew(env, WSAGetLastError(), "create");
}

SetHandleInformation((HANDLE)(UINT_PTR)fd, HANDLE_FLAG_INHERIT, FALSE);

return fd;

}

int NET_Socket (int domain, int type, int protocol) {
SOCKET sock;
sock = socket (domain, type, protocol);
if (sock != INVALID_SOCKET) {
SetHandleInformation((HANDLE)(uintptr_t)sock, HANDLE_FLAG_INHERIT, FALSE);
}
return (int)sock;
}
bind方法
该方法用于将套接字绑定到指定的地址和端口上,如果 SocketAddress 为空,即代表地址和端口都不指定,此时系统会将套接字绑定到所有有效的本地地址,且动态生成一个端口。逻辑如下:

  1. 判断是否已关闭,关闭则抛
    SocketException("Socket is closed")
  2. 判断是否已绑定,绑定则抛
    SocketException("Already bound")
  3. 判断地址是否为空,为空则创建一个 InetSocketAddress,默认是所有有效的本地地址,对应的为
    0.0.0.0
    ,而端口默认为0,由操作系统动态生成。
  4. 判断对象是否为 InetSocketAddress 类型,不是则抛
    IllegalArgumentException("Unsupported address type")
  5. 判断地址是否已经有值了,没有则抛
    SocketException("Unresolved address")
  6. backlog 如果小于1则设为50。
  7. 通过安全管理器检查端口。
  8. 通过套接字实现对象调用
    bind

    listen
    方法。
  9. bound 标识设为 true。

public void bind(SocketAddress endpoint) throws IOException {
bind(endpoint, 50);
}

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());
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;
}
}
套接字实现对象的
bind
方法会间接调用
socketBind
方法,逻辑如下:

  1. 获取本地文件描述符 nativefd。
  2. 判断地址是否为空。
  3. 调用
    bind0
    本地方法。
  4. 如果端口为0还会调用
    localPort0
    本地方法获取本地端口赋值给套接字实现对象的 localport 属性上,目的是获取操作系统动态生成的端口。

void socketBind(InetAddress address, int port) throws IOException {
int nativefd = checkAndReturnNativeFD();

    if (address == null)
        throw new NullPointerException("inet address argument is null.");

    bind0(nativefd, address, port, exclusiveBind);
    if (port == 0) {
        localport = localPort0(nativefd);
    } else {
        localport = port;
    }

    this.address = address;
}

static native void bind0(int fd, InetAddress localAddress, int localport, boolean exclBind)

static native int localPort0(int fd) throws IOException;


每天用心记录一点点。内容也许不重要,但习惯很重要!

NIO 源码分析(02) 从 BIO 至 NIO

原文:https://www.cnblogs.com/binarylei/p/11142083.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!