Netty快速入门

目录

1.编写Echo服务器

1.1 ChannelHandler和业务逻辑

1.2 引导服务器

2.编写Echo客户端

2.1 通过ChannelHandler实现客户端逻辑

2.2 引导客户端

3. 编译运行

4.小结


本章我们将展示如何构建一个基于Netty的客户端和服务器。应用程序非常简单,客户端将消息发送给服务端,而服务器再将消息回送给客户端。开发环境搭建步骤我们直接跳过了,这里有一点需要注意的是我们下载的是JDK,而不是Java运行环境(JRE),其只可以运行Java应用程序,但是不能够编译它们。

下图展示了我们要编写的Echo客户端和服务器应用程序:

Echo客户端和服务器之间的交互非常简单的,在客户端建立一个连接之后,它会向服务器发送一个或多个消息,反过来,服务器又将每个消息回送给客户端。

1.编写Echo服务器

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

  1. 至少一个ChannelHandler:该组件实现了服务器对从客户端接收的数据的处理,即它的业务逻辑。
  2. 引导——这是配置服务器的启动代码。至少,它会将服务器绑定到它要监听连接请求的端口上。

1.1 ChannelHandler和业务逻辑

ChannelHandler是一个接口族的父接口,它的实现负责接收并响应事件通知。在Netty应用程序中,所有的数据处理逻辑都包含在这些核心抽象的实现中。

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

我们项目中主要用到了下面的方法:

  • channelRead():对于每个传入的消息都要调用
  • channelReadComplete():通知ChannelInboundHandler,最后一次对channelRead调用(读取最后一次消息)
  • exceptionCaught():在读取操作期间,有异常抛出时会调用

该Echo服务器的ChannelHandler实现是EchoServerHandler,实现代码如下:

package com.martin.learn.netty.server;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;

import java.nio.charset.Charset;

/**
 * 负责接收并响应时间的通知
 *
 * @author: martin
 * @date: 2018/12/21 20:38
 * @description:
 */

/**
 * 标识一个ChannelHandler可以被多个Channel安全的共享
 */
@ChannelHandler.Sharable
public class EchoServiceHandler extends ChannelInboundHandlerAdapter {

    /**
     * 对于每个传入的消息都要调用
     *
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf in = (ByteBuf) msg;
        System.out.println("Server received:" + in.toString(CharsetUtil.UTF_8));
        //将接收到的消息写给发送者
        ctx.write(in);
    }

    /**
     * 读取操作完成时调用
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        //将未决的消息冲刷到远程节点,并且关闭该Channel
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
    }

    /**
     * 在读取操作期间,有异常抛出时调用
     *
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        //关闭该Channel
        ctx.close();
    }
}

ChannelInboundHandlerAdapter有一个直观的API,并且它的每个方法都可以被重写以挂钩到事件生命周期的恰当点上。因为需要处理所有接收到的数据,所以我们重写了channelRead()方法。

重写exceptionCaught()方法允许我们对Throwable的任何子类型做出处理,在这里我们记录了异常并关闭了连接。

如果我们在这里不捕获异常,会发生什么呢?

每个Channel都拥有一个与之相关联的ChannelPipeline,其持有一个ChannelHandler的实例链。在默认的情况下,ChannelHadler会把对它的方法的调用转发给链中的下一个ChannelHandler。因此,如果exceptionCaught()方法没有被该链中的某处实现,那么所接收的异常将会被传递到ChannelPipeline的尾端并被记录。为此,我们的应用程序应该提供至少一个实现了exceptionCaught()方法的ChannelHandler。

关键点总结:

  • 针对不同类型的事件来调用ChannelHandler。
  • 应用程序通过实现或者扩展ChannelHandler来挂钩到事件的生命周期,并且提供自定义的应用程序逻辑。
  • 在架构上,ChannelHandler有助于保持业务逻辑与网络代理代码的分离,这简化了开发过程。

1.2 引导服务器

引导服务器涉及以下内容:

  • 绑定到服务器将在其上监听并接受传入连接请求的端口。
  • 配置Channel,以便将有关的入站消息通知给EchoServerHandler实例。

实现代码如下:

package com.martin.learn.netty.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

import java.net.InetSocketAddress;

/**
 * 服务器类
 *
 * @author: martin
 * @date: 2018/12/22 9:54
 * @description:
 */
public class EchoServer {
    private final int port;

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

    public void start() throws InterruptedException {
        final EchoServiceHandler serviceHandler = new EchoServiceHandler();
        //创建并分配一个NioEventLoopGroup实例以进行事件的处理,如接受新连接以及读写数据
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(group)
                    //指定所使用的NIO传输Channel
                    .channel(NioServerSocketChannel.class)
                    //使用指定的端口设置套接字的地址
                    .localAddress(new InetSocketAddress(port))
                    //添加一个EchoServerHandler到子Channel的ChannelPipeline
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            //EchoServerHandler被标注为@Shareable,所以我们总是使用相同的实例
                            socketChannel.pipeline().addLast(serviceHandler);
                        }
                    });
            //异步地绑定服务器,调用sync()方法阻塞等待直到绑定完成
            ChannelFuture future = bootstrap.bind().sync();
            //获取Channel的CloseFuture,并且阻塞当前线程直到它完成
            future.channel().closeFuture().sync();
        } finally {
            //关闭EventLoopGroup,释放所有的资源
            group.shutdownGracefully().sync();
        }


    }

    public static void main(String[] args) throws Exception {
        if (args.length != 1) {
            System.err.println("必须指定服务器端口");
        }

        int port = Integer.parseInt(args[0]);
        new EchoServer(port).start();
    }
}

