• Netty实战二之自己的Netty应用程序


    接下来我们将展示如何构建一个基于Netty的客户端和服务器,程序很简单:客户端将消息发送给服务器,而服务器再将消息回送给客户端,这将是一个对你而言很重要的第一个netty的实践经验。

    1、设置开发环境

    编译和运行,我们需要准备JDK和Apache Maven工具,这里建议大家使用Java的集成开发环境(IDE)。

    如果你已经安装了JDK,那么可以略过此步。

    否则,请从http://java.com/en/download/manual.jsp 处获取JDK第8版,请下载JDK,而不是Java运行环境(JRE),其仅仅可以运行Java应用程序,但不够编译它们。

    有关安装说明:

    ——将环境变量JAVA_HOME设置为你的JDK安装位置

    ——将%JAVA_HOME%in添加到你的执行路径

    下面是使用最广泛的Java IDE,都可以免费获取

    ——Eclipse——www.eclipse.org

    ——NetBeans——https://netbeans.org

    ——Intellij IDEA Community Edition——www.jetbrains.com

    有关MAVEN的安装也与Java JDK安装类似

    2、Netty客户端/服务器概览

    图2-1展示了我们将要编写的Echo客户端和服务器应用程序,即使可能我们要编写基于Web的用于被浏览器访问的应用程序,但是通过同时实现客户端和服务器,你一定能更加全面地理解Netty的API。输入图片说明

    虽然图中也展示了我们一开始所说的多个客户端,所能够支持的客户端数量,在理论上,仅受限于系统的可用资源(以及所使用的JDK版本可能会施加的限制)。

    Echo客户端和服务器之间的交互非常简单,其本身也充分体现了客户端/服务器系统中典型的请求-响应交互模式。

    3、编写Echo服务器

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

    ——至少一个ChannelHandler——该组件实现了服务器对从客户端接收的数据的处理,即它的业务逻辑。

    ——引导——配置服务器的启动代码,将服务器绑定到它要监听连接请求的端口上。

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

    ——channelRead():对于每个传入的消息都要调用

    ——channelReadComplete():通知ChannelInboundHandler最后一次对channelRead()的调用时当前批量读取中的最后一条消息

    ——exceptionCaught():在读取操作期间,有异常跑出会调用

    代码清单2-1,展示Echo服务器的ChannelHandler实现EchoServerHandler。

    @ChannelHandler.Sharable    //标示一个ChannelHandler可以被多个Channel安全地共享
    public class EchoServerHandler extends ChannelInboundHandlerAdapter{
    
        @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);//将接受到的消息写给发送者,而不冲刷出站消息·
        }
    
        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            //将未决消息冲刷到远程节点,并且关闭该Channel
            ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace();//打印异常栈跟踪
            ctx.close();//关闭该Channel
        }
    }
    

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

    重写exceptionCaught()方法允许你对Throwable的任何子类做出反应,我们代码中记录了异常并关闭了连接。

    如果不捕获异常,会发生什么呢?

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

    除了ChannelInboundHandlerAdapter之外,还有很多需要学习的ChannelHandler的子类型和实现。

    ——针对不同类型的事件调用ChannelHandler

    ——应用程序通过实现或者扩展ChannelHandler来挂钩到事件的生命周期,并且提供自定义的应用程序逻辑

    ——在架构上,ChannelHandler有助于保持业务逻辑与网络处理代码的分离,这简化了开发过程,因为代码必须不断地演化以响应不断变化的需求

    在讨论过EchoServerHandler实现的核心业务逻辑之后,我们现在可以讨论引导服务器本身的过程:

    ——绑定到服务器将在其上监听并接受请求的端口

    ——配置Channel,以将有关的入站消息通知给EchoServerHandler实例

    传输:

    在网络协议的标准多层视图中,传输层提供了端到端的或者主机到主机的通信服务。

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

    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.out.println("Usage: " + EchoServer.class.getSimpleName() + " <port>");
                return;
            }
            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();//创建EventLoopGroup
            try {
                ServerBootstrap b = new ServerBootstrap();//创建ServerBootstrap
                b.group(group)
                        .channel(NioServerSocketChannel.class)//指定所使用的NIO传输Channel
                        .localAddress(new InetSocketAddress(port))//使用指定的端口设置套接字地址
                        .childHandler(new ChannelInitializer<SocketChannel>() {//添加一个EchoServerHandler到子Channel的ChannelPipeline
                            @Override
                            protected void initChannel(SocketChannel socketChannel) throws Exception {
                                //EchoServerHandler被标注为@Shareable,所以我们可以总是使用同样的实例
                                socketChannel.pipeline().addLast(serverHandler);
                            }
                        });
                ChannelFuture f = b.bind().sync();//异步地绑定服务器,调用sync()方法阻塞等待直到绑定完成
                f.channel().closeFuture().sync();//获取Channel的closeFuture,并且阻塞当前线程直到它完成
            }finally {
                group.shutdownGracefully().sync();//关闭EventLoopGroup释放所有的资源
            }
        }
    }
    

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

    使用一个特殊的类——ChannelInitializer。当一个新的连接被接受时,一个新的子Channel将会被创建,而ChannelInitializer将会把一个你的EchoServerHandler的实例添加到该Channel的ChannelPipeline中,即这个ChannelHandler将会收到有关入站消息的通知。

    虽然NIO是可伸缩的,但是其关于多线程处理的配置并不简单。Netty的设计封装了大部分的复杂性。

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

    使用了NIO,因为得益于它的可扩展性和彻底的异步性,它是目前使用最广泛的传输,可以使用一个不同的传输实现,当然如果你想要在自己的服务器中使用OIO传输,将需要指定OioServerSocketChanne和OioEventLoopGroup。

    让我们回顾一下服务器中的重要步骤:

    ——EchoServerHandler实现了业务逻辑

    ——main()方法引导了服务器

    引导过程中所需的步骤:

    ——创建一个ServerBootstrap的实例以引导和绑定服务器

    ——创建并分配一个NioEventLoopGroup实例以进行事件的处理,如接受新连接以及读/写数据

    ——指定服务器绑定的本地的InetSocketAddress

    ——使用一个EchoServerHandler的实例初始化每一个新的Channel

    ——调用ServerBootstrap.bing()方法以绑定服务器

    4、编写Echo客户端

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

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

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

    ——channelActive():在到服务器的连接已经建立之后将被调用

    ——channelRead():当从服务器接收到一条消息时被调用

    ——exceptionCaught():在处理过程中引发异常时被调用

    @ChannelHandler.Sharable    //标记该类的实例可以被多个Channel共享
    public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf>{
    
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            //当被通知Channel是活跃的时候,发送一条消息
            ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8));
        }
    
        @Override
        protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
            //记录已接收消息的转储
            System.out.println("Client received: " + byteBuf.toString(CharsetUtil.UTF_8));
        }
    
        /**
         * 在发生异常时,记录错误并关闭Channel
         * @param ctx
         * @param cause
         * @throws Exception
         */
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace();
            ctx.close();
        }
    }
    

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

    重写了channelRead0()方法,每当接收数据时,都会调用这个方法。需要注意的是,由服务器发送的消息可能会被分块接收。如果服务器发送了5字节,那么不能保证这5字节会被一次性接收。

    即使是对于这么少量的数据,channelRead0()方法也可能会被调用两次。作为一个面向流的协议,TCP保证了字节数组将会按照服务器发送它们的顺序被接收

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

    SimpleChannelInboundHandler与ChannelLnboundHandler

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

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

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

    消息在EchoServerHandler的channelReadComplete()方法中,当writeAndFlush()方法被调用时被释放。

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

    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();//创建Bootstrap
                b.group(group)//指定EventLoopGroup以处理客户端事件,需要适用于NIO的实现
                        .channel(NioSocketChannel.class)//适用于NIO传输的Channel类型
                        .remoteAddress(new InetSocketAddress(host,port))//设置服务器的InetSocketAddress
                        .handler(new ChannelInitializer<SocketChannel>() {//在创建Channel时,向ChannelPipeline中添加一个EchoClientHandler实例
                            @Override
                            protected void initChannel(SocketChannel socketChannel) throws Exception {
                                socketChannel.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.out.println("Usage: " + EchoClient.class.getSimpleName() + " <host> <port>");
                return;
            }
    
            String host = args[0];
            int port = Integer.parseInt(args[1]);
            new EchoClient(host,port).start();
        }
    }
    

    注意,你可以在客户端和服务器上分别使用不同的传输。在服务器端使用NIO传输,而在客户端使用OIO传输。

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

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

    ——为服务器连接创建了一个InetSocketAddress实例

    ——当连接被建立时,一个EchoClientHandler实例会被安装到(该Channel的)ChannelPipeline中

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

    在本节中虽然只是一个简单的应用程序, 但是它可以伸缩到支持数千并发连接——每秒可以比普通的基于套接字的Java应用程序处理多得多的消息。

    深入地了解Netty对于关注点分离的架构原则的支持,通过提供正确的抽象来解耦业务逻辑和网络编程逻辑。

     
    交流群:628793702
  • 相关阅读:
    代码高亮测试
    自定义Edit控件控制输入范围
    多字节字符与界面 manifest
    实现类成员函数回调
    [VIM插件]fedora22编译vim7.4对perl组件支持的问题
    火车头Ecshop2.7文章采集发布模块
    js 创建对象
    js 属性类型
    JS函数的属性
    JS 函数中返回另一个函数
  • 原文地址:https://www.cnblogs.com/UncleCatMySelf/p/9187234.html
Copyright © 2020-2023  润新知