netty入门(二)——20201015~20201015——暂停于源码剖析

Netty核心技术及源码剖析——尚硅谷

EventLoop组件

ChannelHandlerContext

  • 保存Channel相关的所有上下文信息,同时关联一个ChannelHandler对象
  • 即ChannelHandlerContext中包含一个具体的事件处理器ChannelHandler,同时ChannelHandlerContext也绑定了对应的pipeline和channel的信息,方便对ChannelHandler进行调用。
  • 常用方法 :
    • close
    • flush
    • writeAndFlush:将数据写到ChannelPipeline中当前ChannelHandler的下一个ChannelHandler开始处理(出站)

ChannelOption

  • Netty在创建Channel实例后,一般都需要设置 ChannelOption参数。
  • ChannelOption参数如下
    • ChannelOption.SO_BACKLOG:对应TCP/IP协议listen函数中的backlog参数,用来初始化服务器可连接队列大小。服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接。多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列里等待处理,backlog参数指定了队列的大小。(?是否可扩容)
    • ChannelOption.SO_KEEPALIVE

EventLoopGroup和其实现类NioEventLoopGroup

  • EventLoopGroup是一组EventLoop的抽象,Netty为了更好的利用多核CPU资源,一般会有多个EventLoop同时工作,每个EventLoop维护着一个Selector实例。
  • EventLoopGroup提供next接口,可以从组里面按照一定规则获取其中一个EventLoop来处理任务。在Netty服务器端编程中,我们一般都需要提供两个EventLoopGroup:BossEventLoopGroup和WorkerEventLoopGroup。
  • 通常一个服务端口即一个 ServerSocketChannel对应一个Selector和一个EventLoop线程。BossEventLoop负责接收客户端的连接并将SocketChannel交给WorkerEventLoopGroup来进行IO处理。
  • BossEventLoopGroup通常是一个单线程的EventLoop,EventLoop维护着一个注册了ServerSocketChannel实例BossEventLoop的Selector实例 BossEventLoop不断轮询Selector将连接事件分离出来。
  • 通常是OP_ACCEPT事件,然后将接收到的SocketChannel交给WorkerEventLoopGroup
  • WorkerEventLoopGroup会由next选择其中一个EventLoop来将这个SocketChannel注册到其维护的Selector并对其后续的IO事件进行处理。

Unpooled类

  • Netty提供了一个专门用来操作缓冲区的工具类
  • 常用方法
    • copiedBuffer(Charsequence string,Charset charset)
  • 在Netty的Buffer中不需要flip读写反转,因为底层维护了readerIndex和writerIndex

Netty应用实例——群聊系统

心跳检测机制

  • 当服务器超过3秒没有读时,提示读空闲
  • 当服务器超过5秒没有写时,提示写空闲
  • 当服务器超过7秒没有读写时,提示读写空闲
pipeline.addLast(new IdleStateHandler(3,5,7, TimeUnit.SECONDS));
pipeline.addLast(new GroupServerHeartbeatHandler());
                       
  • IdleStateHandler是netty提供的处理空闲状态的处理器。
  • 当一段时间没有读/写时,会发送一个心跳检测包检测是否连接
  • 当IdleStateHandler触发后,会传递给管道的下一个handler中去处理。
public class GroupServerHeartbeatHandler extends ChannelInboundHandlerAdapter {
    /**
     * 事件触发
     * @param ctx
     * @param evt 事件类型
     * @throws Exception
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent){
            IdleStateEvent event = (IdleStateEvent) evt;
            String eventType = null;
            switch (event.state()){
                case READER_IDLE:
                    eventType = "读空闲";
                    break;
                case WRITER_IDLE:
                    eventType = "写空闲";
                    break;
                case ALL_IDLE:
                    eventType = "读写空闲";
                    break;
            }
            System.out.println(ctx.channel().remoteAddress()+eventType);
            System.out.println("服务器做相应处理");
        }
    }
}

WebSocket长连接

  • 改变http协议多次请求约束,实现长连接,服务器可以发送消息给浏览器
  • 客户端浏览器和服务端可以互相感应。比如服务器关闭了,浏览器会感知,同样浏览器关闭了,服务器会感知。

通过101状态码把HTTP协议升级为WebSocket协议。

bootstrap.group(bossGroup,workerGroup) //设置两个线程组
                    ......
                    .handler(new LoggingHandler(LogLevel.INFO)); //给workerGroup的EventLoop对应的管道设置处理器
public class ServerInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        //加入一个netty提供的 处理Http的编解码器
        pipeline.addLast("MyHttpServerCodec",new HttpServerCodec());
        //以块方式写
        pipeline.addLast(new ChunkedWriteHandler());
        /**
         * 因为http数据在传输过程中是分段的,HttpObjectAggregator可以将分段数据拼接
         */
        pipeline.addLast(new HttpObjectAggregator(8192));
        //对于web socket,数据是以帧(frame)的形式传递
        // WebSocketFrame有六个子类
        // 浏览器发送请求时,ws://localhost:7000/xxxx 表示请求的URI
        // WebSocketServerProtocolHandler的核心功能是将HTTP协议升级为WebSocket协议,保持长连接
        pipeline.addLast(new WebSocketServerProtocolHandler("/hello"));

