Netty介绍及使用

文章目录

  • 什么是Netty
    • 异步和同步的区别
    • 事件驱动
    • 网络通讯框架
    • Nio的介绍
  • Netty的线程模型
    • Reactor模型介绍
    • 单Reactor单线程
    • 单Reactor多线程
    • 主从Reactor多线程
    • Netty模型
      • 简单案例
      • 代码分析
  • Netty实现群聊系统
    • 功能说明
    • 代码实现
    • 代码分析
  • websocket长连接
    • Netty实现和浏览器长连接代码
      • 服务器端代码
      • 浏览器代码
  • ProtoBuf
    • ProtoBuf 简介
    • ProtoBuf 的使用
      • .proto文件编写
      • Protobuf 数据类型对应
      • 使用protoc.exe生成.java文件
      • 服务端代码
      • 服务端Handler代码
      • 客户端代码
      • 客户端Handler代码
  • Netty编解码器
    • java编解码
    • Netty编解码器
      • 用处
      • Netty解码器
      • Netty编码器
      • 注意事项以及总结
  • 粘包拆包
    • 粘包拆包代码演示
    • 粘包拆包解决

什么是Netty

netty是一个异步的,基于事件驱动的网络通讯框架(基于NIO开发的,所以之后会对NIO多做说明)

异步和同步的区别

同步:客户端发送请求给服务器端,在服务端给出响应之前不能做任何事;
异步:和同步相反,客户端发送请求给服务器端,在服务端给出响应之前可以做任何事(ajax就是异步);

事件驱动

简单理解就是有一个页面,点击一个按钮,调用响应的函数,点击然后调用的这个过程就是一个事件驱动;

网络通讯框架

 客户端与服务器之间的通信方式

Nio的介绍

由于Netty是基于Nio开发的,所以最好先对Nio有一定了解。	如不了解,先移步Nio介绍:
https://blog.csdn.net/z_z_k/article/details/122177503?spm=1001.2014.3001.5501

Netty的线程模型

Netty的线程模型是reactor模型

Reactor模型介绍

Reactor模型的别称:分发模型,反应器模型,通知模型
Reactor模式是基于事件驱动的,客户端不直接和服务端进行连接,先连接到Reactor,Reactor根据监听到时事件进行分配,处理事件是由线程池负责的
根据Reactor的数量和线程池的数量,Reactor分为三种模型: 
	单Reactor单线程
	单Reactor多线程
	主从Reactor多线程

单Reactor单线程

Netty介绍及使用_第1张图片

如图所示:
	客户端不直接和服务器端连接,而是和中间的Reactor进行连接;
	Reactor负责监听客户端的事件,分发到不同的事件处理器,处理不同的事件
	事件处理使用的是线程池,分发时根据事件分发给空闲的线程池
优点:
	模型简单,单线程进行通讯,不存在资源竞争
缺点:
	单线程无法发挥多核CPU的优势,不适合高并发场景,分发事件后进行处理时,不能进行其他连接事件,容易出现性能瓶颈。
使用场景:
	客户端数量少的场景,业务处理响应快,比如 Redis

单Reactor多线程

Netty介绍及使用_第2张图片

如图所示:
	和单Reactor单线程相比,区别在于处理业务请求的时候不在是堵塞的了,
	Handle不负责处理事件,只负责响应,业务处理交给线程池处理后,可以去关注其他的请求事件
	线程池处理完后,结果返回给Handle,Handle和client端是有连接的,client端也能接收到结果
优点:
	能发挥多核CPU的优势
缺点:
	多线程场景下,复杂度增加了。单Reactor容易出现性能瓶颈

主从Reactor多线程

Netty介绍及使用_第3张图片

如图所示:
	红色线部分是和单Reactor多线程的区别
	主Reactor判断监听事件是否连接请求的事件,是就直接处理,不是就交给从Reactor
	从Reactor负责处理非连接请求的事件,处理方式同 单Reactor多线程
	图例画了一主一从,实际可以多主多从
优点:
	主从线程分工明确,交互简单
缺点:
	编程复杂度高
使用场景:
	Nginx,Netty

