Netty版Helloworld

作者:禹明明 ,叩丁狼高级讲师。原创文章,转载请注明出处。      

作为一个已经有了几年工作经验的JAVA程序员,网络编程是一个必须接触的一个领域.如果你还只是停留在会用MVC框架,优化只会用用缓存,平常写写CRUD的水平,那么你对编程的理解还停留在一个非常表层的阶段!
如果想要深入理解各种服务器,Netty就是我们要过的第一道坎

Netty是什么

我在NIO的文章中简单学习了NIO的使用,现在回想一下,可以发现NIO的API设计过于复杂,代码量比较大,使用中需要考虑的细节也很多,如果对底层了解不深的同学写出来的代码可能会有各种各样的BUG导致服务不够稳定.
Netty就是为了解决这个问题而开发的一套简单易用封装良好的NIO框架,使用Netty可以快速轻松地开发协议服务器和客户端等网络应用程序。它极大地降低了网络编程的开发难度,极大简化了开发过程。

Netty可以用来干什么?

  • 开发自定义的HTTP服务器
  • 开发自定义的FTP服务器
  • 开发自定义的UDP服务器
  • 开发自定义的RPC服务器,例如Dubbo就是基于Netty
  • 开发自定义的WebSocket服务器
  • 开发自定义的Proxy服务器,例如MySQL的Proxy服务器等
    总之就是可以开发定制符合自己需求的各种自定义协议和服务器

为什么选择Netty?

NIO框架有Netty , Mina , xSocket , Grizzly等,为什么选择Netty呢?
虽然NIO框架有很多,但是使用比较广泛的就是Netty和Mina,从学习成本和后期维护难度上来考虑选择流行的框架可以降低开发维护难度和风险.
Netty 和Mina的作者其实都是同一个人Trustin Lee (韩国人),但是Netty出生的更晚, 作者在写出了Mina之后又搞出了Netty, 所以从这方面来说Netty应该更加完善.
从使用上来讲目前很多著名的开源项目比如阿里的Dubbo,Apache Spark , FaceBook Nifty , Google gRPC 等都是基于Netty.
其实最根本的还是超高的性能和简单的API

Netty架构

这是Netty4.1官方架构图,我们先大概了解一下
Netty版Helloworld

Netty中几个重要概念

Channel

Channel是Netty最核心的接口,一个Channel就是一个联络Socket的通道,通过Channel,你可以对Socket进行各种操作。

ChannelHandler

ChannelHandler:每一个ChannelHandler都用来处理一些逻辑,所有的handler形成一个链表结构,作用类似于springMVC中的拦截栈或者一个个的过滤器

ChannelHandlerContext

ChannelHandlerContext是ChannelPipeline的上下文,负责传递上下文数据. ChannelHandlerContext.channel()方法可以得到和Context绑定的Channel,调用ChannelHandlerContext.handler()方法可以得到和Context绑定的Handler。

ChannelPipeline

ChannelPipeline:可以把ChannelPipeline看成是一个ChandlerHandler的链表,当需要对Channel进行某种处理的时候,Pipeline负责依次调用每一个Handler进行处理。每个Channel都有一个属于自己的Pipeline,调用XXChannel.pipeline()方法可以获得Channel的Pipeline,调用XXPipeline.channel()方法可以获得Pipeline的Channel。
他们的关系图如下:
Netty版Helloworld

简单了解完这些概念之后我们就可以写个helloworld来体验一把Netty了

要求JDK1.6或以上
Netty5.0官方已经不再支持了,不建议大家使用
目前最新稳定版是4.1

添加依赖

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.6.Final</version>
        </dependency>

编写Server端

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

public class NettyServer {

    public static void main(String[] args) throws InterruptedException {
        //NioEventLoopGroup可以看做是一个线程池,parentGroup用来接收所有请求,childGroup用来处理具体IO任务
        NioEventLoopGroup parentGroup = new NioEventLoopGroup();//用来处理服务器端接受客户连接
        NioEventLoopGroup childGroup = new NioEventLoopGroup(); //用来进行网络通信(网络读写)
        ServerBootstrap bootstrap = new ServerBootstrap();      //创建服务器通道配置的辅助工具类
        bootstrap.group(parentGroup,childGroup)                 //配置每个NioEventLoopGroup的用途
                .channel(NioServerSocketChannel.class)          //指定Nio模式为Server模式
                .option(ChannelOption.SO_BACKLOG,1024)    //指定tcp缓冲区
                .option(ChannelOption.SO_SNDBUF,10*1024)  //指定发送缓冲区大小
                .option(ChannelOption.SO_RCVBUF,10*1024)  //指定接收缓冲区大小
                .option(ChannelOption.SO_KEEPALIVE,Boolean.TRUE)//是否保持连接,默认true
                .childHandler(new ChannelInitializer<SocketChannel>() {//具体的数据接收方法
                    @Override
                    protected void initChannel(SocketChannel sc) throws Exception {     //添加ChannelHandler,handler用来自定义消息处理逻辑
                        sc.pipeline().addLast(new ServerHandler());//其实可以添加多个Handler实例对象
                    }
                });

        ChannelFuture cfuture = bootstrap.bind(9999).sync();//异步绑定端口
        cfuture.channel().closeFuture().sync();//阻塞程序,等待关闭
        parentGroup.shutdownGracefully();//关闭应用
        childGroup.shutdownGracefully();
    }
}

