作者简介:☕️大家好,我是Aomsir,一个爱折腾的开发者!
个人主页:Aomsir_Spring5应用专栏,Netty应用专栏,RPC应用专栏-CSDN博客
当前专栏:Netty应用专栏_Aomsir的博客-CSDN博客
文章目录
- 参考文献
- 前言
- 操作演示
- 第一版
- 第二版
- 总结
参考文献
- 孙哥suns说Netty
- Netty官方文档
前言
在我们之前的学习中,我们主要关注了服务端处理的两种事件:ServerSocketChannel的ACCEPT事件和SocketChannel的READ事件。然而,除了这两种事件外,还存在另一种重要的事件类型,即WRITE事件
。在今天的学习中,我将专门对WRITE事件进行探讨。基于我们已经掌握的知识,我们将继续深入理解并掌握如何有效地处理和利用WRITE事件,以提高我们服务端程序的性能和效率。
操作演示
为了避免混淆,这里就只演示ACCEPT和WRITE,不演示READ事件
第一版
如下是第一版的代码和演示,服务端使用了Selector来避免无效的CPU空转。当服务端的ServerSocketChannel接收到ACCEPT事件并获得与客户端的SocketChannel连接后,它会立即向客户端发送大量的数据。然而,我们观察到一个问题:服务端总共发送了9个数据包,但其中有5个是空包。这是因为客户端处理接收数据的速度无法跟上服务端发送数据的速度
,所以TCP为了进行流量控制,发送了几个空包。然而,这种情况并不理想,因为我们的服务端是单线程的。在向客户端发送数据的过程中,CPU资源被持续占用,但是这个线程却在发送空包,这无疑是对资源的浪费。因此,我们需要对代码进行修改,以解决这个问题
public class MyServer5 {
public static void main(String[] args) throws Exception {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8000));
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey sscKey = iterator.next();
iterator.remove();
if (sscKey.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) sscKey.channel();
SocketChannel socketChannel = server.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
// 准备数据写回
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 3000000; i++) {
sb.append("s");
}
ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
while (buffer.hasRemaining()) {
int write = socketChannel.write(buffer);
// 实际每一次写了多少
System.out.println("write = " + write);
}
}
}
}
}
}
public class MyClient1 {
public static void main(String[] args) throws Exception{
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress(8000));
// 读取服务端数据,输出每次读取的字节数
while (true) {
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
int read = socketChannel.read(buffer);
System.out.println("read = " + read);
}
}
}
第二版
在第一版的代码中,当客户端无法及时处理服务端发送的数据时,服务端会发送空包。这种情况下,服务端的线程资源被浪费,因为它们被用来发送无实质内容的空包。为了解决这个问题,我进行了以下优化:
在服务端的ServerSocketChannel接收到与客户端的SocketChannel连接后,我首先将准备好的数据存入buffer。只有当buffer中有数据需要发送时,我才会注册SocketChannel的WRITE事件,并将buffer作为附件附加到SocketChannel上。这样,我们在服务端有数据需要发送给客户端时,我们会监听到WRITE事件并且数据在附件中,多次发送也不会丢掉。
当客户端已经接收并处理完一部分数据,并且需要继续接收新的数据时,服务端的Selector#select()方法会被触发,然后我们可以继续处理WRITE事件,将buffer中的数据发送给客户端。在所有数据被成功发送后,我将取消SocketChannel的附件,并停止监听WRITE事件。
这样,这种优化方法使得我们的服务端程序更加高效,因为我们只在真正需要发送数据时,才会使用CPU资源。同时,通过动态地注册和注销WRITE事件,我们还可以更好地控制我们的服务端程序的行为,使其更加符合我们的需求。
public class MyClient1 {
public static void main(String[] args) throws Exception{
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress(8000));
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
// 读取服务端数据
int read = 0;
while (true) {
read += socketChannel.read(buffer);
System.out.println("read = " + read);
buffer.clear();
}
}
}
public class MyServer5 {
public static void main(String[] args) throws Exception {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8000));
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT, null);
while (true) {
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectedKey = iterator.next();
iterator.remove();
if (selectedKey.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) selectedKey.channel();
SocketChannel socketChannel = server.accept();
socketChannel.configureBlocking(false);
SelectionKey scKey = socketChannel.register(selector, SelectionKey.OP_READ);
// 准备数据写回
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 3000000; i++) {
sb.append("s");
}
ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
if (buffer.hasRemaining()) {
// 为当前的SocketChannel新增一个写事件
scKey.interestOps(scKey.interestOps() + SelectionKey.OP_WRITE);
// 将buffer绑定到当前的key进行传递
scKey.attach(buffer);
}
} else if (selectedKey.isWritable()) {
SocketChannel socketChannel = (SocketChannel) selectedKey.channel();
ByteBuffer buffer = (ByteBuffer) selectedKey.attachment();
socketChannel.write(buffer);
if (!buffer.hasRemaining()) {
selectedKey.attach(null);
// 写完后取消写事件(当前这个selectedKey是一个SocketChannel)
selectedKey.interestOps(selectedKey.interestOps() - SelectionKey.OP_WRITE);
}
}
}
}
}
}
总结
本文主要探讨了服务端在向客户端发送数据过程中可能遇到的问题,并提出了通过使用Selector监听WRITE事件的解决方案。我们发现,当客户端无法及时处理来自服务端的数据时,服务端会发送空包,这无疑浪费了宝贵的CPU资源。为了解决这个问题,我们引入了Selector,并注册了WRITE事件。只有当有数据需要发送时,我们才会监听这个事件,这样就可以避免在客户端无法接收数据时,浪费资源发送空包。
在数据成功发送后,我们取消了对WRITE事件的监听,这样我们的服务端程序就不会在没有必要的情况下占用CPU资源。这种方法让我们的服务端程序运行得更高效,并且能更好地满足我们的需求。
至此,关于Java NIO中Selector的相关内容就讲述完毕。通过本文,我们了解了如何利用Selector来提高服务端程序的效率,并避免资源的浪费。希望这些内容能对你在实际开发中有所帮助。