netty学习笔记

io.netty

PART1 Netty——异步和事件驱动

1.netty概述

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

在网络编程领域,Netty是Java的卓越框架。它驾驭了Java高级API的能力,并将其隐藏在一个易于使用的API之后。Netty使你可以专注于自己真正感兴趣的——你的应用程序的独一无二的价值。

2.netty特性

image-20210807151522284

image-20210807152423113

 非阻塞网络调用使得我们可以不必等待一个操作的完成。完全异步的 I/O 正是基于这个特性构建的,并且更进一步:异步方法会立即返回,并且在它完成时,会直接或者在稍后的某个时间点通知用户。

 选择器使得我们能够通过较少的线程便可监视许多连接上的事件。

3.Netty核心组件

1.Channel;

2.回调;

3.Future;

4.事件和 ChannelHandler。

3.1 Channel

Channel 是 Java NIO 的一个基本构造。

它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执行一个或者多个不同的I/O操作的程序组件)的开放连接,如读操作和写操作 。

目前,可以把 Channel 看作是传入(入站)或者传出(出站)数据的载体。因此,它可以被打开或者被关闭,连接或者断开连接。

3.2 回调

一个回调其实就是一个方法,一个指向已经被提供给另外一个方法的方法的引用。这使得后者 可以在适当的时候调用前者。回调在广泛的编程场景中都有应用,而且也是在操作完成后通知相关方最常见的方式之一。

Netty 在内部使用了回调来处理事件;当一个回调被触发时,相关的事件可以被一个 interface-ChannelHandler 的实现处理。

如下方代码段,当一个新的连接已经被建立时,ChannelHandler 的 channelActive()回调方法将会被调用,并将打印出一条信息。

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

当一个新的连接被建立channelActive(ChannelHandlerContext)将会被调用。

3.3 Future

Future 提供了另一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操作的结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问。

JDK 预置了 interface java.util.concurrent.Future,但是其所提供的实现,只允许手动检查对应的操作是否已经完成,或者一直阻塞直到它完成。这是非常繁琐的,所以 Netty提供了它自己的实现——ChannelFuture,用于在执行异步操作的时候使用。

ChannelFuture提供了几种额外的方法,这些方法使得我们能够注册一个或者多个ChannelFutureListener实例。监听器的回调方法operationComplete(),将会在对应的操作完成时被调用 。然后监听器可以判断该操作是成功地完成了还是出错了。如果是后者,我们可以检索产生的Throwable。简而 言之 ,由ChannelFutureListener提供的通知机制消除了手动检查对应的操作是否完成的必要。

每个 Netty 的出站 I/O 操作都将返回一个 ChannelFuture;也就是说,它们都不会阻塞。

正如我们前面所提到过的一样,Netty 完全是异步和事件驱动的。

connect()方法将会直接返回,而不会阻塞,该调用将会在后台完成。这究竟什么时候会发生则取决于若干的因素,但这个关注点已经从代码中抽象出来了。因为线程不用阻塞以等待对应的操作完成,所以它可以同时做其他的工作,从而更加有效地利用资源。

异步建立连接:

        Channel channel = ...;
        ChannelFuture future = channel.connect(new InetSocketAddress("192.168.0.1", 25));

如何利用 ChannelFutureListener?首先,要连接到远程节点上。然后,要注册一个新的 ChannelFutureListener 到connect()方法的调用所返回的 ChannelFuture 上。当该监听器被通知连接已经建立的时候,要检查对应的状态 。如果该操作是成功的,那么将数据写到该 Channel。否则,要从 ChannelFuture 中检索对应的 Throwable。

Channel channel = ...;
//异步地连接到远程节点
ChannelFuture future = channel.connect(new InetSocketAddress("192.168.0.1", 25));
future.addListener(new ChannelFutureListener() {
   
    //注册一个 ChannelFutureListener,以便在操作完成时获得通知
      public void operationComplete(ChannelFuture future) {
   
           if (future.isSuccess()){
   
               //如果操作是成功的,则创建一个 ByteBuf 以持有数据
               ByteBuf buffer = Unpooled.copiedBuffer("Hello", Charset.defaultCharset());
               //将数据异步地发送到远程节点。返回一个 ChannelFuture
               ChannelFuture wf = future.channel().writeAndFlush(buffer);
               
            } else {
   
               //如果发生错误,则访问描述原因的 Throwable
               Throwable cause = future.cause();
               cause.printStackTrace();
            } }
    });

