Тайм-аут проверки сервера Socket.io для некоторых соединений - PullRequest
0 голосов
/ 17 февраля 2020

У меня проблемы с использованием клиента socket.io JS и сервера, на котором время от времени соединения ping-соединений пропадают. Я искал высоко и низко через SO и остальные сети.

Некоторый контекст проекта - это приложение Electron / ReactJS, у которого есть два отдельных windows, где socket.io клиент подключается при загрузке приложения, при условии, что пользователь вошел в систему. Если пользователь входит в систему после загрузки приложения, вызывается та же функция для подключения к сокету:

  const protocol = Env.isDev() ? 'http' : 'https'
  const socketServer = `${protocol}://${LocalConfig.servers.websocket}:80`
  const socketReceiverId = createReceiverId()

  /**
   * Initiates the websocket connection
   */
  export function init() {

    // Get authourizer ID
    const authorizerId = getAuthId()

    // only allow in the renderer and we have an authorizer id
    // and if the socket connection is null. Socket will not be
    // null if this method is called after HMR
    if (Env.isRenderer() && authorizerId && !getCurrentSocket()) {
      const socket = sockio(`${socketServer}/notification`, {
        timeout: 5000,
        reconnection: false,
        transportOptions: {
          polling: {
            extraHeaders: {
              'origin': `gui://${process.env.PLATFORM}`,
              'x-receiver-token': socketReceiverId,
              'x-authorization': authorizerId
            }
          }
        }
      })
      socket.on('s:connected', onConnected) // called when connection made (custom event with extra data
        .on('n:push', onNotification)       // called when a notification is received
        .on('s:close', onServerClose)       // called when server shuts down
        .on('disconnect', onDisconnect)     // called on disconnect
        .on('error', console.warn)
    }
  }

Мне пришлось оставить reconnection для false потому что мне нужно управлять переподключениями вручную, потому что если пользователь выходит из системы, мы не будем переподключаться, но переподключаемся после успешного входа в систему

Я также использую опрос, чтобы я мог отправлять дополнительные заголовки, включая источник, авторизацию и receiver-token, который представляет собой случайный UUID, который хранится в LocalStorage для уникальной идентификации экземпляра приложения и будет room, к которому мы присоединяемся, поэтому уведомления отправляются на соответствующее устройство, будь то это приложение или мобильный (который будет использовать один и тот же интерфейс веб-сокета)

Когда приложение загружается изначально и веб попытка подключения через сокет, 90% времени, оба windows успешно подключаются, и приходят уведомления pu sh - остальные 10% времени может подключаться только одно окно, тогда как другое не будет (хотя, войдя в систему на клиенте, я вижу, что вызывается обратный вызов onConnected). В конце концов, оба подключатся успешно, но в идеале они должны подключиться мгновенно с первой попытки.

Что действительно портит, так это когда сервер перезагружается (что будет происходить время от времени, как новый код отправляется на сервер и служба перезапускается). Однако, когда сервер отключается, всем клиентам, подключенным в данный момент, отправляется сообщение, где вызывается настраиваемая схема повторного подключения.

Именно здесь начинаются основные проблемы - будь то ручное переподключение или автоматическое c, опрос продолжает сбой с ошибкой ping timeout, даже несмотря на то, что вызывается обратный вызов onConnected.

В конечном итоге все соединения устанавливаются и соединяются - иногда через минуту или две, иногда до 10 минут с половиной час. Это все чрезвычайно случайно.

Обратите внимание, что сейчас все на моем локальном компьютере под управлением Ubuntu 18.04 LTS, поэтому задержка должна быть почти нулевой.

Я вставил подробный вывод отладки здесь: https://pastebin.com/TR6qPe0R

Вот основной код сервера:

import process from 'process'
import http from 'http'
import sockio from 'socket.io'
import { BindAll } from 'lodash-decorators'
import { Handlers } from './Handlers'
import { Receiver } from './Receiver'
import { RedisPool } from './RedisPool'
import { Log } from './Logger'

interface NamespaceGroup {
  ns: SocketIO.Namespace
  receiver: Receiver
}

@BindAll()
class Server {

  private _port: number
  private _socketServer: sockio.Server
  private _pollingServer: http.Server
  private _namespaces: NamespaceGroup[] = []

  constructor() {
    this._port = parseInt(process.env['PORT']) || 18000
    this._pollingServer = http.createServer(Handlers.handleHttpRequest)
  }

  startServer() {
    this._pollingServer.listen(this._port, this.onServerStart)
    process.on('SIGINT', this.close)
    process.on('SIGUSR2', this.close)
  }

  private async onServerStart() {

    Log.info(`Server started on port ${this._port}`)

    this._socketServer = sockio(this._pollingServer)
    this._socketServer.origins(Handlers.handleOrigin)
    this._socketServer.use(Handlers.handleAuthorization)

    const nsNotify = this._socketServer.of('/notification')
      .on('connection', Handlers.handleNotificationConnection)

    this._namespaces.push({
      ns: nsNotify,
      receiver: new Receiver(nsNotify)
    })
  }

  private close() {
    Log.info('Closing server. Killing process')
    for (const nsGroup of this._namespaces) {
      nsGroup.ns.emit('s:close')
    }
    this._socketServer.close()
    RedisPool.closeAll()
    process.exit(0)
  }
}

const server = new Server()
export default server

И обработчики:

import { IncomingMessage, ServerResponse } from 'http'
import { Socket } from 'socket.io'
import { Queries } from './Queries'
import { Log } from './Logger'
import { OutgoingMessage } from './Receiver'