这个实力使用了NIO,因为得益于它的可扩展性和彻底的异步性,它是目前使用最广泛的传输。但是也可以使用一个不同的传输实现,比如OIO传输,将需要指定OioServerSocketChannel和OioEventLoopGroup。引导的步骤如下:

  • 创建一个ServerBootstrap的实例以引导和绑定服务器
  • 创建并分配一个NioEventLoopGroup实例以进行事件的处理,如接受新连接以及读/写数据。
  • 指定服务器绑定的本地的InetSocketAddress
  • 使用一个EchoServerHandler的实例初始化每一个新的Channel
  • 调用ServerBootstrap.bind()方法以绑定服务器

这个时候,服务器已经初始化,并且就绪已经能被使用了。

2.编写Echo客户端

Echo客户端将会:

  1. 连接到服务器
  2. 发送一个或者多个消息
  3. 对于每个消息,等待并接收从服务器发回的相同的消息
  4. 关闭连接

2.1 通过ChannelHandler实现客户端逻辑

客户端将拥有一个用来处理数据的ChannelInboundHandler,在这里我们扩展SimpleChannelInboundHandler类以处理所有必须的任务。这要求重写下面的方法:

  • channelActive():在到服务器的连接已经建立之后将被调用
  • channelRead():当从服务器接收到一条消息时被调用
  • exceptionCaught():在处理过程中引发异常时被调用

实例代码如下:

package com.martin.learn.netty.client;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;

/**
 * @author: martin
 * @date: 2018/12/22 23:00
 * @description:
 */
@ChannelHandler.Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    /**
     * 在到服务器的连接已经建立之后将被调用
     *
     * @param ctx
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        //当被通知Channel是活跃的时候,发送一条消息
        ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8));
    }

    /**
     * 在处理过程中引发异常的时候被调用
     *
     * @param ctx
     * @param cause
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        //在发生异常的时候,记录错误并关闭Channel
        cause.printStackTrace();
        ctx.close();
    }

    /**
     * 每当从服务器接收到一条消息时被调用
     *
     * @param channelHandlerContext
     * @param byteBuf
     */
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) {
        //记录已接收消息的转储
        System.out.println("Client received:" + byteBuf.toString(CharsetUtil.UTF_8));
    }
}

首先,我们重写了channelActive()方法,其将在一个连接建立时被调用,这确保了数据将会被尽可能快地写入服务器。

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

最后,我们重写了exceptionCaught(),记录Throwable,关闭Channel,终止到服务器的连接。

2.2 引导客户端

引导客户端类似于引导服务器,使用代码如下:

package com.martin.learn.netty.client;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

import java.net.InetSocketAddress;

/**
 * 客户端连接
 *
 * @author: martin
 * @date: 2018/12/22 23:36
 * @description:
 */
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以处理客户端的事件;适用于NIO的实现
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            //创建Bootstrap
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    //适用于NIO传输的Channel类型
                    .channel(NioSocketChannel.class)
                    //设置连接服务器的InetSocketAddress
                    .remoteAddress(new InetSocketAddress(host, port))
                    //在创建Channel时,向ChannelPipeline中添加一个EchoClientHandler实例
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new EchoClientHandler());
                        }
                    });
            //连接到远程节点,阻塞等待直到连接完成
            ChannelFuture channelFuture = bootstrap.connect().sync();
            //阻塞直到Channel关闭
            channelFuture.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]);
        new EchoClient(host, port).start();
    }
}

和服务端一样,使用了NIO传输。注意,我们可以在客户端和服务端上分别使用不同的传输。例如,在服务器端使用NIO传输,而在客户端使用OIO传输。实现客户端的关键点如下:

  • 为初始化客户端,创建了一个Bootstrap实例。
  • 为进行事件处理分配了一个NioEventLoopGroup实例,其中事件处理包括创建新的连接以及处理入站和出站数据。
  • 为连接服务器创建了一个InetSocketAddress实例。
  • 当连接被建立时,一个EchoClientHandler实例会被安装到ChannelPipeline中。
  • 在一切都设置完成之后,调用Bootsrap.connect()方法连接到远程节点。

3. 编译运行

完成客户端和服务端的编写之后,先运行服务端,再运行客户端,输出结果如下:

Server received:Netty rocks!

Client received:Netty rocks!

4.小结

在本章中,我们开发了一个简单的Netty客户端和服务端。虽然这只是一个简单的应用程序,但是它可以伸缩到支持数千个并发连接,每秒可以比普通的基于套接字的Java应用程序处理多得多的消息。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章