从NIO编程到Netty的使用

我们在网络编程——NIO编程中,就曾介绍过直接使用NIO进行编程,这里我们介绍下如何使用Netty框架,来完成我们之前直接使用NIO完成的功能,就是一个简单的客户端和服务端的通信。


在这之前,我们先来简单了解一下Netty框架的核心组件:

Channel

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

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


回调和Future

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

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

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

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

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


事件和ChannelHandler

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

Netty事件是按照它们与入站或出站数据流的相关性进行分类的。可能由入站数据或者相关的状态更改而触发的事件包括:

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

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

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

每个事件都可以被分发给ChannelHandler 类中的某个用户实现的方法。可以认为每个Channel-Handler 的实例都类似于一种为了响应特定事件而被执行的回调。

Netty 提供了大量预定义的可以开箱即用的ChannelHandler 实现,包括用于各种协议(如HTTP 和SSL/TLS)的ChannelHandler。



了解过我们在网络编程使用NIO完成的小例子,以及上述Netty核心组件之后,我们来看一看如何使用Netty来完成网络通信,首先来看一看客户端的实现:

public class NettyClient {
     

    private final String host;
    private final int port;

    public NettyClient(String host, int port) {
     
        this.host = host;
        this.port = port;
    }

    public void start() throws InterruptedException {
     
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();    //线程组
        Bootstrap bootstrap = new Bootstrap();      //客户端启动所需

        try {
     
            bootstrap.group(eventLoopGroup)
                    .channel(NioSocketChannel.class)    //指定使用NIO进行网络通讯
                    .remoteAddress(new InetSocketAddress(host, port))   //配置远程服务器地址
                    .handler(new NettyClientHandler());
            ChannelFuture channelFuture = bootstrap.connect().sync();   //连接远程服务器,阻塞,直到连接完成
            channelFuture.channel().closeFuture().sync();   //阻塞关闭,直到channel关闭
        } finally {
     
            eventLoopGroup.shutdownGracefully().sync();
        }
    }

    public static void main(String[] args) throws InterruptedException {
     
        new NettyClient("127.0.0.1", 8888).start();
    }
}
public class NettyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
     

    /**
     * 客户端得知Channel活跃后的处理
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
     
        //向服务器发送数据
        ctx.writeAndFlush(Unpooled.copiedBuffer("Hello, Netty", Charset.forName("UTF-8")));
    }

    /**
     * 客户端接收数据后的处理
     */
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
     
        System.out.println("客户端接收到数据,内容为:" + byteBuf.toString(CharsetUtil.UTF_8));
    }

    /**
     * 异常处理
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
     
        cause.printStackTrace();
        ctx.close();
    }
}

然后我们在看一看Netty服务端的实现,在服务端接受到客户端发送的消息之后,打印出来,并原封不动的再次发送给客户端,如下:

public class NettyServer {
     

    private final int port;

    public NettyServer(int port) {
     
        this.port = port;
    }

    public void start() throws InterruptedException {
     
        final NettyServerHandler serverHandler = new NettyServerHandler();
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();    //线程组
        ServerBootstrap serverBootstrap = new ServerBootstrap();    //服务端启动所需

        try {
     
            serverBootstrap.group(eventLoopGroup)
                    .channel(NioServerSocketChannel.class)    //指定使用NIO进行网络通讯
                    .localAddress(new InetSocketAddress(port))   //指定服务器监听端口
                    //接收到连接请求,新启一个socket通信,即channel,每个channel都有处理自己事件的handler
                    .childHandler(new ChannelInitializer<SocketChannel>() {
     
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
     
                            socketChannel.pipeline().addLast(serverHandler);
                        }
                    });
            ChannelFuture channelFuture = serverBootstrap.bind().sync();   //绑定端口,阻塞,直到连接完成
            channelFuture.channel().closeFuture().sync();   //阻塞关闭,直到channel关闭
        } finally {
     
            eventLoopGroup.shutdownGracefully().sync();
        }
    }

    public static void main(String[] args) throws InterruptedException {
     
        new NettyServer(8888).start();
    }
}
@ChannelHandler.Sharable
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
     

    /**
     * 服务端读取到网络数据后的处理
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
     
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println("服务器接收到数据,内容为:" + byteBuf.toString(Charset.forName("UTF-8")));
        ctx.write(byteBuf);
    }

    /**
     * 服务器读取完成网络数据后的处理
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
     
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)    //flush所有需发送的数据
                .addListener(ChannelFutureListener.CLOSE);  //当flush完成,关闭连接
    }

    /**
     * 发生异常后的处理
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
     
        cause.printStackTrace();
        ctx.close();
    }
}

其中若接收到的客户端的数据很大,需要多次调用上述的channelRead方法来进行发送,完成后会调用channelReadComplete方法,来flush所有的数据,并关闭与客户端的连接,客户端中监测到与服务端连接的channel被关闭后,随之客户端就会关闭了,如下
从NIO编程到Netty的使用_第1张图片


测试结果截图如下:
从NIO编程到Netty的使用_第2张图片
从NIO编程到Netty的使用_第3张图片


另外我们在服务端上添加了 @ChannelHandler.Sharable 注解,这里其实可以理解为该类是一个共享的,即我们在Spring中提到的单例的意思,所以我们在使用时,只需参加一个实例即可,如下
在这里插入图片描述
从NIO编程到Netty的使用_第4张图片

而我们在客户端的实现上就没有添加该注解,所以我们每次使用都会参加一个新的实例出来
在这里插入图片描述

在上述的实现中,我们可以将服务端不添加该注解,每次使用和客户端一样,创建出一个新的实例即可,或者我们也可以在客户端使用添加该注解,但是需要注意的是,我们在使用共享一个实例的时候,我们需要保证该类的线程安全性,至于如何保证一个类的线程安全,我们在并发编程中也以及说过了,这里就不重复介绍了。

你可能感兴趣的:(Netty)