        //自定义的Handler
        pipeline.addLast("MyHttpServerHandler",new HttpServerHandler());
    }
}

页面:注意下面的socket要与上面的WebSocketServerProtocolHandler的路径对应

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>
    var socket;
    //判断当前浏览器是否支持web socket编程
    if (window.WebSocket){
        socket = new WebSocket("ws://localhost:8998/hello");
        //相当于channelRead0
        socket.onmessage = function (ev) {
            var rt = document.getElementById('responseText');
            rt.value += "\n"+ev.data;
        }
        //相当于连接开启
        socket.onopen = function (ev) {
            var rt = document.getElementById('responseText');
            rt.value = "连接开启了";
        }
        socket.onclose = function (ev) {
            var rt = document.getElementById('responseText');
            rt.value = "连接关闭了";
        }
    }else {
        alert("WebSocket not supported")
    }

    function send(message) {
        if (!window.WebSocket){
            return;
        }
        // debugger;
        socket.send(message);
    }

</script>
    <form onsubmit="return false">

        <label>
            <textarea name="message" style="height: 300px;width: 300px"></textarea>
        </label>

        <input type="button" value="发送" onclick="send(this.form.message.value)">

        <label for="responseText">接收消息</label><textarea id="responseText" style="height: 300px;width: 300px" ></textarea>

        <input type="button" value="清空内容" onclick="document.getElementById('responseText').value=''">
    </form>
</body>
</html>

Netty编解码器

编码和解码的基本介绍

  • 编写网络应用程序时,因为数据在网络中传输的都是二进制字节码数据,在发送数据时就需要编码,接收数据时就需要解码
  • codec(编码器)的组成部分有两个:decoder和encoder。encoder负责把业务数据转换成字节码数据,decoder负责把字节码数据转换成业务数据。
  • Netty自身提供了一些codec
    • StingEncoder : 对字符串数据进行编码
    • ObjectEncoder:对java对象进行编码
    • StingDecoder : 对字符串数据进行解码
    • ObjectDecoder:对java对象进行解码
  • Netty自身提供的ObjectEncoder可以用来实现POJO对象或各种业务对象的编码和解码,底层使用的仍是java序列化技术,而java序列化技术本身效率就不高,存在如下问题:
    • 无法跨语言
    • 序列化后体积太大
    • 序列化性能太低
  • 引出新的解决方案 Google的Protobuf

Protobuf

  • 全称Google Protocol Buffers,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化。它很适合做数据存储或RPC数据交换格式。
  • 以message的方式来管理数据的。
  • 支持跨平台,跨语言。
  • 高性能,高可靠性
  • 使用Protobuf编译器自动生成代码,Protobuf将类的定义使用.proto文件进行描述。
  • 然后通过protoc.exe编译器根据.proto自动生成.java文件

入门案例

引入依赖:

<dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>3.13.0</version>
        </dependency>

编写 Student.proto文件

syntax = "proto3";
option java_outer_classname = "StudentPOJO";//生成的外部类名,同时也是文件名
//protobuf 使用message管理数据
message Student{//会在StudentPOJO外部类生成一个内部类Student,它是真正发送到POJO对象
  int32 id = 1;//Student类中有一个属性 名字为id,类型为int32(protobuf的类型) 1表示属性序号,不是值
  string name = 2;
}

protoc 把.proto编译成StudentPOJO.java


protoc --proto_path=src --java_out=src/main/java src/main/proto/simple.proto

略 (p74-078)

编解码和handler的调用机制

Netty入站和出站机制

