Netty学习笔记(二)netty

Reactor 模型

传统IO模型

img

特点:

  • 1)采用阻塞式 I/O 模型获取输入数据;
  • 2)每个连接都需要独立的线程完成数据输入,业务处理,数据返回的完整操作。每个线程都是完全体

存在问题:

  • 1)当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大;
  • 2)连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费。
单 Reactor 单线程

img

其中,Select 是前面 I/O 复用模型介绍的标准网络编程 API,可以实现应用程序通过一个阻塞对象监听多路连接请求,其他方案示意图类似。

方案说明:

  • 1)Reactor 对象通过 Select 监控客户端请求事件,收到事件后通过 Dispatch 进行分发;
  • 2)如果是建立连接请求事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建一个 Handler 对象处理连接完成后的后续业务处理;
  • 3)如果不是建立连接事件,则 Reactor 会分发调用连接对应的 Handler 来响应;
  • 4)Handler 会完成 Read→业务处理→Send 的完整业务流程。

优点:模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成。

缺点:性能问题,只有一个线程,无法完全发挥多核 CPU 的性能。Handler 在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈。

单 Reactor 多线程

img

方案说明:

  • 1)Reactor 对象通过 Select 监控客户端请求事件,收到事件后通过 Dispatch 进行分发;
  • 2)如果是建立连接请求事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建一个 Handler 对象处理连接完成后续的各种事件;
  • 3)如果不是建立连接事件,则 Reactor 会分发调用连接对应的 Handler 来响应;
  • 4)Handler 只负责响应事件,不做具体业务处理,通过 Read 读取数据后,会分发给后面的 Worker 线程池进行业务处理;
  • 5)Worker 线程池会分配独立的线程完成真正的业务处理,如何将响应结果发给 Handler 进行处理;
  • 6)Handler 收到响应结果后通过 Send 将响应结果返回给 Client。

业务分离在不同的线程了

优点:可以充分利用多核 CPU 的处理能力。

缺点:多线程数据共享和访问比较复杂;Reactor 承担所有事件的监听和响应,在单线程中运行,高并发场景下容易成为性能瓶颈。

主从 Reactor 多线程

img

针对单 Reactor 多线程模型中,Reactor 在单线程中运行,高并发场景下容易成为性能瓶颈,可以让 Reactor 在多线程中运行。

方案说明:

  • 1)Reactor 主线程 MainReactor 对象通过 Select 监控建立连接事件,收到事件后通过 Acceptor 接收,处理建立连接事件;
  • 2)Acceptor 处理建立连接事件后,MainReactor 将连接分配 Reactor 子线程给 SubReactor 进行处理;
  • 3)SubReactor 将连接加入连接队列进行监听,并创建一个 Handler 用于处理各种连接事件;
  • 4)当有新的事件发生时,SubReactor 会调用连接对应的 Handler 进行响应;
  • 5)Handler 通过 Read 读取数据后,会分发给后面的 Worker 线程池进行业务处理;
  • 6)Worker 线程池会分配独立的线程完成真正的业务处理,如何将响应结果发给 Handler 进行处理;
  • 7)Handler 收到响应结果后通过 Send 将响应结果返回给 Client。

优点:父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理。

父线程与子线程的数据交互简单,Reactor 主线程只需要把新连接传给子线程,子线程无需返回数据。

这种模型在许多项目中广泛使用,包括 Nginx 主从 Reactor 多进程模型,Memcached 主从多线程,Netty 主从多线程模型的支持。

Netty

