print.

 print. 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

 print. 赞了回答 · 8月18日

解决Go语言 websocket 一个端口建立多个连接会频繁断开

websocket发送消息要加锁,否则会crash

关注 3 回答 2

 print. 提出了问题 · 8月17日

webSocket 频繁断开

生产环境中 websocket 频繁触发重连机制

image.png

本地运行没有问题, 感觉可能是网络原因

问:
如果是断网导致连接断开, 是不是就应该快速重连?

如果不是, 那又可能是哪的原因...?





下面是代码

js代码:

var will_exit = 0;
var last_time_check_ws;

function main() {
    if (!window["WebSocket"]) {
        alert("您的浏览器不支持 WebSocket, 程序无法正常运行!!!");
        return
    }
    setmousedown();
    var url = "ws://" + window.location.host + "/dev_ws?id=" + mac();
    createWS(url);
}
//断线重连
function sundyn_ws_check(s) {
    var obj = eval('(' + s + ')');
    if ("time" in obj) {        
        last_time_check_ws = new Date().getTime();
        setTimeout(function () {
            if ((new Date().getTime() - last_time_check_ws) > 25000) {
                app.RetryConn();
            }
        }, 30000);
    }
}

function mac() {
    var s = window.location.toString();
    s = s.substr(s.indexOf("/devices/") + 9);
    s = s.substr(0, s.indexOf("/"));
    return s;
}

var socket;
function createWS(url) {
    console.log("准备连接" + url);
    socket = new WebSocket(url);

    socket.onclose = function () {
        console.log("连接被断开");
        //重连
        app.RetryConn();
    };

    socket.onopen = function () {
        console.log("已连接");
        app.CaptureScr();
    };

    socket.onerror = function (e) {
        console.log("error:" + e);
    };

    socket.onmessage = function (e) {
        console.log("get json:" + e.data);
        var obj = eval('(' + e.data + ')');

        if (!("eventType" in obj)) {
            console.log("未找到 eventType, 该json格式不受支持");
            return
        }

        var s = obj.eventType;
        var jh = e.data + "";
        s = s + "('" + jh + "')";
        eval(s);
    };
}

main();


处理websocket请求的代码:

http.Handle("/dev_ws", websocket.Handler(DeviceRequestProc))


处理websocket连接的代码:

func DeviceRequestProc(ws *websocket.Conn) {
    defer ws.Close()
    defer ws.Request().Body.Close()
    _ = ws.Request().ParseForm()
    mac := ws.Request().Form.Get("id")
    log.Println("设备", ws.Request().RemoteAddr, "[", mac, "] websocket 连接建立")

    dev := devInfo.getDeviceByMac(mac)
    if dev == nil {
        _ = websocket.Message.Send(ws, "未找到对应设备配置")
        log.Println("设备", ws.Request().RemoteAddr, "[", mac, "] websocket 连接未找到对应设备")
        return
    }
    dev.online = true
    dev.ws = ws
    //发送心跳消息
    dev.WSCheck()

    reply := ""
    for {
        //此处阻塞,等待有数据可读
        err := websocket.Message.Receive(ws, &reply)
        if  err != nil {
            break
        }
        dev.ProcessWSMsg(reply)
    }
    dev.online = false
    dev.ws = nil
    log.Println("设备", ws.Request().RemoteAddr, "[",ws.Request().Form.Get("id"),"] websocket 连接断开")

}


发送心跳消息的代码:

func (d *Device) WSCheck() {
    //go func() { }() 以并发的方式调用匿名函数funcx
    go func() {
        for d.online {
            d.WebsocketSendString(fmt.Sprintf(`{"eventType":"sundyn_ws_check","time":"%s"}`,
                  time.Now().Format("2006-01-02 15:04:05")))
            time.Sleep(time.Second * 20)
        }
    }()
}

