您现在的位置是:首页 >技术交流 >HTML5中websocket通讯_v1.0网站首页技术交流

HTML5中websocket通讯_v1.0

北苇渡江 2024-07-18 12:01:02
简介HTML5中websocket通讯_v1.0

WebSocket原理

  • HTML5推出WebSocket标准,使得浏览器和服务器之间任一方都可主动发送数据给对方,用来代替http轮询。
  • WebSocket跟HTTP协议一样,也是应用层协议。为兼容HTTP协议,它通过HTTP协议进行一次握手,握手后数据就直接从TCP层的Socket传输,与HTTP协议再无关。
  • 在首次的http请求中,浏览器发给服务端的请求会带上跟WebSocket有关的请求头,比如Connection: Upgrade和Upgrade: websocket,服务端回复也带有websocket的响应头。

优点

  • 建立在 TCP 协议之上,服务器端的实现比较容易,不用频繁创建TCP请求及销毁请求,减少网络带宽资源的占用,同时也节省服务器资源;
  • 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
  • 服务器与客户端之间交换的标头信息很小,大概只有2字节,数据格式比较轻量,性能开销小,通信高效。
  • 可以发送文本,也可以发送二进制数据。
  • 没有同源限制,客户端可以与任意服务器通信。
  • 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

代码实现

前端代码

// 定义全局的webSocket对象
var websocket = null;

/**
 * 初始化webSocket对象
 * @param param
 * param.url    请求地址,例如:ws://localhost:8080/queue/websocket/1
 * param.onmessage  接收到服务器消息后的回调
 */
function initWebSocket(param) {
    // 判断当前浏览器是否支持WebSocket
    if ('WebSocket' in window) {
        websocket = new WebSocket(param.url);
    } else {
        alert('当前浏览器 Not support websocket');
        return;
    }
    // 连接发生错误的回调方法
    websocket.onerror = function (e) {
        logger.info("WebSocket连接发生错误:" + e);
    };

    // 连接成功建立的回调方法
    websocket.onopen = function (e) {
        logger.info("WebSocket连接成功:" + JSON.stringify(e));
    };

    // 接收到消息的回调方法
    websocket.onmessage = function (event) {
        logger.info("WebSocket接收到消息["+JSON.stringify(event)+"]");
        if(param.onmessage){
            param.onmessage(event.data);
        }
    };

    // 连接关闭的回调方法
    websocket.onclose = function (event) {
        var code = event.code;
        var reason = event.reason;
        var wasClean = event.wasClean;
        logger.info("WebSocket连接关闭:code[" + code + "],reason[" + reason + "],wasClean[" + wasClean + "]");
    };

    // 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function () {
        // 关闭WebSocket连接
        websocket.close();
    };
}

/*
 * 发送消息
 */
function sendWebSocketMsg(message) {
    /**
     * readyState
     * WebSocket.CONNECTING	值为0,表示正在连接。
     * WebSocket.OPEN	值为1,表示连接成功,可以通信了。
     * WebSocket.CLOSING	值为2,表示连接正在关闭。
     * WebSocket.CLOSED	值为3,表示连接已经关闭,或者打开连接失败。
     */
    if(websocket && WebSocket.OPEN == websocket.readyState){
        websocket.send(message);
    }else{
        throw "WebSocket对象为空或者关闭";
    }
}


var webSocketParam = {
	url: "ws://localhost:8080/queue/websocket/" + orgNo + "_" + deviceNo,
    onmessage: function (data) {
    	console.log(data);
    }
};
initWebSocket(webSocketParam);
    // 心跳保持长连接
var heartbeat = setInterval(function(){
    try {
        logger.info("发送心跳连接...");
        sendWebSocketMsg(".");
    } catch (e) {
        logger.info("心跳连接发生异常,清除定时任务..." + JSON.stringify(e));
        clearInterval(heartbeat);
    }
}, 180000);

maven依赖

<dependency>
	<groupId>org.apache.tomcat</groupId>
	<artifactId>tomcat-websocket</artifactId>
	<version>9.0.8</version>
</dependency>
<dependency>
	<groupId>javax.websocket</groupId>
	<artifactId>javax.websocket-api</artifactId>
	<version>1.1</version>
	<scope>provided</scope>
</dependency>

后端代码

@ServerEndpoint(value = "/websocket/{paramStr}")
public class MyWebSocket {
    private static Logger logger = Logger.getLogger(MyWebSocket.class);
    // 最大连接数量
    private static final int MAX_SOCKET_MAP = 500;
    /**
     * 消息类型
     * 1-分类分级
     */
    public static final String MES_TYPE_CLASS = "1";

    // 存储所有的连接对象
    public static ConcurrentMap<String, MyWebSocket> webSocketMap = new ConcurrentHashMap<>();

    // 在线客户端数
    private static int onlineCount = 0;

    // 与某个客户端的连接会话,与客户端进行接收或者发送数据(每次客户进行一个websocket连接,MyWebSocket都会创建一个新的实例)
    private Session session;

    // 客户端标识:网点号_设备号
    private String id = "";