线程模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5iKOOx1d-1650085348023)(https://unpkg.zhimg.com/youthlql@1.0.0/netty/introduction/chapter_002/0012.png)]

img

Netty 抽象出两组线程池 ,BossGroup 专门负责接收客户端的连接(主reactor),WorkerGroup 专门负责网络的读写(从reactor)
BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup
NioEventLoopGroup 中的线程是 NioEventLoop
NioEventLoop 表示一个不断循环的执行处理任务的线程,每个 NioEventLoop 都有一个 Selector,用于监听绑定在其上的 socket 的网络通讯

BossGroup的线程的select监听SeverSocketChannel即accept连接事件

WorkerGroup的线程select监听SocketChannel即读写事件

img

pipeline(管道),pipeline 中包含了 channel(通道),即通过 pipeline 可以获取到对应通道,管道中维护了很多的处理器。

Handler负责read,write操作,业务部分交给业务线程池,不要交给handler直接处理

I/O线程(Work线程)将消息从TCP缓冲区读取到SocketChannel的接收缓冲区中; 由I/O线程负责生成相应的事件,触发事件向上执行,调度到ChannelPipeline中; I/O线程调度执行ChannelPipeline中Handler链的对应方法,直到业务实现的Last Handler; Last Handler将消息封装成Runnable,放入到业务线程池中执行,I/O线程返回,继续读/写等I/O操作; 业务线程池从任务队列中弹出消息,并发执行业务逻辑。

Echo实例

(114条消息) Netty入门-第二话【万字长文】_youthlql的博客-优快云博客

Bootstrap

引导启动类

单线程模型配置
EventLoopGroup group = new NioEventLoopGroup();
 Bootstrap bootstrap = new Bootstrap();

            //设置相关参数
            bootstrap.group(group) //设置线程组
                    .channel(NioSocketChannel.class) // 设置客户端通道的实现类(反射)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new NettyClientHandler()); //加入自己的处理器
                        }
                    });

主从多线程模型

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(); 

ServerBootstrap bootstrap = new ServerBootstrap();

            //使用链式编程来进行设置
            bootstrap.group(bossGroup, workerGroup) //设置两个线程组
                    .channel(NioServerSocketChannel.class) //使用NioSocketChannel 作为服务器的通道实现
                    .option(ChannelOption.SO_BACKLOG, 128) // 设置线程队列等待连接个数
                    .childOption(ChannelOption.SO_KEEPALIVE, true) //设置保持活动连接状态
//                    .handler(null) // 该 handler对应 bossGroup , childHandler 对应 workerGroup
                    .childHandler(new ChannelInitializer<SocketChannel>() {//创建一个通道初始化对象(匿名对象)
                        //给pipeline 设置处理器
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            System.out.println("客户socketchannel hashcode=" + ch.hashCode()); //可以使用一个集合管理 SocketChannel, 再推送消息时,可以将业务加入到各个channel 对应的 NIOEventLoop 的 taskQueue 或者 scheduleTaskQueue
                            ch.pipeline().addLast(new NettyServerHandler());
                        }
                    }); // 给我们的workerGroup 的 EventLoop 对应的管道设置处理器

属性

channel 负责连接的serverSocketChannel

img

childHandler 读写的socketChannel 处理器Handler

option 负责连接的channel

​ ChannelOption.SO_BACKLOG:存放三次握手等待连接的最大个数

​ 半连接队列:第一次握手存在服务器内核的半连接队列中

​ 全连接队列:握手结束后存在服务器的全连接队列中

​ option(ChannelOption.SO_BACKLOG, 128)参数以小的为准,一般全连接大

​ ChannelOption.TCP_NODELAY:默认false 累计发送

​ true 有数据实时发送

channel

channel:连接通道

handler:读写,业务逻辑处理相关

channelPipeline:负责管理ChannelHandler的有序容器,与channel关联

Handler

读写处理,可以看为作为拦截器对ByteBuf进行处理,多个handler组成链进行传递

继承适配器,适配器模式.适配器把接口的方法都实现默认的了.如果直接实现接口需要实现所有方法.代码冗余

public class EchoServerHandler extends ChannelInboundHandlerAdapter{
    public void channelRegistered(ChannelHandlerContext ctx) {
        System.out.println("注册");
    }
    public void channelActive(ChannelHandlerContext ctx) {
    	System.out.println("激活");
    }
    public void channelInactive(ChannelHandlerContext ctx) {
    	System.out.println("断开");
    }
    public void channelUnregistered(ChannelHandlerContext ctx) {
    	System.out.println("注销");
    }
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
    	System.out.println("读取消息");
    }
    public void channelReadComplete(ChannelHandlerContext ctx)  {
    	System.out.println("消息读取完成");
    }
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
    	System.out.println("用户事件");
    }
    public void channelWritabilityChanged(ChannelHandlerContext ctx){
    	System.out.println("可写状态变更为"+ctx.channel().isWritable());
    }
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
    	System.out.println("发生异常");
    }
