Netty 框架学习 —— 初识 Netty

2年前 (2022) 程序员胖胖胖虎阿
153 0 0

Netty 是一款异步的事件驱动的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器和客户端

Java 网络编程

早期的 Java API 只支持由本地系统套接字库提供的所谓的阻塞函数,下面的代码展示了一个使用传统 Java API 的服务器代码的普通示例

// 创建一个 ServerSocket 用以监听指定端口上的连接请求
ServerSocket serverSocket = new ServerSocket(5000);
// 对 accept 方法的调用将被阻塞,直到一个连接建立
Socket clientSocket = serverSocket.accept();
// 这些流对象都派生于该套接字的流对象
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
String request, response;
// 客户端发送了 "Done" 则退出循环
while ((request = in.readLine()) != null) {
    if ("Done".equals(request)) {
        break;
    }
    // 请求被传递给服务器的处理方法
    response = processRequest(request);
    // 服务器的响应被发送给客户端
    out.println(response);
}

这段代码只能同时处理一个连接,要管理多个客户端,就要为每个新的客户端 Socket 创建一个新的 Thread,让我们来考虑一下这种方案的影响:

  • 在任何时候都会有大量线程处于休眠状态,造成资源浪费
  • 需要为每个线程的调用栈都分配内存
  • 线程的上下文切换会带来开销

这种并发方案对于中小数量的客户端还算理想,但不能很好地支持更大的并发,幸运的是,还有另一种解决方案

Java NIO

NIO(Non-blocking I/O,也称 New I/O),是一种同步非阻塞的 I/O 模型,也是 I/O 多路复用的基础。传统的 IO 流是阻塞的,这意味着,当一个线程调用读或写操作时,线程将被阻塞,直至数据被完全读取或写入。NIO 的非阻塞模式,使一个线程进行读或写操作时,如果目前无数据可用时,就不做操作,而不是保持线程阻塞,所以直至数据就绪以前,线程可以继续做其他事情

class java.nio.channels.Selector 是 Java 非阻塞 IO 实现的关键。它使用事件通知 API 以确定在一组非阻塞套接字中有哪些已经就绪并能进行 IO 相关操作。因为可以在任何时间点任意检查读操作或写操作的完成情况,所以单一线程可以处理多个并发的连接

Netty 框架学习 —— 初识 Netty

与阻塞 IO 模型相比,这种模型提供了更好的资源管理:

  • 使用较少的线程便可以处理许多连接,减少内存管理和上下文切换所带来的开销
  • 当没有 IO 操作需要处理时,线程也可以用户其他任务

尽管 Java NIO 如此高效,但要做到正确和安全并不容易,在高负载下可靠和高效地处理和调度 IO 操作是一项烦琐且容易出错的任务,所幸,有 Netty 能帮助我们解决问题

Netty

Netty 是一个广泛使用的 Java 网络编程框架,它隐藏了 Java 高级 API 背后的复杂性,提供一个易于使用的 API 的客户端/服务器框架。在这里我们将要讨论 Netty 的主要构件:

1. Channel

Channel 是 Java NIO 的一个基本构造,可以把 Channel 简单看作是传入(入站)或传出(出站)数据的载体。因此,它可以被打开或关闭,连接或断开连接

2. 回调

一个回调其实就是一个方法,Netty 使用回调来处理事件,当一个回调被触发时,相关的事件可以被 ChannelHandler 的实现处理。下面的代码展示了这样一个例子:当一个新的连接已经建立,ChannelHandler 的 channelActive() 的回调方法将会被调用,并打印出一条信息

public class ConnectHandler extends ChannelInboundHandlerAdapter {
    
    @override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("Client " + ctx.channel().remoteAddress() + " connected");
    }
}

3. Future

Future 提供了另一种在操作完成时通知应用程序的方式,它将在未来的某个时刻完成,并提供对其结果的访问。虽然 Java 提供了 Future 的一种实现,但需要手动检查对应操作是否已经完成,或一直阻塞直到它完成,十分烦琐。Netty 提供了自己的实现 ChannelFuture,用于执行异步操作的时候使用

ChannelFuture 提供了几种额外的方法,这些方法使得我们能够注册一个或多个 ChannelFutureListener 实例。监听器的回调方法 operationComplete() 将会在对应操作完成时被调用。每个 Netty 的 IO 操作都会返回一个 ChannelFuture,它们都不会被阻塞,可以同时做其他工作,更加有效的利用资源

Channel channel = ...;
// 异步地连接到远程结点
ChannelFuture future = channel.connect(new InetSocketAddress("192.168.0.1", 25));
// 注册一个 ChannelFutureListener
future.addListener(new ChannelFutureListener() {
    
    @Override
    public void operationComplete(ChannelFuture future) {
        // 如果操作成功
        if(future.isSuccess()) {
            ...
        } else {
            // 发生异常
            ...
        }
    }
});

4. 事件和 ChannelHandler

Netty 使用不同的事件来触发合适的动作,事件是按照与入站或出站数据流的相关性进行分类的,可能由入站数据或相关状态更改而触发的事件包括:

  • 连接已被激活或失效
  • 数据读取
  • 用户事件
  • 错误事件

出站事件是未来将会触发的某个动作的操作结果,包括:

  • 打开或关闭到远程结点的连接
  • 将数据写到或冲刷到套接字

每个事件都可分发给 ChannelHandler 类中的某个用户实现的方法,下图展示了一个事件如何被一个 ChannelHandler 链处理

Netty 框架学习 —— 初识 Netty

Netty 提供了大量预定义的 ChannelHandler 实现,供开发者使用

5. 总结

Netty 的异步编程模型是建立在 Future 和回调的概念之上的,将事件派发到 ChannelHandler 拦截并高速地转换入站数据和出站数据,开发者只需要提供回调或者利用返回的 Future 即可。Netty 通过触发事件将 Selector 从应用程序中抽象出来,消除了本需手写的派发代码。在内部,将会为每个 Channel 分配一个 EventLoop,用于处理所有事件,包括:

  • 注册感兴趣的时间
  • 将事件派发给 ChannelHandler
  • 安排进一步的动作

EventLoop 本身只有一个线程驱动,处理了一个 Channel 的所有 IO 事件,这个简单而强大的设计消除了可能在 ChannelHandler 实现中需要进行同步的任何顾虑,因此我们只需专注于提供正确的逻辑即可

版权声明:程序员胖胖胖虎阿 发表于 2022年10月27日 下午4:56。
转载请注明:Netty 框架学习 —— 初识 Netty | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...