У меня проблемы с использованием клиента 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)