    /**
     * 连接建立成功调用的方法
     * @param session 某个客户端的session连接会话
     * @throws Exception
     */
    @OnOpen
    public void onOpen(@PathParam(value = "paramStr") String paramStr, Session session) throws Exception {
        this.id = paramStr;
        this.session = session;

        webSocketMapPut(id, this);
        addOnlineCount();

        logger.info("客户端:" + id + "加入!当前在线连接数为:" + getOnlineCount());      
        // 第一次连接的时候,查询一下,然后给自己发个消息,假设消息是0
        String classificationNum = "0";
        // 通知相对应的网点更新信息
        MyWebSocket.sendtoUser(MyWebSocket.MES_TYPE_CLASS + "_" + classificationNum, id, "service");
    }

    /**
     * 连接关闭调用的方法
     *
     * @throws Exception
     */
    @OnClose
    public void onClose() throws Exception {
        webSocketMap.remove(this.id);
        subOnlineCount();
        logger.info("有一连接关闭,关闭的连接为:"+ this.id +",当前在线人数为 " + getOnlineCount());
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     * @param session 可选的参数
     * @throws Exception
     */
    @OnMessage
    public void onMessage(String message, Session session) throws Exception {
        logger.info("连接:"+ this.id + ",接收到来自客户端的消息:" + message);
        if(".".equals(message)){// 如果前端发送为.时,说明是发心跳的情况
            return;
        }
        //可以自己约定字符串内容,比如 内容|0 表示信息群发,内容|X 表示信息发给id为X的用户
        String sendMessage = message.split("[|]")[0];
        String sendUserId = message.split("[|]")[1];
        if ("0".equals(sendUserId)){
            sendtoAll(sendMessage);
        }else{
            sendtoUser(sendMessage, sendUserId, this.id);
        }
    }

    /**
     * 发生错误时调用
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        logger.info("连接:"+ this.id + ",发生异常", error);
    }

    /**
     * 发送消息方法。
     *
     * @param message
     * @throws IOException
     */
    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }

    /**
     * 发送信息给指定ID用户
     *
     * @param message
     * @param sendUserId
     * @throws IOException
     */
    public static void sendtoUser(String message, String sendUserId, String fromId) throws IOException {
        MyWebSocket myWebSocket = webSocketMap.get(sendUserId);
        if (myWebSocket != null) {
            logger.info("用户[" + fromId + "]发送信息给指定用户["+ sendUserId + "],message["+message+"]");
            myWebSocket.sendMessage(message);
        } else {
            logger.info("用户[" + fromId + "]发送信息给指定用户["+ sendUserId + "],当前用户不在线");
        }
    }

    /**
     * 向指定网点的所有设备进行发送消息
     * @param orgNo
     * @param message
     * @throws Exception
     */
    public static void sendToOrgAllDev(String orgNo, String message) throws Exception {
        for (String key : webSocketMap.keySet()) {
            if(key.indexOf(orgNo + "_") != -1){
                MyWebSocket myWebSocket = webSocketMap.get(key);
                myWebSocket.sendMessage(message);
            }
        }
    }

    /**
     * 发送信息给所有人
     *
     * @param message
     * @throws IOException
     */
    public static void sendtoAll(String message) throws Exception {
        for (MyWebSocket myWebSocket : webSocketMap.values()) {
            myWebSocket.sendMessage(message);
        }
    }


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

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

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

    public static void webSocketMapPut(String key, MyWebSocket value) throws Exception {
        if(webSocketMap.size() > MAX_SOCKET_MAP){
            logger.info("当前在线连接数超过最大值");
            throw new Exception("当前在线连接数超过最大值");
        }
        webSocketMap.put(key, value);
    }
}

后端代码2-node.js

var ws = require("ws"); // 加载ws模块;

// 启动websocket服务器
var wsServer = new ws.Server({
    host: "127.0.0.1",
    port: 8183,
});
console.log('WebSocket sever is listening at port localhost:8183');

// 建立连接,监听客户端请求,绑定对应事件;
function on_server_client_comming (wsObj) {
    console.log("request comming");
    websocket_add_listener(wsObj);
}
wsServer.on("connection", on_server_client_comming);

// 各事件处理逻辑
function websocket_add_listener(wsObj) {
    wsObj.on("message", function(data) {
        console.log("request data:"+data);
        setTimeout(()=>{ //收到请求,回复
            wsObj.send("1秒延时,收到了,正在处理");
        },1000);
        /*****
         * 处理业务逻辑
         */
        setTimeout(()=>{ //完成请求,回复
            wsObj.send("3秒延时,返回数据,关闭连接");
            wsObj.close()
        },3000);
    });

    wsObj.on("close", function() {
        console.log("request close");
    });

    wsObj.on("error", function(err) {
        console.log("request error", err);
    });
}

所遇问题

场景测试

  • 两个用户一对一聊天
  • 用户给多个用户群发
  • 服务器推送消息给指定用户
  • 服务器给多个用户群发

其他

  • spring中实现服务器端对websocket的支持时,单例导致的问题
    • Spring默认实例化的Bean是单例模式,这就意味着在Spring容器加载时,就注入了MapMapper的实例,不管再调用多少次接口,加载的都是这个Bean同一个实例。
    • 而WebSocket是多例模式,在项目启动时第一次初始化实例时,MapMapper的实例的确可以加载成功,但可惜这时WebSocket是无用户连接的。当有第一个用户连接时,WebSocket类会创建第二个实例,但由于Spring的Dao层是单例模式,所以这时MapMapper对应的实例为空。后续每连接一个新的用户,都会再创建新的WebSocket实例,当然MapMapper的实例都为空。
  • 突然关闭前端浏览器会触发服务器后端的关闭或者报错方法
  • Nginx开启websocket
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。