3.4 事件和ChannelHandler

Netty 使用不同的事件来通知我们状态的改变或者是操作的状态。这使得我们能够基于已经发生的事件来触发适当的动作。这些动作可能是:

 记录日志;

 数据转换;

 流控制;

 应用程序逻辑。

Netty 是一个网络编程框架,所以事件是按照它们与入站或出站数据流的相关性进行分类的。可能由入站数据或者相关的状态更改而触发的事件包括:

 连接已被激活或者连接失活;

 数据读取;

 用户事件;

 错误事件。

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

 打开或者关闭到远程节点的连接;

 将数据写到或者冲刷到套接字。

每个事件都可以被分发给 ChannelHandler 类中的某个用户实现的方法。这是一个很好的将事件驱动范式直接转换为应用程序构件块的例子。

image-20210807161449040

你可以认为每个 ChannelHandler 的实例都类似于一种为了响应特定事件而被执行的回调。

Netty 提供了大量预定义的可以开箱即用的 ChannelHandler 实现,包括用于各种协议(如 HTTP 和 SSL/TLS)的 ChannelHandler。在内部,ChannelHandler 自己也使用了事件和 Future,使得它们也成为了你的应用程序将使用的相同抽象的消费者。

Netty的异步编程模型是建立在Future和回调的概念之上的,而将事件派发到ChannelHandler的方法则发生在更深的层次上。结合在一起,这些元素就提供了一个处理环境,使你的应用程序逻辑可以独立于任何网络操作相关的顾虑而独立地演变。

拦截操作以及高速地转换入站数据和出站数据,都只需要你提供回调或者利用操作所返回的Future。这使得链接操作变得既简单又高效,并且促进了可重用的通用代码的编写。

Netty 通过触发事件将 Selector 从应用程序中抽象出来,消除了所有本来将需要手动编写的派发代码。

在内部,将会为每个 Channel 分配一个 EventLoop,用以处理所有事件,包括:

 注册感兴趣的事件;

 将事件派发给 ChannelHandler;

 安排进一步的动作。

EventLoop 本身只由一个线程驱动,其处理了一个 Channel 的所有 I/O 事件,并且在该EventLoop 的整个生命周期内都不会改变。这个简单而强大的设计消除了你可能有的在ChannelHandler 实现中需要进行同步的任何顾虑,因此,你可以专注于提供正确的逻辑,用来在有感兴趣的数据要处理的时候执行。

4.netty实战

4.1 netty - Echo服务器

所有的 Netty 服务器都需要以下两部分。

 至少一个 ChannelHandler—该组件实现了服务器对从客户端接收的数据的处理,即它的业务逻辑。(ChannelHandler,它是一个接口族的父接口,它的实现负责接收并响应事件通知。)

 引导—这是配置服务器的启动代码。至少,它会将服务器绑定到它要监听连接请求的端口上。

因为你的 Echo 服务器会响应传入的消息,所以它需要实现 ChannelInboundHandler 接口,用 来定义响应入站事件的方法。这个简单的应用程序只需要用到少量的这些方法,所以继承 ChannelInboundHandlerAdapter 类也就足够了,它提供了 ChannelInboundHandler 的默认实现。

我们感兴趣的方法是:

 channelRead()—对于每个传入的消息都要调用;

 channelReadComplete()—通知ChannelInboundHandler最后一次对channelRead()的调用是当前批量读取中的最后一条消息;

 exceptionCaught()—在读取操作期间,有异常抛出时会调用

@ChannelHandler.Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
   

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
   
        ByteBuf in = (ByteBuf) msg;
        //将消息记录到控制台
        System.out.println(
                "Server received: " + in.toString(CharsetUtil.UTF_8));
        //讲收到的消息写给发送者而不冲刷出站消息
        ctx.write(in);
    }
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
   
        //将未决消息冲刷到远程节点 并关闭该channel
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
                .addListener(ChannelFutureListener.CLOSE);
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,
                                Throwable cause) {
   
        cause.printStackTrace();
        ctx.close();
    }
}

