Netty4实战第二章:第一个Netty应用

本章内容

  • 获取最新稳定版Netty
  • 设置编译和运行示例环境
  • 创建基于Netty的服务端和客户端
  • 拦截和处理错误
  • 编译和运行基于Netty的服务端和客户端
  本章会介绍Netty的几个核心概念。其中之一就是怎么用Netty拦截和处理异常,这是遇到问题时很重要的解决手段。这一章还会介绍其他一些核心概念,例如客户端和服务端的启动器和Channel Handler代码分离。这些是为继续学习其他章节打下基础,你将使用Netty建立客户端与服务端的连接。不过首先,你要建立你的开发环境。

一、建立开发环境

  建立开发环境,需要以下几步:

  1、安装JDK

  2、安装构建工具,建议Maven或Gradle,本翻译作品采用Gradle

  大家都已经在学Netty了,相信上面两步应该都会很快完成。

二、Netty客户端与服务端概述

  本小节的目标是引导大家创建一个完整的基于Netty的客户端和服务端。可能有的人只对写服务端感兴趣,例如写一个HTTP服务端,这样就可以直接使用浏览器作为客户端。但我们这个例子,如果你同时实现了客户端和服务端,你就会学到Netty更多的知识。

  一个Netty应用的基本结构如下:


  1.客户端连接服务端,2.建立连接收发数据,3.服务端处理所有客户端过来的连接

  有一件事需要说一下,你实现的Netty应用的服务端,会自动处理多个并发客户端。理论上来说,这里的并发限制主要就是你系统的可用资源限制和一些JDK限制。

  为了更容易理解,可以想像一下回声系统,比如你在山谷里大喊一声,不久你会听到山谷给你的回声。这种情况下,你就是客户端,山谷就是服务端。进入山谷,就相当于建立了一个连接。你大喊一声就类似Netty客户端向服务端发送数据。听到回声就类似服务端把客户端发来的数据又返回去。你离开山谷,类似断开连接,如果你又回到山谷类似重连到服务端就可以继续发送更多数据。

  虽然实际工作中不会直接将客户端发来的数据再送回去,不过不管什么样的业务需求,客户端与服务端的工作模式都是这样。本章后面的例子将会慢慢变复杂来说明这一点。

  接下来的几个小节将要介绍开发基于Netty的客户端和服务端的流程。

三、基于Netty的EchoServer

  编写基于Netty的服务端主要包括下面两部分:

  • Bootstrapping-配置服务端,例如线程和端口
  • 实现服务端Handler-实现建立连接和接收到数据后应该执行什么样的业务逻辑

3.1、Bootstrapping

  启动服务端需要创建一个ServerBootstrap实例。这个实例用来配置属性,例如端口,事件线程,以及业务逻辑Handler,这么这个EchoServer的业务逻辑就是将客户端发送来的数据再发送回去,不过也很容易写出更复杂的逻辑。

  首先在Maven或者Gradle的配置文件中加上Netty最新稳定版的依赖,目前最新版是4.1.15.Final,例如我的build.gradle如下。

group 'com.nan.netty'
version '1.0-SNAPSHOT'

apply plugin: 'java'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile(
            'io.netty:netty-all:4.1.15.Final'
    )
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

  服务端主类代码如下。

package com.nan.netty.one;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

import java.net.InetSocketAddress;

public class EchoServer {

    private final int port;

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

