WebSockeret快速入门
1、概念
WebSocket是一种通信协议,可在单个TCP连接上进行全双工通信。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。
2、特点
- 最大的特点就是服务器可以主动地向客户端发送信息,客户端也还是可以主动向服务端发信息,是真正的平等对话,它属于服务器的一种推送技术
- 建立在 TCP 协议之上,服务器端的实现比较容易
- 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器
- 握手阶段采用 HTTP 协议
- 数据格式轻量,性能开销小。客户端与服务端进行数据交换时,服务端到客户端的数据包头只有2到10字节,客户端到服务端需要加上另外4字节的掩码。HTTP每次都需要携带完整头部
- 更好的二进制支持,可以发送文本,和二进制数据
- 没有同源限制(不存在跨域),客户端可以与任意服务器通信
- 协议标识符是ws(如果加密,则是wss),请求的地址就是后端支持websocket的API
3、WebSocket连接过程
客户端发起HTTP握手,告诉服务端进行WebSocket协议通讯,并告知WebSocket协议版本。服务端确认协议版本,升级为WebSocket协议。之后如果有数据需要推送,会主动推送给客户端
//以下为请求头信息
Accept-Encoding: gzip, deflate, br
Accept-Language: zh, zh-TW; q=0.9, en-US; q=0.8, en; q=0.7,zh-CN; q=0.6
Cache-Control: no-cache
Connection: Upgrade
Host: 127.0.0.1:3000
Origin: http://localhost:3000
Pragma: no-cache
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: bwb9SFiJONXhQ/A4pLaXIg==
Sec-WebSocket-Version: 13
Upgrade: websocket
解析
- Connection: Upgrade 表示我要升级协议
- Upgrade: websocket 要升级协议到websocket协议
- Sec-WebSocket-Version 表示websocket的版本。如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号
- Sec-WebSocket-Key 对应服务端响应头的Sec-WebSocket-Accept,由于没有同源限制,websocket客户端可任意连接支持websocket的服务。这个就相当于一个钥匙一把锁,避免多余的,无意义的连接。
//以下是服务器的响应头
Connection: Upgrade
Sec-WebSocket-Accept: 2jrbCWSCPlzPtxarlGTp4Y8XD20=
Upgrade: websocket
解析
Sec-WebSocket-Accept: 用来告知服务器愿意发起一个websocket连接, 值根据客户端请求头的Sec-WebSocket-Key计算出来
4、Springboot + WebSocket
-
新建WebSocketTest项目
-
导入依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-core</artifactId> </dependency> </dependencies>
-
application.yml
server: port: 19266 spring: application: name: WebSocketTest
-
主启动类
@SpringBootApplication public class WebSocketMain { public static void main(String[] args) { SpringApplication.run(WebSocketMain.class, args); } }
-
WebSocketConfig.java
@Configuration public class WebSocketConfig { /** * 注入ServerEndpointExporter, * 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint */ @Bean public ServerEndpointExporter serverEndpointExporter(){ return new ServerEndpointExporter(); } }
-
WebSocket.java
@Component @ServerEndpoint(value = "/websocket/{username}") @Slf4j public class WebSocket { // 记录用户名 private String username; // 记录Session private Session session; // concurrent包的线程安全Set,用来存放每个客户端对应的WebSocket对象。 private static final ConcurrentHashMap<String, WebSocket> webSocketMap = new ConcurrentHashMap<>(); /** * 连接成功调用的方法 */ @OnOpen public void onOpen(Session session, @PathParam("username") String username) { if (webSocketMap.get(username) == null) { this.username = username; this.session = session; sendOneMessage(session, "登录成功!!"); sendAllMessage("【websocket消息】用户(" + username + ")上线!"); webSocketMap.put(username, this); log.info("【websocket消息】用户({})上线!,session={}", username, session); } else { try { sendOneMessage(session, "该用户名已被占用!!"); session.close(); log.info("【websocket消息】用户({})已被登录!,session={}", username, session); } catch (IOException e) { log.error(e.getMessage()); } } } /** * 链接关闭调用的方法 */ @OnClose public void onClone() { // 删除当前WebSocket if (this.username != null) { webSocketMap.remove(this.username); sendAllMessage("【websocket消息】用户(" + username + ")下线!"); log.info("【websocket消息】用户({})下线!", this.username); } } /** * 收到客户端消息后调用的方法 */ @OnMessage public void onMessage(String message) { sendAllMessage(this.username + "说:" + message); } /** * 发送错误时的处理 */ @OnError public void onError(Session session, Throwable error) { log.error("【websocket消息】发生错误!,错误信息:{}", error.getMessage()); } /** * 广播消息 * @param message 消息内容 */ public void sendAllMessage(String message) { for (String username : webSocketMap.keySet()) { if (webSocketMap.get(username).session.isOpen()) { webSocketMap.get(username).session.getAsyncRemote().sendText(message); } } } /** * 此为单点消息(多人) * @param formUsernames 收消息的用户名数组 * @param message 消息内容 */ public void sendMoreMessage(String[] formUsernames, String message) { String[] distinctUsernames = ArrayUtil.distinct(formUsernames); for (String username : distinctUsernames) { webSocketMap.get(username).session.getAsyncRemote().sendText(message); } } /** * 此为单点消息 * @param session 收消息的用户session * @param message 消息内容 */ public void sendOneMessage(Session session, String message) { session.getAsyncRemote().sendText(message); } }
-
前端页面代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>群聊WebSocket测试</title> </head> <body> <table> <tr> <th id="username-prompt">请输入用户名:</th> <td><label for="username"></label><input onkeydown="if(event.keyCode===13) {wsConnection()}" type="text" id="username"> </td> <td><button onclick="wsConnection()" id="login">登录</button></td> </tr> <tr> <th>请输入发送的消息:</th> <td><label for="text"></label><input onkeydown="if(event.keyCode===13) {wsSendMessage()}" type="text" id="text" disabled /></td> <td><button id="send" onclick="wsSendMessage()" disabled>发送</button></td> </tr> </table> <div id="message"></div> <script type="application/javascript"> let usernamePrompt = document.getElementById("username-prompt") let username = document.getElementById("username") let login = document.getElementById("login") let text = document.getElementById("text") let send = document.getElementById("send"); let message = document.getElementById("message") let webSocket; function wsConnection() { webSocket = new WebSocket("ws://localhost:19266/websocket/" + username.value); webSocket.onopen = function () { username.disabled = true login.disabled = true usernamePrompt.textContent = "用户名:" text.disabled = false send.disabled = false } webSocket.onmessage = (ent) => { message.insertAdjacentHTML( 'afterend', "<p><strong>" + ent.data.split(":")[0] + (ent.data.split(":")[1] !== undefined ? ":</strong>" + ent.data.split(":")[1] : "</strong>") + "</p>" ) } webSocket.onerror = (ev) => { alert("连接失败!") console.log(ev) } webSocket.onclose = () => { text.disabled = true send.disabled = true username.value = "" text.value = "" usernamePrompt.textContent = "请输入用户名:" username.disabled = false login.disabled = false message.insertAdjacentHTML("afterbegin","<p><strong>已掉线,请重新登录!!</strong></p>") } } function wsSendMessage() { webSocket.send(text.value) } </script> </body> </html>