ChannelInboundHandlerAdapter 有一个直观的 API,并且它的每个方法都可以被重写以挂钩到事件生命周期的恰当点上。因为需要处理所有接收到的数据,所以你重写了 channelRead()方法。在这个服务器应用程序中,你将数据简单地回送给了远程节点。重写 exceptionCaught()方法允许你对 Throwable 的任何子类型做出反应,在这里你记录了异常并关闭了连接。虽然一个更加完善的应用程序也许会尝试从异常中恢复,但在这个场景下,只是通过简单地关闭连接来通知远程节点发生了错误

image-20210807165750274

 针对不同类型的事件来调用 ChannelHandler;

 应用程序通过实现或者扩展 ChannelHandler 来挂钩到事件的生命周期,并且提供自

定义的应用程序逻辑;

 在架构上,ChannelHandler 有助于保持业务逻辑与网络处理代码的分离。这简化了开发过程,因为代码必须不断地演化以响应不断变化的需求。

4.2 引导服务器

 绑定到服务器将在其上监听并接受传入连接请求的端口;

 配置 Channel,以将有关的入站消息通知给 EchoServerHandler 实例。

除了一些由 Java NIO 实现提供的服务器端性能增强之外,NIO 传输大多数时候指的就是 TCP 传输。

引导过程中所需要的步骤如下:

 创建一个 ServerBootstrap 的实例以引导和绑定服务器;

 创建并分配一个 NioEventLoopGroup 实例以进行事件的处理,如接受新连接以及读/写数据;

 指定服务器绑定的本地的 InetSocketAddress;

 使用一个 EchoServerHandler 的实例初始化每一个新的 Channel;

 调用 ServerBootstrap.bind()方法以绑定服务器。

public class EchoServer {
   
    private final int port;
    public EchoServer(int port) {
   
        this.port = port;
    }
    public static void main(String[] args) throws Exception {
   
//        if (args.length != 1) {
   
//            System.err.println(
//                    "Usage: " + EchoServer.class.getSimpleName() + " <port>");
//        }
//        int port = Integer.parseInt(args[0]);
        int port = 3991;
        new EchoServer(port).start();
    }
    public void start() throws Exception {
   
        final EchoServerHandler serverHandler = new EchoServerHandler();
        //创建EventLoopGroup
        EventLoopGroup group = new NioEventLoopGroup();
        try {
   
            //创建服务器启动类
            ServerBootstrap b = new ServerBootstrap();
            b.group(group)
                    //指定所使用的的NIO传输channel
                    .channel(NioServerSocketChannel.class)
                    //使用指定的端口设置套接字地址
                    .localAddress(new InetSocketAddress(port))
                    //添加一个EchoServerHandler到子channel的ChannelPipeline
                    .childHandler(new ChannelInitializer<SocketChannel>(){
   
                        @Override
                        public void initChannel(SocketChannel ch)
                                throws Exception {
   
                            //EchoServerHandler被标注为@Shareable所以我们可以总是使用相同的实例
                            ch.pipeline().addLast(serverHandler);
                        }
                    });
            //异步绑定服务器 调用sync方法阻塞直到绑定完成
            ChannelFuture f = b.bind().sync();
            //获取Channel的CloseFutrue并阻塞当前线程直到他完成
            f.channel().closeFuture().sync();
        } finally {
   
            //关闭EventLoopGroup释放所有资源。
            group.shutdownGracefully().sync();
        }
    }
}

4.3 netty - Echo客户端

Echo 客户端将会:

(1)连接到服务器;

(2)发送一个或者多个消息;

(3)对于每个消息,等待并接收从服务器发回的相同的消息;

(4)关闭连接。

如同服务器,客户端将拥有一个用来处理数据的ChannelInboundHandler。在这个场景下,你将拓展SimpleChannelInboundHandler 类以处理所有必须的任务

