暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Netty开胃菜—图解NIO Channel

大黄奔跑 2021-03-21
437

微信公众号:大黄奔跑  
关注我,可了解更多有趣的面试和编程相关问题。

写在之前

Hello,大家好,我是只会写 HelloWorld
的程序员大黄。

书接上回,本篇继续更新Netty系列文章——何为 channel。

Channel为何物

说起channel,直观翻译为管道,何为管道?类似于如下东西

管道

想想生活中的管道可以做啥?最主要的作用是传输东西。

NIO 中国年的Channel也类似,主要有三大特点

  1. 既可以从通道中读取数据,又可以写数据到通道。

  2. 通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入

  3. 通道可以异步地读写。

Channel与buffer关系如下:

channel与Buffer关系

Channel的徒子徒孙

Channel中最主要的类型如下:

Channel主要类型

FileChannel

作用:从文件中读写数据,允许多线程读写,本身是线程安全的。

DatagramChannel

作用:能通过UDP读写网络中的数据。

SocketChannel

作用:能通过TCP读写网络中的数据。

ServerSocketChannel

作用:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel;支持异步的读写。

FileChannel如何配合buffer使用

如果我们准备将字符串写入到某个文件中,首先需要一个缓冲空间用来写字符串,然后再利用通道将缓冲区中的信息写入到文件中;Channel可以从buffer中进行读写。

具体流程如下:

FileChannel流程

小demo演示

/**
 * 将message写入到本地文件中
 *
 * @param message           要写入的文本
 * @param descFilePath      本地目标文件
 */

private static void writeToFile (String message, String descFilePath) {
    try {
        // 1. 创建一个用于写入文件的输出流
        FileOutputStream fileOutputStream = new FileOutputStream(descFilePath);

        // 2. 通过fileOutputStream获取对应的fileChannel,而底层实际上是FileChannelImpl
        FileChannel channel = fileOutputStream.getChannel();

        // 3. 创建一个Buffer【解决内容存储的Buffer位置】
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        // 4. 将原message写入到buffer中
        byteBuffer.put(message.getBytes());

        // 5. 对ByteBuffer进行反转,开始读取, 将前几步写入的数据读取出来
        byteBuffer.flip();

        // 6. 将ByteBuffer数据写入到FileChannel,这一步会不断的移动position直到limit
        channel.write(byteBuffer);

        // 7. 关闭
        fileOutputStream.close();

    } catch (IOException e) {
        e.printStackTrace();
    }
}

SocketChannel如何配合buffer使用

SocketChannel 是一个客户端用来进行 TCP 连接的 Channel。

创建一个 SocketChannel 的方法有两种,一种是站在客户端角度;另一种站在服务器角度。

  1. 打开一个 SocketChannel, 然后将其连接到某个服务器中

  2. 当一个 ServerSocketChannel 接受到连接请求时, 会返回一个 SocketChannel 对象.

打开SocketChannel

主要有两种方式

SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://127.0.0.1"80));

读取数据

读取数据方式与FileChannel类似,先分配缓冲区,将数据写入到缓冲区,再从缓冲区读取数据到Channel中

ByteBuffer buf = ByteBuffer.allocate(48);
byteBuffer.put("Hello world");
int bytesRead = socketChannel.read(buf);

写入数据

由上面的代码继续分析,先将buffer由写模式切换到读模式,然后从buffer中读取数据到Channel中。

byteBuffer.flip();
channel.write(buf);

关闭通道

SocketChannel socketChannel = SocketChannel.open();
socketChannel.close();

异步模式

最初介绍SocketChannel 和 ServerSocketChannel 时提到了两者可以设置为异步模式,支持非阻塞的读、写、连接。

比如连接模式下,有可能连接还没有建立, connect 方法就返回了, 因此我们需要检查当前是否是连接到了主机, 因此通过一个 while 循环来判断.

// 先设置非阻塞
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("127.0.0.1"80));

// 只有连接完成了,才开始执行连接之后的任务
while(! socketChannel.finishConnect() ){

}

异步读写模式
在异步模式下, 读写的方式是一样的。
在读取时, 因为是异步的, 因此我们必须检查 read 的返回值, 来判断当前是否读取到了数据。

// 只能允许读了,才开始真正的读操作
if (xxxKey.isReadable()) {
    // 1. 根据key反向获取到对应的Channel
    SocketChannel channel = (SocketChannel) key.channel();
    // 2. 获取该Channel关联的buffer
    ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
    channel.read(byteBuffer);
}

ServerSocketChannel如何配合buffer使用

与SocketChannel使用方法类型,不过ServerSocketChannel是服务于服务器端的设置,用户监听客户端连接

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(6667);
// 服务器绑定端口并且启动
serverSocketChannel.socket().bind(inetSocketAddress);
// 等待客户端连接
SocketChannel socketChannel = serverSocketChannel.accept();

如何监听是否连接

我们可以使用ServerSocketChannel.accept()方法来监听客户端的 TCP 连接请求, accept()方法会阻塞, 直到有连接到来,当有连接时,这个方法会返回一个 SocketChannel 对象

SocketChannel socketChannel = serverSocketChannel.accept();

切换为异步方式

异步情况下,accept()是非阻塞的, 因此如果此时没有连接到来, 那么 accept()方法会返回null;
但是有一个弊端在于系统需要一直循环,比较消耗资源。关于这一点正是下一篇文章想要介绍的内容 Selector,解决这种空轮询问题

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);

while(true){
    SocketChannel socketChannel =serverSocketChannel.accept();

    if(socketChannel != null){
        //do something with socketChannel...
    }
}

UDP好友-DatagramChannel

打开连接

与可靠传输的SocketChannel相比,DatagramChannel只需要绑定端口,因为在某种意义上是不需要进行可靠的连接,然后再进行传输数据。

DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(8080));

读取数据

直接将数据从buffer中读取到channel

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);

发送数据

String mes = "Hello"
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(mes.getBytes());

buf.flip();

int bytesSent = channel.send(buf, new InetSocketAddress("127.0.0.1"8080));

总结

本文主要介绍了 Channel 作用及其用法,秉着说人话的初衷写本篇文章,深知写让人通俗易懂的文章之难,查阅了诸多资料,如有一些思考不正确,还往指正及互相交流。

争取写最通俗易懂的netty教程。


文章转载自大黄奔跑,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论