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测试
就此整个流程就全部完成,之后可以根据自己的业务定义消息协议模板