@ChannelHandler.Sharable
public class EchoClientHandler extends
        SimpleChannelInboundHandler<ByteBuf> {
   

    //当被通知channel是活跃的时候发送一条消息
    //重写了 channelActive()方法,其将在一个连接建立时被调用。这确保了数据
    //将会被尽可能快地写入服务器,其在这个场景下是一个编码了字符串"Netty rocks!"的字节缓冲区。
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
   
        ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!",
                CharsetUtil.UTF_8)); }
                //记录已接收消息的转储
                @Override
    public void channelRead0(ChannelHandlerContext ctx, ByteBuf in) {
   
        System.out.println(
                "Client received: " + in.toString(CharsetUtil.UTF_8));
    }
    //发生异常时记录错误并关闭
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,
                                Throwable cause) {
   
        cause.printStackTrace();
        ctx.close();
    }
}

你重写了 channelRead0()方法。每当接收数据时,都会调用这个方法。需要注意的是,由服务器发送的消息可能会被分块接收。也就是说,如果服务器发送了 5 字节,那么不能保证这 5 字节会被一次性接收。即使是对于这么少量的数据,channelRead0()方法也可能会被调用两次,第一次使用一个持有 3 字节的ByteBufNetty 的字节容器),第二次使用一个持有 2 字节的 ByteBuf。作为一个面向流的协议,TCP 保证了字节数组将会按照服务器发送它们的顺序被接收

image-20210808083715232

4.4 引导客户端

引导客户端类似于引导服务器,不同的是,客户端是使用主机和端口参数来连接远程地址也就是这里的 Echo 服务器的地址,而不是绑定到一个一直被监听的端口。

 为初始化客户端,创建了一个 Bootstrap 实例;

 为进行事件处理分配了一个 NioEventLoopGroup 实例,其中事件处理包括创建新的连接以及处理入站和出站数据;

 为服务器连接创建了一个 InetSocketAddress 实例;

 当连接被建立时,一个 EchoClientHandler 实例会被安装到(该 Channel 的)ChannelPipeline 中;

 在一切都设置完成后,调用 Bootstrap.connect()方法连接到远程节点;

注意:你可以在客户端和服务器上分别使用不同的传输。分别使用NIO与OIO

public class EchoClient {
   
    private final String host;
    private final int port;
    public EchoClient(String host, int port) {
   
        this.host = host;
        this.port = port;
    }
    public void start() throws Exception {
   
        EventLoopGroup group = new NioEventLoopGroup();
        try {
   
            Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .remoteAddress(new InetSocketAddress(host, port))
                    .handler(new ChannelInitializer<SocketChannel>() {
   
                        @Override
                        public void initChannel(SocketChannel ch)
                                throws Exception {
   
                            ch.pipeline().addLast(
                                    new EchoClientHandler());
                        }
                    });
            ChannelFuture f = b.connect().sync();
            f.channel().closeFuture().sync();
        } finally {
   
            group.shutdownGracefully().sync();
        } }
    public static void main(String[] args) throws Exception {
   
//        if (args.length != 2) {
   
//            System.err.println(
//                    "Usage: " + EchoClient.class.getSimpleName() + " <host> <port>");
//            return;
//        }
//        String host = args[0];
//        int port = Integer.parseInt(args[1]);
        String host="192.168.222.130";
        int port = 3391;
        new EchoClient(host, port).start();
    } }

注意理解: 每个 Channel 分配一个 EventLoop 一个Channel能有多个ChannelHandler。EventLoop 本身只由一个线程驱动,其处理了一个 Channel 的所有 I/O 事件,并且在该EventLoop 的整个生命周期内都不会改变。这个简单而强大的设计消除了你可能有的在ChannelHandler 实现中需要进行同步的任何顾虑,因此,你可以专注于提供正确的逻辑,用来在有感兴趣的数据要处理的时候执行。

5.netty组件和设计

netty —基于 Java NIO 的基于异步和事件驱动—基于future和回调的概念

保证了高负载下应用程序性能的最大化和可伸缩性。其次,Netty 也包含了一组设计模式,将应用程序逻辑从网络层解耦,简化了开发过程,同时也最大限度地提高了可测试性、模块化以及代码的可重用性。

  • Channel—Socket;
  • EventLoop—控制流、多线程处理、并发;
  • ChannelFuture—异步通知。

5.1 Channel