// 通过 WebSocket 发送字符串
func (d *Device) WebsocketSendString(str string) bool {
    if !d.online {
        return false
    }
    log.Println(d.ws.Request().RemoteAddr)
    if err := websocket.Message.Send(d.ws, str); err != nil {
        log.Println("设备", d.ws.Request().RemoteAddr,
            "[",d.ws.Request().Form.Get("id"),"] websocket 发送消息失败:%s", err)
        _ = d.ws.Close()
        return false
    }
    return true
}

关注 1 回答 0

 print. 赞了问题 · 8月17日

解决TCP server 为什么一个端口可以建立多个连接?

如果是tcp client用同一个本地端口去连不同的两个服务器ip,连第二个时就会提示端口已被占用。
但服务器的监听端口,可以accept多次,建立多个socket;我的问题是服务器一个端口为什么能建立多个连接而客户端却不行呢?

关注 29 回答 5

 print. 关注了问题 · 8月17日

解决TCP server 为什么一个端口可以建立多个连接?

如果是tcp client用同一个本地端口去连不同的两个服务器ip,连第二个时就会提示端口已被占用。
但服务器的监听端口,可以accept多次,建立多个socket;我的问题是服务器一个端口为什么能建立多个连接而客户端却不行呢?

关注 29 回答 5

 print. 赞了回答 · 8月17日

解决Go语言 websocket 一个端口建立多个连接会频繁断开

能多贴一些代码吗?

关注 3 回答 2

 print. 赞了回答 · 8月14日

websocket长连接中如何定义连接和id的绑定关系

将websocket连接用ID做分类,你要推哪个ID,通过ID就能找到所有需要推送的链接,然后推出去即可。记得要让客户端做websocket重连机制,还要有websocket链接ping机制,及时剔除已经失去意义的链接

关注 4 回答 3

 print. 提出了问题 · 8月14日

解决Go语言 websocket 一个端口建立多个连接会频繁断开

建立websocket连接的URL: var url = "ws://" + window.location.host + "/dev_ws?id=" + mac();

下面是Go语言的发送心跳消息的代码

func (d *Device) WSCheck() {
    //go func() { }() 以并发的方式调用匿名函数funcx
    go func() {
        for d.online {
            d.WebsocketSendString(fmt.Sprintf(`{"eventType":"sundyn_ws_check","time":"%s"}`,
                  time.Now().Format("2006-01-02 15:04:05")))
            time.Sleep(time.Second * 20)
        }
    }()
}

// 通过 WebSocket 发送字符串
func (d *Device) WebsocketSendString(str string) bool {
    if !d.online {
        return false
    }
    log.Println(d.ws.Request().RemoteAddr)
    if err := websocket.Message.Send(d.ws, str); err != nil {
        log.Println("设备", d.ws.Request().RemoteAddr,
            "[",d.ws.Request().Form.Get("id"),"] websocket 发送消息失败:%s", err)
        _ = d.ws.Close()
        return false
    }
    return true
}


本机(192.168.1.182)通过如下URL建立websocket连接的时候, 前端接收心跳正常(20秒一次),连接正常
URL: ws://192.168.1.182:27017/dev_ws?id=F460E249E981
image.png


本机(192.168.1.182)/本机和其他设备(192.168.1.108)  同一时段通过如下两个URL建立websocket连接的时候, 前端接收心跳正常(20秒一次),连接都正常
URL: ws://192.168.1.182:27017/dev_ws?id=F460E249E981
URL: ws://192.168.1.182:27017/dev_ws?id=ECA86B96E961


本机(192.168.1.182)和其他设备(192.168.1.108)  同一时段通过同一个URL建立websocket连接的时候
URL: ws://192.168.1.182:27017/dev_ws?id=F460E249E981
前端接收心跳频率变快
image
频繁重连(ip是108和182)
image.png

前端js重连机制

var timeoutId = "";
function sundyn_ws_check() {
    if(timeoutId != ""){
        clearTimeout(timeoutId);
    }
    timeoutId = setTimeout(function (){ 
        app.RetryConn();
    }, 30000);
}

