WebSocket :用WebSocket实现推送你必须考虑的几个问题

2023-11-07

目录:

1.WebSocket简介

WebSocket_百度百科

2.项目背景、硬件环境及客户端支持

本项目通过WebSocket实现同时在线用户量几千的推送服务器(可内网运行)。且可实时查看用户在线状态。

  • 服务器:centos 6.5、tomcat 7
  • 客户端:移动端(安卓、IOS)、网页端。
  • 服务端第三方库 :javax.websocket

3.本文研究内容

应用的线上环境后各种异常情况处理:

  • 使用WebSocket时,依赖TCP keepalive还是做业务层心跳
  • 服务器如何感知客户端断开(用以查看实时用户在线状态)
  • 客户端如何感知服务端异常(用以决定客户端何时重连)

4.基于javax.websocket服务端代码(源码后续补充git连接)

WebSocketServer.java

package cn.milo.wsdemo;

import org.apache.log4j.Logger;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

/*
 * create : 17-07-21
 * auth : milo
 */
@ServerEndpoint("/connect/{userId}")
public class WebSocketServer {

    private static Logger log = Logger.getLogger(WebSocketServer.class);

    /*
    New Connected
     */
    @OnOpen
    public void onOpen(@PathParam("userId") String userId ,
                       Session session){
        log.info("[WebSocketServer] Connected : userId = "+ userId);
        WebSocketUtils.add(userId , session);
    }

    /*
    Send Message
     */
    @OnMessage
    public String onMessage(@PathParam("userId") String userId,
                            String message) {
        log.info("[WebSocketServer] Received Message : userId = "+ userId + " , message = " + message);
        if (message.equals("&")){
            return "&";
        }else{
            WebSocketUtils.receive(userId , message);
            return "Got your message ("+ message +").";
        }
    }

    /*
    Errot
     */
    @OnError
    public void onError(@PathParam("userId") String userId,
                        Throwable throwable,
                        Session session) {
        log.info("[WebSocketServer] Connection Exception : userId = "+ userId + " , throwable = " + throwable.getMessage());
        WebSocketUtils.remove(userId);
    }

    /*
    Close Connection
     */
    @OnClose
    public void onClose(@PathParam("userId") String userId,
                        Session session) {
        log.info("[WebSocketServer] Close Connection : userId = " + userId);
        WebSocketUtils.remove(userId);
    }
}

WebSocketUtils.java

package cn.milo.wsdemo;

import cn.milo.FileUtils.CreateFile;
import org.apache.log4j.Logger;

import javax.websocket.Session;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;


public class WebSocketUtils {

    private static Logger log = Logger.getLogger(WebSocketUtils.class);

    public static Map<String, Session> clients = new ConcurrentHashMap<String, Session>(); 

    /*
    Add Session
     */
    public static void add(String userId, Session session) {
        clients.put(userId,session);
        log.info("当前连接数 = " + clients.size());

    }

    /*
    Receive Message
     */
    public static void receive(String userId, String message) {
        log.info("收到消息 : UserId = " + userId + " , Message = " + message);
        log.info("当前连接数 = " + clients.size());
    }

    /*
    Remove Session
     */
    public static void remove(String userId) {
        clients.remove(userId);
        log.info("当前连接数 = " + clients.size());

    }

    /*
    Get Session
     */
    public static boolean sendMessage(String userId , String message) {
        log.info("当前连接数 = " + clients.size());
        if(clients.get(userId) == null){
            return false;
        }else{
            clients.get(userId).getAsyncRemote().sendText(message);
            return true;
        }

    }
}

5.客户端代码

<body>
server地址 :  <input id ="serveraddress" type="text" /><br/>
您的用户id :  <input id ="userId" type="text" /><br/>
<button onclick="initSocket()">连接</button><br/>

=====================================================<br/>
消息 :  <input id ="message" type="text" /><br/>
<button onclick="send()">发送</button><br/>
=====================================================<br/>
连接状态 : <button onclick="clearConnectStatu()">清空</button><br/>
<div id="connectStatu"></div><br/>

=====================================================<br/>
收到消息 :<br/>
<div id="receivedMessage"></div><br/>
=====================================================<br/>
心跳 :<br/>
<div id="heartdiv"></div><br/>