基本的 I/O 操作(bind()、connect()、read()和 write())依赖于底层网络传输所提供的原语。在基于 Java 的网络编程中,其基本的构造是 class Socket。Netty 的 Channel 接 口所提供的 API,大大地降低了直接使用 Socket 类的复杂性。此外,Channel 也是拥有许多预定义的、专门化实现的广泛类层次结构的根,下面是一个简短的部分清单:

 EmbeddedChannel;

 LocalServerChannel;

 NioDatagramChannel;

 NioSctpChannel;

 NioSocketChannel。

5.2 EventLoop接口

EventLoop 定义了 Netty 的核心抽象,用于处理连接的生命周期中所发生的事件。

image-20210808095125816

  • 一个 EventLoopGroup 包含一个或者多个 EventLoop;
  • 一个 EventLoop 在它的生命周期内只和一个 Thread 绑定
  • 所有由 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理
  • 一个 Channel 在它的生命周期内只注册于一个 EventLoop
  • 一个 EventLoop 可能会被分配给一个或多个 Channel。
  • 注意,在这种设计中,一个给定 Channel 的 I/O 操作都是由相同的 Thread 执行的,实际上消除了对于同步的需要

5.3 ChannelFuture接口

Netty 中所有的 I/O 操作都是异步的。因为一个操作可能不会立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法。为此,Netty 提供了ChannelFuture 接口,其 addListener()方法注册了一个 ChannelFutureListener,以便在某个操作完成时(无论是否成功)得到通知。

所有属于同一个 Channel 的操作都被保证其将以它们被调用的顺序被执行。

5.4 ChannelHandler与ChannelPipeline

从应用程序开发人员的角度来看,Netty 的主要组件是 ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器。这是可行的,因为 ChannelHandler 的方法是由网络事件(其中术语“事件”的使用非常广泛)触发的。事实上,ChannelHandler 可专门用于几乎任何类型的动作,例如将数据从一种格式转换为另外一种格式,或者处理转换过程中所抛出的异常。

ChannelPipeline 提供了 ChannelHandler 链的容器,并定义了用于在该链上传播入站和出站事件流的 API。当 Channel 被创建时,它会被自动地分配到它专属的 ChannelPipeline。

ChannelHandler 安装到 ChannelPipeline 中的过程如下所示:

  • 一个ChannelInitializer的实现被注册到了ServerBootstrap中
  • 当 ChannelInitializer.initChannel()方法被调用时ChannelInitializer将在 ChannelPipeline 中安装一组自定义的 ChannelHandler;
  • ChannelInitializer 将它自己从 ChannelPipeline 中移除。

ChannelHandler 是专为支持广泛的用途而设计的,可以将它看作是处理往来 ChannelPipeline 事件(包括数据)的任何代码的通用容器。

image-20210808104133006

使得事件流经 ChannelPipeline 是 ChannelHandler 的工作,它们是在应用程序的初始化或者引导阶段被安装的。这些对象接收事件、执行它们所实现的处理逻辑,并将数据传递给链中的下一个 ChannelHandler。它们的执行顺序是由它们被添加的顺序所决定的。实际上,被我们称为 ChannelPipeline 的是这些 ChannelHandler 的编排顺序。

从一个客户端应用程序的角度来看,如果事件的运动方向是从客户端到服务器端,那么我们称这些事件为出站的,反之则称为入站的。

image-20210808104342091

入站和出站 ChannelHandler 可以被安装到同一ChannelPipeline

如果一个消息或者任何其他的入站事件被读取,那么它会从 ChannelPipeline 的头部开始流动,并被传递给第一个 ChannelInboundHandler。这个 ChannelHandler 不一定会实际地修改数据,具体取决于它的具体功能,在这之后,数据将会被传递给链中的下一个ChannelInboundHandler。最终,数据将会到达 ChannelPipeline 的尾端,届时,所有处理就都结束了。

数据的出站运动(即正在被写的数据)在概念上也是一样的。在这种情况下,数据将从ChannelOutboundHandler 链的尾端开始流动,直到它到达链的头部为止。在这之后,出站数据将会到达网络传输层,这里显示为 Socket

通过使用作为参数传递到每个方法的 ChannelHandlerContext,事件可以被传递给当前ChannelHandler 链中的下一个 ChannelHandler。因为你有时会忽略那些不感兴趣的事件,所以 Netty提供了抽象基类 ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter。通过调用 ChannelHandlerContext 上的对应方法,每个都提供了简单地将事件传递给下一个ChannelHandler的方法的实现。随后,你可以通过重写你所感兴趣的那些方法来扩展这些类。