Netty模型

Netty介绍及使用_第4张图片

Netty抽象出两组线程池 BossGroup 专门负责接收客户端的连接, WorkerGroup 专门负责网络的读写
BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup
NioEventLoopGroup 相当于一个事件循环组, 这个组中含有多个事件循环 ,每一个事件循环是 NioEventLoop
NioEventLoop 表示一个不断循环的执行处理任务的线程, 每个NioEventLoop 都有一个selector , 用于监听绑定在其上的socket的网络通讯
NioEventLoopGroup 可以有多个线程, 即可以含有多个NioEventLoop
每个Boss中NioEventLoop 循环执行的步骤有3步
	轮询accept 事件
	 处理accept 事件 , 与client建立连接 , 生成NioScocketChannel , 并将其注册到某个worker中NIOEventLoop上的selector
	 处理任务队列的任务 ,即 runAllTasks
每个 Worker中NIOEventLoop 循环执行的步骤
	轮询read, write 事件
 	处理i/o事件, 即read , write 事件,在对应NioScocketChannel 处理
	c. 处理任务队列的任务 , 即 runAllTasks
 每个Worker NIOEventLoop 处理业务时,会使用pipeline(管道), pipeline 中包含了 channel , 即通过pipeline 可以获取到对应通道, 管道中维护了很多的 处理器

简单案例

实现客户端和服务端进行互通,客户端连接服务端,发送一个消息,服务器端收到消息,在返回一个消息

服务器端代码

package com.netty.test;

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

public class NettyServerTest {

    public static void main(String[] args) {

        //创建 bossLoopGroup 和 workLoopGroup 线程组
        //bossLoopGroup 负责处理连接请求,workLoopGroup 负责真正的业务处理
        EventLoopGroup bossLoopGroup = new NioEventLoopGroup();
        EventLoopGroup workLoopGroup = new NioEventLoopGroup();
        //创建服务器端
        ServerBootstrap serverBootstrap = new ServerBootstrap();

        try {
            //设置服务器端启动参数
            serverBootstrap.group(bossLoopGroup,workLoopGroup)
                    .channel(NioServerSocketChannel.class)//使用 NioServerSocketChannel 作为服务器端的通道实现
                    .option(ChannelOption.SO_BACKLOG,128)//设置线程队列的连接个数
                    .childOption(ChannelOption.SO_KEEPALIVE,true)//设置保持活动连接状态
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        //设置 pipeline 的处理器
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new NettyServerHandler());
                        }
                    });
            //绑定端口,同步执行
            ChannelFuture cf = serverBootstrap.bind(8888).sync();
            //异步关闭通道(不是直接关闭,只是监听有关闭行为后在关闭)
            cf.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            System.out.println("有异常");
        }finally {
            //优雅关闭
            bossLoopGroup.shutdownGracefully();
        }
    }



}

服务器端业务处理代码

package com.netty.test;

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;