</body>

<script src="<%=basePath%>/resources/jquery-1.7.2.min.js"></script>
<script type="text/javascript">
    var heartflag = false;
    var webSocket = null;
    var tryTime = 0;
    $(function () {

//        initSocket();
        window.onbeforeunload = function () {

        };
    });

    /**
     * 初始化websocket,建立连接
     */
    function initSocket() {
        var serveraddress = $("#serveraddress").val();
        var userId = $("#userId").val();

        if (!window.WebSocket) {
            $("#connectStatu").append(getNowFormatDate()+"  您的浏览器不支持ws<br/>");
            return false;
        }

        webSocket = new WebSocket(serveraddress+"/"+userId);

        // 收到服务端消息
        webSocket.onmessage = function (msg) {
            if(msg.data == "&"){

            }else{
                $("#receivedMessage").append(getNowFormatDate()+"  收到消息 : "+msg.data+"<br/>");
            }
        };

        // 异常
        webSocket.onerror = function (event) {
            heartflag = false;
            $("#connectStatu").append(getNowFormatDate()+"  异常<br/>");
        };

        // 建立连接
        webSocket.onopen = function (event) {
            heartflag = true;
            heart();
            $("#connectStatu").append(getNowFormatDate()+"  建立连接成功<br/>");
            tryTime = 0;
        };

        // 断线重连
        webSocket.onclose = function () {
            heartflag = false;
            // 重试10次,每次之间间隔10秒
            if (tryTime < 10) {
                setTimeout(function () {
                    webSocket = null;
                    tryTime++;
                    initSocket();
                    $("#connectStatu").append( getNowFormatDate()+"  第"+tryTime+"次重连<br/>");
                }, 3*1000);
            } else {
                alert("重连失败.");
            }
        };

    }

    function send(){
        var message = $("#message").val();
        webSocket.send(message);
    }

    function clearConnectStatu(){
        $("#connectStatu").empty();
    }

    function getNowFormatDate() {
        var date = new Date();
        var seperator1 = "-";
        var seperator2 = ":";
        var month = date.getMonth() + 1;
        var strDate = date.getDate();
        if (month >= 1 && month <= 9) {
            month = "0" + month;
        }
        if (strDate >= 0 && strDate <= 9) {
            strDate = "0" + strDate;
        }
        var currentdate = date.getFullYear() + seperator1 + month + seperator1 + strDate
                + " " + date.getHours() + seperator2 + date.getMinutes()
                + seperator2 + date.getSeconds();
        return currentdate;
    }

    function heart() {
        if (heartflag){
            webSocket.send("&");
            $("#heartdiv").append(getNowFormatDate()+"  心跳 <br/>");
        }
        setTimeout("heart()", 10*60*1000);

    }
</script>

6.问题探索

首先ws是基于TCP的应用层技术,那么很多同学这里会有疑问TCP本身是有keepalive机制的为什么还要做应用层心跳。原因有以下几个:1.client异常挂死,此时keepalive机制无法反馈真实的client状态; 2.client 异常断电断网出现TCP假死keepalive并不能根本性解决问题,实际上互联网环境很不稳定;3.ws在应用层,基于传输层,在ws中操作TCP也很不方便。封装就意味着易用性提高灵活性降低。

所以我们在应用层开启心跳。1次/10mins

接下来我们聊一聊客户端正常断开异常断开如何处理:

客户端:

client server处理方法 client处理方法 处理思路
关闭浏览器 触发onClose回调 / 应用层ws主动关掉连接(优雅关闭)
杀掉浏览器 触发onClose和onError回调 / 在操作系统中,应用程序对应的进程被干掉的时候会关闭其端口,也就是触发了TCP四次挥手。对于ws来讲直接在外部断开TCP会触发ws异常,对于ws来讲这样的关闭方式为非优雅关闭会触发异常.
断电断网 检测client最后心跳上报时间 触发onClose(断网) server角度:如果client最后上报时间已经超过正常周期*3,server认为其离线
client角度:断电就不说了。断网的情况client之所以触发了onClose我认为可能是当断网时操作系统关闭了所有对外的网络端口或者操作系统通知了浏览器断网(由此看出操作系统的知识真的是太重要了);所以此时三个心跳周期过后当我们认为此session已经断开时不要忘记通知ws close掉这个session,不然有可能出现大量服务端TCP假死.接下来说重连,大家要注意重连对于server是来讲是一个新的连接,大家可以通过断网重连后server产生的session判断出断网重连实际上是产生了一个新的连接。对于server的原session如何处理我做了这样一个测试,当客户端断网后server依然通过原session发送数据给client当发送的数据超过一定时间一定数量没有回复后server会触发onError和onClose方法,对于原session server在client断开后从来不给这个client发消息的情况也就是重连的情况,我们要在新的session产生时及时清掉旧的session.同TCP假死处理一致.


