WebSocket整理

1、WebSocket

1.1、介绍

WebSocket是一种网络通信协议,是HTML5开始提供的一种在单个TCP连接上进行全双工通讯的协议。RFC6455定义了它的通信标准。

1.2、对比HTTP协议

回顾HTTP 协议,它是一种无状态的、无连接的、单向的应用层协议。它采用了请求/响应模型。通信请求只能由客户端发起,服务端对请求做出应答处理。这种通信模型有一个很明显的弊端:

  • HTTP协议无法实现服务器主动向客户端发起消息。

这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。大多数web应用程序将通过频繁的异步AJAX请求实现长轮询。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。

  • HTTP模型

image-20211215145902069

  • WebSocket模型

image-20211215150432254

1.3、协议头

WebSocket协议有两部分,握手和数据传输

  • 握手时基于HTTP协议的,客户端发送的握手请求如下形式
GET ws://localhost/chat HTTP/1.1
Host: localhost
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Key: dGh1IHNhbXBSZSBub25jzQ==
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Version:13
  • 服务端发送的握手请求如下形式
HTTP/1.1 101 Switching Protocols
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOO=
Sec-WebSocket-Extensions: permessage-deflate

字段说明

头名称说明
Connection: Upgrade标识该HTTP请求时一个协议升级请求
Upgrade: WebSocket协议升级为WebSocket协议
Sec-WebSocket-Version: 13客户端支持WebSocket的版本
Sec-WebSocket-Key客户端采用base64编码的24位随机字符序列,服务器接收客户端HTTP协议升级的证明,要求服务端响应一个使用一定算法生成的对应加密的Sec-WebSocket-Accept头信息作为应答
Sec-WebSocket-Extensions协议扩展类型

1.4、客户端(浏览器)实现

1.3.1、WebSocket对象

实现WebSocketWeb浏览器将通过WebSocket对象公开所有必须的客户端功能(主要指支持Html5的浏览器),创建WebSocket对象代码如下:

var ws = new WebSocket(url);
  • 其中url格式需要为:ws://ip:port/index

1.3.2、WebSocket事件

WebSocket对象的相关事件

事件事件处理程序描述
openWebSocket对象.onopen连接建立时触发
messageWebSocket对象.onmessage客户端接收服务端数据时触发
errorWebSocket对象.onerror通信发生错误时触发
closeWebSocket对象.onclose连接关闭时触发

1.3.3、WebSocket方法

相关常用方法为:

方法描述
send()使用连接发送数据

1.5、服务端实现

Tomcat7.0.5版本开始支持WebSocket,并且实现了Java WebSocket规范(JSR356)。

Java WebSocket应用由一系列的WebSocketEndpoint组成。Endpoint是一个Java对象,代表WebSocket链接的一端,即多个客户端分别对应多个Endpoint,对于服务端,我们可以视为处理具体WebSocket消息的接口,就像Servlet之与Http请求一样。我们可以通过两种方式定义Endpoint

  • 编程式:即继承类 javax.websocket.Endpoint并实现其方法
  • 注解式:即定义一个Model,并添加**@ServerEndpoint**注解

Endpoint实例在WebSocket握手时创建,并在客户端与服务端链接过程中有效,最后在链接关闭时结束。在Endpoint接口中明确定义了与其生命周期相关的方法,规范实现者确保生命周期的各个阶段调用实例的相关方法。生命周期方法如下:

方法含义描述注解
onClose当会话关闭时调用@OnClose
onOpen当开启一个新的会话时调用,该方法时客户端与服务端握手成功后调用的方法@OnOpen
onError当连接过程中异常时调用@OnError
  • 如何接收客户端发送的数据?

服务端需要通过属于WebSocket协议里面的Session对象,为其添加MessageHandler消息处理器来接收客户端发送的数据,当采用注解方式定义Endpoint时,我们便可以通过**@OnMessage**注解指定接收消息的方法

  • 如何给客户端发送数据?

发送消息则由RemoteEndpoint完成,其实例由Session维护,根据使用情况,我们可以通过Session.getBasicRemote获取同步的消息发送实例,然后调用其sendXxx()方法就可以发送消息,除此之外,还可以通过Session.getAsyncRemote获取异步消息发送实例

  • 完整示例代码