我刚开始做这块, 我还不太懂, 我感觉可能是websocket.Message.Send(d.ws, str)的问题, 不同设备连接同一个URL会影响消息的发送,只会给后连接的发送心跳消息, 导致前一个接收不到心跳重连


有谁帮忙解答一下, 是哪出的问题及怎么解决
万分感谢!






需要更多代码往下看

js:

function main() {
    if (!window["WebSocket"]) {
        alert("您的浏览器不支持 WebSocket, 程序无法正常运行!!!");
        return
    }
    setmousedown();
    var url = "ws://" + window.location.host + "/dev_ws?id=" + mac();
    createWS(url);
}

//心跳重连
var timeoutId = "";
function sundyn_ws_check() {
    if(timeoutId != ""){
        clearTimeout(timeoutId);
    }
    timeoutId = setTimeout(function (){ 
        app.RetryConn();
    }, 30000);
}


function mac() {
    var s = window.location.toString();
    s = s.substr(s.indexOf("/devices/") + 9);
    s = s.substr(0, s.indexOf("/"));
    return s;
}

function createWS(url) {
    console.log("准备连接" + url);
    var socket = new WebSocket(url);

    socket.onclose = function () {
        console.log("连接被断开");
        app.RetryConn();
    };

    socket.onopen = function () {
        console.log("已连接");
        app.CaptureScr();
    };

    socket.onerror = function (e) {
        console.log("error:" + e);
    };

    socket.onmessage = function (e) {
        console.log("get json:" + e.data);
        var obj = eval('(' + e.data + ')');

        if (!("eventType" in obj)) {
            console.log("未找到 eventType, 该json格式不受支持");
            return
        }

        var s = obj.eventType;
        var jh = e.data + "";
        s = s + "('" + jh + "')";
        eval(s);
    };
}

main();



处理websocket连接的代码:

http.Handle("/dev_ws", websocket.Handler(DeviceRequestProc))


func DeviceRequestProc(ws *websocket.Conn) {
    defer ws.Close()
    defer ws.Request().Body.Close()
    _ = ws.Request().ParseForm()
    mac := ws.Request().Form.Get("id")
    log.Println("设备", ws.Request().RemoteAddr, "[", mac, "] websocket 连接建立")

    dev := devInfo.getDeviceByMac(mac)
    if dev == nil {
        _ = websocket.Message.Send(ws, "未找到对应设备配置")
        log.Println("设备", ws.Request().RemoteAddr, "[", mac, "] websocket 连接未找到对应设备")
        return
    }
    dev.online = true
    dev.ws = ws
    dev.WSCheck()

    reply := ""
    for {
        //此处阻塞,等待有数据可读
        err := websocket.Message.Receive(ws, &reply)
        if  err != nil {
            break
        }
        dev.ProcessWSMsg(reply)
    }
    dev.online = false
    dev.ws = nil
    log.Println("设备", ws.Request().RemoteAddr, "[",ws.Request().Form.Get("id"),"] websocket 连接断开")

}

关注 3 回答 2

 print. 赞了文章 · 8月13日

打造稳定可靠的 websocket 连接

目的

可靠稳固的连接,无感的自动验证、数据同步、多终端同步数据,并保障用户数据安全、隐私,打造与 Telegram 一样专注于 IM 的应用。所以保障与服务端的可靠连接是最重要的事情之一。

little-chat 拥有上述特点,是体验更好的 IM 客户端。

websocket

websocket 连接并不可靠,想要建立稳定可靠的 websocket 链接,最理想的是在 oncloseonerr 的回调中尝试做重连。

但这两个回调有时候并不可靠,特别在移动端,当浏览器被退到后台运行时,即使断开异常也未必触发 oncloseonerr 回调。

那如何保证 websocket 正确地、持续地与服务端连接?

主动断开

浏览器的 DOM 提供了一个 visibilitychange 事件,于是我们便可以通过监听它判断 document.hidden 获知当前页面是否退到后台运行。

function handleVisibilityChange() {
  const isPageBeHide = document.hidden;
  if(isPageBeHide) {
    // ...do something
  }
}
document.addEventListener("visibilitychange", handleVisibilityChange);