public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * 读取数据
     * @param ctx 上下文对象,包含很多
     * @param msg 消息
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println("客户端发送的消息是: " + byteBuf.toString(CharsetUtil.UTF_8));
    }

    /**
     * 读取数据结束
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.copiedBuffer("服务端收到,客户端请说", CharsetUtil.UTF_8));
    }

    /**
     * 出现异常
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

客户端代码

package com.netty.test;

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;

public class NettyClientTest {

    public static void main(String[] args) {
        //创建客户端
        Bootstrap client = new Bootstrap();
        //创建线程组,处理读写事件
        EventLoopGroup eventExecutors = new NioEventLoopGroup();

        try {
            //绑定参数
            client.group(eventExecutors)
                    .channel(NioSocketChannel.class) //使用 NioSocketChannel 作为客户端的通道实现
                    .handler(new ChannelInitializer<SocketChannel>() {

                        //设置 pipeline 的处理器
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new NettyClientHandler());
                        }
                    });
            //连接服务器
            ChannelFuture future = client.connect("localhost", 8888).sync();
            //异步关闭通道(不是直接关闭,只是监听有关闭行为后在关闭)
            future.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

客户端业务代码

package com.netty.test;

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;

public class NettyClientHandler extends ChannelInboundHandlerAdapter {

    /**
     * 读取数据
     * @param ctx 上下文对象,包含很多
     * @param msg 消息
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println("服务端返回的消息是: " + byteBuf.toString(CharsetUtil.UTF_8));
    }

    /**
     * 通道就绪触发此功能
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //发送数据
        ctx.writeAndFlush(Unpooled.copiedBuffer("呼叫服务端,收到请回答", CharsetUtil.UTF_8));
    }

    /**
     * 读取结束
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.close();
    }

    /**
     * 出现异常
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

代码分析

上面的代码分为4块:
	服务器端代码,服务器端处理业务代码,客户端代码,客户端处理业务代码
服务器端代码:
	创建 bossLoopGroup 和 workLoopGroup 线程组
	创建 ServerBootstrap 负责初始化netty服务器,并监听8888端口
	pipeline:
		和Channel是互相包含的关系,用谁都能得到另外一个。
		每个Channel都有一个与之对应的pipeline
		维护了一个ChannelHandlerContext 的双向链表
			ChannelHandlerContext :就是上面自定义的业务处理类,一个pipeline可以绑定多个Handler
	ChannelFuture :
		是一个用于保存Channel异步操作结果的对象
	shutdownGracefully
		优雅退出,结束线程,关闭连接
服务器端业务代码:
	继承ChannelInboundHandlerAdapter类,有很多方法可以重写:
		比如读取数据时,读完数据时,写入数据时,写完数据时调用的方法等
	ChannelHandlerContext:
		上下文对象,可以用它取出很多属性
	byteBuf:
		Netty的字节Buffer,和NIO的ByteBuffer功能类似,他的底层比ByteBuffer多了两个参数,分别记录了 读取数据和写入数据时的下标位置,所有不需要filp进行转换
客户端的代码和服务端类似,就不在赘述了

Netty实现群聊系统

功能说明

实现功能要求:
	客户端 上线,断开 要通知其他客户端
	客户端发送消息,要让自己和其他的客户端都接收到这条消息,并区分是否为自己

代码实现

服务端代码

package com.netty.test.groupchat;

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.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public class NettyGroupChatServer {

    public static void main(String[] args) throws InterruptedException {
        //创建两个 EventLoopGroup
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        //创建Netty启动器
        ServerBootstrap serverBootstrap = new ServerBootstrap();

        try {
            //绑定Netty启动参数
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            //Netty自带的编解码Handler
                            pipeline.addLast("encoder", new StringEncoder());
                            pipeline.addLast("decoder", new StringDecoder());
                            //心跳检测 
                            //第一个参数代表 3秒 没有读操作,则告诉我们的Handler
                            //第二个参数代表 5秒 没有写操作,则告诉我们的Handler
                            //第三个参数代表 7秒 没有读写操作,则告诉我们的Handler
							pipeline.addLast("idle", new IdleStateHandler(3,5,7, TimeUnit.SECONDS));
                            //自定义handler,收发消息使用
                            pipeline.addLast("myServerHandler", new NettyGroupChatServerHandler());
                        }
                    });
            //绑定端口 8888
            ChannelFuture channelFuture = serverBootstrap.bind(8888).sync();
            //异步监听关闭事件
            channelFuture.channel().closeFuture().sync();
        }finally {
            //优雅关闭
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

服务端业务处理代码

package com.netty.test.groupchat;

import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.CharsetUtil;
import io.netty.util.concurrent.GlobalEventExecutor;

public class NettyGroupChatServerHandler extends ChannelInboundHandlerAdapter {

    //Netty自带的,用于保存Channel集合的数据
    //此处用jdk的集合来保存也是没有毛病的,就是处理起来没有自带的便捷
    private static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    //客户端注册连接的处理
    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        //给其他客户端发送消息
        channels.writeAndFlush(Unpooled.copiedBuffer("客户端:" + ctx.channel().remoteAddress() + " 上线了\n" +
                "\n", CharsetUtil.UTF_8));
        //将当前channel加入到集合中
        channels.add(ctx.channel());
    }

    //客户端断开连接的处理
    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        //进入到这个方法中,集合中已经将 channel 删除了

        //给其他客户端发送消息
        channels.writeAndFlush(Unpooled.copiedBuffer("客户端:" + ctx.channel().remoteAddress() + " 下线了\n" +
                "\n", CharsetUtil.UTF_8));
    }

    //转发客户端发送的消息
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        channels.forEach(channel -> {
            if(channel == ctx.channel()){
                channel.writeAndFlush(Unpooled.copiedBuffer("自己:" + msg + "\n\r", CharsetUtil.UTF_8));
            }else{
                channel.writeAndFlush(Unpooled.copiedBuffer("客户端 " + channel.remoteAddress() + ": " + msg + "\n\r", CharsetUtil.UTF_8));
            }
        });
    }

	/**
     * 心跳检测
     * @param ctx
     * @param evt
     * @throws Exception
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if(evt instanceof IdleStateEvent){
            IdleStateEvent event = (IdleStateEvent) evt;
            switch (event.state()){
                case READER_IDLE:
                    System.out.println("读空闲的处理逻辑");
                    break;
                case WRITER_IDLE:
                    System.out.println("写空闲的处理逻辑");
                    // 不处理
                    break;
                case ALL_IDLE:
                    System.out.println("读写空闲的处理逻辑");
                    // 不处理
                    break;
            }
        }
    }
    
    //出现异常
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }

}

客户端代码

package com.netty.test.groupchat;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.util.Scanner;


public class NettyGroupChatClient {

    public static void main(String[] args) throws InterruptedException {
        //创建 EventLoopGroup
        EventLoopGroup group = new NioEventLoopGroup();
        //创建Netty启动器
        Bootstrap bootstrap = new Bootstrap();

        try {
            //绑定Netty启动参数
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            //Netty自带的编解码Handler
                            pipeline.addLast("encoder", new StringEncoder());
                            pipeline.addLast("decoder", new StringDecoder());
							
                            //自定义handler,收发消息使用
                            pipeline.addLast("myClientHandler", new NettyGroupChatClientHandler());
                        }
                    });
            //绑定端口 8888
            ChannelFuture channelFuture = bootstrap.connect("localhost",8888).sync();

            //客户端输入聊天的消息内容
            Scanner scanner = new Scanner(System.in);
            while(scanner.hasNext()){
                String msg = scanner.nextLine();
                channelFuture.channel().writeAndFlush("客户端" + channelFuture.channel().remoteAddress() + "说:" + msg + "\n\r");
            }
            //异步监听关闭事件
            channelFuture.channel().closeFuture().sync();
        }finally {
            //优雅关闭
            group.shutdownGracefully();
        }
    }
}

客户端处理业务代码

package com.netty.test.groupchat;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class NettyGroupChatClientHandler extends ChannelInboundHandlerAdapter {

    //读取数据
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println(msg + "\n\r");
    }
 
    //出现异常
    public v
    oid exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

代码分析

代码和上一个Netty模型时的demo代码高度重合
大致过程:
	创建线程组,创建netty服务器,绑定服务器参数,绑定对应的Handler,用于处理逻辑
心跳检测:
	服务器端的 pipeline 添加一个 IdleStateHandler,这是netty提供的心跳检测处理器,参数为 没读、没写、没读写的 事件调用时间
	添加 IdleStateHandler 后,在它后面的 Handler 中重写 userEventTriggered 方法,pipeline 底层双向链表,后面的就是后添加的那个 Handler 
	当规定时间,有 没读、没写、没读写的 事件发生是,会回调我们重写的 userEventTriggered 方法
	底层就是一个延时定时任务,通过比较时间,触发事件
	心跳检测作用:
		异常断开时,没有调用我们的 正常退出的 方法,但是服务器端不知道,这时候可以通过心跳检测
		可以在客户端写个伪代码,隔一会和服务端通讯一次
		如果N长时间后 没有读写操作,则认为异常断开,手动删除这个客户端
这个demo是一个简单的群聊系统,如果要改为一个一对一聊天,要如何呢?
	改为一对一聊天主要需要改动的是serverHandler
	将channel的集合用map存起来,key是用户对象,value是channel对象
	a用户和b用户私私聊,那就将他们俩个的channel从Map中取出来,就能实现私聊了

websocket长连接

浏览器和服务器之间发送请求都是基于Http协议的,Http协议是基于tcp协议的,建立一次连接要经历3次握手四次挥手
Http协议都是一次性的连接,发送求情->请求响应->请求连接销毁
如果用tcp协议开发一个聊天功能,那么耗费的资源是巨大的,需要频繁的建立断开连接
websocket
	是一种相对于Http请求的长连接,并且这种连接是双向的, 建立连接后,可以进行双向通讯,通讯后也不会立刻销毁当前连接

Netty实现和浏览器长连接代码

代码主要分为服务端代码和浏览器端代码
代码基本和上面案例的区别不大, 所以就不全粘出代码了,只将区别的部分粘贴出来

服务器端代码

服务端代码和以上案例基本一致,只有添加的handler不同
	//打印日志
    pipeline.addLast("logging",new LoggingHandler("DEBUG"));//设置log监听器,并且日志级别为debug,方便观察运行流程
    //websocket协议本身是基于http协议的,所以这边也要使用http解编码器
    pipeline.addLast("http-codec",new HttpServerCodec());
    //netty是基于分段请求的,HttpObjectAggregator的作用是将请求分段再聚合,参数是聚合字节的最大长度
    pipeline.addLast("aggregator",new HttpObjectAggregator(65536));
    //用于大数据的分区传输
    pipeline.addLast("http-chunked",new ChunkedWriteHandler());
    //参数是访问路径  服务客户端访问服务器的时候指定的url是:ws://localhost:8888/sayHello
    pipeline.addLast(new WebSocketServerProtocolHandler("/sayHello"));
    //自定义的业务handler
    pipeline.addLast("handler",new WebSocketHandler());
 
Handler的业务代码 
	集成 SimpleChannelInboundHandler 上面的案例也可以继承这个类,父子关系,添加泛型 TextWebSocketFrame 
	发送消息时 ctx.writeAndFlush(new TextWebSocketFrame("发送数据内容是" + textWebSocketFrame.text() ));

Netty介绍及使用_第5张图片
Netty介绍及使用_第6张图片

浏览器代码

浏览器端的代码大多都是以一种回调的形式实现的
重要的过程包括,建立连接,发送消息,接受回调消息,断开连接等
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script type="text/javascript">
    var socket;

    //如果浏览器支持WebSocket
    if(window.WebSocket){
        //参数就是与服务器连接的地址
        socket = new WebSocket("ws://localhost:8888/sayHello");

        //客户端收到服务器消息的时候就会执行这个回调方法
        socket.onmessage = function (event) {
            var ta = document.getElementById("responseText");
            ta.value = ta.value + "\n"+event.data;
        }

        //连接建立的回调函数
        socket.onopen = function(event){
            var ta = document.getElementById("responseText");
            ta.value = "连接开启";
        }

        //连接断掉的回调函数
        socket.onclose = function (event) {
            var ta = document.getElementById("responseText");
            ta.value = ta.value +"\n"+"连接关闭";
        }
    }else{
        alert("浏览器不支持WebSocket!");
    }

    //发送数据
    function send(message){
        if(!window.WebSocket){
            return;
        }

        //当websocket状态打开
        if(socket.readyState == WebSocket.OPEN){
            socket.send(message);
        }else{
            alert("连接没有开启");
        }
    }
</script>
<form onsubmit="return false">
    <textarea name = "message" style="width: 400px;height: 200px"></textarea>

    <input type ="button" value="给服务器发送数据" onclick="send(this.form.message.value);">

    <textarea id ="responseText" style="width: 400px;height: 200px;"></textarea>

    <input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="清空接收数据">
</form>
</body>
</html>

ProtoBuf

ProtoBuf 简介

平时进行开发接口,最多的数据传输形式就是 Http+json 的形式,但是这种网络传输效率比较差,而且不能跨语言
ProtoBuf是google公司的开源出来的一种效率高,可以跨平台,跨语言,高扩展的结构化数据存储结构
网络传输数据需要是二进制的形式
如果发送的数据是一个对象,传输前将这个对象转换成二进制的形式就是序列化,接受这个二进制数据转换为需要的对象就是反序列化

ProtoBuf 的使用

导入对应的maven依赖
编写.proto文件,定义各种类和属性
使用 proto.exe 将.proto文件编译城java文件
Netty也提供了对ProtoBuf的支持

.proto文件编写

syntax="proto3";//ProtoBuf的依赖版本是3
option optimize_for=SPEED;//加快解析
option java_package="com.netty.test"; //指定生成到那个包下
option java_outer_classname="MyProtoBufInfo"; //指定生成的外部类名称
//protobuf 可以使用message 管理自定义的类
message MyMessage{
  //定义一个枚举
  enum DataType{
    StudentType = 0; //在proto3 要求enum编号从0开始
    Student = 1;
  }
  DataType data_type=1;  //用data_type标识传的是哪一个枚举类型
  //oneof表示每次枚举类型最多只能出现定义的message(Student、Teacher)的其中一个
  oneof dataBody{
    Student student = 2;
    Teacher teacher = 3;
  }
}
//定义传输使用的Student类
//int32 是protoBuf提供的类型,官网有它的类型对应关系,下面也会贴出来
message Student{
  int32 id=1; //1代表的不是id为1,而是这个类的第一个属性
  string name =2;//2代表的不是name为2,而是这个类的第二个属性
}
message Teacher{
   int32 id=1; //1代表的不是id为1,而是这个类的第一个属性
   string name =2;//2代表的不是name为2,而是这个类的第二个属性
}

上面文件中的1,2,3,4不是具体的属性值,而是第几个的意思
因为传输时可能有很多种对象,所以创建了一个枚举,里面是传输时可能用到的对象。
如果传输只会出现一个对象,那么message 的这个代码块就不需要了

Protobuf 数据类型对应

Netty介绍及使用_第7张图片

使用protoc.exe生成.java文件

在网上下载 proto.exe  ,使用对应的命令生成java文件,并放在项目中

服务端代码

bootstrap.group(bossGroup, workerGroup) //设置两个线程组
     .channel(NioServerSocketChannel.class) //使用 NioServerSocketChannel 作为服务器的通道实现
     .option(ChannelOption.SO_BACKLOG, 128) //设置线程队列得到连接个数
     .childOption(ChannelOption.SO_KEEPALIVE, true) //设置保持活动连接状态
     .childHandler(new ChannelInitializer<SocketChannel>() {
     	@Override
		protected void initChannel(SocketChannel socketChannel) throws Exception {
			ChannelPipeline pipeline = socketChannel.pipeline();
			//加入Netty提供的Protobuf编码处理器,参数是我们上面自定义的.proto文件内容
			//如果编写时只有一个类,没有message 代码快,此处不需要.MyMessage
			pipeline .addLast("decoder", new ProtobufDecoder(MyProtoBufInfo.MyMessage.getDefaultInstance()));
			ch.pipeline().addLast(new NettyServerTestHandler());
		}
 });

服务端Handler代码

/**
 * 直接用泛型为 MyProtoBufInfo.MyMessage,上面自定义的
*/
public class NettyServerTestHandler extends SimpleChannelInboundHandler<MyProtoBufInfo.MyMessage> {	
    /**
     * 读取客户端发送过来的消息
     */
    @Override
    public void channelRead0(ChannelHandlerContext ctx, MyProtoBufInfo.MyMessage msg) throws Exception {
        //根据dataType显示不同信息
        MyProtoBufInfo.MyMessage.DataType dataType = msg.getDataType();
        if (dataType == MyProtoBufInfo.MyMessage.DataType.StudentType) {
            MyProtoBufInfo.Student student = msg.getStudent();
            System.out.println("学生id="+student.getId()+"学生姓名="+student.getName());

        } else if (dataType == MyProtoBufInfo.MyMessage.DataType.TeacherType) {
            MyProtoBufInfo.Teacher teacher = msg.getTeacher();
            System.out.println("老师id="+teacher.getId()+"老师姓名="+teacher.getName());
        } else {
            System.out.println("传输的类型不正确");
        }
    }
}