服务端涉及到了一个自定义的Handler用来处理接受到的数据

/*
ChannelInboundHandlerAdapter 中有很多方法可以覆盖,这些方法覆盖了一个请求处理的整个生命周期,
一般来说我们只需要关心channelRead和exceptionCaught 方法
*/
public class ServerHandler extends ChannelInboundHandlerAdapter {

    //数据读取逻辑
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //ByteBuf如果只用来读数据而没有writeAndFlush写数据则使用完必须使用调用release()方法,释放内存
        ByteBuf buf = (ByteBuf) msg;
        byte[] bytes = new byte[buf.readableBytes()];
        buf.readBytes(bytes);
        String rev = new String(bytes,"utf-8");
        System.out.println("server收到数据:"+ rev);
        //给客户端响应一条数据
        ctx.writeAndFlush(Unpooled.copiedBuffer("你好,我是Server".getBytes()))
                //添加监听器,写出数据后关闭通道,原理上只要拿到Futrue对象server端和client端都可以主动关闭,一般在server端关闭较好
                .addListener(ChannelFutureListener.CLOSE);
        buf.release();//释放ByteBuf
    }

    //抛出异常时处理逻辑
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        super.exceptionCaught(ctx, cause);
        System.out.println("exceptionCaught");
    }
}

编写Client端

public class NettyClient {
    public static void main(String[] args) throws InterruptedException {
        //NioEventLoopGroup可以看做是一个线程池,客户端只需要用来处理发送数据任务的NioEventLoopGroup即可
        NioEventLoopGroup group = new NioEventLoopGroup();
        Bootstrap b = new Bootstrap();
        b.group(group)
            .channel(NioSocketChannel.class)//指定Nio模式为Client模式
            .handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel sc) throws Exception {
                    sc.pipeline().addLast(new ClientHandler());//添加自定义的客户端消息处理Handler
                }
            });

        ChannelFuture cf = b.connect("127.0.0.1",9999);         //连接指定host:ip
        cf.channel().write(Unpooled.copiedBuffer("Hello I am Client ".getBytes()));//write是写入缓冲区,
        cf.channel().flush();             //flush缓冲数据,必须flush!! 或者使用writeAndFlush方法发送数据
        cf.channel().closeFuture().sync();//异步监听,传输完毕才执行此代码,然后向下执行关闭操作
        group.shutdownGracefully();       //关闭应用,断开和server连接
    }
}

Client端负责数据处理的Handler


public class ClientHandler extends ChannelInboundHandlerAdapter {

    //数据读取逻辑
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        try{
            ByteBuf buf = (ByteBuf) msg;
            byte[] bytes = new byte[buf.readableBytes()];
            buf.readBytes(bytes);
            String rev = new String(bytes,"utf-8");
            System.out.println("Client 收到数据:"+ rev);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            ReferenceCountUtil.release(msg);//释放ByteBuf
        }
    }

    //抛出错误时处理逻辑
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        super.exceptionCaught(ctx, cause);
        System.out.println("出错了...");
    }
}

启动Server—>启动Client—>查看日志

# server端日志:这里给大家展示覆盖了ChannelInboundHandlerAdapter 中所有方法打印的日志,方便大家理解整个处理流程
handlerAdded
channelRegistered
channelActive
18:13:27.672 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.maxCapacityPerThread: 32768
18:13:27.672 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.maxSharedCapacityFactor: 2
18:13:27.672 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.linkCapacity: 16
18:13:27.672 [nioEventLoopGroup-3-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.ratio: 8
18:13:27.683 [nioEventLoopGroup-3-1] DEBUG io.netty.buffer.AbstractByteBuf - -Dio.netty.buffer.bytebuf.checkAccessible: true
18:13:27.685 [nioEventLoopGroup-3-1] DEBUG io.netty.util.ResourceLeakDetectorFactory - Loaded default ResourceLeakDetector: [email protected]
server收到数据:Hello I am Client 
channelReadComplete
channelInactive
channelUnregistered
handlerRemoved

# Client端日志:
Client 收到数据:你好,我是Server
18:13:29.903 [nioEventLoopGroup-2-1] DEBUG io.netty.buffer.PoolThreadCache - Freed 2 thread-local buffer(s) from thread: nioEventLoopGroup-2-1

单从代码量来看用过NIO的同学应该就可以感受到使用Netty比直接使用NIO要简单了太多了吧,但是Netty更大的好处其实是进行了完好的封装,我们可以少关注很多繁琐的细节的处理

参考资料:
Netty4.0文档(官方推荐使用4.x版本):http://netty.io/4.0/api/index.html
w3cSchool的Netty手册:https://www.w3cschool.cn/netty4userguide/
http://ifeve.com/netty-home/
http://www.cnblogs.com/shanyou/p/4085802.html

Netty版Helloworld