使用SpringBoot和Netty实现一对一(互相)简单聊天

阅读: 评论:0

使用SpringBoot和Netty实现一对一(互相)简单聊天

使用SpringBoot和Netty实现一对一(互相)简单聊天

首先看一下效果图:

依赖前端代码详情请移步:

本样例前端采用JQuery与Vue + Webpack
为了项目尽可能简单,我们一切从简,具体如下:

  • 不涉及复杂的业务逻辑

  • 测试样例从简(Lucy,Jack,Mike),MYSQL表数据如下:

  • 项目存在两个服务器:tomcat服务器,Netty构建的webSocket服务器

  • 项目结构如下:

额,DeleteUselessRepository与本项目无关,只是用来清理maven仓库的无效文件夹。

需要指出的是前端模板本身是很优异的,使用了Vue,也就是我们只需要修改Vue的data的数据即可,然后在关键的部位添加我们自己的方法即可。

另外此tomcat服务于netty服务无交集,即不共享session,此为缺陷之一。

下面讲解代码部分:

CacheLoader.java

public class CacheLoader {public static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);public static Map<Double,Channel> channelMap = new ConcurrentHashMap<>();
}

channelGroup和channelMap都是存储客户端的SocketChannel对象的

channelGroup使用原生对象,使用全局事件处理器,主要用来广播消息

channelMap为自己实现,主要是为了完成一对一通信,所以每一个SocketChannel都与其用户id绑定

HttpRequestHandler.java

public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {// webSocket标识private final String wsUri;public HttpRequestHandler(String wsUri) {this.wsUri = wsUri;}@Overrideprotected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws Exception {// 如果是webSocket请求,请求地址uri等于wsUriif (wsUri.equalsIgnoreCase(fullHttpRequest.uri())) {// 将消息发送到下一个channelHandlerctx.ain());}}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {cause.printStackTrace();ctx.close();}
}

因为此处不需要构建http服务(全权交给tomcat处理),故HttpRequestHandler实现就一个功能,判断请求类型,如果是websocket请求则向下转发。

TextWebSocketFrameHandler.java

public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {private final ChannelGroup channelGroup;private ChatService chatService = new ChatServiceImpl();public TextWebSocketFrameHandler(ChannelGroup channelGroup) {this.channelGroup = channelGroup;}@Overridepublic void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {// 如果ws握手完成if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {// 删除pipeLine中处理http请求的handlerctx.pipeline().remove(HttpRequestHandler.class);// 写一个消息广播到所有的客户端channel//channelGroup.writeAndFlush(new TextWebSocketFrame("Client " + ctx.channel() + " joined!"));// 将当前客户端channel添加进groupchannelGroup.add(ctx.channel());} else {super.userEventTriggered(ctx, evt);}}@Overrideprotected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {// 将接收的消息通过ChannelGroup转发到所有连接的客户端//channelGroup.ain());// 前端组装的消息格式是 {"message":{"text":"项目地址","date":"2018-11-28T02:13:52.437Z"},"to":2,"from":1}Map<String,Object> msg = GsonUtils.().toString(),new TypeToken<Map<String,Object>>(){});String type = (String) ("type");switch (type) {case "REGISTER":ister(channelHandlerContext,msg);break;case "SINGLE_SENDING":chatService.singleSend(channelHandlerContext,msg);break;}}
}

userEventTriggered是客户端连接请求触发函数

channelRead0中则是我们的核心业务逻辑

  • 前端传来的数据是Json字符串
  • Json字符串中存在type字段用来标识消息类型,from字段标识消息来源,to字段标识消息去向,message则为消息体。
  • 因为前端模板需要,message中应该存在两个字段:date与text。二者顾名思义。

ChatServiceImpl.java

public class ChatServiceImpl implements ChatService{@Overridepublic void register(ChannelHandlerContext channelHandlerContext, Map<String, Object> msg) {CacheLoader.channelMap.put(Double.("userId").toString()),channelHandlerContext.channel());}@Overridepublic void singleSend(ChannelHandlerContext channelHandlerContext, Map<String, Object> msg) {Double to = Double.("to").toString());ve("to");ve("type");(to).writeAndFlush(new Json(msg)));}
}

以上两个逻辑函数比较简单,不要吐槽为什么channelMap的id是Double类型,因为我懒QAQ!

单对单聊天中msg需要传到前台的只有from和message两个字段。

ChatServerInitializer.java

public class ChatServerInitializer extends ChannelInitializer<SocketChannel> {private final ChannelGroup channelGroup;public ChatServerInitializer(ChannelGroup channelGroup) {this.channelGroup = channelGroup;}@Overrideprotected void initChannel(SocketChannel channel) throws Exception {channel.pipeline()// 编解码http请求.addLast(new HttpServerCodec())//聚合解码HttpRequest/HttpContent/LastHttpContent到FullHttpRequest//保证接收的Http请求的完整性.addLast(new HttpObjectAggregator(64 * 1024))// 处理FullHttpRequest.addLast(new HttpRequestHandler("/ws"))// 处理其他的WebSocketFrame.addLast(new WebSocketServerProtocolHandler("/ws"))// 处理TextWebSocketFrame.addLast(new TextWebSocketFrameHandler(channelGroup));}
}

netty支持六种WebSocket框架

我们只需要处理TextWebSocketFrameHandler,其他交给netty的WebSocketServerProtocolHandler处理

下面是对前端js的改造和新增

