SpringBoot实现Netty使用Protobuf协议完成Websocket通信

本文介绍了如何在Java项目中使用Maven引入protobuf依赖,创建并生成Proto文件,以及如何利用Netty和WebSocket实现一个简单的聊天服务,包括自定义消息处理器和通道配置。同时提供了HTML客户端测试页面的示例。
摘要由CSDN通过智能技术生成

1、项目导入maven 

<!--这里只放关键依赖-->
<dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>3.19.4</version>
        </dependency>
  <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.77.Final</version>
        </dependency>

2、创建proto文件

// 声明使用proto3
syntax = "proto3";
// 包名
option java_package = "com.huashan.websocket.proto";
// 类名
option java_outer_classname = "MessageBodyProto";


message MessageBody {

  string title = 1; //标题

  string content = 2;//内容

  string time = 3;//发送时间

  uint32 type = 4;//0 文字   1 文件

  string extend = 5;//扩展字段

}

3、根据Proto文件生成java类

        1、安装proto环境

                  下载:Release Protocol Buffers v3.19.4 · protocolbuffers/protobuf · GitHub

                        

下周对应版本

        解压后将文件配置入环境变量

验证安装

2、在IDEA安装proto插件

3、使用插件

打开IDEA-->Tools-->Configure Gen Protobuf Plugin

点击ok,然后右键proto文件,点击图中选项

在当前目录下就会生成Java类

注意版本,我这里使用的3.19.4,那么maven中引入最好是相同版本

4、定义一个自定义消息处理器

package com.huashan.websocket.handler;
import com.huashan.websocket.proto.MessageBodyProto;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.concurrent.GlobalEventExecutor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;


@Slf4j
@Component
@ChannelHandler.Sharable
public class WebRequestHandler extends SimpleChannelInboundHandler<MessageBodyProto.MessageBody> {
    public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

        @Override
        public void userEventTriggered(ChannelHandlerContext ctx, Object msg) throws Exception {
            System.out.println("心跳触发");
            if (msg instanceof IdleStateEvent && ((IdleStateEvent) msg).state().equals(IdleState.READER_IDLE)) {
                ctx.channel().close();
            }
        }

        @Override
        protected void channelRead0 (ChannelHandlerContext ctx, MessageBodyProto.MessageBody msg ) throws Exception {
            ctx.writeAndFlush(MessageBodyProto.MessageBody.newBuilder().setContent(ctx.channel().remoteAddress()+"发送了消息:"+msg.getContent()));
            System.out.println("接收到消息:" + msg.getContent());
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("与客户端断开连接,通道关闭!");
        }

        @Override
        public void exceptionCaught (ChannelHandlerContext ctx, Throwable cause)throws Exception {
            Channel channel = ctx.channel();
            System.out.println("服务器:" + channel.remoteAddress() + "异常");
            cause.printStackTrace();
        }


}

5、定义通道配置

package com.huashan.websocket.channel;
import com.google.protobuf.MessageLite;
import com.google.protobuf.MessageLiteOrBuilder;
import com.huashan.websocket.handler.HttpRequestHandler;
import com.huashan.websocket.handler.WebRequestHandler;
import com.huashan.websocket.proto.MessageBodyProto;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.codec.MessageToMessageDecoder;
import io.netty.handler.codec.MessageToMessageEncoder;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.IdleStateHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * Netty 通道初始化
 *
 * @author qiding
 */
@Component
public class WebChannelInit extends ChannelInitializer<Channel> {

    private static int READ_IDLE_TIME = 50;

    private static int WRITE_IDLE_TIME = 30;
    @Autowired
    WebRequestHandler messageHandler;