鉴于出站操作和入站操作是不同的,你可能会想知道如果将两个类别的 ChannelHandler都混合添加到同一个 ChannelPipeline 中会发生什么。虽然 ChannelInboundHandle 、ChannelOutboundHandle 都扩展自 ChannelHandler,但是 Netty 能区分 ChannelInboundHandler 实现和 ChannelOutboundHandler 实现,并确保数据只会在具有相同定向类型的两个 ChannelHandler 之间传递。

当ChannelHandler 被添加到ChannelPipeline 时,它将会被分配一个ChannelHandlerContext,其代表了 ChannelHandler 和 ChannelPipeline 之间的绑定。虽然这个对象可以被用于获取底层的 Channel,但是它主要还是被用于写出站数据。

在 Netty 中,有两种发送消息的方式。你可以直接写到 Channel 中,也可以 写到和 ChannelHandler相关联的ChannelHandlerContext对象中前一种方式将会导致消息从ChannelPipeline 的尾端开始流动,而后者将导致消息从 ChannelPipeline 中的下一个 ChannelHandler 开始流动。

Netty 以适配器类的形式提供了大量默认的 ChannelHandler 实现,其旨在简化应用程序处理逻辑的开发过程。你已经看到了,ChannelPipeline中的每个ChannelHandler将负责把事件转发到链中的下一个 ChannelHandler。这些适配器类(及它们的子类)将自动执行这个操作,所以你可以只重写那些你想要特殊处理的方法和事件。

image-20210808112214135

5.5 编码器和解码器

当你通过 Netty 发送或者接收一个消息的时候,就将会发生一次数据转换。入站消息会被解码;也就是说,从字节转换为另一种格式,通常是一个 Java 对象。如果是出站消息,则会发生相反方向的转换:它将从它的当前格式被编码为字节。这两种方向的转换的原因很简单:网络数据总是一系列的字节。

所有由 Netty 提供的编码器/解码器适配器类都实现了 ChannelOutboundHandler 或者 ChannelInboundHandler 接口。

你将会发现对于入站数据来说,channelRead 方法/事件已经被重写了。对于每个从入站Channel 读取的消息,这个方法都将会被调用。随后,它将调用由预置解码器所提供的 decode()方法,并将已解码的字节转发给 ChannelPipeline 中的下一个ChannelInboundHandler。

5.6 引导

Netty 的引导类为应用程序的网络层配置提供了容器这涉及将一个进程绑定到某个指定的端口,或者将一个进程连接到另一个运行在某个指定主机的指定端口上的进程

面向连接的协议 请记住,严格来说,“连接”这个术语仅适用于面向连接的协议,如 TCP,其保证了两个连接端点之间消息的有序传递。

image-20210808114900177

image-20210808115800988

6.传输

6.1 阻塞IO(OIO/BIO)

image-20210809075040549

未使用netty的阻塞网络编程:

public class PlainOioServer {
   
public void serve(int port) throws IOException {
   
final ServerSocket socket = new ServerSocket(port);
try {
   
for (;;) {
   
final Socket clientSocket = socket.accept();
System.out.println(
"Accepted connection from " + clientSocket);
    //如上图所示,每有一个新连接,则开启一个新线程来进行读写操作。无法支持高并发。线程上下文切换开销大。
new Thread(new Runnable() {
   
@Override
public void run() {
   
OutputStream out;
try {
   
out = clientSocket.getOutputStream();
    //将消息写给已连接的客户端。
out.write("Hi!\r\n".getBytes(
Charset.forName("UTF-8")));
out.flush();
clientSocket.close();
}
catch (IOException e) {
   
e.printStackTrace();
}
finally {
   
try {
   
clientSocket.close();
}
catch (IOException ex) {
   
// ignore on close
} } }
}).start();
} }
catch (IOException e) {
   
e.printStackTrace();
} } }

6.2 非阻塞IO(NIO)

image-20210809075121599

为了支持高并发 需要改成非阻塞IO的异步网络编程。

public class PlainNioServer {
   
