Java-第十七部分-NIO和Netty-C/S长连接和protobuf编解码器

230 阅读4分钟

NIO和Netty全文

C/S长连接

  • http协议是无状态的,浏览器和服务器的请求响应一次,下一次会重新连接
  • WebSocketServerHandler通过状态码101将http协议升级为ws协议,服务器应客户端升级协议的请求对协议进行切换,请求的uri要统一
  • 第一次发送http请求 image.png
  • 升级为ws协议 image.png
  • 服务器回复的Sec-WebSocket-Accept是对客户端升级协议请求的Sec-WebSocket-Key的验证 image.png

server

public class WebSocketServer {
    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            new ServerBootstrap().group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO)) //日志处理器
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            //基于http协议,使用http编解码器
                            pipeline.addLast(new HttpServerCodec());
                            //是以块的方式写,添加chunkedWriteHandler
                            pipeline.addLast(new ChunkedWriteHandler());
                            //http数据在传输过程中是分段的,HttpObjectAggregator将多个段聚合
                            //当浏览器发送大量数据时,发出多次http请求
                            pipeline.addLast(new HttpObjectAggregator(8192));
                            //对于webSocket,数据以帧(frame)形式传递,webSocketFrame有六个子类
                            //识别浏览器访问的资源,浏览器发送请求时 ws://localhost:8888/hello 请求uri
                            //核心功能将http协议升级为ws协议,保持长连接
                            pipeline.addLast(new WebSocketServerProtocolHandler("/hello"));
                            //处理业务逻辑
                            pipeline.addLast(new WebSocketServerHandler());
                        }
                    }).bind(8888).sync().channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

serverHandler

//TextWebSocketFrame 表示一个文本帧
public class WebSocketServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        System.out.println("server receive: " + msg.text());
        ctx.channel().writeAndFlush(new TextWebSocketFrame("server time: " + LocalDateTime.now() + " - " + msg.text()));
    }
    //当web客户端连接后触发
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        //id 表示唯一的值 asLongText唯一的 asShortText不唯一
        //handlerAdded - acde48fffe001122-00017c52-00000002-2c64cd170f619363-43160a39
        System.out.println("handlerAdded - " + ctx.channel().id().asLongText());
        //handlerAdded - 43160a39
        System.out.println("handlerAdded - " + ctx.channel().id().asShortText());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        System.out.println("handlerRemoved - " + ctx.channel().id().asLongText());
        System.out.println("handlerRemoved - " + ctx.channel().id().asShortText());
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

html

<body>
<script>
    var socket;
    //判断浏览器是否支持websocket
    if (window.WebSocket) {
        socket = new WebSocket("ws://localhost:8888/hello");
        //相当于channelRead0 ev收到服务器端回送的消息
        socket.onmessage = function (ev) {
            var rt = document.getElementById("responseText");
            //拼接消息
            rt.value = rt.value + "\n" + ev.data;
        }
        //相当于连接开启
        socket.onopen = function (ev) {
            var rt = document.getElementById("responseText");
            rt.value = "start..."
        }
        //关闭
        socket.onclose = function (ev) {
            var rt = document.getElementById("responseText");
            rt.value = rt.value + "\n" + "close..."
        }
    } else {
        alert("no support websocket!")
    }
    function send(message) {
        if (!window.socket) { //是否创建好
            return;
        }
        if (socket.readyState == WebSocket.OPEN) {
            //发送消息
            socket.send(message);
        } else {
            alert("no connect...")
        }
    }
</script>
    <!--禁止按enter让表单提交-->
    <form onsubmit="return false">
        <textarea name="message" style="height: 300px; width: 300px"></textarea>
        <input type="button" value="send msg" onclick="send(this.form.message.value)">
        <textarea id="responseText" style="height: 300px; width: 300px"></textarea>
        <input type="button" value="clear" onclick="document.getElementById('responseText').value = ''">
    </form>
</body>

protobuf编解码器

  • 数据在网络中传输的都是二进制字节码数据,发送时需要编码,接收时需要解码 image.png
  • 编解码器codec,包含解码器decoder、编码器encoder
  • netty自带的编解码器,底层使用的是java序列化技术,效率不高,无法跨语言,服务器端和客户端需要同一套语言;序列化体积较大,是二进制编码的5倍多
  1. StringEncoder/StringDecoder 针对字符串
  2. ObjectEncoder/ObjectDecoder 针对java对象

Goole Protobuf

  • 数据存储,结构化数据,适合RPC 远程过程调用 remote procedure call数据交换格式,支持跨语言