ChannelHandlerContext

ChannelPipeline中实际上是维护了一条由ChannelHandlerContext组成的双向链表

ChannelPipeline addFirst(ChannelHandler… handlers),把一个业务处理类(handler)添加到链中的第一个位置ChannelPipeline addLast(ChannelHandler… handlers),把一个业务处理类(handler)添加到链中的最后一个位置

channel,channelPipeline调用方法在管道流通(广播),ChannelHandlerContext只在后续handler节点流通

 Channel channel=ctx.chennel();
 channel.writeAndFlush(Unpooled.copiedBuffer("hello, server: (>^ω^<)喵", CharsetUtil.UTF_8));
 
 ctx.writeAndFlush(Unpooled.copiedBuffer("hello, server: (>^ω^<)喵", CharsetUtil.UTF_8));
入站出站顺序
pipline.addLast(in1)
		.addLast(out2)
		.addLast(in2)
		.addLast(out1);

每次出现读事件时,会从头至尾依次调用Inbound即入站方法处理;
而触发写事件时,则会从尾到头依次调用outbound即出站方法处理。

1. 入站操作主要是指读取数据的操作;而出站操作主要是指写入数据的操作
2. 入站会从先读取,再执行入站的Handler;出站会先执行出站的Handler,再写入

在这里插入图片描述

在这里插入图片描述

到这里其实出入站的操作已经解释完毕,但是底层的问题还没有解决:为什么只用了一个双向链表就可以实现?

虽然看起来像是用了两个链表,其实是通过遍历同一个双向链表来实现入站和出站操作的。

当操作为读时,会调用findContextInbound(int mask)方法,从头至尾遍历InboundHandler,注意,只遍历Inbound操作;
而当操作为写时,会调用findContextOutbound(int mask),从尾到头遍历OutboundHandler,这时只有OutBound操作被执行

在这里插入图片描述

in1->in2:ctx.fireChannelRead(data)
这个方法带有“Read”表示调用下一个in来处理接收缓冲区的数据,它不会调用某个out,因为它是“Read“”方法
in2->out1:ctx.write(data)
按照netty的执行顺序会in2调用这个方法会调用out2而不是out1
out1->out2:ctx.write(data),ctx.writeAndFlush(data)
不多说了,参考Read
in1->out2、out1->out2:ctx.channel().write(data)
这个比较特殊,channel()写数据会调用最后一个out(准确地说离tail最近的out handler)

ChannelHandlerContext总结

read在InboundHandler中顺序找节点执行,write在OutboundHandler中逆序找节点执行

InboundHandler之间数据传递,通过ctx.fireChannelRead(data)

InboundHandler通过ctx.write(data),传递到到OutboundHandler

InboundHandler放在结尾不然,在OutboundHandler之后不执行

服务端InboundHandler先执行(接收请求响应)

ChannelFuture

客户端连接服务器

ChannelFuture channelFuture = bootstrap.connect("localhost", 7000).sync();

channel():返回ChannelFuture关联的Channel;

addListener():将指定的listener添加到Future。Future完成时,将通知指定的listener。如果Future已经完成,则立即通知指定的listener;

addListeners():和上述方法一样,只不过此方法可以新增一系列的listener;

removeListener():从Future中删除第一次出现的指定listener。完成Future时,不再通知指定的listener。如果指定的listener与此Future没有关联,则此方法不执行任何操作并以静默方式返回。

removeListeners():和上述方法一样,只不过此方法可以移除一系列的listener;

sync():等待Future直到其完成,如果这个Future失败,则抛出失败原因;

syncUninterruptibly():不会被中断的sync();

await():等待Future完成;

awaitUninterruptibly():不会被中断的await ();

