2.4 写一个回声客户端

现在,所有的服务端代码都写好了,我们来创建客户端。

客户端包含以下任务:

  • 连接服务端

  • 写数据

  • 等待接收从服务端返回的完全相同的数据

  • 关闭连接

带着思考,让我们来写客户端。

2.4.1 引导客户端

引导客户端和引导服务端类似。不同的是,客户端引导连接同时接受一个host和port,而服务端只有port。

Listing 2.4 Main class for the client
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;
        }

        final String host = args[0];
        final int port = Integer.parseInt(args[1]);
        new EchoClient(host, port).start();
    }
}

和之前一样,这里用的也是NIO传输。值得一提的是,你使用哪种传输都没关系。你可以同时在客户端和服务端使用不同的传输。在服务端使用NIO传输在客户端使用OIO传输也是可能的。你将在第四章学习更多关于如何在特定场景何种传输的内容。

让我们重点关注以下几点:

  • 创建一个Bootstrap实例来引导客户端

  • 创建一个NioEventLoopGroup 实例,指派处理事件,像创建新连接、收发数据等

  • 远程的InetSocketAddress指定客户端连接的对象

  • 设置一个handler,一旦连接建立,它将会被执行

  • 每件事设置完成后, 调用Bootstrap.connect()方法去连接远程端点

2.4.2 实现客户端逻辑

本节的例子依旧保持简单,因为任何至今还未覆盖到的类将在将来的章节讨论。和之前一样,写一个SimpleChannelInboundHandlerAdapter实现去处理任何需要的任务。通过覆盖我们感兴趣的三个方法来处理事件:

  • channActive() -- 建立与服务端的连接后调用

  • channelRead0()--从服务端收到数据后调用

  • exceptionCaught() 处理期间发生任何异常时调用

如前所述,你覆写列表中三个方法。这个三个方法是编写程序是必须实现的。channelActive()是建立与服务端的连接后调用,这里的逻辑很简单,一旦建立连接,发送一串字节到服务端。消息的内容无关紧要,本例使用的是“Netty rocks!”的编码,重写此方法可以让书记尽早写到服务端。

接下来,重写channelRead0()方法。一旦接收到该数据该接口将被调用。注意,收到的字节可能是破碎的,意味着服务端写五个字节,不能保证一次完全接受5个字节。为了接收这5个字节,channelRead0()可能被调用两次。例如,第一次被调用,一个ByteBuffer含有三个字节,第二次含有2个。唯一能保证的是,接收的顺序和发送的一致。这仅适用于TCP和其他流定向协议。

第三个是重写exceptionCaught()。用法和EchoServerHandler一致。记录Throwable日志,关闭管道,意味着和服务端的连接关闭。

你或许会想,为什么这里用SimpleChannelInboundHandler 而不是像EchoServerHandler中使用ChannelInboundHandlerAdapter。“原因是:使用ChannelInboundHandlerAdapter,你需要负责在处理完接收到的数据后,释放资源。使用ByteBuf 的时候,将调用ByteBuf.release()。SimpleChannelInboundHandler则不会出现这种情况。因为一旦channelRead0()完成后,它将释放消息。它由Netty完成,通过处理实现ReferenceCounted的所有消息”。但为什么不在EchoServerHandler中这样呢?这样做的原因是我们要回显消息,这意味着我们还不能释放它,因为在channelRead(...)返回之后写入操作可能会完成(记住写入是异步的)。写入完成后,Netty将自动释放该消息。这就是客户端所需的一切。现在是时候测试您的代码并查看实际效果了。

最后更新于

这有帮助吗?