    public void start() throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            //服务端启动器
            ServerBootstrap b = new ServerBootstrap();
            //配置服务端使用NIO传输,绑定本地端口
            b.group(group)
                    .channel(NioServerSocketChannel.class)
                    .localAddress(new InetSocketAddress(port))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            //Channel增加业务处理类
                            ch.pipeline().addLast(new EchoServerHandler());
                        }
                    });
            //服务端绑定并等待应用关闭释放资源
            ChannelFuture f = b.bind().sync();
            System.out.println(EchoServer.class.getName() + " started and listen on" + f.channel().localAddress());
            f.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully().sync();
        }
    }

    /**
     * 主执行方法
     */
    public static void main(String[] args) throws Exception {
        new EchoServer(9999).start();
    }
}

  例子里面EchoServerHandler还没定义,所以可能上面的代码会在IDE中报错,不过没关系,先不用管。这个例子看起来没什么特别之处,不过第一章的例子里面每件事它也都做了。为了启动服务端,首先要创建一个ServerBootstrap实例。因为要使用NIO的传输方式,所以配置了NioEventLoopGroup去接受并处理新连接,配置NioServerSocketChannel作为Channel类型,并且配置InetSocketAddress绑定地址和端口来接收新连接。

  下一步,配置了一个ChannelHandler当来了一个新连接后会回调它,它会创建一个子Channel。这里还用到了一个重要的类ChannelInitializer。

  虽然上一章的NIO或NIO.2的例子也有很强的扩展性,但是它们还是有可能产生问题。比如线程,JDK NIO不就太容易使用,但是Netty通过设计和封装抽象做了大部分线程工作,主要通过使用EventLoopGroup,SocketChannel和ChannelInitializer,后面的章节会详细讨论这些知识。

  一个Channel的ChannelPipeline包含了所有的ChannelHandlers,所以事先将EchoServerHandler加入到ChannelPipeline中。然后绑定服务知道绑定完成,sync()方法会阻塞,只到绑定完成。然后应用就会一直运行只到服务端的Channel关闭(因为在Channel关闭的future我们调用了sync()方法)。这个时候就可以关闭EventLoopGroup然后释放所有资源,包括创建的所有线程。

  上面的例子中我们使用了NIO传输方式因为这是目前使用量比较多的,不过你也可以选择使用其他的。例如你要是选择使用OIO传输方式,就要使用OioServerSocketChannel。他们的区别以及到底什么是传输方式,后面的章节会进行详解。

  下面我们来列一下编写Netty服务端几个重要的步骤:

  • 创建一个ServerBootstrap实例用来启动服务端,然后绑定
  • 创建NioEventLoopGroup实例并指定用它处理事件,包括接收连接,读写数据等等
  • 设置一个Handler来处理业务逻辑
  • 上面都准备就绪后调用ServerBootstrap.bind()方法绑定服务器地址端口来启动服务

3.2、实现业务逻辑

  Netty使用上一章介绍的Future、回调等技术,再加上其他设计,用来响应不同类型的事件。这些后面再讨论,目前我们的焦点是收到数据后下一步怎么做。想要处理收到的数据,必须编写一个类继承ChannelInboundHandlerAdapter类,并且重写messageReceived方法。这个方法会在每次收到数据后调用,这个例子里我们收到的是字节数组。EchoServer这里要做的就是在这个方法里将收到的内容重新发送给客户端,请看下面的代码。

package com.nan.netty.one;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.nio.charset.Charset;

//Sharable注解可以让此Handler在不同Channel之间共享
@ChannelHandler.Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        //Netty的ByteBuf转成字符串
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println("Server received: " + byteBuf.getCharSequence(0, byteBuf.readableBytes(), Charset.forName("UTF-8")));
        //收到数据并将数据发送到客户端,但是此时数据还没有刷到对等连接点
        ctx.write(msg);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        //读操作完成后,将所有写入的数据(等待中的)刷新到对等连接点,然后关闭此Channel
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        //出现异常关闭此Channel
        ctx.close();
    }
}

  现在我们先使用第一章写的BIO的EchoClient连接上面的服务端,可以发现EchoServer的功能已经实现了。

  Netty使用的Handler有更大的解耦性,所以非常容易进行增加,修改或删除业务逻辑的项目演进。Handler的作用是很明确的,里面的每个方法都可以重写以参与到收发的数据的生命周期,但是一般情况下只有channelRead方法是需要重写的。

3.3、拦截异常

  除了重写了channelRead方法之外,有一个exceptionCaught方法也被重写了。这个方法是用来处理异常的,或者其他Throwable的子类。上面的例子中,记录了异常并关闭掉到客户端的连接,因为出现异常这个链接的状态可能是不确定的。一般情况下可能你也会这么干,不过有一些场景,需要从错误中恢复过来,这就需要发挥你的指挥找到一个聪明的方法去实现。重要的就是你要有一个ChannelHandler实现exceptionCaught方法来处理各种各样的错误。

  Netty提供的这个拦截异常的方法更容易处理不同线程发生的错误。通过统一的简单、集中API将不同线程发生的错误尽可能放在一起处理。

  如果你需要用Netty写一个实际生产的应用或者一个框架就要知道很多其他ChannelHandler的子类和实现,这些后面会讨论。现在只需要记住ChannelHandler的实现会在发生不同的事件调用,你可以继承或实现它们来参与事件的生命周期。

四、编写EchoClient

  上面我们写了很多版本的EchoServer,不过客户端始终只有BIO版本的,现在我们就编写一个基于Netty的客户端。
  客户端的代码主要包含以下逻辑:
  • 连接服务端
  • 发送数据到服务端
  • 等待服务端返回相同的数据
  • 关闭连接
  清楚了要做什么,现在就来实现客户端的逻辑。