多Handler例子
package handler;
import echo.NettyServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class HandlerServer {
    public static void main(String[] args) throws Exception {


        //创建BossGroup 和 WorkerGroup
        //说明
        //1. 创建两个线程组 bossGroup 和 workerGroup
        //2. bossGroup 只是处理连接请求 , 真正的和客户端业务处理,会交给 workerGroup完成
        //3. 两个都是无限循环
        //4. bossGroup 和 workerGroup 含有的子线程(NioEventLoop)的个数
        //   默认实际 cpu核数 * 2
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup(); //8



        try {
            //创建服务器端的启动对象,配置参数
            ServerBootstrap bootstrap = new ServerBootstrap();

            //使用链式编程来进行设置
            bootstrap.group(bossGroup, workerGroup) //设置两个线程组
                    .channel(NioServerSocketChannel.class) //使用NioSocketChannel 作为服务器的通道实现
                    .option(ChannelOption.SO_BACKLOG, 128) // 设置线程队列等待连接个数
                    .childOption(ChannelOption.SO_KEEPALIVE, true) //设置保持活动连接状态
//                    .handler(null) // 该 handler对应 bossGroup , childHandler 对应 workerGroup
                    .childHandler(new ChannelInitializer<SocketChannel>() {//创建一个通道初始化对象(匿名对象)
                        //给pipeline 设置处理器
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            //System.out.println("客户socketchannel hashcode=" + ch.hashCode()); //可以使用一个集合管理 SocketChannel, 再推送消息时,可以将业务加入到各个channel 对应的 NIOEventLoop 的 taskQueue 或者 scheduleTaskQueue
                            ch.pipeline().addLast(new InHandler1());
                            ch.pipeline().addLast(new OutHandler2());
                            ch.pipeline().addLast(new OutHandler1());
                            ch.pipeline().addLast(new InHandler2());
                        }
                    }); // 给我们的workerGroup 的 EventLoop 对应的管道设置处理器

            System.out.println(".....服务器 is ready...");

            //绑定一个端口并且同步生成了一个 ChannelFuture 对象(也就是立马返回这样一个对象)
            //启动服务器(并绑定端口)
            ChannelFuture cf = bootstrap.bind(6669).sync();

            //给cf 注册监听器,监控我们关心的事件

            cf.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (cf.isSuccess()) {
                        System.out.println("监听端口 6669 成功");
                    } else {
                        System.out.println("监听端口 6669 失败");
                    }
                }
            });


            //对关闭通道事件  进行监听
            cf.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }

}
package handler;

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

import java.nio.ByteBuffer;

public class InHandler1 extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx,Object msg)
    {
        ByteBuf data=(ByteBuf) msg;
        System.out.println("In1收到"+data.toString(CharsetUtil.UTF_8));
        ctx.fireChannelRead(Unpooled.copiedBuffer("In1"+data.toString() ,CharsetUtil.UTF_8) );
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx)
    {
        ctx.flush();
    }

    public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause)
    {
        cause.printStackTrace();
        ctx.close();
    }

}

package handler;

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

import java.nio.ByteBuffer;

public class InHandler2 extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
    {
        ByteBuf data=(ByteBuf) msg;
        System.out.println("In2收到"+data.toString(CharsetUtil.UTF_8));
        ctx.writeAndFlush(Unpooled.copiedBuffer("In2"+data.toString() , CharsetUtil.UTF_8) );
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx)
    {
        ctx.flush();
    }

    public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause)
    {
        cause.printStackTrace();
        ctx.close();
    }



}
package handler;


import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import io.netty.util.CharsetUtil;

import java.nio.ByteBuffer;

public class OutHandler1 extends ChannelOutboundHandlerAdapter {

    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
    {
        ByteBuf data=(ByteBuf) msg;
        System.out.println("write"+data.toString());
        ctx.write(Unpooled.copiedBuffer("In2"+data.toString(CharsetUtil.UTF_8), CharsetUtil.UTF_8));
        ctx.flush();
    }
    
}

package handler;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import io.netty.util.CharsetUtil;

public class OutHandler2 extends ChannelOutboundHandlerAdapter {

    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
    {
        ByteBuf data=(ByteBuf) msg;
        System.out.println("write"+data.toString());
        ctx.write(Unpooled.copiedBuffer("In2"+data.toString(CharsetUtil.UTF_8), CharsetUtil.UTF_8));
        ctx.flush();
    }
}
异步模型

