Netty学习笔记(二)编写Echo服务端与客户端

这一节将利用Netty编写简单的Echo服务端与客户端。

一、Echo服务端/客户端概览

我们所要实现的功能很简单,即服务端监听端口,客户端连接服务端后发送消息,服务端将接收的消息作为响应返回给客户端。

 二、编写Echo服务端

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

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

接下来就是Echo服务端的业务逻辑以及引导代码。 

1、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) {
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

我们对EchoServerHandler类使用@ChannelHandler.Sharable注解,表示我们的一个ChannelHandler可以被多个Channel安全的使用。

ChannelInboundHandlerAdapter有一个直观的API,并且它的每个方法都可以被重写以挂钩到事件生命周期的恰当点上。因为需要处理所有接收到的数据,所以我们重写了channelRead()方法。在这个服务器应用程序中,我们将数据简单地回送给了远程节点。

重写exceptionCaught()方法允许我们对 Throwable 的任何子类型做出反应,在这里我们打印了异常并关闭了连接。虽然一个更加完善的应用程序也许会尝试从异常中恢复,但在这个场景下,只是通过简单地关闭连接来通知远程节点发生了错误。 

2、引导服务器

我们的Echo服务器启动后应该作如下两件事:

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

代码如下:

public class EchoServer {

    private final int port;

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

    public void start() throws Exception{
        final EchoServerHandler serverHandler = new EchoServerHandler();
        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(serverHandler);

                        }
                    });
            ChannelFuture f = b.bind().sync();
            f.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully().sync();
        }
    }

    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]);
        new EchoServer(port).start();
    }
}

我们的引导服务器的代码主要逻辑如下:

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

此时我们的Echo服务端已经实现完成,接下来是客户端代码。

二、编写Echo客户端

我们的Echo 客户端启动后将会做如下几件事:

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

编写客户端所涉及的两个主要代码部分也是业务逻辑和引导,跟服务端很像。

1、通过ChannelHandler实现客户端逻辑

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

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

 代码如下:

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

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello netty", 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();
    }

}

首先,我们重写了channelActive()方法,其将在一个连接建立时被调用。这确保了数据将会被尽可能快地写入服务器,其在这个场景下是一个编码了字符串"hello netty"的字节缓冲区。接下来,重写了channelRead0()方法。每当接收数据时,都会调用这个方法。需要注意的是,由服务器发送的消息可能会被分块接收。也就是说,如果服务器发送了 5 字节,那么不 能保证这 5 字节会被一次性接收。即使是对于这么少量的数据,channelRead0()方法也可能会被调用两次,第一次使用一个持有3字节的ByteBuf,第二次使用一个 持有2字节的ByteBuf。作为一个面向流的协议,TCP 保证了字节数组将会按照服务器发送它们的顺序被接收。重写的第三个方法是exceptionCaught()。如同在EchoServerHandler中所示,记录Throwable,关闭Channel,在我们这个场景下,终止服务器的连接。

2、引导客户端

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

public class EchoClient {

    private final String host;
    private final int port;

    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]); new EchoClient(host, port).start();
    }
}

我们客户端引导流程如下:

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

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

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

4)当连接被建立时,一个EchoClientHandler实例会被安装到ChannelPipeline中;

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

 三、测试

接下来我们验证下Echo服务端与客户端。

首先我们启动Echo服务端:

接着启动Echo客户端:

我们看到客户端启动后立即想服务端发消息并收到服务端响应,所以我们服务端也是能正常工作的,它必然收到了客户端的消息:

 

 

posted @ 2020-06-14 14:14  阿拉懒神灯  阅读(362)  评论(0)    收藏  举报