SpringBoot整合WebSocket
WebSocket简介
WebSockets是一种在Web浏览器和服务器之间建立持久连接的技术标准。与HTTP请求不同,WebSockets允许双向通信,使得服务器能够主动向客户端推送数据,而不需要客户端发起请求。这种双向通信的特性使得WebSockets非常适合需要实时数据更新的应用,比如在线游戏、即时聊天和股票报价等。
双向通信:通过WebSockets,服务器和客户端可以同时发送和接收数据,这种实时性非常适合需要及时更新的应用场景。
持久连接:WebSockets建立一次连接后会一直保持开放状态,而不像HTTP需要在每次通信时重新建立连接。这降低了连接开销并提高了效率。
协议和API:WebSockets建立在TCP
协议之上,通过使用WebSocket API在浏览器和服务器之间建立连接。
安全性:WebSockets通过类似HTTPS的安全套接层(SSL/TLS)来保护数据传输的安全性,确保数据不被窃听或篡改。
应用场景:WebSockets被广泛应用在需要实时通信和及时更新的应用中,比如在线游戏、即时通讯、协作工具、实时数据可视化以及股票市场等。
HTTP和WebSockets都是基于TCP协议的应用层协议。
WebSocket协议
本协议有两部分:握手和数据传输。
WebSocket握手阶段就是利用HTTP来协商和确认建立WebSocket连接。一旦握手成功,HTTP协议就不再起作用,连接就会升级为WebSocket协议,之后的通信将遵循WebSocket的规范,允许双向实时通信。
客户端发送WebSocket握手请求:客户端会发送一个HTTP请求到服务器,示意要升级连接为WebSocket协议。这个请求会包含一些特殊的头部字段,比如Upgrade
、Connection
和Sec-WebSocket-Key
等。
GET ws://localhost:8080/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key:dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Protocol: chat,superchat
Sec-WebSocket-Version: 13
服务器响应WebSocket握手:如果服务器支持WebSocket协议,并且检查请求头部的一致性后,会返回一个HTTP响应,表明允许升级为WebSocket连接。响应头中包含Upgrade
和Connection
字段,还有Sec-WebSocket-Accept
字段,这是根据客户端发送的Sec-WebSocket-Key
生成的。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept:s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
握手成功:一旦客户端收到这个响应,握手成功。此时连接就升级为WebSocket协议,后续的通信将遵循WebSocket协议规范,可以发送和接收WebSocket数据帧。
SpringBoot整合WebSocket
引入maven依赖
1 2 3 4 5
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
|
WebSocket配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Configuration public class WebSocketConfig {
@Bean public ServerEndpointExporter serverEndpointExporter(){ return new ServerEndpointExporter(); } }
|
WebSocket操作类
通过该类WebSocket可以进行群推送以及单点推送,类似controller。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
| import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.util.Map; import java.util.concurrent.ConcurrentHashMap;
@Component @ServerEndpoint(value = "/imserver/{username}") public class WebSocketServer {
private static final Logger log = LoggerFactory.getLogger(WebSocketServer.class);
public static final Map<String, Session> sessionMap = new ConcurrentHashMap<>();
@OnOpen public void onOpen(Session session, @PathParam("username") String username) { sessionMap.put(username, session); log.info("有新用户加入,username={}, 当前在线人数为:{}", username, sessionMap.size()); JSONObject result = new JSONObject(); JSONArray array = new JSONArray(); result.put("users", array); for (Object key : sessionMap.keySet()) { JSONObject jsonObject = new JSONObject(); jsonObject.put("username", key); array.add(jsonObject); }
sendAllMessage(result.toString()); }
@OnClose public void onClose(Session session, @PathParam("username") String username) { sessionMap.remove(username); log.info("有一连接关闭,移除username={}的用户session, 当前在线人数为:{}", username, sessionMap.size()); }
@OnMessage public void onMessage(String message, Session session, @PathParam("username") String username) { log.info("服务端收到用户username={}的消息:{}", username, message); JSONObject obj = JSONObject.parseObject(message); String toUsername = obj.getString("to"); String text = obj.getString("text"); Session toSession = sessionMap.get(toUsername); if (toSession != null) { JSONObject jsonObject = new JSONObject(); jsonObject.put("from", username); jsonObject.put("text", text); this.sendMessage(jsonObject.toString(), toSession); log.info("发送给用户username={},消息:{}", toUsername, jsonObject.toString()); } else { log.info("发送失败,未找到用户username={}的session", toUsername); } }
@OnError public void onError(Session session, Throwable error) { log.error("发生错误"); error.printStackTrace(); }
private void sendMessage(String message, Session toSession) { try { log.info("服务端给客户端[{}]发送消息{}", toSession.getId(), message); toSession.getBasicRemote().sendText(message); } catch (Exception e) { log.error("服务端发送消息给客户端失败", e); } }
private void sendAllMessage(String message) { try { for (Session session : sessionMap.values()) { log.info("服务端给客户端[{}]发送消息{}", session.getId(), message); session.getBasicRemote().sendText(message); } } catch (Exception e) { log.error("服务端发送消息给客户端失败", e); } }
}
|
注意:如果后端服务配置了拦截器,需要放行/imserver/**
Vue前端简易实现代码
前端WebSocket使用:
- 创建一个WebSocket对象:
var Socket = new WebSocket(url)
- WebSocket方法:
- Socket.send():向服务器发送数据
- Socket.close():关闭连接
- WebSocket事件:
- Socket.onopen:连接建立时触发
- Socke.onmessage:客户端接受服务端数据时触发
- Socket.onclose:连接关闭时触发
- Socket.onerror:通信错误时触发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
| <template> <div style="padding: 10px; margin-bottom: 50px"> <el-row> <el-col :span="4"> <el-card style="width: 300px; height: 300px; color: #333"> <div style="pading-bottom: 10px; border-bottom: 1px solid #ccc">在线用户<span style = "font-size: 12px">(点击天气泡开始聊天)</span></div> <div style="padding: 10px 0" v-for="user in users" :key="user.username"> <span>{{ user.username }}</span> <i class="el-icon-chat-dot-round" style="margin-left: 10px; font-size: 16px; cursor: pointer" @click="chatUser ? chatUser = '' : chatUser = user.username"></i> <span style="font-size: 12px; color: limegreen; margin-left: 5px;" v-if="user.username === chatUser">chatting...</span> </div> </el-card> </el-col>
<el-col :span="20"> <div style="width: 800px; margin: 0 auto; background-color: white; border-radius: 5px; box-shadow: 0 0 10px #ccc;"> <div style="text-align: center; line-height: 50px;"> Web聊天室({{ chatUser }}) </div> <div style="height: 350px; overflow: auto; border-top: 1px solid #ccc;" v-html="content"></div> <div style="height: 200px;"> <textarea v-model="text" style="height: 160px; width: 100%; padding: 20px; border: none; border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; outline: none;" @keydown.enter="send"></textarea> <div style="text-align: right; padding-right: 10px;"> <el-button type="primary" size="mini" @click="send">发送</el-button> </div> </div> </div> </el-col> </el-row> </div> </template>
<script>
let socket;
export default { name: "Chart", data() { return { circleUrl: this.$store.state.user.avatar, user: {}, isCollapse: false, users: [], chatUser: '', text: '', messages: [], content: '', url: 'ws://localhost:8080/imserver/' } }, created() { this.init(); }, methods: { send() { if (!this.chatUser) { this.$message({type: 'warning', message: "请选择聊天对象"}) return; } if (!this.text) { this.$message({type: 'warning', message: "请输入内容"}) } else { if (typeof (WebSocket) == 'undefined') { console.log("浏览器不支持 WebSocket") } else { console.log("浏览器支持 WebSocket") let message = {from: this.user, to: this.chatUser, text: this.text} socket.send(JSON.stringify(message)); this.messages.push({user: this.user, text: this.text}) this.createContent(null, this.user, this.text) this.text = ''; } } }, createContent(remoteUser, nowUser, text) { console.log("用户头像:", this.$store.state.user.avatar) let html if (nowUser) { html = "<div class=\"el-row\" style=\"padding: 5px 0\">\n" + " <div class=\"el-col el-col-22\" style = \"text-align: right; padding-right: 10px\">\n" + " <div class=\"tip left\">" + text + "</div>\n" + " </div>\n" + " <div class=\"el-col el-col-2\">\n" + " <span class=\"el-avatar el-avatar--circle\" style=\"height: 40px; width: 40px; line-height: 40px;\">\n" + " <img src=\"" + this.circleUrl + "\"style=\"object-fit: cover;\">\n" + " </span>\n" + " </div>\n" + "</div>"; } else if (remoteUser) { html = "<div class=\"el-row\" style=\"padding: 5px 0\">\n" + " <div class=\"el-col el-col-2\" style = \"text-align: right;\">\n" + " <span class=\"el-avatar el-avatar--circle\" style=\"height: 40px; width: 40px; line-height: 40px;\">\n" + " <img src=\"" + this.circleUrl + "\"style=\"object-fit: cover;\">\n" + " </span>\n" + " </div>\n" + " <div class=\"el-col el-col-22\" style=\"text-align: left; padding-left: 10px\">\n" + " <div class=\"tip right\">" + text + "</div>\n" + " </div>\n" + "</div>"; } console.log("消息:", this.messages) this.content += html; }, init() { console.log("用户名:", this.$store.state.user.name) this.user = this.$store.state.user.name let _this = this; if (typeof (WebSocket) == "undefined") { console.log("您的浏览器不支持WebSocket"); } else { console.log("您的浏览器支持WebSocket"); let socketUrl = this.url + this.user; if (socket != null) { socket.close(); socket = null; } socket = new WebSocket(socketUrl); socket.onopen = function () { console.log("websocket已打开"); } socket.onmessage = function (msg) { console.log("数据:", msg.data) let data = JSON.parse(msg.data) if (data.users) { _this.users = data.users.filter(user => user.username !== _this.user) } else { if (data.from === _this.chatUser) { _this.messages.push(data) _this.createContent(data.from, null, data.text)
} } } socket.onclose = function () { console.log("websocket已关闭"); } socket.onerror = function () { console.log("websocket发生了错误") } } } } }; </script>
// 聊天气泡样式 <style> .tip { color: white; text-align: center; border-radius: 10px; font-family: sans-serif; padding: 10px; width:auto; display:inline-block !important; display:inline; }
.right { background-color: deepskyblue; }
.left { background-color: forestgreen; }
</style>
|