客户端代码

bootstrap.group(group) //设置线程组
     .channel(NioSocketChannel.class) //设置客户端通道的实现类(使用反射)
     .handler(new ChannelInitializer<SocketChannel>() {
          @Override
          protected void initChannel(SocketChannel socketChannel) throws Exception {
          		 ChannelPipeline pipeline = socketChannel.pipeline();
          		 //Netty提供的解码器
                 ch.pipeline().addLast("encoder", new ProtobufEncoder());
                  //加入自己的处理器
                 ch.pipeline().addLast(new NettyClientTestHandler());
          }
});

客户端Handler代码

public class NettyClientTestHandler extends ChannelInboundHandlerAdapter {
    /**
     * 当通道就绪就会触发
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //随机发送student或者teacher对象
        int random = new Random().nextInt(2);
        MyProtoBufInfo.MyMessage myMessage = null;
        if (random == 0) {
            myMessage = MyProtoBufInfo.MyMessage.newBuilder().setDataType(MyProtoBufInfo.MyMessage.DataType.StudentType)
                    .setStudent(MyProtoBufInfo.Student.newBuilder().setId(100).setName("我是学生").build()).build();
        }else{
            myMessage = MyProtoBufInfo.MyMessage.newBuilder().setDataType(MyProtoBufInfo.MyMessage.DataType.TeacherType)
                .setTeacher(MyProtoBufInfo.Teacher.newBuilder().setId(666).setName("我是老师").build()).build();
        }
        ctx.writeAndFlush(myMessage);
    }
}

Netty编解码器

java编解码

java的序列化也是一种编解码,序列化也可以称之为编码,反序列化可以称之为解码
	序列化(编码):将浏览器传递的参数进行序列化,成为二进制字节数组在网络中进行传输
	反序列化(解码):从网络接受的二进制的字节数组,转化为对象
java序列化的缺点:
	效率低,二进制后流变大了,无法跨语言

Netty编解码器

用处

编码器:将消息对象转成字节,int,long或其他序列形式在网络上传输。
解码器:负责将消息从字节,int,long或其他序列形式转成指定的消息对象。
不管是编码器还是解码器,其实都是pipeline中handler链中的一员
	所以假设第一个Handler是转成int的,那么转为int后会将这个int传参到第二个handler中
出站时调用编码器:ChannelOutboundHandler,入站时调用解码器:ChannelInboundHandler
	出站:就是需要调用编码器的时候
		从客户端发送消息给服务端
		从服务端发送消息给客户端
	入站:就是需要调用解码器的时候
		服务端接受到从客户端发送的消息
		客户端接收到从服务端发送的消息

Netty解码器

Netty主要提供了 ByteToMessageDecoder 和 MessageToMessageDecoder 两个抽象类
这两个类的父级都是 ChannelInboundHandler,证实了他们就是handler链中的一员
ByteToMessageDecoder :主要用于字节转换为消息体
MessageToMessageDecoder :从一种消息转换为另一种消息(对象)
继承这两个类,都需要重写 decode 方法,写自己的解码逻辑,需要判断字节数,可能出现粘包拆包
public class ToIntegerDecoder extends ByteToMessageDecoder {
	 /**
     * 解码
     * @param ctx 上下文对象
     * @param in 需要解码的数据
     * @param out 解码后的有效数据列表,我们需要将解码后的数据添加到这个List中
     * @throws Exception
     */
    @Override
   public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
    //4是因为int类型是4个字节,这个方法要将编码数据解码为int类型
    //如果上面集成的是 ReplayingDecoder,则不需要判断字节数,ReplayingDecoder是ByteToMessageDecoder 的子类
    if (in.readableBytes() >= 4) {
        out.add(in.readInt());
    } }
}
如果编码和解码的数据不一致的情况,读取数据时,就会直接将编码的数据输出
下图中蓝色的方法,就是判断数据一致的方法

