一 WebSocket理论
1.1 什么是http请求
http链接分为短链接、长链接,短链接是每次请求都要三次握手才能发送自己的信息。即每一个request对应一个response。长链接是在一定的期限内保持链接(但是是单向的,只能从客户端向服务端发消息,然后服务端才能响应数据给客户端,服务端不可以主动给客户端发消息)。保持TCP连接不断开。客户端与服务器通信,必须要有客户端发起然后服务器返回结果。客户端是主动的,服务器是被动的。
1.2 WebSocket
WebSocket他是为了解决客户端发起多个http请求到服务器资源浏览器必须要经过长时间的轮训问题而生的,他实现了多路复用,他是全双工通信。在webSocket协议下客服端和浏览器可以同时发送信息。
建立了WebSocket之后服务器不必在浏览器发送request请求之后才能发送信息到浏览器。这时的服务器已有主动权想什么时候发就可以随时发送信息到浏览器。而且信息当中不必在带有head的部分信息了,与http的长链接通信来对比,这种方式,不仅能降低服务器的压力。而且信息当中也减少了部分多余的信息。
1.3 WebSocket与http关系
相同点:
- 都是基于tcp的,都是可靠性传输协议
- 都是应用层协议
不同点:
- WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息
- HTTP是单向的
- WebSocket是需要浏览器和服务器握手进行建立连接的
- 而http是浏览器发起向服务器的连接,服务器预先并不知道这个连接
联系
WebSocket在建立握手时,数据是通过HTTP传输的。但是建立之后,在真正传输时候是不需要HTTP协议的
http存在的问题
- http是一种无状态协议,每当一次会话完成后,服务端都不知道下一次的客户端是谁,需要每次知道对方是谁,才进行相应的响应,因此本身对于实时通讯就是一种极大的障碍
- http协议采用一次请求,一次响应,每次请求和响应就携带有大量的header头,对于实时通讯来说,解析请求头也是需要一定的时间,因此,效率也更低下
- 最重要的是,需要客户端主动发,服务端被动发,也就是一次请求,一次响应,不能实现主动发送
二 代码实战
SpringBoot集成WebSocket实战
引入相关依赖
<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>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.71</version>
</dependency>
WebSocket配置类
/*** @Author:sgw* @Date:2023/7/13* @Description:WebSocket配置类*/
@Configuration
public class WebSocketConfig {/*** 注入ServerEndpointExporter,* 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint*/@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}
}
websocket处理消息核心代码
package com.ws.websocket.demos.service;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.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;/*** @Author:sgw* @Date:2023/7/13* @Description: 实现websocket处理消息*/
@Component
//userId是项目每次启动时,都要与服务端的websocket建立长连接的userid值,建立长连接后,服务端就可以随时给指定的用户推送消息了
@ServerEndpoint("/ws/asset/{userId}")
public class MyWebSocket {//与某个客户端的连接会话,需要通过它来给客户端发送数据private Session session;//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。//虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。// 注:底下WebSocket是当前类名private static CopyOnWriteArraySet<MyWebSocket> webSockets = new CopyOnWriteArraySet<>();// 用来存:在线连接数private static Map<String, Session> sessionPool = new HashMap<String, Session>();private static Logger log = LoggerFactory.getLogger(MyWebSocket.class);/*** 链接成功调用的方法*/@OnOpenpublic void onOpen(Session session, @PathParam(value = "userId") String userId) {try {this.session = session;webSockets.add(this);sessionPool.put(userId, session);log.info("sessionId值:" + session.getId());log.info("【websocket消息】有新的连接,总数为:" + webSockets.size());sendMessage(session, "连接成功");} catch (Exception e) {}}/*** 发送消息,实践表明,每次浏览器刷新,session会发生变化。** @param session* @param message*/public static void sendMessage(Session session, String message) {try {session.getBasicRemote().sendText(String.format("%s (From Server,Session ID=%s)", message, session.getId()));} catch (IOException e) {log.error("发送消息出错:{}", e.getMessage());e.printStackTrace();}}/*** 链接关闭调用的方法*/@OnClosepublic void onClose() {try {webSockets.remove(this);log.info("【websocket消息】连接断开,总数为:" + webSockets.size());} catch (Exception e) {}}/*** 收到客户端消息后调用的方法** @param message*/@OnMessagepublic void onMessage(String message) {log.info("【websocket消息】收到客户端消息:" + message);}/*** 发送错误时的处理** @param session* @param error*/@OnErrorpublic void onError(Session session, Throwable error) {log.info("用户错误,原因:" + error.getMessage());error.printStackTrace();}// 此为广播消息public void sendAllMessage(String message) {log.info("【websocket消息】广播消息:" + message);for (MyWebSocket webSocket : webSockets) {try {if (webSocket.session.isOpen()) {webSocket.session.getAsyncRemote().sendText(message);}} catch (Exception e) {e.printStackTrace();}}}// 此为单点消息public void sendOneMessage(String userId, String message) {Session session = sessionPool.get(userId);if (session != null && session.isOpen()) {try {log.info("【websocket消息】 单点消息:" + message);//session.getAsyncRemote().sendText(message);session.getAsyncRemote().sendText(String.format("%s (From Server,Session ID=%s)", message, session.getId()));} catch (Exception e) {e.printStackTrace();}}else {log.info("没找到目前已经建立连接的用户,即要推送给的用户目前没登录");}}// 此为单点消息(多人)public void sendMoreMessage(String[] userIds, String message) {for (String userId : userIds) {Session session = sessionPool.get(userId);if (session != null && session.isOpen()) {try {log.info("【websocket消息】 单点消息:" + message);session.getAsyncRemote().sendText(message);} catch (Exception e) {e.printStackTrace();}}}}
}
Controller入口类
package com.ws.websocket.demos.web;import com.alibaba.fastjson.JSONObject;
import com.ws.websocket.demos.service.MyWebSocket;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;/*** @Author:sgw* @Date:2023/7/13* @Description: websocket入口类*/@RequestMapping("/api/ws")
@RestController
public class BasicController {@Resourceprivate MyWebSocket myWebSocket;/*** 发送给单个用户websocket消息** @param userId 用户的id* @param message 消息体* @return*/@RequestMapping("/wsTest")public String websocket(String userId, String message) {JSONObject obj = new JSONObject();obj.put("cmd", "topic");//业务类型obj.put("msgId", "987654321");//消息idobj.put("msgTxt", message);//消息内容//单个用户发送 (userId为用户id)myWebSocket.sendOneMessage(userId, obj.toJSONString());return "index.html";}/*** 发送给所有用户websocket消息** @param message 消息体* @return*/@RequestMapping("/wsTest2")public String websocket2(String message) {JSONObject obj = new JSONObject();obj.put("cmd", "topic");//业务类型obj.put("msgId", "987654321");//消息idobj.put("msgTxt", message);//消息内容//全体发送myWebSocket.sendAllMessage(obj.toJSONString());return "index.html";}/*** 发送给多个指定用户websocket消息** @param message 消息体* @return*/@RequestMapping("/wsTest3")public String websocket3(String message) {JSONObject obj = new JSONObject();obj.put("cmd", "topic");//业务类型obj.put("msgId", "987654321");//消息idobj.put("msgTxt", message);//消息内容//给多个用户发送 (userIds为多个用户id,逗号‘,’分隔)String[] userIds = {"euoiqhfljsak", "jowiqnfdnas"};myWebSocket.sendMoreMessage(userIds, obj.toJSONString());return "index.html";}
}
前端代码
这里的前端代码以最简单的html来进行编写
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>websocket测试</title><style type="text/css">h3, h4 {text-align: center;}</style>
</head>
<body><h3>WebSocket测试,在<span style="color:red">控制台</span>查看测试信息输出!</h3>
<h4>[url=/api/ws/wsTest?message=单发消息内容&userId=none]单发消息链接[/url]<br>[url=/api/ws/wsTest1?message=群发给所有用户消息内容]群发消息链接[/url]<br>[url=/api/ws/wsTest2?message=指定多个用户发消息内容]群发消息链接[/url]
</h4><script type="text/javascript">var socket;if (typeof (WebSocket) == "undefined") {console.log("遗憾:您的浏览器不支持WebSocket");} else {console.log("恭喜:您的浏览器支持WebSocket");//实际业务中,这里需要从系统里获取当前当前登录的用户idvar userId = "54321";/*实现化WebSocket对象指定要连接的服务器地址与端口建立连接注意ws、wss使用不同的端口。我使用自签名的证书测试,无法使用wss,浏览器打开WebSocket时报错ws对应http、wss对应https。*/socket = new WebSocket("ws://localhost:8080/ws/asset/" + userId);//连接打开事件socket.onopen = function () {console.log("Socket 已打开");socket.send("消息发送测试");};//收到消息事件socket.onmessage = function (msg) {console.log(msg.data);};//连接关闭事件socket.onclose = function () {console.log("Socket已关闭");};//发生了错误事件socket.onerror = function () {alert("Socket发生了错误");}//窗口关闭时,关闭连接window.unload = function () {socket.close();};}
</script></body>
</html>
这里模拟登录用户的id是54321(实际业务中,这个id是要从系统里进行获取的),项目启动时,就会加载这段代码,把54321这个用户的前端与后端的websocket服务端进行长连接
三 测试
访问前端首页:http://localhost:8080/,打开f12控制台,可以看到,连接成功
使用postman调接口,给用户id是54321的用户发送消息,看看浏览器这里能不能接收到websocket消息
发现浏览器接收到消息了,如下
如果使用postman,发给其他用户(还没登录的用户),浏览器接收不到消息
浏览器没有新消息:
后端日志:
查看websocket发送与接收的消息