/**
 * @author PengHuanZhi
 * @date 2021年12月15日 21:16
 */
@EqualsAndHashCode
@ServerEndpoint("/webSocket")
public class WebSocketEndpoint {
    /**
     * 每个用户对应一个Endpoint
     **/
    private static final Set<WebSocketEndpoint> WEBSOCKET_SET = new HashSet<>();

    private Session session;

    @OnMessage
    public void onMessage(String messgae, Session session) throws IOException {
        System.out.println("接收的消息是:" + messgae);
        System.out.println(session);
        //将消息发送给其他的用户
        for (WebSocketEndpoint demo : WEBSOCKET_SET) {
            if (demo != this) {
                demo.session.getBasicRemote().sendText(messgae);
            }
        }
    }

    @OnOpen
    public void onOpen(Session session) {
        this.session = session;
        WEBSOCKET_SET.add(this);
    }

    @OnClose
    public void onClose(Session session) {
        System.out.println("连接关闭了");
    }

    @OnError
    public void onError(Session session, Throwable error) {
        System.out.println("出错了...." + error.getMessage());
    }
}

2、聊天室应用练习

2.1、注入ServerEndpointExporter

/**
 * 首先要注入ServerEndpointExporter,这个Bean会自动注册使用了@ServerEndPoint注解声明的WebSocket EndPoint。
 * 要注意的是,如果使用独立的Servlet容器,而不是SpringBoot的内置容器,就不要注入ServerEndpointExporter,因为它将由容器自己提供和管理
 *
 * @author PengHuanZhi
 * @date 2021年12月16日 9:30
 */
@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

2.2、会话连接池

/**
 * @author PengHuanZhi
 * @date 2021年12月16日 9:43
 */
public class SessionPool {
    public static final Map<String, Session> SESSIONS = new ConcurrentHashMap<>();

    public static void close(String sessionId) throws IOException {
        Iterator<Map.Entry<String, Session>> mapIterator = SESSIONS.entrySet().iterator();
        while (mapIterator.hasNext()) {
            Map.Entry<String, Session> entry = mapIterator.next();
            Session session = entry.getValue();
            if (session.getId().equals(sessionId)) {
                session.close();
                mapIterator.remove();
                return;
            }
        }
    }

    public static void sendMessage(String sessionId, String message) {
        Session session = SESSIONS.get(sessionId);
        if (session == null) {
            throw new RuntimeException("Session未找到");
        }
        session.getAsyncRemote().sendText(message);
    }

    public static void sendMessage(String message) {
        SESSIONS.values().forEach(session -> session.getAsyncRemote().sendText(message));
    }

    public static void sendMessage(HashMap<String, Object> message) {
        String toUserId = message.get("toUserId").toString();
        String msg = message.get("msg").toString();
        String fromUserId = message.get("fromUserId").toString();
        msg = "来自" + fromUserId + "的消息 :" + msg;
        Session session = SESSIONS.get(toUserId);
        if (session == null) {
            throw new RuntimeException("用户" + toUserId + "未找到");
        }
        session.getAsyncRemote().sendText(msg);
    }
}

2.3、处理WebSocket消息接口

/**
 * ws://localhost:8080/ws/{userId}
 *
 * @author PengHuanZhi
 * @date 2021年12月16日 9:37
 */
@ServerEndpoint(value = "/ws/{userId}")
@Component
@Slf4j
public class WebSocketEndpoint {

    /**
     * @param session 会话对象
     * @param userId  用户Id
     * @description 当连接建立成功调用
     **/
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId) {
        //把会话存入连接池
        log.info("new session : {}", userId);
        SessionPool.SESSIONS.put(userId, session);
    }

    /**
     * @param session 会话对象
     * @description 当连接关闭调用
     **/
    @OnClose
    public void onClose(Session session) throws IOException {
        SessionPool.close(session.getId());
    }

    /**
     * @param message 客户端发送的消息
     * @param session 会话对象
     * @description 当客户端发送消息时调用
     **/
    @OnMessage
    public void onMessage(String message, Session session) throws IOException {
        if (message.equalsIgnoreCase("ping")) {
            try {
                Map<String, Object> params = new HashMap<>();
                params.put("type", "pong");
                session.getBasicRemote().sendText(JSON.toJSONString(params));
            } catch (Exception e) {
                throw new RemoteException("心跳检测出现错误");
            }
        } else {
            HashMap<String, Object> params = JSON.parseObject(message, HashMap.class);
            SessionPool.sendMessage(params);
        }
    }
}

