Springboot整合websocket
一、短链接与长连接
1、短连接
客户端和服务器每进行一次通讯,就建立一次连接,通讯结束就中断连接。
HTTP是一个简单的请求-响应协议,它通常运行在TCP之上。HTTP/1.0使用的TCP默认是短连接。
2、长连接
是指在建立连接后可以连续多次发送数据,直到双方断开连接。
HTTP从1.1版本起,底层的TCP使用的长连接。
3、短链接与长连接的区别
1)、通讯流程
短连接:创建连接 -> 传输数据 -> 关闭连接
长连接:创建连接 -> 传输数据 -> 保持连接 -> 传输数据 -> …… -> 关闭连接
2)、适用场景
短连接:并发量大,数据交互不频繁情况。
长连接:数据交互频繁,点对点的通讯。
3)、通讯方式
| 说明 | |
|---|---|
| 短连接 | 我跟你发信息,必须等到你回复我或者等了一会等不下去了,就结束通讯了 |
| 长连接 |
二、websocket协议(全双工,即允许服务器向客户端发送数据)
1、
(1)、WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
单工就就像是汽车的单行道,是在只允许甲方向乙方传送信息,而乙方不能向甲方传送 。
单工:信息只能单向传送。
http1.0:单工。因为是短连接,客户端发起请求之后,服务端处理完请求并收到客户端的响应后即断开连接。
半双工:信息能双向传送但不能同时双向传送。
http1.1:半双工。默认开启长连接keep-alive,开启一个连接可发送多个请求。
全双工:信息能够同时双向传送。
http2.0:全双工,允许服务端主动向客户端发送数据。
(2)、
(4)、服务器向客户端发送数据的功能是websocket协议的典型使用场景。
2、
以下 API 用于创建 WebSocket 对象。
var Socket = new WebSocket(url, [protocol] );
WebSocket 事件
以下是 WebSocket 对象的相关事件。假定我们使用了以上代码创建了 Socket 对象:
| 事件处理程序 | 描述 | |
|---|---|---|
| open | Socket.onopen | 连接建立时触发 |
| message | Socket.onmessage | 客户端接收服务端数据时触发 |
| error | Socket.onerror | 通信发生错误时触发 |
| close | Socket.onclose |
WebSocket 方法
| 描述 | |
|---|---|
| Socket.send() | 使用连接发送数据 |
| Socket.close() |
三、创建一个springboot工程
1、添加依赖
org.springframework.boot spring-boot-starter-parent 2.1.5.RELEASE org.springframework.boot spring-boot-starter-websocket org.springframework.boot spring-boot-starter-web org.projectlombok lombok org.apache.commons commons-lang3 3.8.1 com.alibaba fastjson 1.2.58
2、创建启动类
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class); } }
3、WebSocketConfig开启WebSocket的支持
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; //开启WebSocket的支持,并把该类注入到spring容器中 @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
4、WebSocketServer
(1)、因为WebSocket是类似客户端服务端的形式(采用ws协议),那么这里的WebSocketServer其实就相当于一个ws协议的Controller。
(2)、直接@ServerEndpoint("/imserver/{userId}") 、@Component启用即可,然后在里面实现@OnOpen开启连接,@onClose关闭连接,@onMessage接收消息等方法。
(3)、新建一个ConcurrentHashMap webSocketMap 用于接收当前userId的WebSocket,方便IM之间对userId进行推送消息。单机版实现到这里就可以。
(4)、集群版(多个ws节点)还需要借助mysql或者redis等进行处理,改造对应的sendMessage方法即可。
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; 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.concurrent.ConcurrentHashMap; @ServerEndpoint("/wsServer/{userId}") @Component @Slf4j public class WebSocketServer { /** * 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。 */ private static int onlineCount = 0; /** * concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。 */ private static ConcurrentHashMapwebSocketMap = new ConcurrentHashMap<>(); /** * 与某个客户端的连接会话,需要通过它来给客户端发送数据 */ private Session session; /** * 接收userId */ private String userId = ""; /** * 连接建立成功调用的方法 */ @OnOpen public void onOpen(Session session, @PathParam("userId") String userId) { this.session = session; this.userId = userId; if (webSocketMap.containsKey(userId)) { webSocketMap.remove(userId); webSocketMap.put(userId, this); //加入set中 } else { webSocketMap.put(userId, this); // 连接建立成功后,将userId作为key,将websocket作为value存入Map中 //加入set中 addOnlineCount(); //在线数加1 } log.info("用户连接:" + userId + ",当前在线人数为:" + getOnlineCount()); try { sendMessage("连接成功"); // 服务器主动推送数据到客户端 } catch (IOException e) { log.error("用户:" + userId + ",网络异常!!!!!!"); } } /** * 连接关闭调用的方法 */ @OnClose public void onClose() { if (webSocketMap.containsKey(userId)) { webSocketMap.remove(userId); //从set中删除 subOnlineCount(); } log.info("用户退出:" + userId + ",当前在线人数为:" + getOnlineCount()); } /** * 收到客户端消息后调用的方法 * * @param message 客户端发送过来的消息 */ @OnMessage public void onMessage(String message, Session session) { log.info("用户消息:" + userId + ",报文:" + message); //可以群发消息 //消息保存到数据库、redis if (StringUtils.isNotBlank(message)) { try { //解析发送的报文 JSONObject jsonObject = JSON.parseObject(message); //追加发送人(防止串改) jsonObject.put("fromUserId", this.userId); String toUserId = jsonObject.getString("toUserId"); // 传送给对应toUserId用户的websocket if (StringUtils.isNotBlank(toUserId) && webSocketMap.containsKey(toUserId)) { webSocketMap.get(toUserId).sendMessage(jsonObject.toJSONString()); } else { log.error("请求的userId:" + toUserId + "不在该服务器上"); //否则不在这个服务器上,发送到mysql或者redis } } catch (Exception e) { e.printStackTrace(); } } } /** * 出现错误 * * @param session * @param error */ @OnError public void onError(Session session, Throwable error) { log.error("用户错误:" + this.userId + ",原因:" + error.getMessage()); error.printStackTrace(); } /** * 实现服务器主动推送 */ public void sendMessage(String message) throws IOException { this.session.getBasicRemote().sendText(message); } /** * 发送自定义消息 */ public static void sendInfo(String message, @PathParam("userId") String userId) throws IOException { log.info("发送消息到:" + userId + ",报文:" + message); if (StringUtils.isNotBlank(userId) && webSocketMap.containsKey(userId)) { webSocketMap.get(userId).sendMessage(message); } else { log.error("用户" + userId + ",不在线!"); } } public static synchronized int getOnlineCount() { return onlineCount; } public static synchronized void addOnlineCount() { WebSocketServer.onlineCount++; } public static synchronized void subOnlineCount() { WebSocketServer.onlineCount--; } }
5、编写Controller推送新消息
import com.zwh.WebSocketServer; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; import java.io.IOException; @RestController public class DemoController { @GetMapping("index") public ResponseEntityindex(){ return ResponseEntity.ok("请求成功"); } @GetMapping("page") public ModelAndView page(){ return new ModelAndView("websocket"); } @RequestMapping("/push/{toUserId}") public ResponseEntity pushToWeb(@RequestParam String message, @PathVariable String toUserId) throws IOException { WebSocketServer.sendInfo(message,toUserId); return ResponseEntity.ok("MSG SEND SUCCESS:" + message); } }
效果如下:
四、创建一个VUE工程
效果如下:
1、编写WebSocket.vue
2、编写WebSocket1.vue
3、启动项目
1)、点击WebSocket按钮时,就会建立连接,userId为2的websocket对象就会存入Map中。
2)、点击WebSocket1按钮时,就会建立连接,userId为2的websocket对象就会存入Map中。
3)、点击“发消息”按钮,用户2就会给用户3发送一条消息,
{"fromUserId":"2","message":"你好,我是2","toUserId":"3"}
4)、点击“发消息1”按钮,用户3就会给用户2发送一条消息
{"fromUserId":"3","message":"你好,我是3","toUserId":"2"}
页面控制台打印如下:
4、发送消息
用户2:
用户3:
浏览器访问:http://localhost:8080/push/2?message=hello,即给用户2发送消息
此时,用户2的控制台如下:
再打开一个页面,访问:http://localhost:8080/push/3?message=hello,how%20are%20you
此时3的控制台如下: