前言
最近在做一个聊天性能,具体需要:相似微信,在一个好友列表中,点击某个好友就能够建设与该好友的聊天连贯,向该好友发送音讯,对方可能实时显示进去,进行真正意义上的聊天。
在做之前,不论在界面布局,还是性能实现方面都下了一点功夫,最终还是一点点实现了,当初就记录一下。
在编码之前得先理解一下WebSocket
- 什么是
WebSocket
?
WebSocket
,即Web浏览器与Web服务器之间全双工通信规范;是HTML5中的协定,反对长久间断,http协定不反对持久性连贯。Http1.0和HTTP1.1都不反对持久性的链接,HTTP1.1中的keep-alive,将多个http申请合并为1个- 一旦确立
WebSocket
通信连贯,不管服务器还是客户端,任意一方都可间接向对方发送报文
WebSocket
特点?
- 推送性能:反对由服务器向客户端推送数据的推送性能,这样,服务器可间接发送数据,而不用期待客户端的申请
- 缩小通信量:只有建设起
WebSocket
,就能够始终放弃连贯状态头部字段多了上面2个属性:
Upgrade:webSocketConnection:Upgrade
1、实现成果
点击左侧好友列表时,会建设websocket连贯,把以后发消息的用户发送给websocket服务器
输出音讯
2、前端实现
<!-- Chat.vue页面 --><template> <div id="chat"> <!-- 聊天音讯治理 --> <el-container style="height: 620px; border: 1px solid #eee"> <el-aside width="250px"> <user-list :friendList="this.friendList" @set-contact="getcontact" ref="friends" :activeIndex="activeIndex" ></user-list> </el-aside> <el-container> <el-header style="text-align: right; font-size: 12px"> <span> <h2>{{this.username}}</h2> </span> </el-header> <el-main style="height:400px;" class="msg-main"> <chat-msg ref="msg" :user="this.username" id="msg-box"></chat-msg> </el-main> <div class="m-text"> <textarea placeholder="按 Ctrl + Enter 发送" ref="sendMsg" v-model="contentText" @keyup.enter="sendText()" ></textarea> <div class="btn" :class="{['btn-active']:contentText}" @click="sendText()">发送</div> </div> </el-container> </el-container> </div></template><script>import UserList from "../../components/chat/friendList";import ChatMsg from "../../components/chat/message";import InputText from "../../components/chat/text";export default { data() { return { //好友列表 friendList: [], activeIndex: null, //以后聊天好友 activeFriend: [], friends: "", ws: null, count: 0, userId: this.$store.state.userInfo.uid, // 以后用户ID username: this.$store.state.userInfo.username, // 以后用户昵称 avatar: this.$store.state.userInfo.uavatar, // 以后用户头像 msgList: [], // 聊天记录的数组 contentText: "" // input输出的值 }; }, components: { UserList, ChatMsg, InputText }, mounted() { this.getFridends(this.userId); }, destroyed() { // 来到页面时敞开websocket连贯 this.ws.onclose(undefined); }, methods: { select(value) { this.activeIndex = this.friendList.indexOf(value); }, getcontact(list) { console.log(this.$store.state.userInfo); this.activeFriend = list; //保留以后聊天好友 this.friends = list.ffriendName; this.activeIndex = this.friendList.indexOf(this.friends); this.getFridendInfo(this.friends); this.initWebSocket(this.username); this.getFridendMsg(this.friends); }, // 发送聊天信息 sendText() { let _this = this; _this.$refs["sendMsg"].focus(); if (!_this.contentText) { return; } let params = { mfromuser: _this.username, mtouser: _this.activeFriend[0].uusername, mfromavatar: _this.activeFriend[0].uavatar, mtoavatar: _this.avatar, mmsg: _this.contentText, mtype: 1 }; _this.ws.send(JSON.stringify(params)); //调用WebSocket send()发送信息的办法 _this.contentText = ""; setTimeout(() => { _this.scrollToBottom(); }, 200); }, // 进入页面创立websocket连贯 initWebSocket(id) { let _this = this; // 判断页面有没有存在websocket连贯 if (window.WebSocket) { var serverHot = window.location.hostname; // 填写本地IP地址,此处的 :8021端口号要与后端配置的统一! var url = "ws://" + serverHot + ":8021" + "/websocket/" + id; // `ws://127.0.0.1/9101/websocket/10086/老王` let ws = new WebSocket(url); _this.ws = ws; ws.onopen = function(e) { console.log("服务器连贯胜利: " + url); }; ws.onclose = function(e) { console.log("服务器连贯敞开: " + url); }; ws.onerror = function() { console.log("服务器连贯出错: " + url); }; ws.onmessage = function(e) { //接管服务器返回的数据 console.log(e); let resData = e.data.split("|"); console.log(resData); //更新音讯 if (resData[0] == _this.username) { _this.getFridendMsg(_this.friends); } else { _this.$message.error("发送音讯失败,以后用户不存在"); } }; } }, //获取好友列表 async getFridends(id) { console.log(id); const { data: res } = await this.$http.get( "api/business/chat/getfriends/" + id ); if (res.success) { // console.log(res); this.friendList = res.data; } else { this.$message.error("获取好友列表失败:" + res.data.errorMsg); } }, //获取好友音讯列表 async getFridendMsg(name) { const { data: res } = await this.$http.get( "api/business/chat/getFriendMsg/" + name ); if (res.success) { // console.log(res); this.msgList = res.data; //把好友聊天记录传给音讯组件 this.$refs.msg.getMsgList(this.msgList); } else { this.$message.error("获取好友音讯失败:" + res.data.errorMsg); } }, //获取好友信息 async getFridendInfo(name) { const { data: res } = await this.$http.get( "api//system/user/getFriendInfo/" + name ); if (res.success) { console.log(res); //好友信息 this.activeFriend = res.data; } else { this.$message.error("获取好友信息失败:" + res.data.errorMsg); } }, // 滚动条到底部 scrollToBottom() { this.$nextTick(() => { var container = this.$el.querySelector(".msg-main"); // console.log(container); // console.log(container.scrollTop); container.scrollTop = container.scrollHeight; }); } }};</script><style scoped lang="less">#chat { overflow: hidden;}.el-header { background-color: #b3c0d1; color: #333; line-height: 60px;}.el-aside { color: #fff; background-color: #2e3238;}.m-list { li { padding: 12px 15px; border-bottom: 1px solid #292c33; cursor: pointer; transition: background-color 0.1s; &:hover { background-color: rgba(255, 255, 255, 0.03); } &.active { background-color: rgba(255, 255, 255, 0.1); } } .avatar, .name { vertical-align: middle; } .avatar { border-radius: 2px; } .name { display: inline-block; margin: 0 0 0 15px; }}.m-message { padding: 10px 15px; li { margin-bottom: 15px; } .time { margin: 7px 0; text-align: center; > span { display: inline-block; padding: 3px 20px; font-size: 12px; color: #fff; border-radius: 2px; background-color: #dcdcdc; } } .avatar { float: left; margin: 0 15px 0 0; border-radius: 3px; } .text { display: inline-block; position: relative; padding: 3px 17px; max-width: ~"calc(100% - 40px)"; min-height: 30px; line-height: 2.5; font-size: 12px; text-align: left; word-break: break-all; background-color: #fafafa; border-radius: 4px; &:before { content: " "; position: absolute; top: 9px; right: 100%; border: 6px solid transparent; border-right-color: #fafafa; } } .self { text-align: right; .avatar { float: right; margin: 0 0 0 10px; } .text { background-color: #b2e281; &:before { right: inherit; left: 100%; border-right-color: transparent; border-left-color: #b2e281; } } }}.m-text { display: flex; height: 90px; border-top: solid 1px #ddd; textarea { padding-left: 10px; height: 100%; width: 100%; border: none; outline: none; font-family: "Micrsofot Yahei"; resize: none; } .btn { height: 2.3rem; min-width: 4rem; background: #e0e0e0; padding: 0.5rem; font-size: 0.88rem; color: white; text-align: center; border-radius: 0.2rem; margin-left: 0.5rem; transition: 0.5s; } .btn-active { background: #409eff; }}</style>
<!-- friendList.vue --><template> <div class="m-list"> <ul v-for="(friend, i) in friendList"> <li @click="setContact(i)" :class="{'active':currentIndex===i}"> <img class="avatar" width="50" height="50" /> <p class="name">{{friend.ffriendName}}</p> </li> </ul> </div></template><script>export default { data() { return { currentIndex: this.activeIndex }; }, props: ["friendList", "activeIndex"], methods: { setContact(index) { this.currentIndex = index; console.log(this.currentIndex); //与父组件进行通信。把以后点击好友的用户信息传给父组件 this.$emit("set-contact", this.friendList[index]); } }};</script><style lang="less" scoped>.m-list { li { padding: 12px 15px; border-bottom: 1px solid #292c33; cursor: pointer; transition: background-color 0.1s; &:hover { background-color: rgba(255, 255, 255, 0.03); } &.active { background-color: rgba(255, 255, 255, 0.1); } } .avatar, .name { vertical-align: middle; } .avatar { border-radius: 2px; } .name { display: inline-block; margin: 0 0 0 15px; }}</style>
<!-- message.vue --><template> <div class="m-message"> <ul v-for="item in msgList"> <li> <p class="time"> <span>{{item.mcreateTime | time}}</span> </p> <div class="main" :class="{self:item.mfromUser===username}"> <div class="title">{{item.mfromUser}}</div> <img class="avatar" width="40" height="40" /> <div class="text">{{item.mmsg}}</div> </div> </li> </ul> </div></template><script>export default { data() { return { msgList: this.userList, username: this.user }; }, props: ["user", "userList"], methods: { getMsgList(list) { console.log(list); this.msgList = list; }, }, filters: { // 将日期过滤为 yy-mm-dd hh:mm:ss time(date) { if (typeof date === "string") { date = new Date(date); //把定义的工夫赋值进来进行上面的转换 let year = date.getFullYear(); let month = date.getMonth() + 1; let day = date.getDate(); let hour = date.getHours(); let minute = date.getMinutes(); let second = date.getSeconds(); return ( year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second ); } } }};</script><style lang="less" scoped>.m-message { padding: 10px 15px; // overflow-y: scroll; li { margin-bottom: 15px; } .time { margin: 7px 0; text-align: center; > span { display: inline-block; padding: 3px 20px; font-size: 12px; color: #fff; border-radius: 2px; background-color: #dcdcdc; } } .avatar { float: left; margin: 0 15px 0 0; border-radius: 3px; } .text { display: inline-block; position: relative; padding: 3px 17px; max-width: ~"calc(100% - 40px)"; min-height: 30px; line-height: 2.5; font-size: 12px; text-align: left; word-break: break-all; background-color: #fafafa; border-radius: 4px; &:before { content: " "; position: absolute; top: 9px; right: 100%; border: 6px solid transparent; border-right-color: #fafafa; } } .self { text-align: right; .avatar { float: right; margin: 0 0 0 10px; } .text { background-color: #b2e281; &:before { right: inherit; left: 100%; border-right-color: transparent; border-left-color: #b2e281; } } }}</style>
3、后端实现
增加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId></dependency>
// WebSocketController.java@Controller@ServerEndpoint(value = "/websocket/{user}")@Api(tags = "业务模块-websocket连贯接口")public class WebSocketController { // 这里应用动态,让 service 属于类 private static ChatMsgService chatMsgService; // 注入的时候,给类的 service 注入 @Resource public void setChatService(ChatMsgService chatService) { WebSocketController.chatMsgService = chatService; } // 动态变量,用来记录以后在线连接数。应该把它设计成线程平安的。 private static int onlineCount = 0; // concurrent包的线程平安Set,用来寄存每个客户端对应的MyWebSocket对象。若要实现服务端与繁多客户端通信的话,能够应用Map来寄存,其中Key能够为用户标识 private static ConcurrentHashMap<String, WebSocketController> webSocketSet = new ConcurrentHashMap<String, WebSocketController>(); // 与某个客户端的连贯会话,须要通过它来给客户端发送数据 private Session WebSocketSession; // 记录以后发消息的用户 private String user = ""; /** * 连贯建设胜利调用的办法 * * session 可选的参数。session为与某个客户端的连贯会话,须要通过它来给客户端发送数据 */ @OnOpen public void onOpen(@PathParam(value = "user") String param, Session WebSocketsession) { user = param; // System.out.println(user); this.WebSocketSession = WebSocketsession; webSocketSet.put(param, this);// 退出map中 addOnlineCount(); // 在线数加1 // System.out.println("有新连贯退出!以后在线人数为" + getOnlineCount()); } /** * 连贯敞开调用的办法 */ @OnClose public void onClose() { if (!user.equals("")) { webSocketSet.remove(user); // 从set中删除 subOnlineCount(); // 在线数减1 // System.out.println("有一连贯敞开!以后在线人数为" + getOnlineCount()); } } /** * 收到客户端音讯后调用的办法 * * @param chatmsg 客户端发送过去的音讯 * @param session 可选的参数 */ @OnMessage public void onMessage(String chatmsg, Session session) throws SystemException{ JSONObject jsonObject = JSONObject.parseObject(chatmsg); //给指定的人发消息 sendToUser(jsonObject.toJavaObject(ChatMsgVO.class)); //sendAll(message); } /** * 给指定的人发送音讯 * * @param chatMsg 音讯对象 */ public void sendToUser(ChatMsgVO chatMsg) throws SystemException{ String fromUser = chatMsg.getMFromUser(); String mMsg = chatMsg.getMMsg(); System.out.println(fromUser); mMsg= EmojiFilter.filterEmoji(mMsg);//过滤输入法输出的表情 chatMsgService.InsertChatMsg(chatMsg); try { if (webSocketSet.get(fromUser) != null) { webSocketSet.get(fromUser).sendMessage(chatMsg.getMFromUser()+"|"+mMsg); }else{ webSocketSet.get(chatMsg.getMFromUser()).sendMessage("0"+"|"+"以后用户不在线"); } } catch (IOException e) { throw new SystemException(SystemCodeEnum.PARAMETER_ERROR,e.getMessage()); } } /** * 给所有人发消息 * * @param message */ private void sendAll(String message) { String sendMessage = message.split("[|]")[1]; //遍历HashMap for (String key : webSocketSet.keySet()) { try { //判断接管用户是否是以后发消息的用户 if (!user.equals(key)) { webSocketSet.get(key).sendMessage(sendMessage); System.out.println("key = " + key); } } catch (IOException e) { e.printStackTrace(); } } } /** * 产生谬误时调用 * * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { error.printStackTrace(); } /** * 这个办法与下面几个办法不一样。没有用注解,是依据本人须要增加的办法。 * * @param message * @throws IOException */ public void sendMessage(String message) throws IOException { this.WebSocketSession.getBasicRemote().sendText(message); //this.session.getAsyncRemote().sendText(message); } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { WebSocketController.onlineCount++; } public static synchronized void subOnlineCount() { WebSocketController.onlineCount--; }}
//ChatController.java@RestController@RequestMapping("business/chat")public class ChatMsgController { @Resource private ApiUserService apiUserService; @Resource private ChatMsgService chatMsgService; @Resource private ChatFriendsService chatFriendsService; @ApiOperation(value = "获取好友聊天记录", notes = "依据以后用户查问好友聊天记录") @GetMapping("/getFriendMsg/{username}") public ResponseBean<List<ChatMsg>> getFriendMsg(@PathVariable String username) throws SystemException { UserInfoVO userInfoVO = apiUserService.info(); List<ChatMsg> chatMsgs = chatMsgService.getFriendMsg(userInfoVO.getUsername(), username); return ResponseBean.success(chatMsgs); } @ApiOperation(value = "获取用户好友", notes = "依据用户id查问用户好友") @GetMapping("/getfriends/{id}") public ResponseBean<List<ChatFriends>> getFriends(@PathVariable Long id) throws SystemException { List<ChatFriends> chatFriendsList = chatFriendsService.getFriends(id); return ResponseBean.success(chatFriendsList); } }