ChannelHandler充当了处理入站和出站数据的应用程序逻辑的容器。例如:实现ChannelInboundHandler接口(或ChannelInboundHandlerAdapter),你就可以接收入站事件和数据,这些数据会被业务逻辑处理。当要给客户端发送响应时,也可以从ChannelInboundHandler冲刷数据。业务逻辑通常写在一个或者多个ChannelInboundHandler中。ChannelOutboundHandler原理一样。

  • ChannelPipeline提供了ChannelHandler链的容器。以客户端应用程序为例,如果事件的运动方向是从客户端到服务端的,那么我们称这些事件为出站的,即客户端发送给服务端的数据会通过Pipeline的一系列ChannelOutboundHandler,并被这些Handler处理,反之则称为入站的。

  • 当Netty发送或者接收一个消息的时候,就会发生一次数据转换。入站消息会被解码:从字节转换成另一种格式(比如java对象);如果是出站消息,它会被编码成字节。

  • Netty提供一系列实用的编解码器,他们都实现了ChannelInboundHandler 或者ChannelOutboundHandler接口。在这些类中,channelRead方法已经被重写了。以入站为例,对于每个从入站Channel读取的消息,这个方法会被调用。随后,它将调用由解码器所提供的decode()方法进行解码,并将已经解码的字节转发给ChannelPipeline中的下一个ChannelInboundHandler。

解码器 ByteToMessageDecoder
继承 ChannelInboundHandlerAdapter (ChannelInboundHandlerAdapter 又继承自ChannelHandlerAdapter ,实现了ChannelInboundHandler接口)

  • 由于不可能知道远程节点是否一次性发送一个完整的信息,tcp有可能出现粘包拆包的问题,这个类会对入站数据进行缓冲,直到它准备好被处理。

  • 举例来说:每次入站从ByteBuf中读取4字节,将其解码成一个int,然后将它添加到下一个List中。当没有更多元素可以被添加到该List中时,它的内容将会被发送给下一个ChannelInboundHandler 。int在被添加到List中时,会被自动装箱为Integer。在调用readInt()方法前必须验证所输入的 ByteBuf是否有足够的数据。

编解码案例:传输数据,以long的形式

流程图

编解码流程

  • 不论对服务器和客户端,Decoder都是入站Handler,Encoder都是出站Handler。

自定义编解码器

自定义编码器:

public class MyLongToByteEncoder extends MessageToByteEncoder<Long> {


    @Override
    protected void encode(ChannelHandlerContext ctx, Long msg, ByteBuf out) throws Exception {
        System.out.println("MyLongToByteEncoder encode() 被调用");
        System.out.println("msg="+msg);
        out.writeLong(msg);
    }
}

自定义解码器:

public class MyByteToLongDecoder extends ByteToMessageDecoder {
    /**
     * decode会根据接收的数据,被调用多次,直到确定没有新的元素被添加到list
     * 或者是ByteBuf没有更多的可读字节为止。
     * 如果List out 不为空,就会将out的内容传递给下一个ChannelInboundHandler处理
     * 该处理器的方法也会被调用多次
     * @param ctx
     * @param in
     * @param out
     * @throws Exception
     */
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        // long 8个字节
        System.out.println("MyByteToLongDecoder decode() 被调用");
        if (in.readableBytes()>=8){
            out.add(in.readLong());
        }
    }
}

编写ChannelInitializer(往pipeline里加入Handler):

public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        // 加入 Long To Byte (Encoder)
        pipeline.addLast(new MyLongToByteEncoder());
        // 加入 Decoder
        pipeline.addLast(new MyByteToLongDecoder());
        // 加入主处理器,实现发送接收数据等
        pipeline.addLast(new MyClientHandler());
    }
}

结论:

  • 不论解码器handler还是编码器handler即接收的消息类型必须与待处理的消息类型一致,否则handler不会被执行。
  • 在解码器进行数据解码时,需要判断缓存区ByteBuf的数据是否足够,否则接收到的结果会根期望结果可能不一致。

其它编解码器

解码器 ReplayingDecoder

  • ByteToMessageDecoder的子类,对其进行了扩展,不必调用readableBytes方法。参数S指定了用户状态管理的类型,其中Void代表不需要状态管理。
  • 局限性:
    • 并不是所有的ByteBuf操作都被支持,如果调用了一个不被支持的方法,将会抛出一个UnsupportedOperationException。
    • ReplayingDecoder在某些情况下可能稍慢于ByteToMessageDecoder,例如网络缓慢并且消息格式复杂时,消息会被拆成了多个碎片,速度变慢。

可将上面的解码器改写为(去掉了一个判断):

public class MyByteToLongDecoder2 extends ReplayingDecoder<Void> {

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        out.add(in.readLong());
    }
}