服务端:

server server处理方法 client处理方法 处理思路
重启tomcat / 触发onClose 应用层ws主动关掉连接(优雅关闭)
杀掉tomcat(kill -9 pid / 触发onClose和onError回调 (同client被杀死)
断电断网 检测client最后心跳上报时间 心跳异常 (见下表:server断电断网时client如何感知),也就是说对于client来讲,只要正常发送心跳给server就可以了。如果server断开网络超过20分钟(心跳:次/10mins)所有client均会掉线


server断电断网时client如何感知

心跳周期 client现象
次/1s 断网/断电后167s(中间经历了167次心跳)触发client onClose方法
次/1min 断网/断电后6mins 40s(中间经历了6次心跳)触发client onClose方法
次/10mins 第一次测试:断网/断电后11mins 27s(中间经历了1次心跳)触发client onClose方法
第二次测试:断网/断电后14mins 28s(中间经历了1次心跳)触发client onClose方法
第三次测试:断网/断电后15mins 54s(中间经历了1次心跳)触发client onClose方法

8月3日补充 中间线路断网情况

补充一下中间线路断网情况:
如:中间nat设备断网(互联网环境中间nat设备是非常多的)或者server网络断开.这里大家注意client断网不算是中间线路断网,因为client端断网应用程序马上可以感知.但是client所在局域网的出口nat断开的就算是中间网络断开.

其实上边已经提到了server网络断开的情况,分别说明了server和client各自的检测办法.但是很多网络不稳定的情况,如:断开18分钟后网络又恢复了,这里涉及到一个重连机制,首先大家要明白当中间网络断开时实际上是两段各自维护本端tcp的.最终会触发tcp强制拆链(不发送四次挥手).分为两种情况讨论:

(1)网络恢复时,client已经将自己连接断开了,但是server认为网络还在连接中,和tcp假死很像.这种情况在服务端检测心跳超时之前,服务端推送消息是没有办法到达客户端的.但是这时服务端的试图发消息动作会触发服务端发现这个连接已经断开了. 从现象看ws重连时间为: 网络恢复时间——>server发现连接断开(server发消息)+超时/server心跳检测超时 (前提:网络断开后到网络恢复中间这段时间server没法过消息给client,如果发送过可能网络连接上立即触发服务端发现连接断开.)
(2)网络恢复时,client没有将自己连接断开,但是server已经断开.这种情况在client下一次心跳发送后会触发tcp重发,重发一定时间没有回复client也会进行强制拆链.ws重连时间为:网络恢复时间——>client下一次心跳时间+超时. (前提:网络断开后到网络恢复中间这段时间client没发过心跳给server,如果发送过可能网络连接上立即触发客户端发现连接断开.)

上边两个前提有点难懂,意思是当网络断开到网络恢复中间这段时间发送过消息,那么这个消息第一次发送肯定是到不了对端,但是这时就已经开始tcp重传机制了,可能网络恢复时恰好有一次重传,你的消息可以发到对端了,但是对端tcp端口已经关闭,tcp发生异常也就立即触发了本端tcp的关闭.

综上:tcp重连是需要时间的,这个时间肯定是越短越好,但是又不能太短,这个时间的确定大家可以参考本篇最后的测试.

7.如何做到支持几千个client同时在线人数

首先tomcat最大线程数默认肯定到不了几千,所以我们需要调tomcat最大线程数及运行内存。具体参数大家百度一下吧。我这边最大运行内存3个g,最大线程调到5k的情况下,3k个client同时在线是没问题的。再者大家注意下linux操作系统本身有些涉及到tcp连接的配置也可能需要修改。

8.后续

之前本来是想通过udp打洞方式实现内网推送的,但是上周花了一个周末的时间测试结果都不是很理想。有时间我会针对udp打洞原理专门写篇博客。

9.8月3日补充(相关测试)

这几天做了主流浏览器的测试工作,测试结果如下:

浏览器 心跳间隔:次/10mins 心跳间隔:(无心跳)
360浏览器 (不支持ws) (不支持ws)
ie10/ie11 48h稳定(只测了48h) 32mins断开(错误号:1005)
google浏览器 18h稳定(只测了18h) 30mins断开
火狐浏览器 5mins断开 5mins断开
傲游浏览器 18h稳定(只测了18h) 30mins断开
UC浏览器 18h稳定(只测了18h) 30mins断开
橘子浏览器 3h5mins 异常(且没有错误号e.code) 3h断开
搜狗浏览器 18h稳定(只测了18h) 30mins断开
QQ浏览器 18h稳定(只测了18h) 18h稳定(只测了18h)
猎豹浏览器 18h稳定(只测了18h) 18h稳定(只测了18h)

这里有几个点说明一下:
1.除特殊说明的橘子浏览器,其他浏览器断开时错误号均为1006
2.橘子浏览器:心跳间隔次/10mins情况下,发生异常且没有错误号,我初步判断为浏览器内部发生异常,可见橘子浏览器很不稳定呀.
3.火狐浏览器很特殊,心跳次/10min情况下也会断开,所以我这边把心跳时间调整为4分半,目前1小时连接正常.
4.ie浏览器无心跳情况下32mins断开,错误号1005,1005意思为超时.
结论:由此也证明了[问题探索]中的开启应用层心跳是非常有必要的.不然连接超过一定时间后自动断开,且心跳推荐时间为4分半,用以适配所有浏览器.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

WebSocket :用WebSocket实现推送你必须考虑的几个问题 的相关文章

随机推荐

  • Unity--人物走近调节景物的不透明度

    思路 给景物添加碰撞体后 写入对应脚本 再由角色触碰后调用脚本里改变透明度的函数 设置好对应的碰撞体 导入渐变的组件 设置一下 统一一个集中存放设置的脚本 using System Collections using System Coll
  • Java中的泛型(类,方法,接口)与可变参数

    泛型 泛型的本质是参数化类型 也就是说所操作的数据类型被指定为一个参数 参数化类型 将类型由原来的具体的类型参数化 然后在使用 调用时传入具体的类型 参数化类型可以用在类 方法和接口中 分别被称为 泛型类 泛型方法 泛型接口 泛型的好处 把
  • C++容器deque的用法

    目录 1 deque容器概念 2 deque对象的构造 2 1deque对象的默认构造 2 2deque对象的带参数构造 3 deque头部和末尾的添加移除操作 4 deque的数据存取 5 deque与迭代器 6 deque的赋值 7 d
  • 大数据-玩转数据-Dataphin调度节点参数设置

    一 节点参数配置项 默认参数值 yyyyMMdd 为业务日期 即当前日期的前一天T 1 yyyyMMdd 为执行日期 即当前日期T 默认参数 bizdate nodeid taskid nodeid是节点id taskid是节点生成实例时候
  • 计算机就业哀鸿遍野,为何高考生还疯狂涌入计算机专业?

    计算机行业不缺梦想 但缺愿意为了梦想不断进步的人 雷军可以说是认真实现自己梦想的代表人物 8月14日晚 雷军在三小时的2023 雷军年度演讲 和年度新品发布中 以 所有人生难题 都将在成长中找到答案 为主题 分享了过去30多年经历的几次关键
  • 机器学习 KNN算法

    参考B站简博士 一 KNN基本概念 最近邻 k Nearest Neighbors KNN 算法是一种分类算法 该算法的思想是 一个样本与数据集中的k个样本最相似 如果这k个样本中的大多数属于某一个类别 则该样本也属于这个类别 二 距离度量
  • 怎么给虚拟服务器安装系统,虚拟机安装系统图文详解

    虚拟机安装系统 虚拟机是一个非常实用的应用程序 因为我们可以在虚拟机里面测试一些软件的稳定性 并且虚拟机可以安装好几种系统 操作也非常的简便 下面 小编就给大家介绍一下虚拟机安装系统的操作步骤 虚拟机安装系统图文详解 准备工具 VMware
  • Unity2019_动画系统

    动画的播放控制 选中怪物 点击Avtar中的资源 在Asset资源目录下的模型 动画类型为泛型 工程目录下鼠标右键创建动画控制器 找到下面的动画拖到Entry中 添加动画参数类型Int Id 选中到Ready的箭头 右侧添加条件Id 1 为
  • [C语言]数据是如何存储的(整型篇)?(一)

    新手小白写的第一篇博客 记录自己的学习过程 希望我写的文章能给你们帮助 如果有不足的地方欢迎在评论区留言交流 首先我要介绍数据分为哪些类型 其次介绍整型在数据中是怎样存储的 最后介绍浮点型在内存中的存储 一 数据类型 基本的内置类型 cha
  • Docker映像存储在哪里? Docker容器路径介绍

    Docker has been widely adopted and is used to run and scale applications in production Additionally it can be used to st
  • Fundamental concepts about Lithography

    https dunham ece uw edu ee528 notes Chapter5 pdf Text Silicon VLSI technology fundamentals practice and modeling Aerial
  • 在Power Designer生成的类图中同时显示name和code

    在PowerDesigner生成的类图中同时显示name和code 问题 由显示name改为显示code 同时显示name和Code 问题 Power Designer是一款非常强大的设计工具 缺省的类图只显示name 一般地我们在name
  • 牛顿法(Newton’s method)

    牛顿法通常都是用来寻找一个根 同时也可以理解为最大化目标函数的局部二次近似 设我们的目标函数为f x 那么一个关于x0的二次近似就有 我们用f进行匹配 可以得到 如果b lt 0 g的最大值为a 得到更新规则 这是牛顿法在最优化方面的表述
  • 使用wps转换文本中001、002章节名为第001章、第002章

    1 首先ctrl f打开查找和替换 2 点击弹窗中高级搜索 选中使用通配符 3 可先输入 lt 0 9 3 尝试是否可以查找出文本内章节名 可以找到的话进行下一步 4 切换到替换tab 替换为输入框内输入第 1章 之后点击底部全部替换 即可
  • 万向区块链肖风:元宇宙的十大经济规则

    本文为万向区块链董事长兼总经理肖风为华泰证券研究所科技及电子行业首席分析师黄乐平 万向区块链首席经济学家邹传伟联合撰写的 元宇宙经济学 所作序言 元宇宙是什么 按照我的理解 元宇宙是一个由分布式网络技术 分布式账本和分布式社会 商业构成的三
  • 【Linux网络编程笔记】TCP短连接产生大量TIME_WAIT导致无法对外建立新TCP连接的原因及解决方法—实践篇

    上篇笔记主要介绍了与TIME WAIT相关的基础知识 本文则从实践出发 说明如何解决文章标题提出的问题 1 查看系统网络配置和当前TCP状态 在定位并处理应用程序出现的网络问题时 了解系统默认网络配置是非常必要的 以x86 64平台Linu
  • Python raise用法(超级详细,看了无师自通)

    当程序出现错误时 系统会自动引发异常 除此之外 Python 也允许程序自行引发异常 自行引发异常使用 raise 语句来完成 异常是一种很 主观 的说法 以下雨为例 假设大家约好明天去爬山郊游 如果第二天下雨了 这种情况会打破既定计划 就
  • 软件测试测试环境搭建很难?一天学会这份测试环境搭建教程

    如何搭建测试环境 这既是一道高频面试题 又是困扰很多小伙伴的难题 因为你在网上找到的大多数教程 乃至在一些培训机构的课程 都不会有详细的说明 你能找到的大多数项目 是在本机电脑环境搭建环境 或是别人已经搭建好的环境 你很难上手体验在服务器上
  • IntersectionObserver实现滚动加载

    加载模板及样式 template div class lazy more div
  • WebSocket :用WebSocket实现推送你必须考虑的几个问题

    目录 目录 WebSocket简介 项目背景硬件环境及客户端支持 本文研究内容 基于javaxwebsocket服务端代码源码后续补充git连接 客户端代码 问题探索 8月3日补充 中间线路断网情况 如何做到支持几千个client同时在线人