经过测试,visibilitychange 事件在在 Chrome, Safari, Firefox 等主流浏览器都是起作用的,包括 Android 和 iOS 终端下的 webview,所以 React-Native 封装的 webview 也适用。

于是可以尝试这样的机制:

  1. 当页面被退到后台时,主动断开 websocket,停止心跳检测、消息发送,并清除 websocket 实例。
  2. 当页面被激活时,再次建立 websocket 连接。
let $WS

// 启动 websocket
function initWS() {
  if(!$WS) {
    $WS = new websocket('ws://api');
    $WS.onopen = () => {

    }
    $WS.onclose = () => {

    }
    $WS.onerror = () => {

    }
  }
}
// 关闭 websocket
function closeWS() {
  if($WS) {
    // 主动断开
    $WS.close();
    // 清除 websocket 实例
    $WS = null;
  }
}
// 重连 websocket
function reconnect() {
  closeWS();
  initWS();
}
// 应用启动时
initWS();

function handleVisibilityChange() {
  const isPageBeHide = document.hidden;
  if(isPageBeHide) {
    closeWS();
  } else {
    reconnect();
  }
}
document.addEventListener("visibilitychange", handleVisibilityChange);

上述可以主动掌握连接,但这不能确保 $WS.send() 会在 onopen 后执行,毕竟这操作的主动权在应用业务中,所以我们还需要确保消息一定在 onopen 之后发送。

未连接成功前的请求队列

以下为基础 websocket 封装类,设置了 unSendQueue 来保存未连接成功时的请求,具体如下(被隐去了很多细节):

import { EventEmitter, EventEmitterClass, Call } from 'basic-helper';

const onOpenMark = 'onOpen';
const onMessageMark = 'onMessage';

function wrapWSUrl(hostname) {
  if (!/wss?:\/\//.test(hostname)) {
    console.warn('websocket host 不正确', hostname);
  }
  return hostname;
}

class SocketHelper extends EventEmitterClass {
  // ... 被隐去的细节

  // 未连接成功前发起的请求
  unSendQueue: UnSendEntity = {};

  permissionsQueue: UnSendEntity = {};

  constructor(params: SocketParams) {
    super();
    this.params = params;
    this.initWS();
  }

  initWS = () => {
    if (this.connecting) return;
    this.connecting = true;
    const { apiHost } = this.params;
    if (!apiHost) {
      console.error('请传入 apiHost');
      return;
    }
    const wsApiHost = wrapWSUrl(apiHost);
    this.socket = new WebSocket(wsApiHost);
    this.socket.binaryType = 'arraybuffer';

    this.socket.onopen = this.onOpen;
    this.socket.onmessage = this.onMessage;
    this.socket.onerror = this.onErr;
    this.socket.onclose = this.onClose;
  }

  setReqQuquq = (requestID, success, fail) => {
    this.reqQueue[requestID.toString()] = {
      success,
      fail,
    };
  }

  clearQueue = () => {
    this.reqQueue = {};
    this.permissionsQueue = {};
  }

  send = (sendOptions) => {
    const {
      apiName, bufData, requestID,
      success, fail, needAuth
    } = sendOptions;
    if (!this.connected) {
      /**
       * 如果还没 onOpen 打开的,放入待发送队列中
       */
      // console.error('尚未连接');
      this.unSendQueue[requestID.toString()] = sendOptions;
      if (!this.isClosed) this.initWS();
    } else if (this.socket) {
      this.socket.send(data);
      this.setReqQuquq(requestID, success, fail);
    }
  }

  /**
   * 在 onopen 的时候发送在未 open 时候发送请求
   */
  sendNotComplete = (queue: UnSendEntity) => {
    const unSendList = Object.keys(queue);
    if (unSendList.length === 0) return;
    unSendList.forEach((requestID) => {
      const sendOptions = queue[requestID];
      this.send(sendOptions);
      delete queue[requestID];
    });
  }