Netty介绍及使用_第8张图片

Netty编码器

Netty主要提供了 ByteToMessageCodec 和 MessageToMessageCodec 两个抽象类
这个编码器的两个类和上面的解码器类的对应关系一目了然了
继承这两个类,都需要重写 encode 方法,写自己的解码逻辑
//可以直接指定泛型进行后续处理
public class IntegerToByteEncoder extends MessageToByteEncoder<Integer> {
   /**
     * 解码
     * @param ctx 上下文对象
     * @param in 需要解码的数据
     * @param out 输出流
     * @throws Exception
     */
    @Override
    protected void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out) throws Exception {
    	//将Integer转成二进制字节流写入ByteBuf中
        out.writeInt(msg);
    }
}

注意事项以及总结

解码时要判断传输的数据字节长度,因为可能出现粘包拆包的情况
在读取数据的时候,如果编解码类型不一致,则直接输出编码的数据,就不是解码的数据了
假如解码时是按4个字节(int)进行处理的,而编码时传的是 一个长度为 16 的UTF8字符串,那么,会循环调用四次解码方法
还有很多其他的编解码器就不一一介绍了

粘包拆包

tcp协议发送数据是以流的形式,它并不知道业务数据是什么样子的,它会对发送的数据进行拆分或者合并
如果发送的流数据小,并且在很小的间隔区间有很多次,则会将这些流数据合并进行发送,如果很大,就拆分

