Echo客户端

Echo,如同Linux的Echo命令,就是打印输出的内容,它的工作内容:

  • 连接服务器
  • 发送信息
  • 等待和接收从服务器返回的同样的信息
  • 关闭连接

使用ChannelHandler实现客户端业务逻辑

如同服务器一样,使用ChannelInboundHandler接口来处理数据。服务器继承了ChannelInboundHandlerAdapter,这里客户端我们使用SimpleChannelInboundHandler来处理所有的任务,需要覆写三个方法:
- channelActive() - 服务器的连接被建立后调用
- channelRead0() - 从服务器收到数据后调用
- exceptonCaught() - 捕获一个异常时调用
代码如下:

// 1.@Sharable标记这个类的实例可以在 channel 里共享
@ChannelHandler.Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // 2.当被通知该channel是活动的时候就发送消息
        ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks !", CharsetUtil.UTF_8));
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        // 3.记录接收到消息
        System.out.println("Client received: " + msg.toString(CharsetUtil.UTF_8));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        // 4.捕获错误,并关闭连接
        cause.printStackTrace();
        ctx.close();
    }
}

注:
1、建立连接后,channelActive()方法被调用一次,我们使用Netty编码一个字符串,发送到服务器。
2、覆写channelRead0()方法,在接收到数据时调用。(注意,由服务器发送的消息可以以块的形式被接收。如:当服务器发送 5 个字节是不是保证所有的 5 个字节会立刻收到,即使是只有 5 个字节,channelRead0() 方法可被调用两次,第一次用一个ByteBuf(Netty的字节容器)装载3个字节和第二次一个 ByteBuf 装载 2 个字节。唯一要保证的是,该字节将按照它们发送的顺序分别被接收。 (注意,这是真实的,只有面向流的协议如TCP))

SimpleChannelInboundHandler vs. ChannelInboundHandler

何时用这两个要看具体业务的需要。在客户端,当 channelRead0() 完成,我们已经拿到的入站的信息。当方法返回时,SimpleChannelInboundHandler 会小心的释放对 ByteBuf(保存信息) 的引用。而在 EchoServerHandler,我们需要将入站的信息返回给发送者,由于 write() 是异步的,在 channelRead() 返回时,可能还没有完成。所以,我们使用 ChannelInboundHandlerAdapter,无需释放信息。最后在 channelReadComplete() 我们调用 ctxWriteAndFlush() 来释放信息。

客户端代码

客户端引导需要 host 、port 两个参数连接服务器。

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 {
            // 1.客户端创建Bootstrap,服务器创建的是ServerBootstrap
            Bootstrap b = new Bootstrap();
            // 2.指定EventLoopGroup来处理客户端事件,因为用了NIO传输,所以用NioEventLoopGroup的实现
            b.group(group)
                    // 3.使用的 channel 类型是一个用于 NIO 传输
                    .channel(NioSocketChannel.class)
                    // 4.设置服务器的 InetSocketAddress
                    .remoteAddress(new InetSocketAddress(host, port))
                    // 5.当建立一个连接和一个新的通道时,创建添加到 EchoClientHandler 实例到channel pipeline
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new EchoClientHandler());
                        }
                    });
            // 6.连接到远程;等待连接完成
            ChannelFuture cf = b.connect().sync();
            // 7.阻塞直到 Channel 关闭
            cf.channel().closeFuture().sync();
        } finally {
            // 8.调用 shutdownGracefully() 来关闭线程池和释放所有资源
            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;
        }
        final String host = args[0];
        final int port = Integer.parseInt(args[1]);
        new EchoClient(host, port).start();
    }
}

流程:
1、创建一个Bootstrap来初始化客户端。
2、一个NioEventLoopGroup实例被分配给处理该事件的处理,这包括创建新的连接和处理入站和出站数据。
3、创建一个InetSocketAddress以连接到服务器。
4、连接好服务器之时,将安装一个EchoClientHandler在pipeline,处理业务逻辑。
5、之后Bootstrap.connect()被调用连接到远程的的服务器。