2.2 NIO
由于上面BIO的弊端,以及为了解决C10K的问题,出现了NIO模型(NonBlockingIO)。java中的nio指new io,而linux中的nio指NonblockingIO。
NIO是同步非阻塞模型。
代码如下:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
/**
* @author shuai.zhao@going-link.com
* @date 2021/6/2
*/
public class SocketNIO {
public static void main(String[] args) throws IOException {
ArrayList<SocketChannel> clients = new ArrayList<>();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(9000));
ssc.configureBlocking(false);
while (true) {
// 如果没有客户端连接进来则返回null
SocketChannel newClient = ssc.accept();
if (newClient != null) {
System.out.println("client " + newClient.getRemoteAddress() + " is in");
newClient.configureBlocking(false);
clients.add(newClient);
}
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
for (SocketChannel client : clients) {
int read = client.read(byteBuffer);
if (read > 0) {
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
System.out.println("client:" + client.socket().getPort() + " write: " + new String(bytes));
byteBuffer.clear();
}
}
}
}
}
用一个线程去处理10K个客户端连接。
再次使用strace去追踪启动服务端,查看主线程系统调用:
........
accept(4, 0x7f2fbc0d21a0, [28]) = -1 EAGAIN (资源暂时不可用)
accept(4, 0x7f2fbc0d4d50, [28]) = -1 EAGAIN (资源暂时不可用)
accept(4, 0x7f2fbc72f070, [28]) = -1 EAGAIN (资源暂时不可用)
accept(4, 0x7f2fbc0d19e0, [28]) = -1 EAGAIN (资源暂时不可用)
accept(4, 0x7f2fbc0d21a0, [28]) = -1 EAGAIN (资源暂时不可用)
accept(4, 0x7f2fbc0d4d50, [28]) = -1 EAGAIN (资源暂时不可用)
accept(4, 0x7f2fbc72f070, [28]) = -1 EAGAIN (资源暂时不可用)
accept(4, 0x7f2fbc0d19e0, [28]) = -1 EAGAIN (资源暂时不可用)
可以看到服务端是不再阻塞的,每次循环accept()
方法如果没有接收到客户端连接,就返回-1,对应java里就返回null,然后继续下一次循环。使用nc连接服务端,发送数据,然后可以在主线程的系统调用文件out.13101中找到发送的数据,而且后续调用为:
accept(4, 0x7f2fbc0d19e0, [28]) = -1 EAGAIN (资源暂时不可用)
read(5, 0x7f2fbc0ff6a0, 1024) = -1 EAGAIN (资源暂时不可用)
accept(4, 0x7f2fbcea0cb0, [28]) = -1 EAGAIN (资源暂时不可用)
read(5, 0x7f2fbc0ffab0, 1024) = -1 EAGAIN (资源暂时不可用)
accept(4, 0x7f2fbc0d4d50, [28]) = -1 EAGAIN (资源暂时不可用)
read(5, 0x7f2fbc0ffec0, 1024) = -1 EAGAIN (资源暂时不可用)
accept(4, 0x7f2fbc0d21a0, [28]) = -1 EAGAIN (资源暂时不可用)
read(5, 0x7f2fbc1002d0, 1024) = -1 EAGAIN (资源暂时不可用)
accept(4, 0x7f2fbc0d19e0, [28]) = -1 EAGAIN (资源暂时不可用)
read(5, 0x7f2fbc1006e0, 1024) = -1 EAGAIN (资源暂时不可用)
同一个线程同时accept和从客户端read数据,没有时都返回null。
使用C10K客户顿端调用,服务端控制台输出结果如下:
// 服务端
client /127.0.0.1:20210 is in
client /127.0.0.1:20211 is in
client /127.0.0.1:20212 is in
Exception in thread "main" java.io.IOException: Too many open files
at sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method)
at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:424)
// 客户端
connection time consuming:52739
clients = 10214
(ps: 因为我是本地调用,所以可用fd不够,可以在linux下通过cat /proc/sys/fs/file-max
命令查看当前用户可打开的fd数量,使用root用户时这个数量非常大,可以正常连接完这10w个连接数的。但是NIO仍无法避免的问题一个问题就是 fd文件不够,因为在生产环境不可能使用root用户来启动服务)
虽然本地无法启动建立完所有的连接,但是使用在linux上是可以建立的。我们使用c10K问题主要是为了看IO效率,至于fd不够的问题,通过扩容来解决(一台不够就再加一台),但是在使用NIO的时候,瓶颈并不在连接数上,因为只从fd考虑,NIO单台服务器就可以支持十万。从效率上看,我本地建立10214个连接耗时 52739毫秒。这里记一下下面我们会有更高效的方法