export namespace Handlers {

  const allowedOrigins = [
    'gui://win32',
    'gui://darwin',
    'gui://linux',
    'ast://android',
    'ast://ios',
    'mob://android',
    'mob://ios',
  ]

  export function handleOrigin(origin: string, callback: (error: string | null, success: boolean) => void) {
    if (allowedOrigins.indexOf(origin) == -1) {
      callback('Invalid Origin: ' + origin, false)
    } else {
      callback(null, true)
    }
  }

  export async function handleAuthorization(socket: Socket, next: (err?: any) => void) {

    const auth = socket.handshake.headers['x-authorization']
    const receiverToken = socket.handshake.headers['x-receiver-token']

    if (!auth) {
      next(new Error('Authorization not provided'))
      socket.disconnect()
    } else {
      try {
        const credentials = await Queries.findClientCredentials(auth)
        if (!!credentials) {
          await Queries.saveCurrentToken(auth, receiverToken)
          next()
        } else {
          Log.warn('Client rejected [auth:%s] (Invalid Authorization)', auth)
          next(new Error('Invalid Authorization: ' + auth))
          socket.disconnect()
        }
      } catch (err) {
        next(err)
        Log.error('Client auth error', err)
        socket.disconnect()
      }
    }
  }

  export async function handleNotificationConnection(socket: Socket) {

    const auth = socket.handshake.headers['x-authorization']
    const receiverToken = socket.handshake.headers['x-receiver-token']

    const namespace = socket.nsp
    const ns = namespace.name

    // Emit connection message
    socket.emit('s:connected', ns, socket.id) // lets the client know connection is OK

    // Join room specific to an application instance
    socket.join(receiverToken)

    Log.debug('Connect: Client connected to %s [auth:%s] - %s (%d active sockets)',
      ns, auth, socket.id, socket.adapter.rooms[receiverToken].length)

    // Listens to the c:close (client request close) event
    socket.on('c:close', async reason => {
      socket.leave(receiverToken)
      socket.disconnect(true)
      Log.debug('Disconnect: (%s) %s : %s', reason, receiverToken, socket.id)
    })
  }

  // Default HTTP handler for the main Node HTTP server
  export function handleHttpRequest(request: IncomingMessage, response: ServerResponse) {
    response.statusCode = 200
    response.setHeader('Content-Type', 'text/plain')
    response.end(`${request.method} ${request.url}\n`)
  }
}

Остальная часть кода на сервере side работает как надо, например, RedisPool и экземпляры класса Receiver. Проблема связана с подключением, поэтому я включил только класс Server и пространство имен Handlers.

И на клиенте соответствующий код (то же пространство имен, что и у функции init выше):

  // Flag to tell us if the disconnection
  // was instigated via the client after a
  // log out, so we know if we need to
  // reconnect
  let isManualDisconnect = false

  /**
   * Manually disconnect from the websocket server
   * normally used when user logs out
   */
  export function disconnect() {
    const socket = getCurrentSocket()
    isManualDisconnect = true // lets us know not to attempt reconnects
    socket.emit('c:close', 'manual client disconnect', socket.id)
  }

  /**
   * Handle a successful connection to the websocket server
   * @param ns the namespace connected to (should always be /notification)
   * @param socketId the socket id (SID)
   */
  function onConnected(ns: string, socketId: string) {
    console.log('Connection to %s Accepted. Receiver ID: %s', ns, socketId)
    isManualDisconnect = false
  }

  /**
   * Handles the disconnection from the socket server
   * @param reason the reason for the disconnect
   */
  function onDisconnect(reason: DisconnectReason) {
    console.log('Disconnected (%s) Manual:', reason, isManualDisconnect)
    if (!isManualDisconnect) {
      if (getAuthId())
        getCurrentSocket().connect()
    }
  }

  /**
   * Handles a socket being disconnected when the
   * socket server shuts down
   */
  function onServerClose() {
    console.log('Socket server interrupt')
    window.setTimeout(reconnect, 5000)
  }

  /**
   * Attempts a reconnection every 5 seconds until
   * a conneciton is re-established
   */
  function reconnect() {
    const socket = getCurrentSocket()
    console.log('Attempting reconnect')
    if (socket.disconnected) {
      console.log('Socket confirmed disconnected. Opening...')
      socket.open()
    } else {
      window.setTimeout(reconnect, 5000)
    }
  }

  /**
   * Gets the current authorization id from local storage
   */
  function getAuthId(): string {
    return window.localStorage.getItem('auth')
  }

  function getCurrentSocket(): SocketIOClient.Socket {
    const manager = sockio.managers[socketServer]
    if (manager)
      return manager.nsps['/notification']
  }

  /**
   * Gets the current receiver id (application instance id)
   */
  export function getReceiverId(): string {
    return socketReceiverId
  }

Наконец, конфигурация NGINX для сокет-сервера:

server {
    listen          80;
    server_name     [server name here];
    location / {
        proxy_pass          "http://127.0.0.1:18000";
        proxy_set_header    Upgrade $http_upgrade;
        proxy_set_header    Connection 'upgrade';
        proxy_set_header    X-Forwarded-For $remote_addr;
        proxy_set_header    Host $host;
        proxy_http_version  1.1;
    }
}

Я понимаю, что примеры кода многословны, но я целый день пытался найти проблему, но без решений. Другая проблема заключается в том, что почти все примеры для socket.io включают приложения чата, которые не охватывают авторизацию, происхождение или способы правильного отключения, и в каком порядке.

Клиентские и серверные версии Socket.io одинаковы (2.2.0)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...