Java 实现 Websocket 通信

代码:https://github.com/ioufev/websocket-springboot-demo
代码备份下载:

示例就是客户端和服务端互相发送简单的字符串。

视频说明

WebSocket

WebSocket 协议

客户端和服务端,都有6个API(准确说是4个事件2个方法),所以说客户端和服务端是对等的。

🟢 onOpen()
🟢 onClose()
🟢 onError()
🟢 onMessage()
🟢 sendMessage()
🟢 close()

Java 端的 4个事件2个方法

js 端的 4个事件2个方法

代码

WebSocketConfig

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
@EnableWebSocket
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

WebSocketServer

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.CopyOnWriteArraySet;

@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {

    private static int onlineCount = 0;
    private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<>();
    private Session session;
    private String sid = "";

    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
        this.session = session;
        webSocketSet.add(this);     //加入set中
        addOnlineCount();           //在线数加1
        this.sid = sid;
        sendMessage("conn_success");

        System.out.println("新的窗口" + sid + "已连接!当前在线人数为" + getOnlineCount());
    }

    @OnClose
    public void onClose() {
        webSocketSet.remove(this);  //从set中删除
        subOnlineCount();           //在线数减1

        System.out.println("连接窗口" + sid + "关闭!当前在线人数为" + getOnlineCount());
    }

    @OnMessage
    public void onMessage(String message, Session session) {

        if(message.startsWith("target-")){
            int index = message.indexOf(":");
            String sid = message.substring(7,index);
            sendInfo(message.substring(index + 1), sid);
            return;
        }

        this.session = session;
        sendMessage("服务端收到来自窗口" + sid + "发送的消息:" + message);

    }

    @OnError
    public void onError(Session session, Throwable error) {
        this.session = session;
        error.printStackTrace();
    }

    private void sendMessage(String message) {
        try {
            this.session.getBasicRemote().sendText(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 群发消息
    /**
     * 群发自定义消息
     */
    public static void sendInfo(String message, @PathParam("sid") String sid) {
        System.out.println("推送消息到窗口" + sid + ",推送内容:" + message);

        for (WebSocketServer item : webSocketSet) {
            //这里可以设定只推送给这个sid的,为null则全部推送
            if (sid == null) {
//                    item.sendMessage(message);
            } else if (item.sid.equals(sid)) {
                item.sendMessage(message);
            }
        }
    }

    public static void sendInfo2(String message) {
        System.out.println("推送消息到所有窗口" + ",推送内容:" + message);
        for (WebSocketServer item : webSocketSet) {
            item.sendMessage(message);
        }
    }

    /**
     * 群发自定义消息
     */
    public static void sendInfo2One(String message, @PathParam("sid") String sid) {
        System.out.println("推送消息到窗口" + sid + ",推送内容:" + message);

        for (WebSocketServer item : webSocketSet) {
            if (item.sid.equals(sid)) {
                item.sendMessage(message);
            }
        }
    }

    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
        WebSocketServer.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        WebSocketServer.onlineCount--;
    }

    public static CopyOnWriteArraySet<WebSocketServer> getWebSocketSet() {
        return webSocketSet;
    }

}

ws-demo.html文件

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>测试WS</title>
</head>
<body>

<div>随机ID:
  <span id="ws-id">

  </span>
</div>

<button id="connect">开始连接</button>
<br/>

<label for="message">发送内容: </label><input id="message" type="text" placeholder="请输入要发送的消息内容">
<br/>
<label for="target-id">发送给谁: </label><input id="target-id" type="text" placeholder="请输入要发送给谁">
<br/>
<button id="sendButton">发送消息</button>
<br/>

<label for="back">收到消息: </label><textarea id="back" placeholder="提示消息" style="width: 600px;height: 200px"></textarea>

<br>
<button id="disconnect">断开连接</button>

<script>

  let sendButton = window.document.getElementById("sendButton");
  sendButton.addEventListener("click", () => {
    send();
  })

  document.getElementById("disconnect").addEventListener("click", () => {
    closeWebSocket();
  })

  let websocket;
  document.getElementById("connect").addEventListener("click" , () =>{

    if(websocket){
      closeWebSocket()
    }

    // 随机整数
    let random = Math.floor(Math.random()*10000);
    window.document.getElementById("ws-id").innerText = random + '';


    //判断当前浏览器是否支持WebSocket,是则创建WebSocket
    if ('WebSocket' in window) {
      websocket = new WebSocket("ws://localhost:8080/ws/" + random);
    }else {
      alert('当前浏览器 Not support websocket')
    }

    //连接发生错误的回调方法
    websocket.onerror = function () {
      console.log("WebSocket连接发生错误");
    };
    //连接成功建立的回调方法
    websocket.onopen = function () {
      console.log("WebSocket连接成功");
    }
    //接收到消息的回调方法
    websocket.onmessage = function (event) {
      console.log(event.data);
      window.document.getElementById("back").value = event.data;
    }
    //连接关闭的回调方法
    websocket.onclose = function () {
      console.log("WebSocket连接关闭");
    }
  })

  websocket = null;

  //关闭WebSocket连接
  function closeWebSocket() {
    websocket.close();
  }
  //发送消息
  function send() {
    let message = document.getElementById('message').value;
    let target_id = document.getElementById('target-id').value;
    if (target_id != ""){
      websocket.send('target-' + target_id + ':' + message);
      return;
    }

    websocket.send(message);
  }

  //如果websocket连接还没断开就关闭了窗口,后台server端会抛异常。
  //所以增加监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接
  window.onbeforeunload = function () {
    closeWebSocket();
  }

</script>
</body>
</html>

服务端推送
DemoController

import com.ioufev.websocketspringbootdemo.ws.WebSocketServer;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class DemoController {
    //推送数据接口
    @PostMapping("/push/{cid}")
    public Map pushToWeb(@PathVariable String cid, @RequestBody String message) {
        Map<String,Object> result = new HashMap<>();
        WebSocketServer.sendInfo(message, cid);
        result.put("code", cid);
        result.put("msg", message);
        return result;
    }

    @PostMapping("/push")
    public Map pushToWeb2(@RequestBody String message) {
        Map<String,Object> result = new HashMap<>();
        WebSocketServer.sendInfo2(message);
        result.put("msg", message);
        return result;
    }
}

动图演示

过程和问题

关于 WebSocket 协议的一些资料

rfc6455

关于 WebSocketServer 类中的 static 关键字

WebSocketServer 类的作用域是原型(Prototype),而其他 Bean 的作用域是单例(Singleton)。

自动装配其他 Bean 实例时需要一些设置

关于 WebSocket 集群转发

分享我使用的 Redis 发布订阅的实现,还可以使用消息队列MQ,ZooKeeper。

关于 WebSocket 的子协议

把 WebSocket 当作传输层

可以传递应用层协议,比如:MQTT、STOMP、AMQP、OPCUA(看代码有),或者自定义的协议,反正只要知道别人发的消息是什么意思就行。

posted @ 2022-09-05 17:27  ioufev  阅读(1778)  评论(0编辑  收藏  举报