Netty介绍及使用_第9张图片

以上图为例:
	要发送的数据为 a和b
	流数据可能会把a分为4份.第一次发送的是a的第一份数据,然后之后,会把a的剩余数据和b一起传输
	也可能会把a和b一起传输
	这个会根据实际情况就行拆分数据,并封装在一起

粘包拆包代码演示

客户端处理器循环5次发送消息

 //客户端注册连接的处理
    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {

        for(int i = 1; i < 6;i++){
            ctx.writeAndFlush(Unpooled.copiedBuffer("我是第"+i+"次发送",CharsetUtil.UTF_8));
        }
    }
相同代码我启动了4次,每次服务端接受到的消息都是不一样的

Netty介绍及使用_第10张图片

粘包拆包解决

解决方案:
	使用自定义的编解码方式,创建一个对象,假设对象有两个值一个content(内容)和一个length(内容的长度)
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

/**
 * 自定义编码器
 */
public class MyEncoder extends MessageToByteEncoder<TcpInfo> {

    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, TcpInfo tcpInfo, ByteBuf byteBuf) throws Exception {
        //输出内容和内容长度
        byteBuf.writeInt(tcpInfo.getLength());
        byteBuf.writeBytes(tcpInfo.getContent());
    }
}
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ReplayingDecoder;