    public void serve(int port) throws IOException {
   
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.configureBlocking(false);
        ServerSocket ssocket = serverChannel.socket();
        InetSocketAddress address = new InetSocketAddress(port);
        ssocket.bind(address);
        Selector selector = Selector.open();
        //将ServerSocket注册到selector以接受连接。
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        final ByteBuffer msg = ByteBuffer.wrap("Hi!\r\n".getBytes());
        for (; ; ) {
   
            try {
   
                //等待需要处理的新事件;阻塞 将一直持续到下一个传入事件。
                selector.select();
            } catch (IOException ex) {
   
                ex.printStackTrace();
// handle exception
                break;
            }
            //获取所有接受时间的SelectionKey实例。
            Set<SelectionKey> readyKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = readyKeys.iterator();
            while (iterator.hasNext()) {
   
                SelectionKey key = iterator.next();
                iterator.remove();
                try {
   
                    if (key.isAcceptable()) {
   
                        //检查事件是否是一个新的已经就绪可以被接受的连接
                        ServerSocketChannel server =
                                (ServerSocketChannel) key.channel();
                        SocketChannel client = server.accept();
                    //接受客户端,并将它注册到选择器
                        client.configureBlocking(false);
                        client.register(selector, SelectionKey.OP_WRITE |
                                SelectionKey.OP_READ, msg.duplicate());
                        System.out.println(
                                "Accepted connection from " + client);
                    }
                    if (key.isWritable()) {
   
                        //检查套接字是否已经准备好写数据。
                        SocketChannel client =
                                (SocketChannel) key.channel();
                        ByteBuffer buffer =
                                (ByteBuffer) key.attachment();
                        while (buffer.hasRemaining()) {
   
                            if (client.write(buffer) == 0) {
   
                                break;
                            }
                        }
                        client.close();
                    }
                } catch (IOException ex) {
   
                    key.cancel();
                    try {
   
                        key.channel().close();
                    } catch (IOException cex) {
   
// ignore on close
                    }
                }
            }
        }
    }
}

6.3 netty+OIO

public class NettyOioServer {
   
    public void server(int port) throws Exception {
   
        final ByteBuf buf = Unpooled.unreleasableBuffer(
                Unpooled.copiedBuffer("Hi!\r\n", Charset.forName("UTF-8")));
        EventLoopGroup group = new OioEventLoopGroup();
        try {
   
            //创建ServerBootStrap类
            ServerBootstrap b = new ServerBootstrap();
            b.group(group)
                  //使用OIOEventLoopGroup以阻塞模式
                .channel(OioServerSocketChannel.class)
                    .localAddress(new InetSocketAddress(port))
                //指定channelInitializer对于每个已接收的连接都调用它
                    .childHandler(new ChannelInitializer<SocketChannel>() {
   
                        @Override
                        public void initChannel(SocketChannel ch)
                                throws Exception {
   
                            ch.pipeline().addLast(
                                //添加一个handler拦截和处理事件
                                    new ChannelInboundHandlerAdapter() {
   
                                        @Override
                                        public void channelActive(
                                                ChannelHandlerContext ctx)
                                                throws Exception {
   
                                            ctx.writeAndFlush(buf.duplicate())
                                                //添加一个listener以便消息一写完就关闭连接。
                                                    .addListener(
                                                            ChannelFutureListener.CLOSE);
                                        }
                                    });
                        }
                    });
            //绑定服务器以接受连接
            ChannelFuture f = b.bind().sync();
            f.channel().closeFuture().sync();
        } finally {
   
            //释放所有资源
            group.shutdownGracefully().sync();
        }
    }
}

6.4 netty+NIO

public class NettyNioServer {
   
    public void server(int port) throws Exception {
   
        final ByteBuf buf = Unpooled.copiedBuffer("Hi!\r\n",
                Charset.forName("UTF-8"));
        EventLoopGroup group = new NioEventLoopGroup();
        try {
   
            ServerBootstrap b = new ServerBootstrap();
            b.group(group).channel(NioServerSocketChannel.class)
                    .localAddress(new InetSocketAddress(port))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
   
                        @Override
                        public void initChannel(SocketChannel ch)
                                throws Exception {
   
                            ch.pipeline().addLast(
                                    new ChannelInboundHandlerAdapter() {
   
                                        @Override
                                        public void channelActive(
                                                ChannelHandlerContext ctx) throws Exception {
   
                                            ctx.writeAndFlush(buf.duplicate())
                                                    .addListener(
                                                            ChannelFutureListener.CLOSE);
                                        }
                                    });
                        }
                    });
            ChannelFuture f = b.bind().sync();
            f.channel().closeFuture().sync();
        } finally {
   
            group.shutdownGracefully().sync();
        }
    }
}

