SocketChannel
1. ServerSocketChannel
1.1 ServerSocketChannel 的创建
1.1.1 获取 SelectorProvider 对象
SelectorProvider 这个类用来获取 selectors and selectable channels
Service-provider class for selectors and selectable channels.
其 provider 方法用来查找当前系统中 SelectorProvider 的实现,其查找过程如下:
If the system property java.nio.channels.spi.SelectorProvider is defined then it is taken to be the fully-qualified name of a concrete provider class. The class is loaded and instantiated; if this process fails then an unspecified error is thrown.
If a provider class has been installed in a jar file that is visible to the system class loader, and that jar file contains a provider-configuration file named java.nio.channels.spi.SelectorProvider in the resource directory META-INF/services, then the first class name specified in that file is taken. The class is loaded and instantiated; if this process fails then an unspecified error is thrown.
Finally, if no provider has been specified by any of the above means then the system-default provider class is instantiated and the result is returned.
上面对应是实现:
获得系统属性
1
2
3String cn = System.getProperty("java.nio.channels.spi.SelectorProvider");
Class<?> c = Class.forName(cn, true,ClassLoader.getSystemClassLoader());
provider = (SelectorProvider)c.newInstance();通过 ServiceLoader 查找 SelectorProvider
1
2
3ServiceLoader<SelectorProvider> sl =
ServiceLoader.load(SelectorProvider.class,
ClassLoader.getSystemClassLoader());提供JDK默认的实现
1
provider = sun.nio.ch.DefaultSelectorProvider.create();
而这个 DefaultSelectorProvider 类,在不同的操作系统平台上有不同的实现。注意 DefaultSelectorProvider 这个类本身和 SelectorProvider 没有任何关系(例如:继承,实现等)。它相当于一个JDK到不同平台的一个中间层,不同的平台通过提供这个中间层来向上层提供服务。这个中间层在编译连接JDK时,就会根据不同的平台连接相对应的源码实现文件。
windows – WindowsSelectorProvider
1
2
3
4
5
6/**
* Returns the default SelectorProvider.
*/
public static SelectorProvider create() {
return new sun.nio.ch.WindowsSelectorProvider();
}linux – EPollSelectorProvider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30/**
* Returns the default SelectorProvider.
*/
public static SelectorProvider create() {
String osname = AccessController.doPrivileged(
new GetPropertyAction("os.name"));
if ("SunOS".equals(osname)) {
return new sun.nio.ch.DevPollSelectorProvider();
}
// use EPollSelectorProvider for Linux kernels >= 2.6
if ("Linux".equals(osname)) {
String osversion = AccessController.doPrivileged(
new GetPropertyAction("os.version"));
String[] vers = osversion.split("\\.", 0);
if (vers.length >= 2) {
try {
int major = Integer.parseInt(vers[0]);
int minor = Integer.parseInt(vers[1]);
if (major > 2 || (major == 2 && minor >= 6)) {
return new sun.nio.ch.EPollSelectorProvider();
}
} catch (NumberFormatException x) {
// format not recognized
}
}
}
return new sun.nio.ch.PollSelectorProvider();
}
由此可知:SelectorProvider 在 windows 平台上的实现是:sun.nio.ch.WindowsSelectorProvider
,在 linux 平台上的实现是:sun.nio.ch.EPollSelectorProvider
WindowsSelectorProvider
1
2
3
4
5public class WindowsSelectorProvider extends SelectorProviderImpl {
public AbstractSelector openSelector() throws IOException {
return new WindowsSelectorImpl(this);
}
}EPollSelectorProvider
1
2
3
4
5
6
7
8
9public class EPollSelectorProvider extends SelectorProviderImpl {
public AbstractSelector openSelector() throws IOException {
return new EPollSelectorImpl(this);
}
public Channel inheritedChannel() throws IOException {
return InheritedChannel.getChannel();
}
}
可以看到这两个类都继承自 SelectorProviderImpl 类,SelectorProviderImpl 这个是不同平台所共享的。
不同的一点就是这两个类重写了 openSelector 方法,提供了不同平台上 java.nio.channels.Selector 的实现。由此可知 Selector 在 windows 平台上的实现是: WindowsSelectorImpl,而在 linux 平台上的实现则是: EPollSelectorImpl
1.1.2 SelectorProviderImpl — SelectorProvider的实现
查看 SelectorProviderImpl 实现,总结 SelectorProvider 的各个 open 方法的实现如下:
openDatagramChannel – DatagramChannel
new DatagramChannelImpl(this);
openPipe – Pipe
new PipeImpl(this);
openServerSocketChannel – ServerSocketChannel
new ServerSocketChannelImpl(this);
openSocketChannel – SocketChannel
new SocketChannelImpl(this);
openSelector – AbstractSelector
windows
new WindowsSelectorImpl(this);
linux
new EPollSelectorImpl(this);
1.2 ServerSocketChannel 的实现 – ServerSocketChannelImpl
由上面的分析可知 SelectorProviderImpl 通过 openServerSocketChannel 方法创建一个 ServerSocketChannelImpl 对象来实现 ServerSocketChannel。
1 | class ServerSocketChannelImpl |
上面静态代码段和构造函数就是整个 ServerSocketChannel 创建时所执行的操作。
1.3 ServerSocketChannel 的使用
ServerSocketChannel 创建成功之后,并不能立即使用,此时的 socket 还是 unbound 的。
The new channel’s socket is initially unbound; it must be bound to a specific address via one of its socket’s bind methods before connections can be accepted.
所以在使用 accept 方法之前需要调用 bind 方法。
1.3.1 bind
使用就是调用 socket 的 bind 和 listen 方法,使得 socket 处于监听状态。可以接受外部请求。
1 | // ServerSocketChannelImpl 实现的 bind 方法 |
1.3.2 accept
调用底层的 accept 方法返回一个新的 socket 连接。
1 | // ServerSocketChannelImpl 实现的 accept 方法 |
2. SocketChannel
SocketChannel 对应于 Socket 的实现,表示通过的一端(peer). 对于 Socket 的实现默认是阻塞的(block),并且也不能够配置其是否阻塞。但是对于 SocketChannel 是可以配置其是否阻塞的。
使用 AbstractSelectableChannel.configureBlocking 方法,用来 Adjusts this channel’s blocking mode.
2.1 SocketChannel 的实现 – SocketChannelImpl
1 | class SocketChannelImpl |
所以创建一个 SocketChannel 其实就是调用 socket 函数创建系统对应的 Socket。
2.2 connect
通过上面的方法创建一个 socket 之后并不能立即使用,而是需要使用 connect 函数和ServerSocket建立连接之后才可以进行 read 和 write 操作。
这个方法实现,同样依赖于底层的 connect 方法。关于 connect 方法的描述:
If the connection or binding succeeds, zero is returned. On error, -1 is returned, and errno is set appropriately.
如果 connect 方法连接成功,则返回0,否则返回 -1,其中 errno 被设置成失败原因。
其中 errno 有这样一项:
EINPROGRESS: The socket is non-blocking and the connection cannot be completed immediately. use getsockopt(2) to read the SO_ERROR option at level SOL_SOCKET to determine whether connect() completed successfully (SO_ERROR is zero) or unsuccessfully (SO_ERROR is one of the usual error codes listed here, explaining the reason for the failure).
可见,当 connect 函数在 non-blocking 模式下,如果连接还未建立成功,则返回 EINPROGRESS。可以使用 getsockopt 来查询一个 socket 是否建立成功连接。
2.3 configureBlocking
设置 socket 的阻塞模式
windows
使用 ioctlsocket 来实现
1
2
3
4
5
6
7
8
9
10
11#define SET_BLOCKING 0
#define SET_NONBLOCKING 1
if (blocking == JNI_FALSE) {
argp = SET_NONBLOCKING;
} else {
argp = SET_BLOCKING;
/* Blocking fd cannot be registered with EventSelect */
WSAEventSelect(fd, NULL, 0);
}
result = ioctlsocket(fd, FIONBIO, &argp);The ioctlsocket function controls the I/O mode of a socket.
msdn中关于 ioctlsocket 的使用:
当这个函数的第二个参数是 FIONBIO,其第三个参数的意义如下:Set *argp to a nonzero value if the nonblocking mode should be enabled, or zero if the nonblocking mode should be disabled. When a socket is created, it operates in blocking mode by default
linux
使用 fcntl 函数来实现。
1
2
3
4int flags = fcntl(fd, F_GETFL);
int newflags = blocking ? (flags & ~O_NONBLOCK) : (flags | O_NONBLOCK);
return (flags == newflags) ? 0 : fcntl(fd, F_SETFL, newflags);fcntl 调用设置 socket 的属性
fcntl – manipulate file descriptor
第二个参数是F_SETFL: Set the file status flags to the value specified by arg.
O_NONBLOCK: When possible, the file is opened in non-blocking mode.
2.4 channel 的读写
从上面的分析可知,执行网络相关的函数时使用的时 sun.nio.ch.Net 类,这个类封装了操作系统提供的 socket, bind,listen, connect, setsockopt, getsockopt 函数的包装。这几个都是标准的 BSD Socket API, 用来进行网络编程的接口,所以将其封装到 Net 类中。
而对于 Socket 的读写操作,也就是 socket 的 IO 操作其实各个平台有不同的抽象和实现。这类的 API 常常和文件IO的API共用,所以JDK的实现对 socket channel 的读写时,并没有将 read, write 方法封装到 Net 类中,其实对于 read 和 write API在不同的平台上其作用可以和文件IO共用,所以 JDK 在实现的将IO操作封装到 sun.nio.ch.NativeDispatcher 这个抽象中。
1 | /** |
NativeDispatcher 这个类的作用抽象不同平台的 read/write 操作。这个类的继承层次如下:
- NativeDispatcher
- DatagramDispatcher
- FileDispatcher
- FileDispatcherImpl
- SocketDispatcher
NIO关于 FileChannel, SocketChannel, DatagramChannel 的IO操作就由于上面的对应的抽象来完成。
所以 SocketChannel 读写就是由 SocketDispatcher 来实现的,
在不同的平台下,提供不同的 SocketDispatcher.java/SocketDispatcher.c 文件,
2.4.1 linux平台下的 read/write
由于在 linux 平台下,socket的IO,可以完全当作文件IO来执行,所以这里 SocketChannel 的实现,直接由 FileDispatcherImpl 来实现。
1 | class SocketDispatcher extends NativeDispatcher |
可以看到,SocketDispatcher 把 read/write 直接转发给 FileDispatcherImpl。
2.4.2 windows平台下的 read/write
在 windows 平台下,有关于 socket的 Win32 API, 所以 SocketDispatcher 的实现,由这些 win32 API 来实现。
read
WSARecv
The WSARecv function receives data from a connected socket.
wirte
WSASend
The WSASend function sends data on a connected socket.
close
WSASendDisconnect
The WSASendDisconnect function initiates termination of the connection for the socket and sends disconnect data.
3. 基于流的Socket API 和基于 chnanel 的 Socket 对比
3.1 异常信息
基于 Channel 的 Socket 比 基于流的 Socket 提供的 API 抛出的 异常信息 更加明确。由于网络通信的复杂性,更加明确的异常,有利于开发更加健壮的程序。
3.2 读写效率
基于 Channel 的 socket 在进行数据IO时使用的是 DirectByteBuffer
这个类的作用是可以直接分配,不由JVM托管的内存。所以内存可以直接在 native 代码中使用,减少了数据缓冲区的复制次数,提高读写效率。
使用 ByteBuffer.allocate 分配的是 JVM 堆内存,使用 ByteBuffer.allocateDirect 分配的则是 native code 可以直接使用的内存。