    @Override
        protected void initChannel(Channel channel) {
        channel.pipeline()
                .addLast(new HttpServerCodec())
                .addLast(new HttpObjectAggregator(65536))//把多个消息转换为一个单一的FullHttpRequest或是FullHttpResponse,
                .addLast(new ChunkedWriteHandler())//大数据处理
                .addLast(new WebSocketServerCompressionHandler())// WebSocket数据压缩
                .addLast(new WebSocketServerProtocolHandler("/ws", null, true,65536,true))
                //协议包解码
                .addLast(new MessageToMessageDecoder<WebSocketFrame>() {
                    @Override
                    protected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List<Object> objs) throws Exception {
                        ByteBuf buf = ((BinaryWebSocketFrame) frame).content();
                        objs.add(buf);
                        buf.retain();
                    }
                })
                //协议包编码
                .addLast(new MessageToMessageEncoder<MessageLiteOrBuilder>() {
                    @Override
                    protected void encode(ChannelHandlerContext ctx, MessageLiteOrBuilder msg, List<Object> out) throws Exception {
                        ByteBuf result = null;
                        if (msg instanceof MessageLite) {
                            result = Unpooled.wrappedBuffer(((MessageLite) msg).toByteArray());
                        }
                        if (msg instanceof MessageLite.Builder) {
                            result = Unpooled.wrappedBuffer(((MessageLite.Builder) msg).build().toByteArray());
                        }
                        // ==== 上面代码片段是拷贝自TCP ProtobufEncoder 源码 ====
                        // 然后下面再转成websocket二进制流,因为客户端不能直接解析protobuf编码生成的
                        WebSocketFrame frame = new BinaryWebSocketFrame(result);
                        out.add(frame);
                    }
                })
                .addLast(new ProtobufDecoder(MessageBodyProto.MessageBody.getDefaultInstance()))
                .addLast(new IdleStateHandler(READ_IDLE_TIME, WRITE_IDLE_TIME, 0, TimeUnit.SECONDS))//心跳检测
                .addLast(messageHandler);
        }
}

6、定义一个启动关闭的接口

package com.huashan.websocket.server;

import javax.annotation.PreDestroy;

public interface IWebSocketServer {


    /**
     * 主启动程序,初始化参数
     *
     * @throws Exception 初始化异常
     */
    void start(String host,Integer prot) throws Exception;


    /**
     * 优雅的结束服务器
     *
     * @throws InterruptedException 提前中断异常
     */
    @PreDestroy
    void destroy() throws InterruptedException;
}

7、接口实现

@Component
@Slf4j
public class WebSocketServer implements IWebSocketServer {
    @Autowired
    WebChannelInit channelInit;
    private Integer prot;
    private String host;
    @Override
    public void start(String host,Integer prot) throws Exception {
        log.info("初始化 server ...");
        this.host=host;
        this.prot=prot;
        this.tcpServer();
    }

    /**
     * 初始化
     */
    private void tcpServer() {
        //创建两个线程组,含有的子线程NioEventLoop的个数默认是cup核数的两倍
        //bossGroup处理连接请求,真正和客户端业务处理的是group完成
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup group = new NioEventLoopGroup();
        ServerBootstrap bootstrap = new ServerBootstrap();
        try {
            bootstrap.group(group,bossGroup)
                    .channel(NioServerSocketChannel.class) //设置通道
                    .childHandler(channelInit)

                    .option(ChannelOption.SO_KEEPALIVE, true)
                    .option(ChannelOption.SO_BACKLOG, 1024);
            log.info("server启动成功!开始监听端口:{}", this.prot);
            ChannelFuture channelFuture = bootstrap.bind(this.prot).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally{
            //关闭主线程组
            group.shutdownGracefully();
            //关闭工作线程组
            bossGroup.shutdownGracefully();
        }
    }

    /**
     * 销毁
     */
    @PreDestroy
    @Override
    public void destroy() {
       // workerGroup.shutdownGracefully();
    }
}

8、启动类


import com.huashan.udp.server.SocketServer;
import com.huashan.websocket.server.WebSocketServer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@Slf4j
public class HuashanVrApplication implements ApplicationRunner {
    @Autowired
    SocketServer socketServer;
    @Autowired
    WebSocketServer webSocketServer;

    public static void main(String[] args) {
        SpringApplication.run(HuashanVrApplication.class, args);
        log.info("----------------start-success------------------");
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
//        socketServer.start();
        webSocketServer.start("127.0.0.1",10900);
    }
}

9、启动测试连接

心跳也开始触发,注意的是现在还不能正常通过测试工具发送消息,因为我们使用的protobuf协议,例如我们现在随便发送一个消息,服务器会报错提示不支持的数据格式

下面接着写一个前端的测试页面,自己创建一个前端工程

10、编写HTML客户端测试页面

        1、生成proto文件的js文件

                和上面说到的生成Java类似,只是更换js

和第3步中的步骤一样,重新右键点击生成,生成MessageBody_pb.js文件,注意这里生成的js文件不能直接使用,要通过编译才能

2、编译js文件

