Netty学习笔记:1.从入门示例分析ChannelHandler的使用

Netty的强大,我也不多说了(主要是还没有用到多强大的功能,不知道到底有多强大,哈哈哈)

想要熟练掌握一个框架的使用,阅读源码和多敲代码多测试才是正道,看太多的介绍都是虚的。

话不多说,直奔主题,上代码!

首先,新建一个SpringBoot项目(SpringBoot不是必需,任意新建一个Java项目都可以,主要是本人习惯了用SpringBoot)

NettyDemoApplication--启动类

@SpringBootApplication
public class NettyDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(NettyDemoApplication.class, args);
        new Server().start(5000);
    }

}

启动类很简单,直接创建一个Server,绑定5000端口并启动,下面是Server的代码

Server--netty服务端

public class Server {

    public void start(int port) {
        new Thread("netty-server-thread") {
            @Override
            public void run() {
                super.run();
                NioEventLoopGroup bossGroup = null;
                NioEventLoopGroup workerGroup = null;
                try {
                    ServerBootstrap bootstrap = new ServerBootstrap();
                    bossGroup = new NioEventLoopGroup();
                    workerGroup = new NioEventLoopGroup();
                    bootstrap.group(bossGroup, workerGroup)
                            .channel(NioServerSocketChannel.class)
                            .childHandler(new ChannelInitializer<NioSocketChannel>() {
                                @Override
                                protected void initChannel(NioSocketChannel ch) throws Exception {
//                                    ch.pipeline().addLast(new StringDecoder());
//                                    ch.pipeline().addLast(new ServerHandler());
                                    ch.pipeline().addLast(new WriteHandler1());
                                    ch.pipeline().addLast(new WriteHandler2());
                                    ch.pipeline().addLast(new ReadHandler1());
                                    ch.pipeline().addLast(new ReadHandler2());
                                }
                            })
                            .option(ChannelOption.SO_BACKLOG, 128)
                            .childOption(ChannelOption.SO_KEEPALIVE, true);
                    System.out.println("[" + Thread.currentThread().getName() + "]" + Server.class.getSimpleName() + ": server start");
                    bootstrap.bind(port).sync().channel().closeFuture().sync();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (bossGroup != null) {
                        bossGroup.shutdownGracefully();
                    }
                    if (workerGroup != null) {
                        workerGroup.shutdownGracefully();
                    }
                }

            }
        }.start();
    }
}

ServerBootstrap可以说是Netty服务的一个启动辅助类,一般需要给他设置两个线程组,其中bossGroup用于绑定端口后监听客户端的连接和读写事件,监听到事件后,使用workerGroup线程组里的线程接收客户端发起的请求和回应请求结果给客户端。

.channel(NioServerSocketChannel.class),指定该服务使用的Channel为NIO类型的,说到NIO,就要提到NIO之前的BIO,BIO全称为Blocking IO,即阻塞式IO,比如服务启动后,监听端口的方法accept()会一直阻塞,直到有新的连接创建,程序才会继续往下执行,然后监听是否接收到客户端发来的请求信息,又会调用read()方法,一直阻塞,直到读取到客户端有数据过来。这势必对线程造成大大的资源浪费。之后,Java官方设计出了NIO,全称为Non-blocking IO,即非阻塞式IO,它引入了Selector模式,即选择器或者叫多路复用器模式,所有的连接、读、写事件都注册到Selector,它自身不断轮询,检测到有事件发生后,回调相应的事件方法处理业务。关于这一块内容,感兴趣的自行问度娘吧,作者实在太懒了。。

然后就是给Channel添加ChannelHandler了,这里添加了四个ChannelHandler。ChannelHandler是用于处理连接相关的触发事件的,比如通道激活,取消激活,通道注册,通道注销,通道读写事件等等。

ReadHandler1

public class ReadHandler1 extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//        super.channelRead(ctx, msg);
        ByteBuf buf = (ByteBuf) msg;
        byte[] bytes = new byte[buf.readableBytes()];
        buf.readBytes(bytes);
        String str = new String(bytes);
        buf.release();
        String str1 = " ReadHandler1 channelRead: " + str;
        System.out.println(ctx.toString() + str1);
        ctx.fireChannelRead(str1);
    }
}

ReadHandler2

public class ReadHandler2 extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//        super.channelRead(ctx, msg);
        System.out.println(ctx.toString() + " ReadHandler2 channelRead: " + msg);
        String response = "hello";
        ByteBuf buf = ctx.alloc().buffer(response.length());
        buf.writeBytes(response.getBytes());
        ctx.write(buf);
    }
}