异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态、通知和回调来通知调用者。
Netty 中的 I/O 操作是异步的,包括 Bind、Write、Connect 等操作会首先简单的返回一个 ChannelFuture。
调用者并不能立刻获得结果,而是通过 Future-Listener 机制,用户可以方便的主动获取或者通过通知机制获得 IO 操作结果。
Netty 的异步模型是建立在 future 和 callback 的之上的。callback 就是回调。重点说 Future,它的核心思想是:假设一个方法 fun,计算过程可能非常耗时,等待 fun 返回显然不合适。那么可以在调用 fun 的时候,立马返回一个 Future,后续可以通过 Future 去监控方法 fun 的处理过程(即:Future-Listener 机制)

Future 说明

表示异步的执行结果,可以通过它提供的方法来检测执行是否完成,比如检索计算等等。

ChannelFuture 是一个接口:public interface ChannelFuture extends Future 我们可以添加监听器,当监听的事件发生时,就会通知到监听器。

Future方法

通过 isDone 方法来判断当前操作是否完成;
通过 isSuccess 方法来判断已完成的当前操作是否成功;
通过 getCause 方法来获取已完成的当前操作失败的原因;
通过 isCancelled 方法来判断已完成的当前操作是否被取消;
通过 addListener 方法来注册监听器,当操作已完成(isDone方法返回完成),将会通知指定的监听器;如果 Future 对象已完成,则通知指定的监听器

//绑定一个端口并且同步,生成了一个ChannelFuture对象
//启动服务器(并绑定端口)
ChannelFuture cf = bootstrap.bind(6668).sync();
//给cf注册监听器,监控我们关心的事件
cf.addListener(new ChannelFutureListener() {
   @Override
   public void operationComplete (ChannelFuture future) throws Exception {
      if (cf.isSuccess()) {
         System.out.println("监听端口6668成功");
      } else {
         System.out.println("监听端口6668失败");
      }
   }
});

netty编码解码
decoder

ByteToMessageDecoder

用于将字节转为消息,需判断缓冲区是否有足够的数据

public class ToIntegerDecoder extends ByteToMessageDecoder {  //1

    @Override
    public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
            throws Exception {
        if (in.readableBytes() >= 4) {  //2
            out.add(in.readInt());  //3
        }
    }
}

ReplayingDecoder

使用ReplayingDecoder就无需自己检查;若ByteBuf中有足够的字节,则会正常读取;若没有足够的字节则会停止解码。

ReplayingDecoder 略慢于 ByteToMessageDecoder

取出bytebuf的二进制数组,反序列化对象,加入对象列表。将out传给后续handler

public class ToIntegerDecoder2 extends ReplayingDecoder<Void> {  

    @Override
    public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
            throws Exception {
        int length = in.readInt();
        byte[] bytes = new byte[length];
        in.readBytes(bytes);
        Object obj = serializer.deserialize(bytes, packageClass);
        out.add(in.readInt());  
    }
}
Ecoder

MessageToByteEncoder

encoder 收到了 msg 消息,编码序列化对象为二进制,并把他们写入 ByteBuf。 ByteBuf 接着前进到下一个 pipeline 的ChannelOutboundHandler。

 @Override
    protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
        out.writeInt(MAGIC_NUMBER);
        if (msg instanceof RpcRequest) {
            out.writeInt(PackageType.REQUEST_PACK.getCode());
        } else {
            out.writeInt(PackageType.RESPONSE_PACK.getCode());
        }
        out.writeInt(serializer.getCode());
        byte[] bytes = serializer.serialize(msg);
        out.writeInt(bytes.length);
        out.writeBytes(bytes);
    }
TCP粘包
什么是TCP粘包问题?

粘包由tcp引起,但防止的是应用层数据间(完整的请求响应)粘而不是tcp数据包的数据之间粘。多个tcp可能装一个应用层数据,也可能一个装多个应用层数据(nagle算法)。粘发生在tcp首部拆除后,缓冲区应用层数据堆积造成混淆

tcp默认启用nagle算法(发送方原因)

一组tcp包发到缓冲区,去掉头部tcp首部,套接字读取缓冲区数据,读取数据速度慢于接收速度若无粘包处理则产生粘包。(接收方原因)

同一个应用层数据tcp下的数据可粘。不同的应用层数据需要分割。