import java.util.List;

/**
 * 自定义解码器,继承ReplayingDecoder不用判断字节长度
 */
public class MyDecoder extends ReplayingDecoder {

    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {

         //创建TcpInfo对象
        TcpInfo info = new TcpInfo();
        //内容的长度
        info.setLength(byteBuf.readInt());
        //内容
        byte[] content = new byte[info.getLength()];
        byteBuf.readBytes(content);
        info.setContent(content);

        list.add(info);
    }
}
	/**
     * 客户端连接后发送消息
     * @param ctx
     * @throws Exception
     */
public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for(int i = 1; i < 6;i++){
        	//每次发送的消息用TcpInfo 对象封装起来
            TcpInfo info = new TcpInfo();
            byte[] bytes = ("我是第"+i+"次发送").getBytes();
            info.setContent(bytes);
            info.setLength(bytes.length);
            ctx.writeAndFlush(info);
        }
    }
	/**
     * 服务端接收客户端发送的消息
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        TcpInfo tcpInfo = (TcpInfo) msg;
        System.out.println(new String(tcpInfo.getContent(),CharsetUtil.UTF_8));
    }
编解码器的Handler要加入到对应的pipeline中
在次执行后发现,打印数据的顺序是我们想要的顺序了,不会像一开始一样打印的结果每次都不一样

你可能感兴趣的:(java,netty)