在上篇文章写I/O的时候,从最基础的文件读取和socket讲述了I/O存在的线程阻塞问题。
可能纯理论的东西,对于很多人(包括我)来说,还是挺难理解的,所以这篇文章就从代码入手,还原I/O和NIO下,如何实现socket的的通信。本篇文章从I/O和NIO之间的传输模式、线程分配以及数据容器方面入手,同是围绕着下面的数据交互图,来对比两者的区别于联系。
I/O数据交互:
NIO数据交互:
先使用传统的Socket方式,实现一个客户端和服务端,且看两者的通信过程。
实现一个服务端,监听7777端口,等待客户端的连接,然后发起会话。
ServerSocket serverSocket = new ServerSocket(7777);
Socket clientSocket = serverSocket.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
String message;
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while ((message = in.readLine()) != null) {
System.out.println("客户端:" + message);
System.out.print("请输入回复:");
String response = br.readLine();
out.println(response);
}
serverSocket.close();
因为是对话,所以要使用while来读取客户端发送的每行数据,然后再从服务端键盘输入回复客户端。接着我们来实现一个客户端,与服务端进行数据交互。
Socket socket = new Socket("localhost", 7777);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
String message;
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while (true) {
System.out.print("客户端:");
message = br.readLine();
out.println(message);
String response = in.readLine();
System.out.println("服务端:" + response);
}
先启动服务端,启动客户端。我现在客户端1输入hello。
服务端收到消息之后回复hi,然后就开始进行交互。
这时候,又启动了客户端2连接服务端,然后礼貌性的和服务端打了个招呼。
结果再看服务端,还在等待客户端1的消息呢,根本没空去处理客户端2的消息。
从这里问题就浮现出来了。在传统的socket中,想要实现对话,就得使用while循环等待消息,这样,服务端的一个线程,只能处理一个客户端的连接。
在当前情况下,要想处理多个客户端的消息,就得使用多线程或者线程池,将处理客户端的消息的逻辑封装在Runnable的run()中。但是,这种情况下,也是1个线程处理一个客户端。
假如你的服务器主机CPU有40个core,不考虑超线程的情况,最多也只能同时处理40个线程,也就是40个客户端。 在上面的那种情况,当第41个客户端连接的时候,就会陷入等待。除非在服务端增加关闭客户端连接的逻辑。
所以,socket的IO总结就是:连接与线程绑定,一个连接会独占一个线程、一个cpu。
而且,在I/O中,不论是文件读写还是socket,在链路上数据都是以二进制byte存在的,数据容器使用的都是字节数组byte[] 。
从上面的分析中,我们发现传统I/O最大的弊端就是:同步阻塞,独占cpu。所以为了解决这个弊端,NIO出现了。
IO重新定义了socket的概念,NIO刚开始有很多人翻译成Non-blocked I/O,即非阻塞的I/O,从功能上理解是没错,但是翻译成New I/O比较贴切,一个全新的I/O。
数据是以byte的形式传输的。在I/O中,使用byte[]数组来存放读取的byte,而NIO则使用Buffer缓冲区来作为数据容器,而且Buffer是一个对象,意味着提供了很多方法可以处理这个数据载体。
在Buffer中,提供了对数据的结构化访问、清空、重置以及维护读写位置等信息
在I/O中,是基于Stream(流)来读写数据。读数据需要InputStream,写数据需要OutputStream,都是单向传输数据。而在NIO中,基于Channel(通道)来读写数据,与流不同之处是Channel是双向的,也就意味着我在这个channel中既可以读,也可以写。
NIO引入了Selector,它是一种高效的多路复用器,一个线程可以管理多个通道,而非I/O中一个线程只能读取一个流的数据,这样线程就不会在交互空闲时被占用。
按照上面的NIO的概念,使用NIO来重构上面服务端的代码:
// 创建 Selector
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(7777));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
// 等待客户端连接
while (true) {
selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel client = channel.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
System.out.println("客户端连接:" + client.getRemoteAddress());
} else if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = client.read(buffer);
if (bytesRead > 0) {
buffer.flip();
byte[] data = new byte[bytesRead];
buffer.get(data);
System.out.println("客户端:" + new String(data));
// 回复客户端消息
System.out.print("请输入回复:");
String response = br.readLine();
ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
client.write(responseBuffer);
}
}
keyIterator.remove();
}
}
通道可以以非阻塞模式进行读写操作,这意味着如果通道中,对端没有发送数据,无法立即进行读取或写入操作,读取或者写入操作将立即返回,而不是阻塞等待。可以通过configureBlocking(false)来设置非阻塞模式。
这时候,在服务端同样是一个线程在读取客户端的数据,当我启动多个客户端发送数据,服务端将所有的客户端发送的数据都进行了处理,如图所示:
这也就意味着,一个线程是可以读取多个连接中的数据。假如我刚结束读取channel A的数据,刚开始读取channel B的数据,这时候A再来数据,只能下一次循环再处理了。从代码中可以看出:NIO服务端将所有客户端连接的处理,都交给了Selector,对比在while循环逻辑,I/O中只是对一个连接进行数据等待,而NIO是对Selector中所有的连接进行处理。
同时,如图所示,如上面缓冲区所讲,在链路(TCP)上,数据以byte二进制的形式进行传输,但是在NIO接收和发送的内部流程中,使用Buffer来进行存放。我调用NIO的write()写入Buffer,NIO自动会将Buffer中的数据,转换成链路上的byte进行传输,这个我们无需担心。
温故而知新,学有所获。这也是在五六年后再次学习NIO,写完这篇博客也算是对以前学习的零散NIO知识的一个整理。
同时,学习NIO也是为了引出为什么要使用Netty,从I/O到NIO再到Netty,而不是从I/O直接到Netty实现一个大的跨越,让使用Netty的人只知道我要用Netty,而非为什么要用Netty。NIO既出,下一篇写的就是为什么要用Netty。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。