可以看到netty+NIO只在netty+OIO的代码上修改了两行。

比之前不使用netty时修改的幅度要小许多,代码可重用性更强。

因为 Netty 为每种传输的实现都暴露了相同的 API,所以无论选用哪一种传输的实现,你的代码都仍然几乎不受影响。在所有的情况下,传输的实现都依赖于 interface Channel、ChannelPipeline 和 ChannelHandler。

6.5 传输API

传输 API 的核心是 interface Channel,它被用于所有的 I/O 操作。

image-20210809085912294

每个 Channel 都将会被分配一个 ChannelPipeline 和 ChannelConfig。ChannelConfig 包含了该 Channel 的所有配置设置,并且支持热更新。由于特定的传输可能具有独特的设置,所以它可能会实现一个 ChannelConfig 的子类型。(请参考 ChannelConfi实现对应的 Javadoc。)

由于 Channel 是独一无二的,所以为了保证顺序将 Channel 声明为 java.lang.Comparable 的一个子接口。因此,如果两个不同的 Channel 实例都返回了相同的散列码,那 么 AbstractChannel 中的 compareTo()方法的实现将会抛出一个 Error。

ChannelPipeline 持有所有将应用于入站和出站数据以及事件的 ChannelHandler 实例,这些 ChannelHandler 实现了应用程序用于处理状态变化以及数据处理的逻辑。

ChannelHandler 的典型用途包括:

  • 将数据从一种格式转换为另一种格式;
  • 提供异常的通知;
  • 提供 Channel 变为活动的或者非活动的通知;
  • 提供当 Channel 注册到 EventLoop 或者从 EventLoop 注销时的通知;
  • 提供有关用户自定义事件的通知。

拦截过滤器 ChannelPipeline 实现了一种常见的设计模式—拦截过滤器(Intercepting Filter)。UNIX 管道是另外一个熟悉的例子:多个命令被链接在一起,其中一个命令的输出端将连接到命令行中下一个命令的输入端

除了访问所分配的 ChannelPipeline 和 ChannelConfig 之外,也可以利用 Channel的其他方法,

image-20210809090651857

请记住,Netty 所提供的广泛功能只依赖于少量的接口。这意味着,你可以对你的应用程序逻辑进行重大的修改,而又无需大规模地重构你的代码库。

考虑一下写数据并将其冲刷到远程节点这样的常规任务。

Channel channel=...
        ByteBuf buf=Unpooled.copiedBuffer("your data",CharsetUtil.UTF_8);
        ChannelFuture cf=channel.writeAndFlush(buf);
        cf.addListener(new ChannelFutureListener(){
   
@Override
public void operationComplete(ChannelFuture future){
   
        if(future.isSuccess()){
   
        System.out.println("Write successful");
        }else{
   System.err.println("Write error");
        future.cause().printStackTrace();
        }}
        });

Netty 的 Channel 实现是线程安全的

因此你可以存储一个到 Channel 的引用,并且每当你需要向远程节点写数据时,都可以使用它,即使当时许多线程都在使用它,需要注意的是,消息将会被保证按顺序发送。

final Channel channel = ...
final ByteBuf buf = Unpooled.copiedBuffer("your data",
CharsetUtil.UTF_8).retain();
Runnable writer = new Runnable() {
   
@Override
public void run() {
   
channel.writeAndFlush(buf.duplicate());
}
};
Executor executor = Executors.newCachedThreadPool();
// write in one thread
executor.execute(writer);
// write in another thread
executor.execute(writer);

6.6 内置传输

Netty 内置了一些可开箱即用的传输。因为并不是它们所有的传输都支持每一种协议,所以你必须选择一个和你的应用程序所使用的协议相容的传输。

名称 描述
NIO io.netty.channel.socket.nio 使用 java.nio.channels 包作为基础——基于选择器的方式
Epoll
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值