  • 改造发送逻辑,新增send方法
  • 改造加载逻辑,新增fetch方法
  • 改造在原末班的main.js进行,新增handle.js

handle.js

var R$_globalVM;
// 加载初始化信息
R$_fetch = function () {// 下面的ajax方法为同步var result = null;$.ajaxSettings.async = false$.get("/user/getInitialData",null,function (data) {result =  data;});return result;$.ajaxSettings.async = true
}
var socket;
if (!window.WebSocket) {window.WebSocket = window.MozWebSocket;
}
if (window.WebSocket) {socket = new WebSocket("ws://localhost:2048/ws");ssage = function (event) {// 后端发送过来的数据中存在userId,messagevar source = JSON.parse(event.data);console.log(source);var vm_data = R$_globalVM.$data.sessionList;for(var i = 0;i<vm_data.length;i++){if (vm_data[i].userId == source.from){vm_data[i].messages.ssage);break;}}};// 连接成功1秒后,将用户信息注册到服务器在线用户表pen = setTimeout(function(event){console.log("连接开启!");var data = {"userId" : R$_globalVM.$data.user.id,"type" : "REGISTER"};send(JSON.stringify(data));}, 1000);lose = function (event) {console.log("连接被关闭");};
} else {alert("你的浏览器不支持 WebSocket!");
}function send(value) {if (!window.WebSocket) {return;}if (adyState == WebSocket.OPEN) {socket.send(value);} else {alert("连接没有开启.");}
}R$_send = function (value) {send(JSON.stringify(value));return true;
}

/user/getInitialData对应的控制器逻辑:

@RequestMapping(value = "getInitialData",method = RequestMethod.GET)
@ResponseBody
public Map<String,Object> getInitialData(HttpSession session){User user = (User) Attribute("user");Map<String,Object> result = new HashMap<>();// userresult.put("user",userService.userToMap(user));// userList,sessionListList<Map<String,Object>> userList = new ArrayList<>();List<Map<String,Object>> sessionList = new ArrayList<>();String[] friends = Friends().split(",");for (String friend : friends){userList.add(userService.(Integer.parseInt(friend))));Map<String,Object> tmp_map = new HashMap<>();tmp_map.put("messages",new ArrayList<>());tmp_map.put("userId",Integer.parseInt(friend));sessionList.add(tmp_map);}result.put("userList",userList);result.put("sessionList",sessionList);return result;
}

需要注意的是,在main.js中需要:

R$_globalVM = new Vue(o["default"])

改造发送逻辑

<div class=m-text><textarea placeholder="按 Ctrl + Enter 发送" v-model=text @keyup=inputing></textarea></div>
找到inputing方法,改造前后对比:

改造前:

inputing: function (e) {e.ctrlKey && 13 === e.keyCode && length && (ssages.push({text: ,date: new Date,self: !0 // !0表示是自己发送的消息(其实就是true),0表示是对方发送的消息}),  = "")}

改造后:

inputing: function (e) {e.ctrlKey && 13 === e.keyCode && length && (ssages.push({text: ,date: new Date,self: !0 // !0表示是自己发送的消息(其实就是true),0表示是对方发送的消息}),R$_send({"message":{text: ,date: new Date},// 发给谁的数据"to": this.session.userId,"from":R$_globalVM.$data.user.id,"type":"SINGLE_SENDING"}),  = "")}

改造加载逻辑

改造前:

var i = s(14), o = r(i), n = "VUE-CHAT-v3";if (!Item(n)) {var a = new Date, l = {user: {id: 1, name: "Coffce", img: "dist/images/1.jpg"},userList: [{id: 2, name: "示例介绍", img: "dist/images/2.png"}, {id: 3,name: "webpack",img: "dist/images/3.jpg"}],sessionList: [{userId: 2,messages: [{text: "Hello,这是一个基于Vue + Webpack构建的简单chat示例,聊天记录保存在localStorge。简单演示了Vue的基础特性和webpack配置。",date: a}, {text: "项目地址: ", date: a}]}, {userId: 3, messages: []}]};localStorage.setItem(n, (0, o["default"])(l))}t["default"] = {fetch: function () {return JSON.Item(n))}, save: function (e) {localStorage.setItem(n, (0, o["default"])(e))}}

改造后:

t["default"] = {fetch: function () {// 加载历史聊天记录return R$_fetch();}, save: function (e) {// 暂时不做//localStorage.setItem(n, (0, o["default"])(e))//R$_save((0, o["default"])(e));}}

ok,至此,大功告成!!至于tomcat服务,对不起,那完全是无关紧要的,不在此处讲解。

看看成果?

hah,Jack,Lucy,Mike三人可以愉快地聊天了。

此外,websocket通信也推荐nodejs的socket.io框架,很不错喔!

本文发布于:2024-02-01 10:12:27,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170675354535908.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:简单   SpringBoot   Netty
留言与评论(共有 0 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23