2.4、前端页面

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
        <script id="code ">
            let wsObj = null;
            let wsUri = null;
            let userId = -1;
            let lockReconnect = false;
            let wsCreateHandler = null;

            function createWebSocket() {
                const host = window.location.host;
                userId = GetQueryString("userId");
                wsUri = "ws://" + host + "/ws/" + userId;

                try {
                    wsObj = new WebSocket(wsUri);
                    initWsEventHandle();
                } catch (e) {
                    writeToScreen("执行关闭事件,开始重连");
                    reconnect();
                }
            }

            function initWsEventHandle() {
                try {
                    wsObj.onopen = function (event) {
                        onWsOpen(event);
                    };
                    wsObj.onmessage = function (event) {
                        onWsMessage(event);
                    };
                    wsObj.onclose = function (event) {
                        writeToScreen("执行关闭事件");
                        onWsClose(event);
                        reconnect();
                    };
                    wsObj.onerror = function (event) {
                        writeToScreen("执行error事件");
                        onWsError(event);
                        reconnect();
                    };
                } catch (e) {
                    writeToScreen("绑定事件没有成功");
                    reconnect();
                }
            }

            function onWsOpen(event) {
                writeToScreen("Connected");
            }

            function onWsClose(event) {
                writeToScreen("DisConnected");
            }

            function onWsError(event) {
                writeToScreen(event.data);
            }

            function writeToScreen(message) {
                const debuggerInfo = $("#debuggerInfo");
                if (DEBUG_FLAG) {
                    debuggerInfo.val(debuggerInfo.val() + "\n" + message);
                }
            }

            function GetQueryString(name) {
                //获取Url之中?符后的字符串并正则匹配
                let reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
                let r = window.location.search.substr(1).match(reg);
                let context = "";
                if (r != null) {
                    context = r[2];
                }
                reg = null;
                r = null;
                return context == null || context === "" || context === "undefined" ? "" : context;
            }

            function reconnect() {
                if (lockReconnect) {
                    return;
                }
                writeToScreen("1秒后重连");
                lockReconnect = true;
                wsCreateHandler && clearTimeout(wsCreateHandler);
                wsCreateHandler = setTimeout(function () {
                    writeToScreen("重连" + wsUri);
                    createWebSocket();
                    lockReconnect = false;
                }, 1000)
            }

            let heartCheck = {
                timeout: 15000,
                timeoutObj: null,
                serverTimeoutObj: null,
                reset: function () {
                    clearTimeout(this.timeoutObj);
                    clearTimeout(this.serverTimeoutObj);
                    this.start();
                },
                start: function () {
                    let self = this;
                    this.timeoutObj && clearTimeout(this.serverTimeoutObj);
                    this.timeoutObj = setTimeout(
                        function () {
                            writeToScreen("发送ping到后台");
                            try {
                                wsObj.send("ping");
                            } catch (e) {
                                writeToScreen("发送ping异常");
                            }
                            self.serverTimeoutObj = setTimeout(function () {
                                writeToScreen("没有收到后台的数据,关闭连接");
                                reconnect();
                            }, self.timeout);
                        }, this.timeout);
                }
            }

            const DEBUG_FLAG = true;
            debugger
            $(function () {
                createWebSocket();
            });

            function onWsMessage(event) {
                const jsonStr = event.data;
                writeToScreen(jsonStr);
            }

            function sendMessageBySocket() {
                const toUserId = $("#userId").val();
                const msg = $("#msg").val();
                const data = {"fromUserId": userId, "toUserId": toUserId, "msg": msg};
                wsObj.send(JSON.stringify(data));
            }
        </script>
    </head>
    <body>
        <label for="debuggerInfo"></label><textarea id="debuggerInfo"></textarea>
        <div>用户:<label for="msg"></label><label for="userId"></label><input type="text" id="userId"/></div>
        <div>消息:<label for="msg"></label><input type="text" id="msg"/></div>
        <div><input type="button" value="发送消息" onclick="sendMessageBySocket()"/></div>
    </body>
</html>
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值