《Netty 实战》Netty In Action中文版 第2章——你的第一款Netty应用程序(二)

2.3.2 引导服务器

在讨论过由EchoServerHandler实现的核心业务逻辑之后,我们现在可以探讨引导服务器本身的过程了,具体涉及以下内容:

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

传输

 

在这一节中,你将遇到术语传输。在网络协议的标准多层视图中,传输层提供了端到端的或者主机到主机的通信服务。

因特网通信是建立在TCP传输之上的。除了一些由Java NIO实现提供的服务器端性能增强之外,NIO传输大多数时候指的就是TCP传输。

我们将在第4章对传输进行详细的讨论。

代码清单2-2展示了EchoServer类的完整代码。

代码清单2-2 EchoServer

public class EchoServer {
    private final int port;

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

    public static void main(String[] args) throws Exception {
        if (args.length != 1) {
            System.err.println(
                "Usage: " + EchoServer.class.getSimpleName() +
                " ");
        }
        int port = Integer.parseInt(args[0]);   ⇽--- 设置端口值(如果端口参数的格式不正确,则抛出一个NumberFormatException
        new EchoServer(port).start();    ⇽---  调用服务器的start()方法
    }
    public void start() throws Exception {
        final EchoServerHandler serverHandler = new EchoServerHandler();
        EventLoopGroup group = new NioEventLoopGroup();    ⇽---   创建Event-LoopGroup
        try {
             ServerBootstrap b = new ServerBootstrap();    ⇽---    创建Server-Bootstrap b.group(group)
                 .channel(NioServerSocketChannel.class)   ⇽---   指定所使用的NIO传输Channel
                 .localAddress(new InetSocketAddress(port))   ⇽---   使用指定的端口设置套接字地址
                .childHandler(new ChannelInitializer(){    ⇽---   添加一个EchoServer-
Handler到子ChannelChannelPipeline
                 @Override
                public void initChannel(SocketChannel ch)
                    throws Exception { ch.pipeline().addLast(serverHandler);[4]   ⇽---  EchoServerHandler被标注为@Shareable,所以我们可以总是使用同样的实例
                    }
                 });
            ChannelFuture f = b.bind().sync();    ⇽---    异步地绑定服务器;调用sync()方法阻塞等待直到绑定完成 f.channel().closeFuture().sync();  ⇽---   获取ChannelCloseFuture,并且阻塞当前线程直到它完成
        } finally {
            group.shutdownGracefully().sync();    ⇽---    关闭EventLoopGroup,释放所有的资源
        }
    }
}

在处,你创建了一个ServerBootstrap实例。因为你正在使用的是NIO传输,所以你指定了NioEventLoopGroup来接受和处理新的连接,并且将Channel的类型指定为NioServer-SocketChannel。在此之后,你将本地地址设置为一个具有选定端口的InetSocket-Address。服务器将绑定到这个地址以监听新的连接请求。

在处,你使用了一个特殊的类——ChannelInitializer。这是关键。当一个新的连接被接受时,一个新的子Channel将会被创建,而ChannelInitializer将会把一个你的EchoServerHandler的实例添加到该ChannelChannelPipeline中。正如我们之前所解释的,这个ChannelHandler将会收到有关入站消息的通知。

虽然NIO是可伸缩的,但是其适当的尤其是关于多线程处理的配置并不简单。Netty的设计封装了大部分的复杂性,而且我们将在第3章中对相关的抽象(EventLoopGroupSocket-ChannelChannelInitializer)进行详细的讨论。

接下来你绑定了服务器,并等待绑定完成。(对sync()方法的调用将导致当前Thread阻塞,一直到绑定操作完成为止)。在处,该应用程序将会阻塞等待直到服务器的Channel关闭(因为你在ChannelClose Future上调用了sync()方法)。然后,你将可以关闭EventLoopGroup,并释放所有的资源,包括所有被创建的线程。

这个示例使用了NIO,因为得益于它的可扩展性和彻底的异步性,它是目前使用最广泛的传输。但是也可以使用一个不同的传输实现。如果你想要在自己的服务器中使用OIO传输,将需要指定OioServerSocketChannelOioEventLoopGroup。我们将在第4章中对传输进行更加详细的探讨。

与此同时,让我们回顾一下你刚完成的服务器实现中的重要步骤。下面这些是服务器的主要代码组件:

