- 定义proto结构
Message.proto内容如下:
syntax = "proto3";
package com.test.bf.nio.netty.webChat.websocket;
option java_outer_classname="MessageProto";
message Model {
string version = 1;//接口版本号
string deviceId = 2;//设备uuid
uint32 cmd = 3;//请求接口命令字 1绑定 2心跳 3上线 4下线
string sender = 4;//发送人
string receiver = 5;//接收人
string groupId =6;//用户组编号
uint32 msgtype = 7;//请求1,应答2,通知3,响应4 format
uint32 flag = 8;//1 rsa加密 2aes加密
string platform = 9;//mobile-ios mobile-android pc-windows pc-mac
string platformVersion = 10;//客户端版本号
string token = 11;//客户端凭证
string appKey = 12;//客户端key
string timeStamp = 13;//时间戳
string sign = 14;//签名
bytes content = 15;//请求数据
}
MessageBody.proto内容如下:
syntax = "proto3";
package com.test.bf.nio.netty.webChat.websocket;
option java_outer_classname="MessageBodyProto";
message MessageBody {
string title = 1; //标题
string content = 2;//内容
string time = 3;//发送时间
uint32 type = 4;//0 文字 1 文件
string extend = 5;//扩展字段
}
2. 利用proto.exe生成java文件和js文件
java proto使用
https://blog.csdn.net/erica_1230/article/details/78746757
js proto使用
https://bobjin.com/blog/view/54737b708031d8931158129c5c44a843.html
3. 定义WebSocketFrameHandler,对websocket数据处理
/**
* 创建于:2019年9月17日 下午4:36:49
* 所属项目:
* 文件名称:TextWebSocketFrameHandler.java
* 作者:jcy
* 版权信息:
*/
package com.test.bf.nio.netty.webChat.websocket;
import io.netty.channel.Channel;
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.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.AttributeKey;
import io.netty.util.concurrent.GlobalEventExecutor;
import java.util.UUID;
import com.test.bf.nio.netty.webChat.websocket.MessageBodyProto.MessageBody;
public class WebSocketFrameHandler extends
SimpleChannelInboundHandler<MessageProto.Model> {
public static ChannelGroup channels = new DefaultChannelGroup(
GlobalEventExecutor.INSTANCE);
public static final AttributeKey<String> SERVER_SESSION_ID = AttributeKey
.valueOf("sessionId");
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object o)
throws Exception {
String sessionId = ctx.channel().attr(SERVER_SESSION_ID).get();
if (o instanceof IdleStateEvent
&& ((IdleStateEvent) o).state().equals(IdleState.WRITER_IDLE)) {
if (sessionId != null && !"".equals(sessionId)) {
MessageProto.Model.Builder builder = MessageProto.Model.newBuilder();
MessageBody messageBody = MessageBodyProto.MessageBody.newBuilder().setContent("心跳包").build();
builder.setContent(messageBody.toByteString());
ctx.channel().writeAndFlush(builder);
}
}
if (o instanceof IdleStateEvent
&& ((IdleStateEvent) o).state().equals(IdleState.READER_IDLE)) {
ctx.channel().close();
}
}
/*
* (non-Javadoc)
*
* @see
* io.netty.channel.SimpleChannelInboundHandler#channelRead0(io.netty.channel
* .ChannelHandlerContext, java.lang.Object)
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx,
MessageProto.Model msg) throws Exception {
Channel channel = ctx.channel();
if (msg.getCmd() == 3) {
// 绑定
channels.writeAndFlush(new TextWebSocketFrame("服务器:" + channel.remoteAddress() + "加入群聊"));
channels.add(channel);
channel.attr(SERVER_SESSION_ID).set(UUID.randomUUID().toString());
return;
}
if(msg.getCmd() == 4){
String sessionId = channel.attr(SERVER_SESSION_ID).get();
if (sessionId != null && !"".equals(sessionId)) {
channels.writeAndFlush(new TextWebSocketFrame("服务器:" + channel.remoteAddress() + "退出群聊"));
// 关闭的channel自动从channelGroup中删除
}
return;
}
MessageBodyProto.MessageBody content = MessageBodyProto.MessageBody.parseFrom(msg.getContent());
String contentMsg = content.getContent();
for (Channel temp : channels) {
if (temp != channel) {
MessageBody messageBody = MessageBodyProto.MessageBody.newBuilder()
.setContent("服务器:" + channel.remoteAddress() + "发送:" + contentMsg).build();
MessageProto.Model model = MessageProto.Model.newBuilder()
.setContent(messageBody.toByteString()).build();
temp.writeAndFlush(model);
} else {
MessageBody messageBody = MessageBodyProto.MessageBody
.newBuilder().setContent("你发的:" + contentMsg).build();
MessageProto.Model model = MessageProto.Model.newBuilder()
.setContent(messageBody.toByteString()).build();
channel.writeAndFlush(model);
}
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
Channel channel = ctx.channel();
System.out.println("服务器:" + channel.remoteAddress() + "异常");
cause.printStackTrace();
}
}
注意:不要使用channelActive和channelInActive去监听连接打开和关闭,因为HttpRequestHandler的事件会往下传播。
4. 定义HttpRequestHandler,对http请求进行处理
/**
* 创建于:2019年9月17日 下午4:28:28
* 所属项目:
* 文件名称:HttpRequestHandler.java
* 作者:jcy
* 版权信息:
*/
package com.test.bf.nio.netty.webChat.websocket;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.DefaultFileRegion;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedNioFile;
import io.netty.util.AttributeKey;
import java.io.File;
import java.io.RandomAccessFile;
import java.net.URISyntaxException;
import java.net.URL;
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest>{
private static File INDEX;
public static final AttributeKey<String> AGREEMENT_TYPE = AttributeKey.valueOf("agreement_type");
static{
URL location = HttpRequestHandler.class.getProtectionDomain().getCodeSource().getLocation();
try {
String path = location.toURI() + "WebsocketChatClient.html";
path = !path.contains("file:") ? path : path.substring(5);
INDEX = new File(path);
} catch (URISyntaxException e) {
throw new IllegalStateException("Unable to locate WebsocketChatClient.html", e);
}
}
/* (non-Javadoc)
* @see io.netty.channel.SimpleChannelInboundHandler#channelRead0(io.netty.channel.ChannelHandlerContext, java.lang.Object)
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request)
throws Exception {
if("/ws".equalsIgnoreCase(request.getUri())){
ctx.fireChannelRead(request.retain());
}else{
if (HttpHeaders.is100ContinueExpected(request)) {
send100Continue(ctx); //3
}
RandomAccessFile file= null;
if(request.getUri().indexOf(".js") != -1){
URL location = HttpRequestHandler.class.getProtectionDomain().getCodeSource().getLocation();
String path = location.toURI() + request.getUri();
path = !path.contains("file:") ? path : path.substring(5);
file = new RandomAccessFile(new File(path), "r");//4
}else{
file = new RandomAccessFile(INDEX, "r");//4
}
HttpResponse response = new DefaultHttpResponse(request.getProtocolVersion(), HttpResponseStatus.OK);
response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/html; charset=UTF-8");
boolean keepAlive = HttpHeaders.isKeepAlive(request);
if (keepAlive) { //5
response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, file.length());
response.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
}
ctx.write(response); //6
if (ctx.pipeline().get(SslHandler.class) == null) { //7
ctx.write(new DefaultFileRegion(file.getChannel(), 0, file.length()));
} else {
ctx.write(new ChunkedNioFile(file.getChannel()));
}
ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); //8
if (!keepAlive) {
future.addListener(ChannelFutureListener.CLOSE); //9
}
file.close();
}
}
private static void send100Continue(ChannelHandlerContext ctx) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
ctx.writeAndFlush(response);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
Channel incoming = ctx.channel();
System.out.println("Client:"+incoming.remoteAddress()+"异常");
// 当出现异常就关闭连接
cause.printStackTrace();
ctx.close();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
ctx.channel().attr(AGREEMENT_TYPE).set("http");
}
}
5. 定义channelHandler链,WebSocketServerInitializer
/**
* 创建于:2019年9月17日 下午4:43:42
* 所属项目:
* 文件名称:WebSocketServerInitializer.java
* 作者:jcy
* 版权信息:
*/
package com.test.bf.nio.netty.webChat.websocket;
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 java.util.List;
import java.util.concurrent.TimeUnit;
import com.google.protobuf.MessageLite;
import com.google.protobuf.MessageLiteOrBuilder;
public class WebSocketServerInitializer extends ChannelInitializer<Channel>{
private static int READ_IDLE_TIME = 50;
private static int WRITE_IDLE_TIME = 30;
/* (non-Javadoc)
* @see io.netty.channel.ChannelInitializer#initChannel(io.netty.channel.Channel)
*/
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new HttpServerCodec())//Http解码
.addLast(new HttpObjectAggregator(65536))//把多个消息转换为一个单一的FullHttpRequest或是FullHttpResponse,
.addLast(new ChunkedWriteHandler())//大数据处理
.addLast(new HttpRequestHandler())
.addLast(new WebSocketServerCompressionHandler())// WebSocket数据压缩
.addLast(new WebSocketServerProtocolHandler("/ws", null ,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(MessageProto.Model.getDefaultInstance()))
.addLast(new IdleStateHandler(READ_IDLE_TIME, WRITE_IDLE_TIME, 0, TimeUnit.SECONDS))//心跳检测
.addLast(new WebSocketFrameHandler());
}
}
6. 定义启动类
WebSocketChatServer
/**
* 创建于:2019年9月17日 下午4:48:52
* 所属项目:
* 文件名称:WebSocketChatServer.java
* 作者:jcy
* 版权信息:
*/
package com.test.bf.nio.netty.webChat.websocket;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class WebSocketChatServer {
public static void main(String[] args) {
EventLoopGroup group = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
try {
bootstrap.group(group)
.channel(NioServerSocketChannel.class)
.childHandler(new WebSocketServerInitializer())
.option(ChannelOption.SO_KEEPALIVE, true);
ChannelFuture future = bootstrap.bind(9999).sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
group.shutdownGracefully();
}
}
}
7. 在classes目录下新建html, WebsocketChatClient.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Chat</title>
<script type="text/javascript" src="http://localhost:9999/message.js"></script>
<script type="text/javascript" src="http://localhost:9999/messageBody.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="Welcome to www.waylau.com">
<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://localhost:9999/ws");
socket.binaryType = "arraybuffer";
socket.onmessage = function(event) {
var ta = document.getElementById('responseText');
if (event.data instanceof ArrayBuffer){
var msg = proto.com.test.bf.nio.netty.webChat.websocket.Model.deserializeBinary(event.data); //如果后端发送的是二进制帧(protobuf)会收到前面定义的类型
var msgCon = proto.com.test.bf.nio.netty.webChat.websocket.MessageBody.deserializeBinary(msg.getContent());
ta.value = ta.value + '\n' + msgCon.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.com.test.bf.nio.netty.webChat.websocket.Model();
messageModel.setCmd(3);
socket.send(messageModel.serializeBinary());
};
socket.onclose = function(event) {
var ta = document.getElementById('responseText');
ta.value = ta.value + "\n连接被关闭";
var messageModel = new proto.com.test.bf.nio.netty.webChat.websocket.Model();
messageModel.setCmd(4);
socket.send(messageModel.serializeBinary());
};
} else {
alert("你的浏览器不支持 WebSocket!");
}
function send(message) {
if (!window.WebSocket) {
return;
}
if (socket.readyState == WebSocket.OPEN) {
//var fullMessage = '{"id":1,"toUser":"1,2,3","message":"'+message+'"}';
//socket.send(fullMessage);
var messageModel = new proto.com.test.bf.nio.netty.webChat.websocket.Model();
var content = new proto.com.test.bf.nio.netty.webChat.websocket.MessageBody();
content.setContent(message);
messageModel.setContent(content.serializeBinary())
socket.send(messageModel.serializeBinary());
} else {
alert("连接没有开启.");
}
}
</script>
</html>
8. 启动WebSocketChatServer,访问http://localhost:9999/,可以进行简单的群聊