WriteHandler1

public class WriteHandler1 extends ChannelOutboundHandlerAdapter {

    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
//        super.write(ctx, msg, promise);
        System.out.println(ctx.toString() + " WriteHandler1 write");
        String response = "WriteHandler1 write ";
        ByteBuf buf = ctx.alloc().buffer(response.length());
        buf.writeBytes(response.getBytes());
        buf.writeBytes((ByteBuf)msg);
        ctx.write(buf);
        ctx.flush();
    }
}

WriteHandler2

public class WriteHandler2 extends ChannelOutboundHandlerAdapter {

    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
//        super.write(ctx, msg, promise);
        System.out.println(ctx.toString() + " WriteHandler2 write");
        String response = "WriteHandler2 write ";
        ByteBuf buf = ctx.alloc().buffer(response.length() * 2);
        buf.writeBytes(response.getBytes());
        buf.writeBytes((ByteBuf)msg);
        ctx.write(buf);
    }
}

ChannelHandler又可以分为ChannelInboundHandlerChannelOutboundHandler,这里的In和Out是从服务器的角度看的,外面进来的(客户端请求)是Inbound,里面出去的(发往客户端)是Outbound,我这里增加了四个Handler,其中两个WriteHandler继承自OutboundHandler,两个ReadHandler继承自InboundHandler,Channel引入了一个叫Pipeline的东西,顾名思义就是管道的意思,它的内部实现机制也非常的“管道”,往管道pipeline里添加Handler(addLast()方法),即把Handler按照顺序一个个塞进管道里,WriteHandler1在最前面(从服务端角度看是最外面),ReadHandler2在最后面(从服务端角度看是最里面),外面的客户端发送数据进来时,通过管道最外面,逐个逐个地“流经”每个Handler,当然Handler的类型不同,只有InboundHandler才会关注进来的数据,所以数据先后会经过ReadHandler1和ReadHandler2,而响应客户端时,OutboundHandler才关心发送出去的数据,数据会先后经过WriteHandler2和WriteHandler1,这跟往管道添加Handler的顺序是反过来的。

我们在各个Handler里面打印一些信息,然后运行起来,验证上面的说法

使用SocketTool工具,创建一个TCP Client客户端,然后连接5000端口,如上图所示,接下来,我们向服务器打个招呼,发送“hi”,然后观察服务端的打印信息,以及服务端回应客户端的信息

可以看到,服务端接收数据时先经过ReadHandler1,然后经过ReadHandler2,发送数据时先经过WriteHandler2,然后经过 WriteHandler1,和上面的说法一致

这里客户端收到的是WriteHandler1在前面?有读者可能疑惑了,注意看上面四个Handler的代码,回应客户端时,我是从ReadHandler2发送了“hello”,然后先经过 WriteHandler2,在hello前拼接了一段字符串,再经过WriteHandler1,在WriteHandler2拼接的基础上,再拼接了WriteHandler1的字符串。

至此,入门示例就讲解完了,关于ChannelInboundHandler,它有很多的子类,比如经常用到的有:

SimpleChannelInboundHandler,简单易用的类,内部重写了channelRead方法,主要是自动释放了ByteBuf的引用(ByteBuf的引用计数器是为了控制垃圾回收的,以后有时间再讨论ByteBuf吧)

MessageToMessageDecoder,消息体转换的解码器,举个简单的例子,客户端发过来一串由数字组成的字符串,你想把它们转换成int数值然后再传给下一个Handler处理这个数值(假设下一个Handler只能处理int数值),你就可以使用这个类把String类型转成Integer

DelimiterBasedFrameDecoder,基于分隔符的解码器,创建时给定一个分隔符,比如美元符号“$”,它会不断接收数据,直到接收到$,才会把前面接收到的数据丢给下一个Handler处理

LineBasedFrameDecoder,其实就是特殊的DelimiterBasedFrameDecoder,它是基于行的解码器(即分隔符就是\r\n或者\n),它会不断接收数据,直到接收到换行,才会把前面接收到的数据丢给下一个Handler处理

StringDecoder,字符串解码器,非常的简单,就是把收到的数据(网络层传输的其实都是字节数组byte[])直接转成String,如果你设计的系统只使用明文ascii字符串作为唯一通信的数据类型,在通道的最开始添加使用这个解码器,可以大大减少你后面Handler需要把Object msg强转为String的操作

第一次写博客,有不足之处,请多多指教!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值