  • EchoServerHandler实现了业务逻辑;
  • main()方法引导了服务器;

引导过程中所需要的步骤如下:

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

在这个时候,服务器已经初始化,并且已经就绪能被使用了。在下一节中,我们将探讨对应的客户端应用程序的代码。

2.4 编写Echo客户端

Echo客户端将会:

(1)连接到服务器;

(2)发送一个或者多个消息;

(3)对于每个消息,等待并接收从服务器发回的相同的消息;

(4)关闭连接。

编写客户端所涉及的两个主要代码部分也是业务逻辑和引导,和你在服务器中看到的一样。

2.4.1 通过ChannelHandler实现客户端逻辑

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

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

代码清单2-3 客户端的ChannelHandler

@Sharable     ⇽---  标记该类的实例可以被多个Channel共享
public class EchoClientHandler extends
    SimpleChannelInboundHandler<ByteBuf> {
    @Override
    public void channelActive(ChannelHandlerContext ctx) { ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!",     ⇽---  当被通知Channel是活跃的时候,发送一条消息
        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,     ⇽---  在发生异常时,记录错误并关闭Channel 
        Throwable cause) { cause.printStackTrace(); ctx.close();
    }
}

首先,你重写了channelActive()方法,其将在一个连接建立时被调用。这确保了数据将会被尽可能快地写入服务器,其在这个场景下是一个编码了字符串"Netty rocks!"的字节缓冲区。

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

重写的第三个方法是exceptionCaught()。如同在EchoServerHandler(见代码清单2-2)中所示,记录Throwable,关闭Channel,在这个场景下,终止到服务器的连接。

SimpleChannelInboundHandler与ChannelInboundHandler

 

你可能会想:为什么我们在客户端使用的是SimpleChannelInboundHandler,而不是在Echo- ServerHandler中所使用的ChannelInboundHandlerAdapter呢?这和两个因素的相互作用有关:业务逻辑如何处理消息以及Netty如何管理资源。

在客户端,当channelRead0()方法完成时,你已经有了传入消息,并且已经处理完它了。当该方法返回时,SimpleChannelInboundHandler负责释放指向保存该消息的ByteBuf的内存引用。

EchoServerHandler中,你仍然需要将传入消息回送给发送者,而write()操作是异步的,直到channelRead()方法返回后可能仍然没有完成(如代码清单2-1所示)。为此,EchoServerHandler扩展了ChannelInboundHandlerAdapter,其在这个时间点上不会释放消息。

消息在EchoServerHandlerchannelReadComplete()方法中,当writeAndFlush()方法被调用时被释放(见代码清单2-1)。

第5章和第6章将对消息的资源管理进行详细的介绍。

2.4.2 引导客户端

如同将在代码清单2-4中所看到的,引导客户端类似于引导服务器,不同的是,客户端是使用主机和端口参数来连接远程地址,也就是这里的Echo服务器的地址,而不是绑定到一个一直被监听的端口。

代码清单2-4 客户端的主类

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
            Bootstrap b = new Bootstrap();     ⇽---  指定EventLoopGroup以处理客户端事件;需要适用于NIO的实现 b.group(group)    
                 .channel(NioSocketChannel.class)     ⇽---  适用于NIO传输的Channel类型
                 .remoteAddress(new InetSocketAddress(host, port))     ⇽---  设置服务器的InetSocketAddr-ess ![](/api/storage/getbykey/screenshow?key=17043add7e9c14a5d3f7)                .handler(new ChannelInitializer<SocketChannel>() {    ⇽---  在创建Channel时,向ChannelPipeline中添加一个Echo-ClientHandler实例
                 @Override
                public void initChannel(SocketChannel ch)
                    throws Exception { ch.pipeline().addLast(
                        new EchoClientHandler());
                    }
                });
            ChannelFuture f = b.connect().sync();     ⇽---  连接到远程节点,阻塞等待直到连接完成 f.channel().closeFuture().sync();      ⇽---  阻塞,直到Channel关闭
        } 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传输。在第4章,我们将探讨影响你选择适用于特定用例的特定传输的各种因素和场景。

让我们回顾一下这一节中所介绍的要点:

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

完成了客户端,你便可以着手构建并测试该系统了。

转载自 并发编程网 – ifeve.comhttp://ifeve.com/

点赞