解码器 LineBasedFrameDecoder:这个类在Netty内部也有使用,它使用行尾控制字符(\n或\r\n)作为分隔符来解析数据。

解码器 DelimiterBasedFrameDecoder:使用自定义的特殊字符作为消息的分隔符。

HttpObjectDecoder:一个HTTP数据的解码器

LengthFieldBasedFrameDecoder:通过指定长度来标识整包消息,这样就可以自动的处理粘包和半包消息。

ZlibDecoder、Bzip2Decoder等等。

整合Log4j

  • 添入依赖
  • 配置Log4j,在resources/log4j.properties

略,需要再搜

TCP 粘包和拆包及解决方案

TCP 粘包和拆包基本介绍

  • TCP是面向连接的,面向流的,提供高可靠服务。收发两端都要有一一成对的socket。因此,发送端为了将多个发给接收端的包,更有效的发给对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的
  • 由于TCP无消息保护边界,需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题。
  • 如图,客户端发送了两个数据包D1和D2,服务器端读取的情况可能如下:
    TCP粘包拆包图解.PNG
    案例:
    客户端发送10条数据:
//发送10条数据2
        for (int i = 0; i < 10; i++) {
             ByteBuf msg = Unpooled.copiedBuffer("hello ,server\n", CharsetUtil.UTF_8);
            ctx.writeAndFlush(msg);
        }

服务端接收的情况

hello ,server

服务器端接收到消息量 = 1
hello ,server

服务器端接收到消息量 = 2
hello ,server

服务器端接收到消息量 = 3
hello ,server

服务器端接收到消息量 = 4
hello ,server
hello ,server

服务器端接收到消息量 = 5
hello ,server
hello ,server

服务器端接收到消息量 = 6
hello ,server
hello ,server

服务器端接收到消息量 = 7

接收的次数是不确定的。

解决方案

  1. 使用自定义协议+编解码器来解决
  • 整一个自定义的类(规定长度和格式相当于协议)
  • 编写编解码器,把自定义协议类 和 字节码之间转换。
  • 在initializer里加入编解码器handler

源码

引导类将通过传入的Class对象反射创建ChannelFactory。然后添加一些TCP的参数。

ServerBootstrap是个空构造,但是有默认的成员变量

private static final InternalLogger logger = InternalLoggerFactory.getInstance(ServerBootstrap.class);

    private final Map<ChannelOption<?>, Object> childOptions = new LinkedHashMap<ChannelOption<?>, Object>();
    private final Map<AttributeKey<?>, Object> childAttrs = new LinkedHashMap<AttributeKey<?>, Object>();
    private final ServerBootstrapConfig config = new ServerBootstrapConfig(this);
    private volatile EventLoopGroup childGroup;
    private volatile ChannelHandler childHandler;

还有一部分参数定义在AbstractBootstrap里

public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C extends Channel> implements Cloneable {

    volatile EventLoopGroup group;
    @SuppressWarnings("deprecation")
    private volatile ChannelFactory<? extends C> channelFactory;
    private volatile SocketAddress localAddress;
    private final Map<ChannelOption<?>, Object> options = new LinkedHashMap<ChannelOption<?>, Object>();
    private final Map<AttributeKey<?>, Object> attrs = new LinkedHashMap<AttributeKey<?>, Object>();
    private volatile ChannelHandler handler;
}

.option()方法会把传入的参数放到options 里。

.bind()方法最终会链接到AbstractBootstrap的doBind()方法

private ChannelFuture doBind(final SocketAddress localAddress) {
        final ChannelFuture regFuture = initAndRegister();
        final Channel channel = regFuture.channel();
        if (regFuture.cause() != null) {
            return regFuture;
        }

        if (regFuture.isDone()) {
            // At this point we know that the registration was complete and successful.
            ChannelPromise promise = channel.newPromise();
            doBind0(regFuture, channel, localAddress, promise);
            return promise;
        } else {
            // Registration future is almost always fulfilled already, but just in case it's not.
            final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
            regFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    Throwable cause = future.cause();
                    if (cause != null) {
                        // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an
                        // IllegalStateException once we try to access the EventLoop of the Channel.
                        promise.setFailure(cause);
                    } else {
                        // Registration was successful, so set the correct executor to use.
                        // See https://github.com/netty/netty/issues/2586
                        promise.registered();

                        doBind0(regFuture, channel, localAddress, promise);
                    }
                }
            });
            return promise;
        }
    }

addLast(Handler h)方法:
newContext(),然后addLast(cxt)

源码剖析从P92开始

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值