  onOpen = () => {
    // this.params.onOpen();
    this.connected = true;
    this.connecting = false;
    this.emit(onOpenMark, {});
    this.emit(CONNECT_READY, {});
    // 在 onopen 发送未连接时发起的请求
    this.sendNotComplete(this.unSendQueue);
    this.isClosed = false;
  }

  onMessage = (event) => {
  }

  onErr = (e) => {
    console.log('onErr');
    /** 如果发生错误,则主动关闭 websocket 链接 */
    this.socket && this.socket.close();
  }

  onClose = (e) => {
    console.log('onClose');
    this.handleException(e);
  }

  handleException = (event) => {
    this.connected = false;
    this.socket = null;
    this.isClosed = true;
    this.clearQueue();
    EventEmitter.emit(ON_CONNECT_CLOSE, event);
  }
}

export default SocketHelper;

以下为基于 SocketHelper 的更进一步的封装(让 API 的用法与 HTTP 一致):

import SocketHelper from './socket';

let $WS;
let prevWSParams;

function GetWS() {
  if (!$WS) console.error(SDKErrorDesc);
  return $WS;
}

function WSSend<T extends Api, S>(api: T, apiName: string, data?, needAuth = true): Promise<S> {
  return new Promise((resolve, reject) => {
    if (!$WS) {
      console.error(SDKErrorDesc);
      return reject(SDKErrorDesc);
    }
    const requestID = BigInt(UUID(16));
    const msgWrite = api.create(data);
    const bufData = api.encode(msgWrite).finish();

    // const finalData = encodeData(apiName, bufData, requestID);
    $WS.send({
      apiName,
      bufData,
      requestID,
      success: (res) => {
        resolve(res);
      },
      fail: (res) => {
        failResHandler(res);
        reject(res);
      },
      needAuth
    });
  });
}

function InitSDK(params: Params = prevWSParams) {
  /** 保存上一个参数 */
  if (params) prevWSParams = params;
  const { apiHost } = params;
  $WS = new SocketHelper({
    apiHost
  });
  return $WS;
}

/**
 * 检查是否正常链接
 */
function CheckConnectState() {
  let isConnecting = false;
  if (!$WS) return isConnecting;
  isConnecting = $WS.connected;
  return isConnecting;
}

/**
 * 关闭 websocket 链接
 */
function CloseWS() {
  if ($WS) {
    if ($WS.socket) $WS.socket.close();
    $WS = null;
  }
}

export {
  InitSDK, GetWS, WSSend, CheckConnectState, CloseWS
};

以下为发起请求的 API:

export async function ApplyLogin(form: IUserLoginReq) {
  const res = await WSSend<typeof UserLoginReq, IUserLoginResp>(
    UserLoginReq, 'UserLoginReq', form, false
  );
  if (res.SessionID) {
    /**
     * 1. 成功后设置 sessionID
     * 2. 设置 websocket 的权限
     */
    setHeaderSSID(res.SessionID);
    GetWS().setPermissions(true);
  }
  const result = Object.assign({}, res, {
    UserName: form.UserName,
    ...res.User
  });
  return result;
}

最后在业务应层调用此 API:

const business = () => {
  ApplyLogin({
    // ...
  })
    .then((res) => {
      // ...
    })
}

当然还有一个问题是,有少部分请求可以不带 session,例如登陆,但是其他请求需要,这个需要在 SocketHelper 中再做进一步的验证封装,在未验证通过时,把需要验证的请求缓存到队列,然后连接成功并且验证成功后再发送,这样可以达到无感登陆地数据同步的体验。

详情参考 little-chat

查看原文

赞 3 收藏 0 评论 0

 print. 关注了专栏 · 8月13日

SegmentFault 行业快讯

第一时间为开发者提供行业相关的实时热点资讯

关注 25113

 print. 关注了专栏 · 8月13日

Java 小词典

百无一用是书生,春鸟秋虫自做声。

关注 927

认证与成就

  • 获得 0 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 8月12日
个人主页被 86 人浏览