4.1、客户端启动器

  客户端的启动器和服务端的启动器很类似。有一点区别,服务器启动器一般只需要端口号,而客户端需要服务端的IP地址和端口,因为它要去连接这个IP和端口。

package com.nan.netty.one;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

import java.net.InetSocketAddress;

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();
            //使用NioEventLoopGroup接收事件,和服务端一样,这里使用NIO传输
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .remoteAddress(new InetSocketAddress(host, port))
                    //指定ChannelHandler,一旦连接简历就会创建Channel
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            //指定EchoClientHandler处理业务逻辑
                            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 {
        new EchoClient("localhost", 9999).start();
    }
}
  这里我们用的还是NIO传输。这里先提一下,暂时也不用关系你用的什么传输方式;客户端和服务端也可以使用不同的传输方式。有些开发者就是在服务端使用NIO传输并在客户端使用OIO传输。我们将会在第四章学习到底什么是传输方式。

  我们列一下客户端的重点:

  • 创建一个启动器实例启动客户端
  • 使用NioEventLoopGroup处理事件,包括新连接、读写数据等
  • InetSocketAddress里面的地址和端口就是要连接的服务端的地址和端口
  • 还注册了一个Handler作为连接成功后的回调,目前还没写这个Handler,所以代码会在IDE里报错
  • 上面的步骤完成之后,调用ServerBootstrap.connect()方法去连接服务端

4.2、客户端业务逻辑

  这里实现的客户端逻辑尽量简单,里面用到的一些类的细节会在后面的章节详解。

  客户端的Handler首先继承了SimpleChannelInboundHandler,然后重写了下面三个方法,这三个方法对我们现在而言都是有用的。

  • channelActive()- 连接服务端成功后回调
  • channelRead0()- 从服务端收到数据后回调
  • exceptionCaught()- 出现异常时回调
package com.nan.netty.one;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;

//不同Channel之间共享此Handler
@ChannelHandler.Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //连接成功后向服务端发送数据
        ctx.writeAndFlush(Unpooled.copiedBuffer("Hello server 110", CharsetUtil.UTF_8));
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        //打印从服务端收到的数据,然后关闭连接
        System.out.println("Received from server: " + msg.getCharSequence(0, msg.readableBytes(), CharsetUtil.UTF_8));
        ctx.close();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //打印遇到的异常,然后关闭连接
        cause.printStackTrace();
        ctx.close();
    }
}
  这里重写的三个方法在一般应用中都是有必要重写的。
  一旦连接成功就会回调 channelActive()。上面的例子很简单,连接成功后向服务端发送一串字符。发送的内容目前不重要,目前这个不是重点。重写这个方法就是保证尽快将信息发送给服务端。
  下一个重写的方法是channelRead0()。一旦接收到信息就会回调这个方法。这里要注意消息内容可能会分片,比如服务端发送5个字节给客户端但是客户端并不是一下子把5个字节全部接收了。例如,5个字节,可能一次只能收3个,也就是说这个方法第一接收3个字节,第二次接收2个字节,所以这个方法会回调2次。唯一能保证的就是接收顺序与发送顺序是一致的,不过这也仅适用于TCP或其他可靠协议。
  第三个重写的方法是exceptionCaught()。基本上和服务端的一样。记录异常并关闭连接。
  这里大家可能会有个疑问,为什么客户端用的是SimpleChannelInboundHandler,而服务端用的是ChannelInboundHandlerAdapter。使用ChannelInboundHandlerAdapter的主要场景是处理完接收到的消息后要手动释放资源。比如用ByteBuf可以调用ByteBuf.release()释放资源。而使用SimpleChannelInboundHandler会在channelRead0(...)方法执行完后自动释放资源,这个从SimpleChannelInboundHandler的源码可以很容易看出来。Netty的消息类都通过实现ReferenceCounted接口去释放资源的。
  但是为什么服务端就不需要释放资源呢?这是因为服务端需要将消息重新发送给客户端,所以写操作完成之前不能释放资源,而且写还是异步。Netty会在写操作完成之后自动释放资源。
  刚写完服务端的时候,我们使用的是BIO的客户端验证的,现在可以用基于Netty的客户端去验证。直接在IDE中执行main方法,可以发现EchoServer的功能正常实现了,如果你执行过程中遇到什么问题,请仔细检查下自己的代码,或者加我的微信wangjinn一起讨论,谢谢。

五、总结

  这一章我们使用Netty实现了一个简单的服务端和客户端,对于启动,配置,读写数据以及捕获异常都有介绍,现在对于Netty算是有一些基本的了解,这些知识对于后面的学习是个很好的基础。


  

  



  

  

  

  

  

  

  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值