http+json -> tcp+protobuf

<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>3.19.4</version>
</dependency>
  • .proto文件转换为.java文件,控制台需要跳转到.proto文件目录下
protoc --java_out=. Student.proto

image.png

发送Student对象

proto文件

  • 生成的.java文件放入项目中使用
syntax = "proto3"; //协议版本
option java_outer_classname = "StudentPOJO"; //外部类名,同时也是文件名
//以message管理数据
message Student { //会在StudentPOJO 外部类生成一个内部类 Student,真正发送的POJO对象
  int32 id = 1; //Student类中有一个属性,名字为id,类型为int32(proto中的类型),对应java中的int,1表示属性序号
  string name = 2;
}

服务端添加handler

ch.pipeline().addLast(new ProtobufDecoder(StudentPOJO.Student.getDefaultInstance()));
ch.pipeline().addLast(new SimpleServerHandler());

客户端添加handler

//在pipeline中加入编码器
ch.pipeline().addLast(new ProtobufEncoder());
ch.pipeline().addLast(new ClientHandler()); //加入自定义处理器

ChannelInboundHandlerAdapter

  • Handler继承ChannelInboundHandlerAdapter
  • 客户端
//当通道就绪时触发
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
    //发送Student对象到服务器
    StudentPOJO.Student student = StudentPOJO.Student.newBuilder().setId(4).setName("xiaoming").build();
    ctx.writeAndFlush(student);
}
  • 服务端
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    System.out.println("go on...");
    //读取从客户端发送的Student
    StudentPOJO.Student student = (StudentPOJO.Student) msg;
    System.out.println("client: id - " + student.getId() + " name - " + student.getName());
}

SimpleHandler

public class SimpleServerHandler extends SimpleChannelInboundHandler<StudentPOJO.Student> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, StudentPOJO.Student msg) throws Exception {
        System.out.println("client: id - " + msg.getId() + " name - " + msg.getName());
    }
}

发送多个对象

proto文件

syntax = "proto3";
option optimize_for = SPEED; //加快解析
option java_package="com.java.netty.codec2"; //指定生成到哪个包
option java_outer_classname="MyDataInfo"; //外部类名称

//protobuf 可以使用meeage 管理其他的message
message MyMessage {
  //定义枚举类型
  enum DataType {
    StudentType = 0; //要求enum的编号从0开始
    WorkerType = 1;
  }
  //用data_type标识传的枚举类型
  //1表示第一个属性
  DataType data_type = 1;
  //表示下面的属性最多只能出现其中的一个,也就是每个枚举类型只能出现一个
  oneof dataBody {
    Student student = 2;
    Worker worker = 3;
  }
}
message Student {
  int32 id = 1;
  string name = 2;
}
message Worker{
  string name = 1;
  int32 age = 2;
}

clientHandler

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
    //随机发送student和worker
    int random = new Random().nextInt(2);
    MyDataInfo.MyMessage message = null;
    if (0 == random) {
        message = MyDataInfo.MyMessage.newBuilder().setDataType(MyDataInfo.MyMessage.DataType.StudentType)
                .setStudent(MyDataInfo.Student.newBuilder().setId(1).setName("xiaoming").build()).build();
    } else {
        message = MyDataInfo.MyMessage.newBuilder().setDataType(MyDataInfo.MyMessage.DataType.WorkerType)
                .setWorker(MyDataInfo.Worker.newBuilder().setAge(60).setName("laoming").build()).build();
    }
    ctx.writeAndFlush(message);
}

server

  • 服务端配置
//加入解码器,指定对哪种对象进行解码
ch.pipeline().addLast(new ProtobufDecoder(MyDataInfo.MyMessage.getDefaultInstance()));
ch.pipeline().addLast(new SimpleServerHandler());
  • SimpleServerHandler
public class SimpleServerHandler extends SimpleChannelInboundHandler<MyDataInfo.MyMessage> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MyDataInfo.MyMessage msg) throws Exception {
        //根据dataType显示不同信息
        MyDataInfo.MyMessage.DataType dataType = msg.getDataType();
        if (dataType == MyDataInfo.MyMessage.DataType.StudentType) {
            System.out.println("student id: " + msg.getStudent().getId() + " name: " + msg.getStudent().getName());
        } else if (dataType == MyDataInfo.MyMessage.DataType.WorkerType) {
            System.out.println("worker age: " + msg.getWorker().getAge() + " name: " + msg.getWorker().getName());
        } else {
            System.out.println("no found...");
        }
    }
}