传统 HTTP

短轮询 (Polling)

短轮询的实现思路就是浏览器端每隔几秒钟向服务器端发送 HTTP 请求,服务端在收到请求后,不论是否有数据更新,都直接进行响应,服务端响应之后就会关闭这个 TCP 连接

优点:实现简单

缺点:会造成数据在一小段时间内不同步和大量无效的请求,安全性差、浪费资源

长轮询 (Long Polling)

客户端发送请求后服务器端不会立即返回数据,而是阻塞请求连接,直到服务器端有数据更新或者是连接超时才返回,客户端才再次发出请求新建连接,如此反复从而获取最新数据

img

什么是 WebSocket

WebSocket协议是基于TCP的一种新的网络协议,它实现了浏览器与服务器全双工(full-duplex)通信,即允许服务器主动发送信息给客户端

为什么需要 WebSocket

我们已经有了 HTTP 协议,为什么还需要 WebSocket 协议?

答案很简单,HTTP 协议有一个很大的缺陷:通信只能由客户端发起,然后服务端接收并响应,却做不到服务端主动向客户端推送消息

试想这么一种情况:

我们想要知道某个商品是否已经上架,如果单方面采用轮询方式,向服务端发送查询请求,效率非常低,而且浪费资源

如果商品上架时,服务端能主动推送该消息给我们,这样省心又省力

WebSocket 如何建立连接

WebSocket 的工作原理很简单,首先与服务器建立常规 HTTP 连接,然后通过发送 Upgrade 标头将其升级为双向 Websocket 连接

Demo

SpringBoot 集成 WebSocket

服务端

@ServerEndpoint("/server/{userId}")
@Component
public class WebSocketServer {
    private static Log log = LogFactory.get(WebSocketServer.class);
    
    private static int onlineCount = 0;
    private static ConcurrentHashMap<String, WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
    
    private Session session;
    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);
        }else {
            webSocketMap.put(userId, this);
            onlineCount++;
        }
        
        log.info("用户连接: " + userId + " 当前在线人数为:" + onlineCount);
        
        sendMessage("连接建立成功.");
    }
    
    @OnClose
    public void onClose(){
        if (webSocketMap.containsKey(userId)){
            webSocketMap.remove(userId);
            onlineCount--;
        }

        log.info("用户退出: " + userId + " 当前在线人数为:" + onlineCount);
    }
    
    @OnMessage
    public void onMessage(String message, Session session){
        log.info("用户消息:" + userId + ",报文:" + message);
        
        if (!message.isBlank()){
            JSONObject jsonObject = JSON.parseObject(message);
            jsonObject.put("fromUserId", userId);
            String toUserId = jsonObject.getString("toUserId");
            
            if (!toUserId.isBlank() && webSocketMap.containsKey(toUserId)){
                webSocketMap.get(toUserId).sendMessage(jsonObject.toJSONString());
            }else {
                log.error("请求的用户不在服务器上:" + toUserId);
            }
        }
    }
    
    @OnError
    public void onError(Session session, Throwable throwable){
        log.error("用户: " + userId + "发生错误: " + throwable.getMessage());
        throwable.printStackTrace();
    }
    
    public void sendMessage(String message){
        try {
            session.getBasicRemote().sendText(message);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    
    public static void sendMessageById(String message,
                                @PathParam("userId") String userId){
        if (!userId.isBlank() && webSocketMap.containsKey(userId)){
            webSocketMap.get(userId).sendMessage(message);
        }else {
            log.error("用户不在线:" + userId);
        }
    }
}
@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}
@RestController
public class Controller {
    @GetMapping("index")
    public ResponseEntity<String> index(){
        return ResponseEntity.ok("请求成功");
    }
    
    @GetMapping("page")
    public ModelAndView page(){
        return new ModelAndView("WebSocket");
    }
    
    @RequestMapping("/push/{toUserId}")
    public ResponseEntity<String> push(String message,
                                       @PathVariable String toUserId){
        WebSocketServer.sendMessageById(message, toUserId);
        return ResponseEntity.ok("发送消息成功");
    }
}

客户端

不同用户修改 UserId 即可

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>websocket通讯</title>
</head>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
<script>
    var socket;
    function openSocket() {
        if(typeof(WebSocket) == "undefined") {
            console.log("您的浏览器不支持WebSocket");
        }else{
            console.log("您的浏览器支持WebSocket");
            //实现化WebSocket对象,指定要连接的服务器地址与端口  建立连接
            //等同于socket = new WebSocket("ws://localhost:8888/xxxx/im/25");
            //var socketUrl="${request.contextPath}/im/"+$("#userId").val();
            var socketUrl="http://localhost:9999/server/"+$("#userId").val();
            socketUrl=socketUrl.replace("https","ws").replace("http","ws");
            console.log(socketUrl);
            if(socket!=null){
                socket.close();
                socket=null;
            }
            socket = new WebSocket(socketUrl);
            //打开事件
            socket.onopen = function() {
                console.log("websocket已打开");
                //socket.send("这是来自客户端的消息" + location.href + new Date());
            };
            //获得消息事件
            socket.onmessage = function(msg) {
                console.log(msg.data);
                //发现消息进入    开始处理前端触发逻辑
            };
            //关闭事件
            socket.onclose = function() {
                console.log("websocket已关闭");
            };
            //发生了错误事件
            socket.onerror = function() {
                console.log("websocket发生了错误");
            }
        }
    }
    function sendMessage() {
        if(typeof(WebSocket) == "undefined") {
            console.log("您的浏览器不支持WebSocket");
        }else {
            console.log("您的浏览器支持WebSocket");
            console.log('{"toUserId":"'+$("#toUserId").val()+'","contentText":"'+$("#contentText").val()+'"}');
            socket.send('{"toUserId":"'+$("#toUserId").val()+'","contentText":"'+$("#contentText").val()+'"}');
        }
    }
</script>
<body>
<p>【userId】:<div><input id="userId" name="userId" type="text" value="114514"></div>
<p>【toUserId】:<div><input id="toUserId" name="toUserId" type="text" value="1919"></div>
<p>【toUserId】:<div><input id="contentText" name="contentText" type="text" value="hello websocket"></div>
<p>【操作】:<div><a onclick="openSocket()">开启socket</a></div>
<p>【操作】:<div><a onclick="sendMessage()">发送消息</a></div>
</body>

</html>

测试结果

Chrome 打开控制台

image-20220501233217146

image-20220501233233855