嗯,简单一点来理解,WS其实也是一种网络通信协议。
从HTML5 开始,提供了一种基于单个TCP连接上进行全双工通讯的协议,这个协议就是WebSocket协议。
在没WebSocket之前,都只能由客户端发起请求,服务端接收并相应请求,这样很多功能的实现就只能通过轮训服务端接口来解决,带来了很多不便利性。
这就好像我有一个快递,为了快递有没有到这件事,我必须要每天都主动去营业点问一下,这样的单向查询方式很低效且浪费时间,那有没有什么办法可以让营业点在快递到的时候主动通知我呢?
有,那就是预留我们的手机号码,快递到了的时候营业点给我们发个短信就行。
那对应到wb,营业点就是服务端,我就是客户端,手机号码就是session。
由此,引出在wb中有三个重要的概念,服务器客户端和session。
服务端持有与某个客户端连接的session,只要服务器有结果,就能通过这个session发送到指定的客户端中。
好,那让我们在springboot中来实现一下wb的功能,得益于tomact对wb的支持,在sb中能够很轻松的构造出一个wb的应用。
在springboot中,想要使用wb,只要引入下面这个包,就能够支持wb
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
首先,构造一个Service类,需要注意的是,这个类上面的注解要用@ServerEndpoint,以表明这个一个wb的服务接入点。
package com.tmx.mengxiangspringcloud.websocket;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import javax.websocket.OnClose;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
@Component
@ServerEndpoint("/websocket1")
public class WebsocketService {
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
//key 可以为用户名、支付订单号、用户id等
public static ConcurrentHashMap<String, WebsocketService> webSocketMap = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
this.session = session;
//加入map
try {
webSocketMap.put("1",this);
sendMessage(String.valueOf("加入成功啦"));
} catch (IOException e) {
}
}
@OnClose
public void onClose() {
//从map中删除
webSocketMap.remove("1");
}
/**
* 向客户端发送消息
*/
public void sendMessage(String message) throws IOException {
WebsocketService websocketService = webSocketMap.get("1");
websocketService.session.getBasicRemote().sendText(message);
//this.session.getAsyncRemote().sendText(message);
}
/**
* 列表群发
*/
public static void sendInfo(String message) throws IOException {
for (String key : webSocketMap.keySet()) {
try {
webSocketMap.get(key).sendMessage(message);
} catch (IOException e) {
continue;
}
}
}
}
可以看到,在成员变量中,我们构建了一个ConcurrentHashMap变量,这个变量就是用来保存服务器和客户端session的关系。
在方法中,我们一共使用了两个注解,一个注解是@onOpen,这个注解的意思是在客户端与服务器建立连接时需要执行的逻辑;一个注解是@onClose,这个注解的意思是在客户端与服务器关闭连接时需要执行的逻辑。
sendMessage方法是向客户端发送消息的;sendInfo是向所有客户端群发消息的。
详见sendMessage方法,服务器向客户端发送消息,是通过获取某个客户端的session,再利用session的sendText方法发送消息。
客户端需要通过ws协议连接到服务端,以下通过html页面的方式实现
DOCTYPE html>
<html>
<head>
<title>Simple WebSocket Exampletitle>
head>
<body>
<script>
var socket = new WebSocket("ws://127.0.0.1:8082/websocket1");
socket.onopen = function() {
console.log("Connection established.");
socket.send("Hello, server!");
};
socket.onmessage = function(event) {
console.log("Received: " + event.data);
};
socket.onclose = function(event) {
console.log("Connection closed.");
};
script>
body>
html>
首先,启动服务端,然后浏览器中打开index.html页面,按F12进入控制台。
可以看到,客户端向服务器发起请求已经成功并创建了一个session对象,并且服务器发送的消息客户端也已经接受到了,但是服务器如何主动向客户端推送消息呢?请接着往下看。
我们定义一个Controller向客户端主动发送消息,其中调用的是WebsocketService中的sendMessage方法。
package com.tmx.mengxiangspringcloud.controller;
import com.tmx.mengxiangspringcloud.websocket.WebsocketService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class HelloController1 {
@Resource
private WebsocketService websocketService;
@RequestMapping("/sendMsg")
public String sendMsg(){
try {
websocketService.sendMessage("哈哈哈哈为所欲为");
// webSocketSession.sendMessage(new org.springframework.web.socket.TextMessage("Hello, client!"));
}catch (Exception e){
e.printStackTrace();
}
return "hello world";
}
}
打开浏览器,输入http://127.0.0.1:8082/sendMsg调用接口,可以看到客户端控制台中已经输出了服务器发送的内容
至此,我们就实现了一个简单wb应用,但是还存在很多可以优化的点,比如在往ConcurrentHashMap中设值的时候写死了一个1作为key,这样就会导致所有的客户端连接session都会被覆盖,优化的方向可以是一个客户端请求的时候带上唯一请求值,比如说订单号、交易单号、ip地址等等。同时,在sendMessage方法中,通过key取到对应的session发送消息。
wb的出现,是为了解决服务器不能往客户端发消息的痛点,这样做的好处是提高了效率,但是也需要考虑服务端同时维护的session数,如果session数过多,但资源不足,会引起很多生产事故,还需要考虑与前端的合作,因为这个协议是需要前后端都支持的,所以能不能使用wb,还需要看实际。