应用层数据(http例子)和tcp:

在这里插入图片描述

如何处理粘包现象?

(1)发送方

对于发送方造成的粘包问题,可以通过关闭Nagle算法来解决,使用TCP_NODELAY选项来关闭算法。

(2)接收方

接收方没有办法来处理粘包现象,只能将问题交给应用层来处理。

UDP会不会产生粘包问题呢?

TCP为了保证可靠传输并减少额外的开销(每次发包都要验证),采用了基于流的传输,基于流的传输不认为消息是一条一条的,是无保护消息边界的(保护消息边界:指传输协议把数据当做一条独立的消息在网上传输,接收端一次只能接受一条独立的消息)。

UDP则是面向消息传输的,是有保护消息边界的,接收方一次只接受一条独立的信息,所以不存在粘包问题。

举个例子:有三个数据包,大小分别为2k、4k、6k,如果采用UDP发送的话,不管接受方的接收缓存有多大,我们必须要进行至少三次以上的发送才能把数据包发送完,但是使用TCP协议发送的话,我们只需要接受方的接收缓存有12k的大小,就可以一次把这3个数据包全部发送完毕。

粘包实例

(115条消息) Netty入门-第三话_youthlql的博客-优快云博客 中实例的基础上

public class MyServer {
    public static void main(String[] args) throws Exception{

        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {

            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class).childHandler(new MyServerInitializer()); //自定义一个初始化类


            ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
            channelFuture.channel().closeFuture().sync();

        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }
}

class MyServerInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        //pipeline.addLast(new LineBasedFrameDecoder(1024));
        //pipeline.addLast(new StringDecoder());
        pipeline.addLast(new MyServerHandler());
    }
}

public class MyServerHandler extends ChannelInboundHandlerAdapter {
    private int count;
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

//        String body=(String) msg;
//        System.out.println("服务器收到"+body+" 次数"+ ++count);

        ByteBuf body=(ByteBuf) msg;
        System.out.println("服务器收到"+body.toString()+" 次数"+ ++count);
        System.out.println("!!!!!!!!!!!!!!!");

    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //cause.printStackTrace();
        ctx.close();
    }
}
public class MyClientHandler extends ChannelInboundHandlerAdapter {

    private int count;
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //使用客户端发送10条数据 hello,server 编号
        for(int i= 0; i< 10; ++i) {
            System.out.println("发送 "+i);

            byte[] req=("In2收到"+System.getProperty("line.separator")).getBytes();
            ByteBuf buffer = Unpooled.buffer(req.length);
            buffer.writeBytes(req);
            ctx.writeAndFlush(buffer);
        }
    }
    

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

结果

服务端:
服务器收到PooledUnsafeDirectByteBuf(ridx: 0, widx: 110, cap: 2048) 次数1
!!!!!!!!!!!!!!!

客户端:
发送 0
发送 1
发送 2
发送 3
发送 4
发送 5
发送 6
发送 7
发送 8
发送 9
应用层协议的消息长度

发送方的应用层在实际数据(消息体)之前加协议头(消息头),tcp传输后去掉头部,接收方获得应用层数据(头和体)进行处理

1.decode(解码组件)读取缓冲区数据

2.读取消息头,消息头部各部分定长,不会乱

3.根据消息头的数据长度,创建byte数组

4.将数据的消息体读进byte数组

协议:

packgeCode(int)

length(int)

数据(变长)

class:Decode

int packageCode = byteBuf.readInt();
Class<?> packageClass;
if (packageCode == 0) {
    packageClass = RpcRequest.class;
} else if (packageCode == 1) {
    packageClass = RpcResponse.class;
} else {
    logger.error("不识别的数据包: {}", packageCode);
    throw new Exception("未知数据包");
}
int length = byteBuf.readInt();
byte[] bytes = new byte[length];
byteBuf.readBytes(bytes);
Object obj = deserializer.deserialize(bytes, packageClass);
list.add(obj);

packageCode决定反序列为response or request对象

length:数据体长度

换行分割

使用 LineBasedFrameDecoder解码器

以\n分割

class MyServerInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new LineBasedFrameDecoder(1024));
        pipeline.addLast(new StringDecoder());
        pipeline.addLast(new MyServerHandler());
    }
}
public class MyClientHandler extends ChannelInboundHandlerAdapter {

    private int count;
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //使用客户端发送10条数据 hello,server 编号
        for(int i= 0; i< 10; ++i) {
            System.out.println("发送 "+i);

            byte[] req=("In2收到"+System.getProperty("line.separator")).getBytes();//客户端数据后面加\n
            ByteBuf buffer = Unpooled.buffer(req.length);
            buffer.writeBytes(req);
            ctx.writeAndFlush(buffer);
        }
    }


    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}
public class MyServerHandler extends ChannelInboundHandlerAdapter {
    private int count;
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        String body=(String) msg;
        System.out.println("服务器收到"+body+" 次数"+ ++count);

        //ByteBuf body=(ByteBuf) msg;
        //System.out.println("服务器收到"+body.toString()+" 次数"+ ++count);

    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //cause.printStackTrace();
        ctx.close();
    }
}
分割符分割

使用 DelimiterBasedFrameDecoder解码器

 @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        ByteBuf decode= Unpooled.copiedBuffer("$$".getBytes(StandardCharsets.UTF_8));
        pipeline.addLast(new DelimiterBasedFrameDecoder(1024,decode) );
        pipeline.addLast(new StringDecoder());
        pipeline.addLast(new MyServerHandler());
    }

MyClientHandler

 public void channelActive(ChannelHandlerContext ctx) throws Exception {
        String message="继承SimpleChannelInboundHandler,因为Sim$$" +
                "pleChannelInboundHandler已经帮我们 把与业务无关的逻辑$$" +
                "在ChannelRead方法实现了,我们只需要实现它的chan$$" +
                "nelRead0方法来完成我们的逻辑就够了:$$";
        ByteBuf mes=null;
        mes=Unpooled.buffer(message.getBytes(CharsetUtil.UTF_8).length);
        System.out.println("发送");
        mes.writeBytes(message.getBytes());
        ctx.writeAndFlush(mes);
    }
ByteBuf

handler间传递数据的字节容器

byteBuf 是一个已经经过优化的很好使用的数据容器,字节数据可以有效的被添加到 ByteBuf 中或者也可以从 ByteBuf 中直接获取数据。ByteBuf中有两个索引:一个用来读,一个用来写。这两个索引达到了便于操作的目的。我们可以按顺序的读取数据,也可以通过调整读取数据的索引或者直接将读取位置索引作为参数传递给get方法来重复读取数据。

是NIO的byteBuffer的优化,如两个索引

handler可以看作对bytebuf处理的拦截器

心跳机制
场景

心跳机制是检查长连接是否还可连,了解对方是否宕机,断开则close节约资源或重新连接

优化

正常来说,应该是客户端发心跳包服务端回复心跳包。但服务端并发量远大于客户端,所以服务端知道客户端宕机释放资源比较重要,而客户端没并发有多余的长连接影响不大,所以也不需要服务器给大量客户端发心跳包而是采用不回复。因为相比数据库主从间的心跳涉及从代替主,netty的心跳更多是为了关闭无效的长连接减少资源占用

流程
1)客户端连接服务端

2)在客户端的的ChannelPipeline中加入一个比较特殊的IdleStateHandler,设置一下客户端的写空闲时间,例如5s

3)当客户端的所有ChannelHandler5s内没有write事件,则会触发userEventTriggered方法(上文介绍过)

4)我们在客户端的userEventTriggered中对应的触发事件下发送一个心跳包给服务端,检测服务端是否还存活,防止服务端已经宕机,客户端还不知道

5)同样,服务端要对心跳包做出响应,其实给客户端最好的回复就是“不回复”,这样可以服务端的压力,假如有10w个空闲Idle的连接,那么服务端光发送心跳回复,则也是费事的事情,那么怎么才能告诉客户端它还活着呢,其实很简单,因为5s服务端都会收到来自客户端的心跳信息,那么如果10秒内收不到,服务端可以认为客户端挂了,可以close链路

6)加入服务端因为什么因素导致宕机的话,就会关闭所有的链路链接,所以作为客户端要做的事情就是短线重连




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值