        (1)第一步:在目录下创建一个js文件

var address = require('./MessageBody_pb');
module.exports = {
    DataProto: address
}

(2)第二步:

//安装库文件的引用库
npm install -g require
//安装打包成前端使用的js文件
npm install -g browserify
//打包js文件export.js
npm install google-protobuf

在这个文件目录下打开cmd,执行:browserify exprort.js -o msgtest_main.js

(3)这时在目录下会生成一个msgtest_main.js文件(这个文件就是可以用在html中的)

3、编写HTML文件

<!DOCTYPE html>

<html>

<head>
    <meta charset="UTF-8">
    <title>WebSocket Chat</title>
    <script type="text/javascript" src="./msgtest_main.js"></script>
</head>
<body>
    <form onsubmit="return false;">
        <h3>WebSocket 聊天室:</h3>
        <textarea id="responseText" style="width: 500px; height: 300px;"></textarea>
        <br>
        <input type="text" name="message" style="width: 300px" value="你好,欢迎使用Websocket+Protobuf">
        <input type="button" value="发送消息" onclick="send(this.form.message.value)">
        <input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="清空聊天记录">
    </form>
    <br>
    <br>
</body>

<script type="text/javascript">
    var socket = null;
    if (!window.WebSocket) {
        window.WebSocket = window.MozWebSocket;
    }
    if (window.WebSocket) {
        socket = new WebSocket("ws://127.0.0.1:10900/ws");
        socket.binaryType = "arraybuffer";
        socket.onmessage = function (event) {
            var ta = document.getElementById('responseText');
            if (event.data instanceof ArrayBuffer) {
                var msg =  proto.MessageBody.deserializeBinary(event.data);      //如果后端发送的是二进制帧(protobuf)会收到前面定义的类型
                ta.value = ta.value + '\n服务器回应:' + msg.getContent();
            } else {
                var data = event.data;                //后端返回的是文本帧时触发
                ta.value = ta.value + '\n' + data;
            }
        };

        socket.onopen = function (event) {
            var ta = document.getElementById('responseText');
            ta.value = "连接开启!";
           // var messageModel = new proto.MessageBody();
           // messageModel.setContent("123");
           // socket.send(messageModel.serializeBinary());
        };

        socket.onclose = function (event) {
            var ta = document.getElementById('responseText');
            ta.value = ta.value + "\n连接被关闭";
        };

    } else {
        alert("你的浏览器不支持 WebSocket!");
    }

    function send(message) {
        if (!window.WebSocket) {
            return;
        }
        if (socket.readyState == WebSocket.OPEN) {
            var content = new proto.MessageBody();
            content.setContent(message);
            socket.send(content.serializeBinary());
        } else {
            alert("连接没有开启.");
        }
    }
</script>

</html>

11、启动客户端HTML测试

就此整个流程就全部完成,之后可以根据自己的业务定义消息协议模板

对于使用Netty 4.1和ProtobufWebSocket编码器,你可以按照以下步骤进行设置: 1. 首先,确保你已经添加了NettyProtobuf的依赖到你的项目中。 2. 创建一个WebSocket编码器类,该类将负责将Protobuf消息编码为WebSocket帧。下面是一个示例代码: ```java import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToByteEncoder; public class ProtobufWebSocketEncoder extends MessageToByteEncoder<MessageLite> { @Override protected void encode(ChannelHandlerContext ctx, MessageLite msg, ByteBuf out) throws Exception { byte[] bytes = msg.toByteArray(); out.writeBytes(bytes); } } ``` 3. 在你的Netty初始化代码中,添加WebSocket编码器到你的ChannelPipeline中。下面是一个示例代码: ```java import io.netty.channel.ChannelInitializer; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; public class WebSocketServerInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline() .addLast(new HttpServerCodec()) .addLast(new HttpObjectAggregator(65536)) .addLast(new WebSocketServerProtocolHandler("/websocket")) .addLast(new ProtobufWebSocketEncoder()) .addLast(new YourCustomWebSocketHandler()); } } ``` 在上面的代码中,`YourCustomWebSocketHandler`是你自己实现处理WebSocket消息的处理器。 4. 最后,在你的Netty服务器启动代码中,绑定正确的端口并启动服务器。下面是一个示例代码: ```java import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; public class WebSocketServer { public static void main(String[] args) throws InterruptedException { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new WebSocketServerInitializer()); ChannelFuture f = b.bind(8080).sync(); f.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } } ``` 确保将端口号8080更改为你实际使用的端口号。 以上就是使用Netty 4.1和ProtobufWebSocket编码器的基本设置。请根据你的实际需求进行适当的修改和扩展。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yang疯狂打码中

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值