netty+protobuf+websocket实现在线聊天--简易实现

  1. 定义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/,可以进行简单的群聊

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值