序言 What is Webscoket ? websocket 應用場景 簡版群聊實現 代碼例子 小結 Webscoket Websokcet 是一種單個 "TCP" 連接上進行 "全雙工" 通信的協議,通過 "HTTP" /1.1 協議的101狀態碼進行握手。 http://websocket. ...
序言
-
What is Webscoket ?
-
websocket 應用場景
-
簡版群聊實現
-
代碼例子
-
小結
Webscoket
Websokcet 是一種單個TCP連接上進行全雙工通信的協議,通過HTTP/1.1 協議的101狀態碼進行握手。
Websocket 應用場景
Websocket 和 http 協議都是web通訊協議,兩者有何區別?先說Http,它是一種請求響應協議,這種模型決定了,只能客戶端請求,服務端被動回答。如果我們有服務端主動推送給客戶端的需求怎麼辦?比如一個股票網站,我們會選擇主動輪詢,也就是”拉模式“。
大家可以思考下主動輪詢帶來的問題是什麼?
主動輪詢其實會產生大量無效請求,增加了伺服器壓力。
由此,websocket 協議的補充,為我們帶來了新的解決思路。
簡版群聊實現
利用Websocket 實現一個簡陋群聊功能,加深一下Websocket 理解。
- 假設李雷和韓梅梅都登錄線上;
- 李雷通過瀏覽器發送消息轉nginx 代理到Ws伺服器;
- Ws伺服器載入所有線上會話廣播消息;
- 韓梅梅接受到消息。
代碼例子
後端(shop-server)
-
引入pom.xml 依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
-
配置類
package com.onlythinking.shop.websocket; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * <p> The describe </p> * * @author Li Xingping */ @Slf4j @Configuration public class WebSocketConfiguration { @Bean public ServerEndpointExporter endpointExporter() { return new ServerEndpointExporter(); } }
-
接受請求端點
package com.onlythinking.shop.websocket; import com.alibaba.fastjson.JSON; import com.google.common.collect.Maps; import com.onlythinking.shop.websocket.handler.ChatWsHandler; import com.onlythinking.shop.websocket.handler.KfWsHandler; import com.onlythinking.shop.websocket.handler.WsHandler; import com.onlythinking.shop.websocket.store.WsReqPayLoad; import com.onlythinking.shop.websocket.store.WsRespPayLoad; import com.onlythinking.shop.websocket.store.WsStore; import com.onlythinking.shop.websocket.store.WsUser; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.Map; /** * <p> The describe </p> * * @author Li Xingping */ @Slf4j @Component @ServerEndpoint("/ws") public class WebsocketServerEndpoint { private static Map<String, WsHandler> wsHandler = Maps.newConcurrentMap(); static { wsHandler.put("robot", new KfWsHandler()); wsHandler.put("chat", new ChatWsHandler()); } @OnOpen public void onOpen(Session session) { log.info("New ws connection {} ", session.getId()); WsStore.put(session.getId(), WsUser.builder().id(session.getId()).session(session).build()); respMsg(session, WsRespPayLoad.ok().toJson()); } @OnClose public void onClose(Session session, CloseReason closeReason) { WsStore.remove(session.getId()); log.warn("ws closed,reason:{}", closeReason); } @OnMessage public void onMessage(String message, Session session) { log.info("accept client messages: {}" + message); WsReqPayLoad payLoad = JSON.parseObject(message, WsReqPayLoad.class); if (StringUtils.isBlank(payLoad.getType())) { respMsg(session, WsRespPayLoad.ofError("Type is null.").toJson()); return; } WsUser wsUser = WsStore.get(session.getId()); if (null == wsUser || StringUtils.isBlank(wsUser.getUsername())) { WsStore.put(session.getId(), WsUser.builder() .id(session.getId()) .username(payLoad.getUsername()) .avatar(payLoad.getAvatar()) .session(session) .build() ); } WsHandler handler = wsHandler.get(payLoad.getType()); if (null != handler) { WsRespPayLoad resp = handler.onMessage(session, payLoad); if (null != resp) { respMsg(session, resp.toJson()); } } else { respMsg(session, WsRespPayLoad.ok().toJson()); } } @OnError public void onError(Session session, Throwable e) { WsStore.remove(session.getId()); log.error("WS Error: ", e); } private void respMsg(Session session, String content) { try { session.getBasicRemote().sendText(content); } catch (IOException e) { log.error("Ws resp msg error {} {}", content, e); } } }
-
聊天業務處理器
package com.onlythinking.shop.websocket.handler; import com.onlythinking.shop.websocket.store.*; import lombok.extern.slf4j.Slf4j; import javax.websocket.Session; import java.util.Date; import java.util.List; /** * <p> The describe </p> * * @author Li Xingping */ @Slf4j public class ChatWsHandler implements WsHandler { @Override public WsRespPayLoad onMessage(Session session, WsReqPayLoad payLoad) { // 廣播消息 List<WsUser> allSessions = WsStore.getAll(); for (WsUser s : allSessions) { WsRespPayLoad resp = WsRespPayLoad.builder() .data( WsChatResp.builder() .username(payLoad.getUsername()) .avatar(payLoad.getAvatar()) .msg(payLoad.getData()) .createdTime(new Date()) .self(s.getId().equals(session.getId())) .build() ) .build(); log.info("Broadcast message {} {} ", s.getId(), s.getUsername()); s.getSession().getAsyncRemote().sendText(resp.toJson()); } return null; } }
前端(shop-web-mgt)
-
引入依賴
npm install vue-native-websocket --save
-
添加Store
import Vue from 'vue' const ws = { state: { wsData: { hasNewMsg: false, }, socket: { isConnected: false, message: '', reconnectError: false, } }, mutations: { SET_WSDATA(state, data) { state.wsData.hasNewMsg = data.hasNewMsg }, RESET_WSDATA(state, data) { state.wsData.hasNewMsg = false }, SOCKET_ONOPEN(state, event) { Vue.prototype.$socket = event.currentTarget; state.socket.isConnected = true }, SOCKET_ONCLOSE(state, event) { state.socket.isConnected = false }, SOCKET_ONERROR(state, event) { console.error(state, event) }, // default handler called for all methods SOCKET_ONMESSAGE(state, message) { state.socket.message = message }, // mutations for reconnect methods SOCKET_RECONNECT(state, count) { console.info(state, count) }, SOCKET_RECONNECT_ERROR(state) { state.socket.reconnectError = true; }, }, actions: { AskRobot({rootGetters}, data) { return new Promise((resolve, reject) => { console.log('Ask robot msg', data); const payLoad = { type: 'robot', username: rootGetters.loginName, data: data }; Vue.prototype.$socket.sendObj(payLoad) resolve(1) }) }, SendChatMsg({rootGetters}, data) { return new Promise((resolve, reject) => { console.log('Send chat msg', data); const payLoad = { type: 'chat', username: rootGetters.loginName, data: data }; Vue.prototype.$socket.sendObj(payLoad) resolve(1) }) }, MessageRead({commit, state}, data) { commit('RESET_WSDATA', {}) }, } }; export default ws
-
編寫組件
<template> <div> <ot-drawer title="聊天" :visible.sync="chatVisible" direction="rtl" :before-close="handleClose"> <div class="chat-body"> <div id="msgList" style="margin-bottom: 200px" class="chat-msg"> <div class="chat-msg-item" v-for="item in msgList"> <div v-if="!item.self"> <div class="msg-header"> <img :src="baseUrl+'/api/insecure/avatar?code='+item.avatar+'&size=64'" class="user-avatar" > <span class="avatar-name">{{item.username}}</span> <div style="display: inline-block; float: right"> {{item.createdTime | parseTime('{h}:{i}')}} </div> </div> <div class="msg-body" style="float: left;"> {{item.msg}} </div> </div> <div v-else> <div class="msg-header clearfix"> <img :src="baseUrl+'/api/insecure/avatar?code='+item.avatar+'&size=64'" class="user-avatar" style="float: right" > </div> <div class="msg-body" style="float: right;background-color: #67C23A"> {{item.msg}} </div> </div> </div> </div> </div> <div class="chat-send"> <el-input v-model="text" autocomplete="off" placeholder="請輸入你想說的內容..." @keyup.enter.native="handleSendMsg" ></el-input> <div class="chat-btns"> <el-button class="action-item" @click="handleClearMsg" >清空 </el-button> <el-button type="success" class="action-item" @click="handleSendMsg" v-scroll-to="{ el: '#msgList', offset: 140 }" >發送 </el-button> </div> </div> </ot-drawer> </div> </template> <script> import {mapGetters} from 'vuex' import store from '@/store' import {config} from '@/utils/config' import OtDrawer from '@/components/OtDrawer' import Cookies from 'js-cookie' export default { name: 'UserChat', components: {OtDrawer}, props: { visible: { type: Boolean, default: false } }, data() { return { baseUrl: config.baseUrl, text: '', msgList: [], } }, computed: { ...mapGetters([ 'roles', 'isConnected', 'message', 'reconnectError' ]), chatVisible: { get() { return this.visible }, set(val) { this.$emit('update:visible', val) } } }, beforeDestroy() { if (this.isConnected) { this.$disconnect() } }, mounted() { console.log('Chat mounted.') if (!this.isConnected) { this.$connect(config.wsUrl, { format: 'json', store: store }) } // 監聽消息接收 this.$options.sockets.onmessage = (res) => { const data = JSON.parse(res.data); console.log('收到消息', data); if (data.code === 0) { // 連接建立成功 if (!data.data.msg) { return; } this.msgList.push(data.data) } else if (data.code === 400) { this.$message({ type: 'warning', message: data.data }) } }; }, methods: { handleSendMsg() { if (!this.text) { this.$message({ type: 'warning', message: '請輸入內容' }); return; } this.$store.dispatch('SendChatMsg', this.text).then(data => { this.text = '' }) }, handleClearMsg() { this.msgList = []; Cookies.remove('chatMsg'); // 刪除 }, // 聊天關閉前 handleClose() { // 緩存消息到本地 Cookies.set('chatMsg', JSON.stringify(this.msgList)); this.$emit('update:visible', false) } }, created() { // 載入緩存數據 const chatMsg = Cookies.get('chatMsg'); if (chatMsg) { this.msgList = JSON.parse(chatMsg); } } } </script> <style> .el-drawer__body { height: 100%; box-sizing: border-box; overflow-y: auto; background-color: rgba(244, 244, 244, 1); scroll-snap-type: y proximity; } </style> <style rel="stylesheet/scss" lang="scss" scoped> .user-avatar { width: 20px; height: 20px; border-radius: 4px; vertical-align: middle; } .msg-header { font-size: 12px; color: rgba(109, 114, 120, 1); } .avatar-name { vertical-align: middle; } .msg-body { text-align: center; max-width: 300px; min-width: 100px; word-wrap: break-word; margin: 4px 0; padding: 4px; line-height: 24px; border-radius: 4px; background-color: rgba(255, 255, 255, 1); } .chat-body { height: 100%; position: relative; } .chat-msg { padding: 10px; .chat-msg-item { margin-top: 10px; height: 65px; } } .chat-send { padding: 20px; background-color: rgba(255, 255, 255, 1); position: absolute; left: 50%; width: 100%; transform: translateX(-50%); bottom: 0px; } .chat-btns { text-align: center; } .action-item { margin-top: 10px; } </style>
Nginx 代理配置 nginx.conf (如有需要可添加)
map $http_upgrade $connection_upgrade { default upgrade; '' close; } upstream websocket { server 127.0.0.1:8300; } server { server_name shop-web-mgt.onlythinking.com; listen 443 ssl; location / { proxy_pass http://websocket; proxy_read_timeout 300s; proxy_send_timeout 300s; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; } ssl_certificate /etc/data/shop-web-mgt.onlythinking.com/full.pem; ssl_certificate_key /etc/data/shop-web-mgt.onlythinking.com/privkey.pem; }
實現效果圖
界面比較醜,因為不太擅長,請大家別見笑!!
項目地址
項目演示地址
小結
該篇學習Websocket,寫此Demo加深印象!