feat: 更新WebSocket 优化逻辑

Signed-off-by: MiaoWoo <admin@yumc.pw>
This commit is contained in:
MiaoWoo 2021-08-03 16:59:43 +08:00
parent def62e2940
commit 586b6acbbc
45 changed files with 5465 additions and 2376 deletions

View File

@ -23,7 +23,7 @@ export class WebSocketManager {
}
}
export const managers = new WebSocketManager()
export const manager = new WebSocketManager()
export class WebSocket extends EventEmitter {
public static CONNECTING = 0
@ -31,6 +31,7 @@ export class WebSocket extends EventEmitter {
public static CLOSING = 2
public static CLOSED = 3
public binaryType: 'blob' | 'arraybuffer'
protected manager: WebSocketManager
protected _url: string
protected _headers: WebSocketHeader = {}
@ -39,6 +40,7 @@ export class WebSocket extends EventEmitter {
constructor(url: string, subProtocol: string = '', headers: WebSocketHeader = {}) {
super()
this.manager = manager
this._url = url
this._headers = headers
try {
@ -51,12 +53,12 @@ export class WebSocket extends EventEmitter {
}
this.client.on('open', (event) => {
this.onopen?.(event)
managers.add(this)
manager.add(this)
})
this.client.on('message', (event) => this.onmessage?.(event))
this.client.on('close', (event) => {
this.onclose?.(event)
managers.del(this)
manager.del(this)
})
this.client.on('error', (event) => this.onerror?.(event))
setTimeout(() => this.client.connect(), 20)

View File

@ -0,0 +1,21 @@
const PACKET_TYPES = Object.create(null) // no Map = no polyfill
PACKET_TYPES["open"] = "0"
PACKET_TYPES["close"] = "1"
PACKET_TYPES["ping"] = "2"
PACKET_TYPES["pong"] = "3"
PACKET_TYPES["message"] = "4"
PACKET_TYPES["upgrade"] = "5"
PACKET_TYPES["noop"] = "6"
const PACKET_TYPES_REVERSE = Object.create(null)
Object.keys(PACKET_TYPES).forEach(key => {
PACKET_TYPES_REVERSE[PACKET_TYPES[key]] = key
})
const ERROR_PACKET = { type: "error", data: "parser error" }
export = {
PACKET_TYPES,
PACKET_TYPES_REVERSE,
ERROR_PACKET
}

View File

@ -0,0 +1,48 @@
const { PACKET_TYPES_REVERSE, ERROR_PACKET } = require("./commons")
export const decodePacket = (encodedPacket, binaryType) => {
if (typeof encodedPacket !== "string") {
return {
type: "message",
data: mapBinary(encodedPacket, binaryType)
}
}
const type = encodedPacket.charAt(0)
if (type === "b") {
const buffer = Buffer.from(encodedPacket.substring(1), "base64")
return {
type: "message",
data: mapBinary(buffer, binaryType)
}
}
if (!PACKET_TYPES_REVERSE[type]) {
return ERROR_PACKET
}
return encodedPacket.length > 1
? {
type: PACKET_TYPES_REVERSE[type],
data: encodedPacket.substring(1)
}
: {
type: PACKET_TYPES_REVERSE[type]
}
}
const mapBinary = (data, binaryType) => {
switch (binaryType) {
case "arraybuffer":
return Buffer.isBuffer(data) ? toArrayBuffer(data) : data
case "nodebuffer":
default:
return data // assuming the data is already a Buffer
}
}
const toArrayBuffer = buffer => {
const arrayBuffer = new ArrayBuffer(buffer.length)
const view = new Uint8Array(arrayBuffer)
for (let i = 0; i < buffer.length; i++) {
view[i] = buffer[i]
}
return arrayBuffer
}

View File

@ -0,0 +1,26 @@
const { PACKET_TYPES } = require("./commons")
export const encodePacket = ({ type, data }, supportsBinary, callback) => {
console.trace('encodePacket', type, JSON.stringify(data))
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
const buffer = toBuffer(data)
return callback(encodeBuffer(buffer, supportsBinary))
}
// plain string
return callback(PACKET_TYPES[type] + (data || ""))
}
const toBuffer = data => {
if (Buffer.isBuffer(data)) {
return data
} else if (data instanceof ArrayBuffer) {
return Buffer.from(data)
} else {
return Buffer.from(data.buffer, data.byteOffset, data.byteLength)
}
}
// only 'message' packets can contain binary, so the type prefix is not needed
const encodeBuffer = (data, supportsBinary) => {
return supportsBinary ? data : "b" + data.toString("base64")
}

View File

@ -0,0 +1,42 @@
import { encodePacket } from "./encodePacket"
import { decodePacket } from "./decodePacket"
const SEPARATOR = String.fromCharCode(30) // see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text
const encodePayload = (packets, callback) => {
// some packets may be added to the array while encoding, so the initial length must be saved
const length = packets.length
const encodedPackets = new Array(length)
let count = 0
packets.forEach((packet, i) => {
// force base64 encoding for binary packets
encodePacket(packet, false, encodedPacket => {
encodedPackets[i] = encodedPacket
if (++count === length) {
callback(encodedPackets.join(SEPARATOR))
}
})
})
}
const decodePayload = (encodedPayload, binaryType) => {
const encodedPackets = encodedPayload.split(SEPARATOR)
const packets = []
for (let i = 0; i < encodedPackets.length; i++) {
const decodedPacket = decodePacket(encodedPackets[i], binaryType)
packets.push(decodedPacket)
if (decodedPacket.type === "error") {
break
}
}
return packets
}
export default {
protocol: 4,
encodePacket,
encodePayload,
decodePacket,
decodePayload
}

View File

@ -0,0 +1,27 @@
/**
* Module dependencies.
*/
import * as server from "../server"
// const http = require("http")
// const Server = require("./server")
import { Server } from './server'
/**
* Captures upgrade requests for a http.Server.
*
* @param {http.Server} server
* @param {Object} options
* @return {Server} engine server
* @api public
*/
function attach(srv, options) {
const engine = new Server(options)
engine.attach(server.attach(srv, options), options)
return engine
}
export = {
attach
}

View File

@ -0,0 +1,690 @@
const qs = require("querystring")
const parse = require("url").parse
// const base64id = require("base64id")
import transports from './transports'
import { EventEmitter } from 'events'
// const EventEmitter = require("events").EventEmitter
import { Socket } from './socket'
// const debug = require("debug")("engine")
const debug = function (...args) { }
// const cookieMod = require("cookie")
// const DEFAULT_WS_ENGINE = require("ws").Server;
import { WebSocketServer } from '../server'
import { Transport } from './transport'
const DEFAULT_WS_ENGINE = WebSocketServer
import { Request } from '../server/request'
import { WebSocketClient } from '../server/client'
export class Server extends EventEmitter {
public static errors = {
UNKNOWN_TRANSPORT: 0,
UNKNOWN_SID: 1,
BAD_HANDSHAKE_METHOD: 2,
BAD_REQUEST: 3,
FORBIDDEN: 4,
UNSUPPORTED_PROTOCOL_VERSION: 5
}
public static errorMessages = {
0: "Transport unknown",
1: "Session ID unknown",
2: "Bad handshake method",
3: "Bad request",
4: "Forbidden",
5: "Unsupported protocol version"
}
private clients = {}
private clientsCount = 0
public opts: any
private corsMiddleware: any
private ws: any
private perMessageDeflate: any
constructor(opts: any = {}) {
super()
this.opts = Object.assign(
{
wsEngine: DEFAULT_WS_ENGINE,
pingTimeout: 20000,
pingInterval: 25000,
upgradeTimeout: 10000,
maxHttpBufferSize: 1e6,
transports: Object.keys(transports),
allowUpgrades: true,
httpCompression: {
threshold: 1024
},
cors: false,
allowEIO3: false
},
opts
)
// if (opts.cookie) {
// this.opts.cookie = Object.assign(
// {
// name: "io",
// path: "/",
// httpOnly: opts.cookie.path !== false,
// sameSite: "lax"
// },
// opts.cookie
// )
// }
// if (this.opts.cors) {
// this.corsMiddleware = require("cors")(this.opts.cors)
// }
// if (opts.perMessageDeflate) {
// this.opts.perMessageDeflate = Object.assign(
// {
// threshold: 1024
// },
// opts.perMessageDeflate
// )
// }
// this.init()
}
// /**
// * Initialize websocket server
// *
// * @api private
// */
// init() {
// if (!~this.opts.transports.indexOf("websocket")) return
// if (this.ws) this.ws.close()
// this.ws = new this.opts.wsEngine({
// noServer: true,
// clientTracking: false,
// perMessageDeflate: this.opts.perMessageDeflate,
// maxPayload: this.opts.maxHttpBufferSize
// })
// if (typeof this.ws.on === "function") {
// this.ws.on("headers", (headersArray, req) => {
// // note: 'ws' uses an array of headers, while Engine.IO uses an object (response.writeHead() accepts both formats)
// // we could also try to parse the array and then sync the values, but that will be error-prone
// const additionalHeaders = {}
// const isInitialRequest = !req._query.sid
// if (isInitialRequest) {
// this.emit("initial_headers", additionalHeaders, req)
// }
// this.emit("headers", additionalHeaders, req)
// Object.keys(additionalHeaders).forEach(key => {
// headersArray.push(`${key}: ${additionalHeaders[key]}`)
// })
// })
// }
// }
/**
* Returns a list of available transports for upgrade given a certain transport.
*
* @return {Array}
* @api public
*/
upgrades(transport): Array<any> {
if (!this.opts.allowUpgrades) return []
return transports[transport].upgradesTo || []
}
// /**
// * Verifies a request.
// *
// * @param {http.IncomingMessage}
// * @return {Boolean} whether the request is valid
// * @api private
// */
// verify(req, upgrade, fn) {
// // transport check
// const transport = req._query.transport
// if (!~this.opts.transports.indexOf(transport)) {
// debug('unknown transport "%s"', transport)
// return fn(Server.errors.UNKNOWN_TRANSPORT, { transport })
// }
// // 'Origin' header check
// const isOriginInvalid = checkInvalidHeaderChar(req.headers.origin)
// if (isOriginInvalid) {
// const origin = req.headers.origin
// req.headers.origin = null
// debug("origin header invalid")
// return fn(Server.errors.BAD_REQUEST, {
// name: "INVALID_ORIGIN",
// origin
// })
// }
// // sid check
// const sid = req._query.sid
// if (sid) {
// if (!this.clients.hasOwnProperty(sid)) {
// debug('unknown sid "%s"', sid)
// return fn(Server.errors.UNKNOWN_SID, {
// sid
// })
// }
// const previousTransport = this.clients[sid].transport.name
// if (!upgrade && previousTransport !== transport) {
// debug("bad request: unexpected transport without upgrade")
// return fn(Server.errors.BAD_REQUEST, {
// name: "TRANSPORT_MISMATCH",
// transport,
// previousTransport
// })
// }
// } else {
// // handshake is GET only
// if ("GET" !== req.method) {
// return fn(Server.errors.BAD_HANDSHAKE_METHOD, {
// method: req.method
// })
// }
// if (!this.opts.allowRequest) return fn()
// return this.opts.allowRequest(req, (message, success) => {
// if (!success) {
// return fn(Server.errors.FORBIDDEN, {
// message
// })
// }
// fn()
// })
// }
// fn()
// }
/**
* Prepares a request by processing the query string.
*
* @api private
*/
prepare(req) {
// try to leverage pre-existing `req._query` (e.g: from connect)
if (!req._query) {
req._query = ~req.url.indexOf("?") ? qs.parse(parse(req.url).query) : {}
}
}
/**
* Closes all clients.
*
* @api public
*/
close() {
debug("closing all open clients")
for (let i in this.clients) {
if (this.clients.hasOwnProperty(i)) {
this.clients[i].close(true)
}
}
if (this.ws) {
debug("closing webSocketServer")
this.ws.close()
// don't delete this.ws because it can be used again if the http server starts listening again
}
return this
}
// /**
// * Handles an Engine.IO HTTP request.
// *
// * @param {http.IncomingMessage} request
// * @param {http.ServerResponse|http.OutgoingMessage} response
// * @api public
// */
// handleRequest(req, res) {
// debug('handling "%s" http request "%s"', req.method, req.url)
// this.prepare(req)
// req.res = res
// const callback = (errorCode, errorContext) => {
// if (errorCode !== undefined) {
// this.emit("connection_error", {
// req,
// code: errorCode,
// message: Server.errorMessages[errorCode],
// context: errorContext
// })
// abortRequest(res, errorCode, errorContext)
// return
// }
// if (req._query.sid) {
// debug("setting new request for existing client")
// this.clients[req._query.sid].transport.onRequest(req)
// } else {
// const closeConnection = (errorCode, errorContext) =>
// abortRequest(res, errorCode, errorContext)
// this.handshake(req._query.transport, req, closeConnection)
// }
// }
// if (this.corsMiddleware) {
// this.corsMiddleware.call(null, req, res, () => {
// this.verify(req, false, callback)
// })
// } else {
// this.verify(req, false, callback)
// }
// }
/**
* generate a socket id.
* Overwrite this method to generate your custom socket id
*
* @param {Object} request object
* @api public
*/
generateId(req) {
return req.id
}
/**
* Handshakes a new client.
*
* @param {String} transport name
* @param {Object} request object
* @param {Function} closeConnection
*
* @api private
*/
// @java-patch sync handshake
handshake(transportName, req, closeConnection: (code: number) => void) {
console.debug('engine.io server handshake transport', transportName, 'from', req.url)
const protocol = req._query.EIO === "4" ? 4 : 3 // 3rd revision by default
if (protocol === 3 && !this.opts.allowEIO3) {
debug("unsupported protocol version")
this.emit("connection_error", {
req,
code: Server.errors.UNSUPPORTED_PROTOCOL_VERSION,
message:
Server.errorMessages[Server.errors.UNSUPPORTED_PROTOCOL_VERSION],
context: {
protocol
}
})
closeConnection(Server.errors.UNSUPPORTED_PROTOCOL_VERSION)
return
}
let id
try {
id = this.generateId(req)
} catch (e) {
console.debug("error while generating an id")
this.emit("connection_error", {
req,
code: Server.errors.BAD_REQUEST,
message: Server.errorMessages[Server.errors.BAD_REQUEST],
context: {
name: "ID_GENERATION_ERROR",
error: e
}
})
closeConnection(Server.errors.BAD_REQUEST)
return
}
console.debug('engine.io server handshaking client "' + id + '"')
try {
var transport: Transport = new transports[transportName](req)
if ("websocket" !== transportName) {
throw new Error('Unsupport polling at MiaoScript!')
}
// if ("polling" === transportName) {
// transport.maxHttpBufferSize = this.opts.maxHttpBufferSize
// transport.httpCompression = this.opts.httpCompression
// } else if ("websocket" === transportName) {
transport.perMessageDeflate = this.opts.perMessageDeflate
// }
if (req._query && req._query.b64) {
transport.supportsBinary = false
} else {
transport.supportsBinary = true
}
} catch (e) {
console.ex(e)
this.emit("connection_error", {
req,
code: Server.errors.BAD_REQUEST,
message: Server.errorMessages[Server.errors.BAD_REQUEST],
context: {
name: "TRANSPORT_HANDSHAKE_ERROR",
error: e
}
})
closeConnection(Server.errors.BAD_REQUEST)
return
}
console.debug(`engine.io server create socket ${id} from transport ${transport.name} protocol ${protocol}`)
const socket = new Socket(id, this, transport, req, protocol)
transport.on("headers", (headers, req) => {
const isInitialRequest = !req._query.sid
if (isInitialRequest) {
if (this.opts.cookie) {
headers["Set-Cookie"] = [
// cookieMod.serialize(this.opts.cookie.name, id, this.opts.cookie)
]
}
this.emit("initial_headers", headers, req)
}
this.emit("headers", headers, req)
})
transport.onRequest(req)
this.clients[id] = socket
this.clientsCount++
socket.once("close", () => {
delete this.clients[id]
this.clientsCount--
})
this.emit("connection", socket)
}
// /**
// * Handles an Engine.IO HTTP Upgrade.
// *
// * @api public
// */
// handleUpgrade(req, socket, upgradeHead) {
// this.prepare(req)
// this.verify(req, true, (errorCode, errorContext) => {
// if (errorCode) {
// this.emit("connection_error", {
// req,
// code: errorCode,
// message: Server.errorMessages[errorCode],
// context: errorContext
// })
// abortUpgrade(socket, errorCode, errorContext)
// return
// }
// const head = Buffer.from(upgradeHead) // eslint-disable-line node/no-deprecated-api
// upgradeHead = null
// // delegate to ws
// this.ws.handleUpgrade(req, socket, head, websocket => {
// this.onWebSocket(req, socket, websocket)
// })
// })
// }
/**
* Called upon a ws.io connection.
*
* @param {ws.Socket} websocket
* @api private
*/
onWebSocket(req: Request, socket, websocket: WebSocketClient) {
websocket.on("error", onUpgradeError)
if (
transports[req._query.transport] !== undefined &&
!transports[req._query.transport].prototype.handlesUpgrades
) {
console.debug("transport doesnt handle upgraded requests")
websocket.close()
return
}
// get client id
const id = req._query.sid
// keep a reference to the ws.Socket
req.websocket = websocket
if (id) {
const client = this.clients[id]
if (!client) {
console.debug("upgrade attempt for closed client")
websocket.close()
} else if (client.upgrading) {
console.debug("transport has already been trying to upgrade")
websocket.close()
} else if (client.upgraded) {
console.debug("transport had already been upgraded")
websocket.close()
} else {
console.debug("upgrading existing transport")
// transport error handling takes over
websocket.removeListener("error", onUpgradeError)
const transport = new transports[req._query.transport](req)
if (req._query && req._query.b64) {
transport.supportsBinary = false
} else {
transport.supportsBinary = true
}
transport.perMessageDeflate = this.perMessageDeflate
client.maybeUpgrade(transport)
}
} else {
// transport error handling takes over
websocket.removeListener("error", onUpgradeError)
// const closeConnection = (errorCode, errorContext) =>
// abortUpgrade(socket, errorCode, errorContext)
this.handshake(req._query.transport, req, () => { })
}
function onUpgradeError() {
console.debug("websocket error before upgrade")
// websocket.close() not needed
}
}
/**
* Captures upgrade requests for a http.Server.
*
* @param {http.Server} server
* @param {Object} options
* @api public
*/
attach(server, options: any = {}) {
// let path = (options.path || "/engine.io").replace(/\/$/, "")
// const destroyUpgradeTimeout = options.destroyUpgradeTimeout || 1000
// // normalize path
// path += "/"
// function check(req) {
// return path === req.url.substr(0, path.length)
// }
// cache and clean up listeners
// const listeners = server.listeners("request").slice(0)
// server.removeAllListeners("request")
server.on("close", this.close.bind(this))
// server.on("listening", this.init.bind(this))
// @java-patch transfer to Netty Server
server.on("connect", (request: Request, websocket: WebSocketClient) => {
console.debug('Engine.IO connect client from', request.url)
this.prepare(request)
this.onWebSocket(request, undefined, websocket)
})
// set server as ws server
this.ws = server
// // add request handler
// server.on("request", (req, res) => {
// if (check(req)) {
// debug('intercepting request for path "%s"', path)
// this.handleRequest(req, res)
// } else {
// let i = 0
// const l = listeners.length
// for (; i < l; i++) {
// listeners[i].call(server, req, res)
// }
// }
// })
// if (~this.opts.transports.indexOf("websocket")) {
// server.on("upgrade", (req, socket, head) => {
// if (check(req)) {
// this.handleUpgrade(req, socket, head)
// } else if (false !== options.destroyUpgrade) {
// // default node behavior is to disconnect when no handlers
// // but by adding a handler, we prevent that
// // and if no eio thing handles the upgrade
// // then the socket needs to die!
// setTimeout(function () {
// if (socket.writable && socket.bytesWritten <= 0) {
// return socket.end()
// }
// }, destroyUpgradeTimeout)
// }
// })
// }
}
}
// /**
// * Close the HTTP long-polling request
// *
// * @param res - the response object
// * @param errorCode - the error code
// * @param errorContext - additional error context
// *
// * @api private
// */
// function abortRequest(res, errorCode, errorContext) {
// const statusCode = errorCode === Server.errors.FORBIDDEN ? 403 : 400
// const message =
// errorContext && errorContext.message
// ? errorContext.message
// : Server.errorMessages[errorCode]
// res.writeHead(statusCode, { "Content-Type": "application/json" })
// res.end(
// JSON.stringify({
// code: errorCode,
// message
// })
// )
// }
// /**
// * Close the WebSocket connection
// *
// * @param {net.Socket} socket
// * @param {string} errorCode - the error code
// * @param {object} errorContext - additional error context
// *
// * @api private
// */
// function abortUpgrade(socket, errorCode, errorContext: any = {}) {
// socket.on("error", () => {
// debug("ignoring error from closed connection")
// })
// if (socket.writable) {
// const message = errorContext.message || Server.errorMessages[errorCode]
// const length = Buffer.byteLength(message)
// socket.write(
// "HTTP/1.1 400 Bad Request\r\n" +
// "Connection: close\r\n" +
// "Content-type: text/html\r\n" +
// "Content-Length: " +
// length +
// "\r\n" +
// "\r\n" +
// message
// )
// }
// socket.destroy()
// }
// module.exports = Server
/* eslint-disable */
// /**
// * From https://github.com/nodejs/node/blob/v8.4.0/lib/_http_common.js#L303-L354
// *
// * True if val contains an invalid field-vchar
// * field-value = *( field-content / obs-fold )
// * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
// * field-vchar = VCHAR / obs-text
// *
// * checkInvalidHeaderChar() is currently designed to be inlinable by v8,
// * so take care when making changes to the implementation so that the source
// * code size does not exceed v8's default max_inlined_source_size setting.
// **/
// // prettier-ignore
// const validHdrChars = [
// 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, // 0 - 15
// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 32 - 47
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 48 - 63
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 80 - 95
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 112 - 127
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 128 ...
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // ... 255
// ]
// function checkInvalidHeaderChar(val) {
// val += ""
// if (val.length < 1) return false
// if (!validHdrChars[val.charCodeAt(0)]) {
// debug('invalid header, index 0, char "%s"', val.charCodeAt(0))
// return true
// }
// if (val.length < 2) return false
// if (!validHdrChars[val.charCodeAt(1)]) {
// debug('invalid header, index 1, char "%s"', val.charCodeAt(1))
// return true
// }
// if (val.length < 3) return false
// if (!validHdrChars[val.charCodeAt(2)]) {
// debug('invalid header, index 2, char "%s"', val.charCodeAt(2))
// return true
// }
// if (val.length < 4) return false
// if (!validHdrChars[val.charCodeAt(3)]) {
// debug('invalid header, index 3, char "%s"', val.charCodeAt(3))
// return true
// }
// for (let i = 4; i < val.length; ++i) {
// if (!validHdrChars[val.charCodeAt(i)]) {
// debug('invalid header, index "%i", char "%s"', i, val.charCodeAt(i))
// return true
// }
// }
// return false
// }

View File

@ -0,0 +1,530 @@
import { EventEmitter } from "events"
import { Server } from "./server"
import { Transport } from "./transport"
import type { Request } from "../server/request"
// const debug = require("debug")("engine:socket")
export class Socket extends EventEmitter {
public id: string
private server: Server
private upgrading = false
private upgraded = false
public readyState = "opening"
private writeBuffer = []
private packetsFn = []
private sentCallbackFn = []
private cleanupFn = []
public request: Request
public protocol: number
public remoteAddress: any
public transport: Transport
private checkIntervalTimer: NodeJS.Timeout
private upgradeTimeoutTimer: NodeJS.Timeout
private pingTimeoutTimer: NodeJS.Timeout
private pingIntervalTimer: NodeJS.Timeout
/**
* Client class (abstract).
*
* @api private
*/
constructor(id: string, server: Server, transport: Transport, req: Request, protocol: number) {
super()
this.id = id
this.server = server
this.request = req
this.protocol = protocol
// Cache IP since it might not be in the req later
if (req.websocket && req.websocket._socket) {
this.remoteAddress = req.websocket._socket.remoteAddress
} else {
this.remoteAddress = req.connection.remoteAddress
}
this.checkIntervalTimer = null
this.upgradeTimeoutTimer = null
this.pingTimeoutTimer = null
this.pingIntervalTimer = null
this.setTransport(transport)
this.onOpen()
}
/**
* Called upon transport considered open.
*
* @api private
*/
onOpen() {
this.readyState = "open"
// sends an `open` packet
this.transport.sid = this.id
this.sendPacket(
"open",
JSON.stringify({
sid: this.id,
upgrades: this.getAvailableUpgrades(),
pingInterval: this.server.opts.pingInterval,
pingTimeout: this.server.opts.pingTimeout
})
)
if (this.server.opts.initialPacket) {
this.sendPacket("message", this.server.opts.initialPacket)
}
this.emit("open")
if (this.protocol === 3) {
// in protocol v3, the client sends a ping, and the server answers with a pong
this.resetPingTimeout(
this.server.opts.pingInterval + this.server.opts.pingTimeout
)
} else {
// in protocol v4, the server sends a ping, and the client answers with a pong
this.schedulePing()
}
}
/**
* Called upon transport packet.
*
* @param {Object} packet
* @api private
*/
onPacket(packet: { type: any; data: any }) {
if ("open" !== this.readyState) {
console.debug("packet received with closed socket")
return
}
// export packet event
// debug(`received packet ${packet.type}`)
this.emit("packet", packet)
// Reset ping timeout on any packet, incoming data is a good sign of
// other side's liveness
this.resetPingTimeout(
this.server.opts.pingInterval + this.server.opts.pingTimeout
)
switch (packet.type) {
case "ping":
if (this.transport.protocol !== 3) {
this.onError("invalid heartbeat direction")
return
}
// debug("got ping")
this.sendPacket("pong")
this.emit("heartbeat")
break
case "pong":
if (this.transport.protocol === 3) {
this.onError("invalid heartbeat direction")
return
}
// debug("got pong")
this.schedulePing()
this.emit("heartbeat")
break
case "error":
this.onClose("parse error")
break
case "message":
this.emit("data", packet.data)
this.emit("message", packet.data)
break
}
}
/**
* Called upon transport error.
*
* @param {Error} error object
* @api private
*/
onError(err: string) {
// debug("transport error")
this.onClose("transport error", err)
}
/**
* Pings client every `this.pingInterval` and expects response
* within `this.pingTimeout` or closes connection.
*
* @api private
*/
schedulePing() {
clearTimeout(this.pingIntervalTimer)
this.pingIntervalTimer = setTimeout(() => {
// debug(
// "writing ping packet - expecting pong within %sms",
// this.server.opts.pingTimeout
// )
this.sendPacket("ping")
this.resetPingTimeout(this.server.opts.pingTimeout)
}, this.server.opts.pingInterval)
}
/**
* Resets ping timeout.
*
* @api private
*/
resetPingTimeout(timeout: number) {
clearTimeout(this.pingTimeoutTimer)
this.pingTimeoutTimer = setTimeout(() => {
if (this.readyState === "closed") return
this.onClose("ping timeout")
}, timeout)
}
/**
* Attaches handlers for the given transport.
*
* @param {Transport} transport
* @api private
*/
setTransport(transport: Transport) {
console.debug(`engine.io socket ${this.id} set transport ${transport.name}`)
const onError = this.onError.bind(this)
const onPacket = this.onPacket.bind(this)
const flush = this.flush.bind(this)
const onClose = this.onClose.bind(this, "transport close")
this.transport = transport
this.transport.once("error", onError)
this.transport.on("packet", onPacket)
this.transport.on("drain", flush)
this.transport.once("close", onClose)
// this function will manage packet events (also message callbacks)
this.setupSendCallback()
this.cleanupFn.push(function () {
transport.removeListener("error", onError)
transport.removeListener("packet", onPacket)
transport.removeListener("drain", flush)
transport.removeListener("close", onClose)
})
}
/**
* Upgrades socket to the given transport
*
* @param {Transport} transport
* @api private
*/
maybeUpgrade(transport: Transport) {
console.debug(
'might upgrade socket transport from "', this.transport.name, '" to "', transport.name, '"'
)
this.upgrading = true
// set transport upgrade timer
this.upgradeTimeoutTimer = setTimeout(() => {
console.debug("client did not complete upgrade - closing transport")
cleanup()
if ("open" === transport.readyState) {
transport.close()
}
}, this.server.opts.upgradeTimeout)
const onPacket = (packet: { type: string; data: string }) => {
if ("ping" === packet.type && "probe" === packet.data) {
transport.send([{ type: "pong", data: "probe" }])
this.emit("upgrading", transport)
clearInterval(this.checkIntervalTimer)
this.checkIntervalTimer = setInterval(check, 100)
} else if ("upgrade" === packet.type && this.readyState !== "closed") {
// debug("got upgrade packet - upgrading")
cleanup()
this.transport.discard()
this.upgraded = true
this.clearTransport()
this.setTransport(transport)
this.emit("upgrade", transport)
this.flush()
if (this.readyState === "closing") {
transport.close(() => {
this.onClose("forced close")
})
}
} else {
cleanup()
transport.close()
}
}
// we force a polling cycle to ensure a fast upgrade
const check = () => {
if ("polling" === this.transport.name && this.transport.writable) {
// debug("writing a noop packet to polling for fast upgrade")
this.transport.send([{ type: "noop" }])
}
}
const cleanup = () => {
this.upgrading = false
clearInterval(this.checkIntervalTimer)
this.checkIntervalTimer = null
clearTimeout(this.upgradeTimeoutTimer)
this.upgradeTimeoutTimer = null
transport.removeListener("packet", onPacket)
transport.removeListener("close", onTransportClose)
transport.removeListener("error", onError)
this.removeListener("close", onClose)
}
const onError = (err: string) => {
// debug("client did not complete upgrade - %s", err)
cleanup()
transport.close()
transport = null
}
const onTransportClose = () => {
onError("transport closed")
}
const onClose = () => {
onError("socket closed")
}
transport.on("packet", onPacket)
transport.once("close", onTransportClose)
transport.once("error", onError)
this.once("close", onClose)
}
/**
* Clears listeners and timers associated with current transport.
*
* @api private
*/
clearTransport() {
let cleanup: () => void
const toCleanUp = this.cleanupFn.length
for (let i = 0; i < toCleanUp; i++) {
cleanup = this.cleanupFn.shift()
cleanup()
}
// silence further transport errors and prevent uncaught exceptions
this.transport.on("error", function () {
// debug("error triggered by discarded transport")
})
// ensure transport won't stay open
this.transport.close()
clearTimeout(this.pingTimeoutTimer)
}
/**
* Called upon transport considered closed.
* Possible reasons: `ping timeout`, `client error`, `parse error`,
* `transport error`, `server close`, `transport close`
*/
onClose(reason: string, description?: string) {
if ("closed" !== this.readyState) {
this.readyState = "closed"
// clear timers
clearTimeout(this.pingIntervalTimer)
clearTimeout(this.pingTimeoutTimer)
clearInterval(this.checkIntervalTimer)
this.checkIntervalTimer = null
clearTimeout(this.upgradeTimeoutTimer)
// clean writeBuffer in next tick, so developers can still
// grab the writeBuffer on 'close' event
process.nextTick(() => {
this.writeBuffer = []
})
this.packetsFn = []
this.sentCallbackFn = []
this.clearTransport()
this.emit("close", reason, description)
}
}
/**
* Setup and manage send callback
*
* @api private
*/
setupSendCallback() {
// the message was sent successfully, execute the callback
const onDrain = () => {
if (this.sentCallbackFn.length > 0) {
const seqFn = this.sentCallbackFn.splice(0, 1)[0]
if ("function" === typeof seqFn) {
// debug("executing send callback")
seqFn(this.transport)
} else if (Array.isArray(seqFn)) {
// debug("executing batch send callback")
const l = seqFn.length
let i = 0
for (; i < l; i++) {
if ("function" === typeof seqFn[i]) {
seqFn[i](this.transport)
}
}
}
}
}
this.transport.on("drain", onDrain)
this.cleanupFn.push(() => {
this.transport.removeListener("drain", onDrain)
})
}
/**
* Sends a message packet.
*
* @param {String} message
* @param {Object} options
* @param {Function} callback
* @return {Socket} for chaining
* @api public
*/
send(data: any, options: any, callback: any) {
this.sendPacket("message", data, options, callback)
return this
}
write(data: any, options: any, callback?: any) {
this.sendPacket("message", data, options, callback)
return this
}
/**
* Sends a packet.
*
* @param {String} packet type
* @param {String} optional, data
* @param {Object} options
* @api private
*/
sendPacket(type: string, data?: string, options?: { compress?: any }, callback?: undefined) {
if ("function" === typeof options) {
callback = options
options = null
}
options = options || {}
options.compress = false !== options.compress
if ("closing" !== this.readyState && "closed" !== this.readyState) {
// console.debug('sending packet "%s" (%s)', type, data)
const packet: any = {
type: type,
options: options
}
if (data) packet.data = data
// exports packetCreate event
this.emit("packetCreate", packet)
this.writeBuffer.push(packet)
// add send callback to object, if defined
if (callback) this.packetsFn.push(callback)
this.flush()
}
}
/**
* Attempts to flush the packets buffer.
*
* @api private
*/
flush() {
if (
"closed" !== this.readyState &&
this.transport.writable &&
this.writeBuffer.length
) {
console.trace("flushing buffer to transport")
this.emit("flush", this.writeBuffer)
this.server.emit("flush", this, this.writeBuffer)
const wbuf = this.writeBuffer
this.writeBuffer = []
if (!this.transport.supportsFraming) {
this.sentCallbackFn.push(this.packetsFn)
} else {
this.sentCallbackFn.push.apply(this.sentCallbackFn, this.packetsFn)
}
this.packetsFn = []
this.transport.send(wbuf)
this.emit("drain")
this.server.emit("drain", this)
}
}
/**
* Get available upgrades for this socket.
*
* @api private
*/
getAvailableUpgrades() {
const availableUpgrades = []
const allUpgrades = this.server.upgrades(this.transport.name)
let i = 0
const l = allUpgrades.length
for (; i < l; ++i) {
const upg = allUpgrades[i]
if (this.server.opts.transports.indexOf(upg) !== -1) {
availableUpgrades.push(upg)
}
}
return availableUpgrades
}
/**
* Closes the socket and underlying transport.
*
* @param {Boolean} optional, discard
* @return {Socket} for chaining
* @api public
*/
close(discard?: any) {
if ("open" !== this.readyState) return
this.readyState = "closing"
if (this.writeBuffer.length) {
this.once("drain", this.closeTransport.bind(this, discard))
return
}
this.closeTransport(discard)
}
/**
* Closes the underlying transport.
*
* @param {Boolean} discard
* @api private
*/
closeTransport(discard: any) {
if (discard) this.transport.discard()
this.transport.close(this.onClose.bind(this, "forced close"))
}
}

View File

@ -0,0 +1,121 @@
import { EventEmitter } from 'events'
import parser_v4 from "../engine.io-parser"
import type { WebSocketClient } from '../server/client'
/**
* Noop function.
*
* @api private
*/
function noop() { }
export abstract class Transport extends EventEmitter {
public sid: string
public req /**http.IncomingMessage */
public socket: WebSocketClient
public writable: boolean
public readyState: string
public discarded: boolean
public protocol: Number
public parser: any
public perMessageDeflate: any
public supportsBinary: boolean = false
/**
* Transport constructor.
*
* @param {http.IncomingMessage} request
* @api public
*/
constructor(req) {
super()
this.readyState = "open"
this.discarded = false
this.protocol = req._query.EIO === "4" ? 4 : 3 // 3rd revision by default
this.parser = parser_v4//= this.protocol === 4 ? parser_v4 : parser_v3
}
/**
* Flags the transport as discarded.
*
* @api private
*/
discard() {
this.discarded = true
}
/**
* Called with an incoming HTTP request.
*
* @param {http.IncomingMessage} request
* @api private
*/
onRequest(req) {
console.debug(`engine.io transport ${this.socket.id} setting request`, JSON.stringify(req))
this.req = req
}
/**
* Closes the transport.
*
* @api private
*/
close(fn?) {
if ("closed" === this.readyState || "closing" === this.readyState) return
this.readyState = "closing"
this.doClose(fn || noop)
}
/**
* Called with a transport error.
*
* @param {String} message error
* @param {Object} error description
* @api private
*/
onError(msg: string, desc?: string) {
if (this.listeners("error").length) {
const err: any = new Error(msg)
err.type = "TransportError"
err.description = desc
this.emit("error", err)
} else {
console.debug(`ignored transport error ${msg} (${desc})`)
}
}
/**
* Called with parsed out a packets from the data stream.
*
* @param {Object} packet
* @api private
*/
onPacket(packet) {
this.emit("packet", packet)
}
/**
* Called with the encoded packet data.
*
* @param {String} data
* @api private
*/
onData(data) {
this.onPacket(this.parser.decodePacket(data))
}
/**
* Called upon transport close.
*
* @api private
*/
onClose() {
this.readyState = "closed"
this.emit("close")
}
abstract get supportsFraming()
abstract get name()
abstract send(...args: any[])
abstract doClose(d: Function)
}

View File

@ -0,0 +1,3 @@
export default {
websocket: require("./websocket").WebSocket
}

View File

@ -0,0 +1,116 @@
import { Transport } from '../transport'
// const debug = require("debug")("engine:ws")
export class WebSocket extends Transport {
public perMessageDeflate: any
/**
* WebSocket transport
*
* @param {http.IncomingMessage}
* @api public
*/
constructor(req) {
super(req)
this.socket = req.websocket
this.socket.on("message", this.onData.bind(this))
this.socket.once("close", this.onClose.bind(this))
this.socket.on("error", this.onError.bind(this))
this.writable = true
this.perMessageDeflate = null
}
/**
* Transport name
*
* @api public
*/
get name() {
return "websocket"
}
/**
* Advertise upgrade support.
*
* @api public
*/
get handlesUpgrades() {
return true
}
/**
* Advertise framing support.
*
* @api public
*/
get supportsFraming() {
return true
}
/**
* Processes the incoming data.
*
* @param {String} encoded packet
* @api private
*/
onData(data) {
// debug('received "%s"', data)
super.onData(data)
}
/**
* Writes a packet payload.
*
* @param {Array} packets
* @api private
*/
send(packets) {
// console.log('WebSocket send packets', JSON.stringify(packets))
const packet = packets.shift()
if (typeof packet === "undefined") {
this.writable = true
this.emit("drain")
return
}
// always creates a new object since ws modifies it
const opts: any = {}
if (packet.options) {
opts.compress = packet.options.compress
}
const send = data => {
if (this.perMessageDeflate) {
const len =
"string" === typeof data ? Buffer.byteLength(data) : data.length
if (len < this.perMessageDeflate.threshold) {
opts.compress = false
}
}
console.trace('writing', data)
this.writable = false
this.socket.send(data, opts, err => {
if (err) return this.onError("write error", err.stack)
this.send(packets)
})
}
if (packet.options && typeof packet.options.wsPreEncoded === "string") {
send(packet.options.wsPreEncoded)
} else {
this.parser.encodePacket(packet, this.supportsBinary, send)
}
}
/**
* Closes the transport.
*
* @api private
*/
doClose(fn) {
// debug("closing")
this.socket.close()
fn && fn()
}
}

View File

@ -1,7 +1,7 @@
/// <reference types="@ccms/nashorn" />
/// <reference types="@javatypes/tomcat-websocket-api" />
import { Server, ServerOptions } from './socket-io'
import { Server, ServerOptions } from './socket.io'
interface SocketIOStatic {
/**
@ -44,7 +44,7 @@ let io: SocketStatic = function (pipeline: any, options: Partial<ServerOptions>)
}
io.Instance = Symbol("@ccms/websocket")
export default io
export * from './socket-io'
export * from './socket.io'
export * from './client'
export * from './server'
export * from './transport'
export * from './engine.io/transport'

View File

@ -1,24 +1,26 @@
import { Transport } from '../transport'
import { AttributeKeys } from './constants'
import { WebSocketClient } from '../server/client'
const TextWebSocketFrame = Java.type('io.netty.handler.codec.http.websocketx.TextWebSocketFrame')
export class NettyClient extends Transport {
export class NettyClient extends WebSocketClient {
private channel: any
constructor(server: any, channel: any) {
super(server)
this.remoteAddress = channel.remoteAddress() + ''
this.request = channel.attr(AttributeKeys.Request).get()
this._id = channel.id() + ''
constructor(channel: any) {
super()
this.id = channel.id() + ''
this.channel = channel
}
doSend(text: string) {
this.channel.writeAndFlush(new TextWebSocketFrame(text))
send(text: string, opts?: any, callback?: (err?: Error) => void) {
try {
this.channel.writeAndFlush(new TextWebSocketFrame(text))
callback?.()
} catch (error) {
callback?.(error)
}
}
doClose() {
close() {
this.channel.close()
}
}

View File

@ -1,6 +1,7 @@
import { JavaServerOptions } from '../server'
import { HttpRequestHandlerAdapter } from './adapter'
import { AttributeKeys } from './constants'
import { ServerOptions } from 'socket-io'
const DefaultHttpResponse = Java.type('io.netty.handler.codec.http.DefaultHttpResponse')
const DefaultFullHttpResponse = Java.type('io.netty.handler.codec.http.DefaultFullHttpResponse')
@ -18,7 +19,7 @@ const ChannelFutureListener = Java.type('io.netty.channel.ChannelFutureListener'
export class HttpRequestHandler extends HttpRequestHandlerAdapter {
private ws: string
private root: string
constructor(options: ServerOptions) {
constructor(options: JavaServerOptions) {
super()
this.root = options.root
this.ws = options.path

View File

@ -1,65 +1,70 @@
import { EventEmitter } from 'events'
import { ServerOptions } from '../socket-io'
import { ServerEvent } from '../socket-io/constants'
import { JavaServerOptions, ServerEvent, WebSocketServer } from '../server'
import { Request } from '../server/request'
import { NettyClient } from './client'
import { Keys } from './constants'
import { AttributeKeys, Keys } from './constants'
import { WebSocketDetect } from './websocket_detect'
import { WebSocketHandler } from './websocket_handler'
class NettyWebSocketServer extends EventEmitter {
private pipeline: any
private clients: Map<string, NettyClient>
class NettyWebSocketServer extends WebSocketServer {
constructor(pipeline: any, options: JavaServerOptions) {
super(pipeline, options)
}
constructor(pipeline: any, options: ServerOptions) {
super()
this.clients = new Map()
this.pipeline = pipeline
let connectEvent = options.event
try { this.pipeline.remove(Keys.Detect) } catch (error) { }
this.pipeline.addFirst(Keys.Detect, new WebSocketDetect(connectEvent).getHandler())
protected initialize() {
let connectEvent = this.options.event
try { this.instance.remove(Keys.Detect) } catch (error) { }
this.instance.addFirst(Keys.Detect, new WebSocketDetect(connectEvent).getHandler())
connectEvent.on(ServerEvent.detect, (ctx, channel) => {
channel.pipeline().addFirst(Keys.Handler, new WebSocketHandler(options).getHandler())
channel.pipeline().addFirst(Keys.Handler, new WebSocketHandler(this.options).getHandler())
ctx.fireChannelRead(channel)
})
connectEvent.on(ServerEvent.connect, (ctx) => {
let cid = ctx?.channel().id() + ''
let nettyClient = new NettyClient(this, ctx.channel())
this.clients.set(cid, nettyClient)
this.emit(ServerEvent.connect, nettyClient)
this.onconnect(ctx)
})
connectEvent.on(ServerEvent.message, (ctx, msg) => {
let cid = ctx?.channel().id() + ''
if (this.clients.has(cid)) {
this.emit(ServerEvent.message, this.clients.get(cid), msg.text())
} else if (global.debug) {
console.error(`unknow client ${ctx} reciver message ${msg.text()}`)
}
this.onmessage(ctx, msg.text())
})
connectEvent.on(ServerEvent.disconnect, (ctx, cause) => {
let cid = ctx?.channel().id() + ''
if (this.clients.has(cid)) {
this.emit(ServerEvent.disconnect, this.clients.get(cid), cause)
} else if (global.debug) {
console.error(`unknow client ${ctx} disconnect cause ${cause}`)
}
this.ondisconnect(ctx, cause)
})
connectEvent.on(ServerEvent.error, (ctx, cause) => {
let cid = ctx?.channel().id() + ''
if (this.clients.has(cid)) {
this.emit(ServerEvent.error, this.clients.get(cid), cause)
} else if (global.debug) {
console.error(`unknow client ${ctx} cause error ${cause}`)
console.ex(cause)
}
connectEvent.on(ServerEvent.error, (ctx, error) => {
this.onerror(ctx, error)
})
}
close() {
if (this.pipeline.names().contains(Keys.Detect)) {
this.pipeline.remove(Keys.Detect)
protected getId(ctx: any) {
try {
return ctx.channel().id() + ''
} catch (error) {
console.log(Object.toString.apply(ctx))
console.ex(error)
}
}
protected getRequest(ctx) {
let channel = ctx.channel()
let req = channel.attr(AttributeKeys.Request).get()
let headers = {}
let nativeHeaders = req.headers()
nativeHeaders.forEach(function (header) {
headers[header.getKey()] = header.getValue()
})
let request = new Request(req.uri(), req.method().name(), headers)
request.connection = {
remoteAddress: channel.remoteAddress() + ''
}
return request
}
protected getSocket(ctx) {
return new NettyClient(ctx.channel())
}
protected doClose() {
if (this.instance.names().contains(Keys.Detect)) {
this.instance.remove(Keys.Detect)
}
this.clients.forEach(client => client.close())
}
}

View File

@ -1,11 +1,11 @@
import { EventEmitter } from 'events'
import { ServerOptions } from '../socket-io'
import { ServerEvent } from '../socket-io/constants'
import { JavaServerOptions, ServerEvent } from '../server'
import { TextWebSocketFrameHandlerAdapter } from './adapter'
export class TextWebSocketFrameHandler extends TextWebSocketFrameHandlerAdapter {
private event: EventEmitter
constructor(options: ServerOptions) {
constructor(options: JavaServerOptions) {
super()
this.event = options.event
}

View File

@ -1,6 +1,7 @@
import { EventEmitter } from 'events'
import { WebSocketHandlerAdapter } from "./adapter"
import { ServerEvent } from '../socket-io/constants'
import { ServerEvent } from '../server'
export class WebSocketDetect extends WebSocketHandlerAdapter {
private event: EventEmitter

View File

@ -1,5 +1,4 @@
import { ServerOptions } from '../socket-io'
import { ServerEvent } from '../socket-io/constants'
import { JavaServerOptions, ServerEvent } from '../server'
import { Keys } from './constants'
import { HttpRequestHandler } from './httprequest'
@ -13,8 +12,8 @@ const HttpObjectAggregator = Java.type('io.netty.handler.codec.http.HttpObjectAg
const WebSocketServerProtocolHandler = Java.type('io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler')
export class WebSocketHandler extends WebSocketHandlerAdapter {
private options: ServerOptions
constructor(options: ServerOptions) {
private options: JavaServerOptions
constructor(options: JavaServerOptions) {
super()
this.options = options
}

View File

@ -0,0 +1,7 @@
import { EventEmitter } from 'events'
export abstract class WebSocketClient extends EventEmitter {
public id: string
public _socket: any
abstract send(text: string, opts?: any, callback?: (err?: Error) => void)
abstract close()
}

View File

@ -1,52 +1,90 @@
import { EventEmitter } from 'events'
import { ServerOptions } from '../socket.io'
import { WebSocketClient } from './client'
import { Transport } from '../transport'
import type { Request } from './request'
interface ServerOptions {
export enum ServerEvent {
detect = 'detect',
request = 'request',
upgrade = 'upgrade',
connect = 'connect',
connection = 'connection',
message = 'message',
error = 'error',
disconnecting = 'disconnecting',
disconnect = 'disconnect',
}
export interface JavaServerOptions extends ServerOptions {
event?: EventEmitter
root?: string
/**
* name of the path to capture
* @default "/socket.io"
*/
path: string
}
interface WebSocketServerImpl extends EventEmitter {
close(): void
}
export class WebSocketServer extends EventEmitter {
options: Partial<ServerOptions>
private websocketServer: WebSocketServerImpl
constructor(instance: any, options: Partial<ServerOptions>) {
export abstract class WebSocketServer extends EventEmitter {
protected instance: any
protected options: JavaServerOptions
private clients: Map<string, WebSocketClient>
constructor(instance: any, options: JavaServerOptions) {
super()
if (!instance) { throw new Error('instance can\'t be undefiend!') }
this.options = Object.assign({
event: new EventEmitter(),
path: '/ws',
root: root + '/wwwroot',
}, options)
this.selectServerImpl(instance)
this.instance = instance
this.options = options
this.clients = new Map()
console.debug('create websocket server from ' + this.constructor.name)
this.initialize()
}
on(event: "connect", cb: (transport: Transport) => void): this
on(event: "message", cb: (transport: Transport, text: string) => void): this
on(event: "disconnect", cb: (transport: Transport, reason: string) => void): this
on(event: "error", cb: (transport: Transport, cause: Error) => void): this
on(event: string, cb: (transport: Transport, extra?: any) => void): this {
this.websocketServer.on(event, cb)
return this
protected onconnect(handler: any) {
let id = this.getId(handler)
console.log('client', id, 'connect')
let request = this.getRequest(handler)
request.id = id
let websocket = this.getSocket(handler)
this.clients.set(this.getId(handler), websocket)
this.emit(ServerEvent.connect, request, websocket)
}
private selectServerImpl(instance: any) {
let WebSocketServerImpl = undefined
if (instance.class.name.startsWith('io.netty.channel')) {
WebSocketServerImpl = require("../netty").NettyWebSocketServer
} else {
WebSocketServerImpl = require("../tomcat").TomcatWebSocketServer
protected onmessage(handler: any, message: string) {
this.execute(handler, (websocket) => websocket.emit(ServerEvent.message, message))
}
protected ondisconnect(handler: any, cause: string) {
this.execute(handler, (websocket) => websocket.emit(ServerEvent.disconnect, cause))
}
protected onerror(handler: any, error: Error) {
if (global.debug) {
console.ex(error)
}
this.websocketServer = new WebSocketServerImpl(instance, this.options)
this.execute(handler, (websocket) => websocket.emit(ServerEvent.error, error))
}
protected execute(handler: any, callback: (websocket: WebSocketClient) => void) {
let id = this.getId(handler)
if (this.clients.has(id)) {
this.clients.has(id) && callback(this.clients.get(id))
} else {
console.debug('ignore execute', handler, 'callback', callback)
}
}
public close() {
this.clients.forEach(websocket => websocket.close())
this.doClose()
}
protected abstract initialize(): void
protected abstract getId(handler: any): string
protected abstract getRequest(handler: any): Request
protected abstract getSocket(handler: any): WebSocketClient
protected abstract doClose(): void
}
export const attach = (instance, options) => {
if (!instance) { throw new Error('instance can\'t be undefiend!') }
options = Object.assign({
event: new EventEmitter(),
path: '/ws',
root: root + '/wwwroot',
}, options)
let WebSocketServerImpl = undefined
if (instance.class.name.startsWith('io.netty.channel')) {
WebSocketServerImpl = require("../netty").NettyWebSocketServer
} else {
WebSocketServerImpl = require("../tomcat").TomcatWebSocketServer
}
return new WebSocketServerImpl(instance, options)
}

View File

@ -0,0 +1,23 @@
import { WebSocketClient } from "./client"
interface HttpHeaders {
[name: string]: string
}
interface Connection {
remoteAddress: string
}
export class Request {
public id: string
public url: string
public method: string
public headers: HttpHeaders
public connection: Connection
public websocket: WebSocketClient
public _query: any
constructor(url: string, method = "GET", headers = {}) {
this.url = url
this.method = method
this.headers = headers
}
}

View File

@ -1,164 +0,0 @@
import { EventEmitter } from 'events'
import { Namespace } from './namespace'
import { Parser } from './parser'
import { Socket } from './socket'
export type SocketId = string
export type Room = string
export interface BroadcastFlags {
volatile?: boolean
compress?: boolean
local?: boolean
broadcast?: boolean
binary?: boolean
}
export interface BroadcastOptions {
rooms: Set<Room>
except?: Set<SocketId>
flags?: BroadcastFlags
}
export class Adapter extends EventEmitter implements Adapter {
rooms: Map<Room, Set<SocketId>>
sids: Map<SocketId, Set<Room>>
private readonly encoder: Parser
parser: Parser
constructor(readonly nsp: Namespace) {
super()
this.rooms = new Map()
this.sids = new Map()
this.parser = nsp.server._parser
this.encoder = this.parser
}
/**
* Adds a socket to a list of room.
*
* @param {String} socket id
* @param {String} rooms
* @param {Function} callback
* @api public
*/
addAll(id: SocketId, rooms: Set<Room>): Promise<void> | void {
for (const room of rooms) {
if (!this.sids.has(id)) {
this.sids.set(id, new Set())
}
this.sids.get(id).add(room)
if (!this.rooms.has(room)) {
this.rooms.set(room, new Set())
}
this.rooms.get(room).add(id)
}
}
del(id: string, room: string, callback?: (err?: any) => void): void {
if (this.sids.has(id)) {
this.sids.get(id).delete(room)
}
if (this.rooms.has(room)) {
this.rooms.get(room).delete(id)
if (this.rooms.get(room).size === 0) this.rooms.delete(room)
}
callback && callback.bind(null, null)
}
delAll(id: string): void {
if (!this.sids.has(id)) {
return
}
for (const room of this.sids.get(id)) {
if (this.rooms.has(room)) {
this.rooms.get(room).delete(id)
if (this.rooms.get(room).size === 0) this.rooms.delete(room)
}
}
this.sids.delete(id)
}
/**
* Broadcasts a packet.
*
* Options:
* - `flags` {Object} flags for this packet
* - `except` {Array} sids that should be excluded
* - `rooms` {Array} list of rooms to broadcast to
*
* @param {Object} packet the packet object
* @param {Object} opts the options
* @public
*/
public broadcast(packet: any, opts: BroadcastOptions): void {
const rooms = opts.rooms
const except = opts.except || new Set()
const flags = opts.flags || {}
const packetOpts = {
preEncoded: true,
volatile: flags.volatile,
compress: flags.compress
}
const ids = new Set()
packet.nsp = this.nsp.name
const encodedPackets = this.encoder.encode(packet)
if (rooms.size) {
for (const room of rooms) {
if (!this.rooms.has(room)) continue
for (const id of this.rooms.get(room)) {
if (ids.has(id) || except.has(id)) continue
const socket = this.nsp.sockets.get(id)
if (socket) {
socket.packet(encodedPackets as any, packetOpts)
ids.add(id)
}
}
}
} else {
for (const [id] of this.sids) {
if (except.has(id)) continue
const socket = this.nsp.sockets.get(id)
if (socket) socket.packet(encodedPackets as any, packetOpts)
}
}
}
/**
* Gets a list of sockets by sid.
*
* @param {Set<Room>} rooms the explicit set of rooms to check.
*/
public sockets(rooms: Set<Room>): Promise<Set<SocketId>> {
const sids = new Set<SocketId>()
if (rooms.size) {
for (const room of rooms) {
if (!this.rooms.has(room)) continue
for (const id of this.rooms.get(room)) {
if (this.nsp.sockets.has(id)) {
sids.add(id)
}
}
}
} else {
for (const [id] of this.sids) {
if (this.nsp.sockets.has(id)) sids.add(id)
}
}
return Promise.resolve(sids)
}
/**
* Gets the list of rooms a given socket has joined.
*
* @param {SocketId} id the socket id
*/
public socketRooms(id: SocketId): Set<Room> | undefined {
return this.sids.get(id)
}
}

View File

@ -1,360 +0,0 @@
import { EventEmitter } from 'events'
import { Parser } from './parser'
import { Packet } from './packet'
import { Namespace, Server, Socket } from './index'
import { PacketTypes, SubPacketTypes } from './types'
import { ServerEvent } from './constants'
import { SocketId } from './adapter'
import { Transport } from '../transport'
const parser = new Parser()
export class Client extends EventEmitter {
public readonly conn: Transport
/**
* @private
*/
readonly id: string
private readonly server: Server
// private readonly encoder: Encoder
private readonly decoder: any
private sockets: Map<SocketId, Socket>
private nsps: Map<string, Socket>
private connectTimeout: NodeJS.Timeout
private checkIntervalTimer: NodeJS.Timeout
private upgradeTimeoutTimer: NodeJS.Timeout
private pingTimeoutTimer: NodeJS.Timeout
private pingIntervalTimer: NodeJS.Timeout
constructor(server: Server, conn) {
super()
this.server = server
this.conn = conn
// this.encoder = server.encoder
this.decoder = server._parser
this.id = this.conn.id + ''
this.setup()
// =============================
this.sockets = new Map()
this.nsps = new Map()
// ================== engine.io
this.onOpen()
// ================== Transport
this.conn.on(ServerEvent.disconnect, (reason) => {
this.onclose(reason)
})
}
/**
* @return the reference to the request that originated the Engine.IO connection
*
* @public
*/
public get request(): any /**IncomingMessage */ {
return this.conn.request
}
/**
* Sets up event listeners.
*
* @private
*/
private setup() {
// @ts-ignore
// this.decoder.on("decoded", this.ondecoded)
this.conn.on("data", this.ondata.bind(this))
this.conn.on("error", this.onerror.bind(this))
this.conn.on("close", this.onclose.bind(this))
console.debug(`setup client ${this.id}`)
this.connectTimeout = setTimeout(() => {
if (this.nsps.size === 0) {
console.debug("no namespace joined yet, close the client")
this.close()
} else {
console.debug("the client has already joined a namespace, nothing to do")
}
}, this.server._connectTimeout)
}
/**
* Connects a client to a namespace.
*
* @param {String} name - the namespace
* @param {Object} auth - the auth parameters
* @private
*/
private connect(name: string, auth: object = {}) {
console.debug(`client ${this.id} connecting to namespace ${name} has: ${this.server._nsps.has(name)}`)
if (this.server._nsps.has(name)) {
return this.doConnect(name, auth)
}
this.server._checkNamespace(name, auth, (dynamicNsp: Namespace) => {
if (dynamicNsp) {
console.debug(`dynamic namespace ${dynamicNsp.name} was created`)
this.doConnect(name, auth)
} else {
console.debug(`creation of namespace ${name} was denied`)
this._packet({
type: PacketTypes.MESSAGE,
sub_type: SubPacketTypes.ERROR,
nsp: name,
data: {
message: "Invalid namespace"
}
})
}
})
}
doConnect(name, auth: object) {
if (this.connectTimeout) {
clearTimeout(this.connectTimeout)
this.connectTimeout = null
}
const nsp = this.server.of(name)
nsp._add(this, auth, (socket: Socket) => {
this.sockets.set(socket.id, socket)
this.nsps.set(nsp.name, socket)
})
}
/**
* Disconnects from all namespaces and closes transport.
*
* @private
*/
_disconnect() {
for (const socket of this.sockets.values()) {
socket.disconnect()
}
this.sockets.clear()
this.close()
}
/**
* Removes a socket. Called by each `Socket`.
*
* @private
*/
_remove(socket: Socket) {
if (this.sockets.has(socket.id)) {
this.sockets.delete(socket.id)
this.nsps.delete(socket.nsp.name)
} else {
console.debug(`ignoring remove for ${socket.id}`,)
}
process.nextTick(() => {
if (this.sockets.size == 0) {
this.onclose('no live socket')
}
})
}
/**
* Closes the underlying connection.
*
* @private
*/
private close() {
console.debug(`client ${this.id} close`)
if ("open" == this.conn.readyState) {
console.debug("forcing transport close")
this.onclose("forced server close")
this.conn.close()
}
}
/**
* Writes a packet to the transport.
*
* @param {Object} packet object
* @param {Object} opts
* @private
*/
_packet(packet: Packet, opts = { preEncoded: false }) {
// opts = opts || {}
// const self = this
// // this writes to the actual connection
// function writeToEngine(encodedPackets) {
// if (opts.volatile && !self.conn.transport.writable) return
// for (let i = 0; i < encodedPackets.length; i++) {
// self.conn.write(encodedPackets[i], { compress: opts.compress })
// }
// }
// if ("open" == this.conn.readyState) {
// debug("writing packet %j", packet)
// if (!opts.preEncoded) {
// // not broadcasting, need to encode
// writeToEngine(this.encoder.encode(packet)) // encode, then write results to engine
// } else {
// // a broadcast pre-encodes a packet
// writeToEngine(packet)
// }
// } else {
// debug("ignoring packet write %j", packet)
// }
if ("open" == this.conn.readyState) {
this.conn.send(opts.preEncoded ? packet as unknown as string : parser.encode(packet))
} else {
console.debug(`ignoring write packet ${JSON.stringify(packet)} to client ${this.id} is already close!`)
}
}
/**
* Called with incoming transport data.
*
* @private
*/
private ondata(data) {
// try/catch is needed for protocol violations (GH-1880)
try {
this.decoder.add(data)
} catch (e) {
this.onerror(e)
}
}
/**
* Called when parser fully decodes a packet.
*
* @private
*/
ondecoded(packet: Packet) {
if (SubPacketTypes.CONNECT == packet.sub_type) {
this.connect(packet.nsp, packet.data)
} else {
process.nextTick(() => {
const socket = this.nsps.get(packet.nsp)
if (socket) {
socket._onpacket(packet)
} else {
console.debug(`client ${this.id} no socket for namespace ${packet.nsp}.`)
}
})
}
}
/**
* Handles an error.
*
* @param {Object} err object
* @private
*/
private onerror(err) {
for (const socket of this.sockets.values()) {
socket._onerror(err)
}
this.conn.close()
}
onclose(reason?: string) {
this.conn.readyState = "closing"
// ======= engine.io
this.onClose(reason)
// cleanup connectTimeout
if (this.connectTimeout) {
clearTimeout(this.connectTimeout)
this.connectTimeout = null
}
console.debug(`client ${this.id} close with reason ${reason}`)
// ignore a potential subsequent `close` event
// `nsps` and `sockets` are cleaned up seamlessly
for (const socket of this.sockets.values()) {
socket._onclose(reason)
}
this.sockets.clear()
// this.decoder.destroy(); // clean up decoder
}
destroy() {
// this.conn.removeListener('data', this.ondata);
// this.conn.removeListener('error', this.onerror);
// this.conn.removeListener('close', this.onclose);
// this.decoder.removeListener('decoded', this.ondecoded);
}
//================== engine.io
onOpen() {
this.conn.readyState = "open"
this._packet({
type: PacketTypes.OPEN,
data: {
sid: this.id,
upgrades: [],
pingInterval: this.server.options.pingInterval,
pingTimeout: this.server.options.pingTimeout
}
})
this.schedulePing()
}
onPacket(packet: Packet) {
if ("open" === this.conn.readyState) {
// export packet event
// debug("packet")
// this.emit("packet", packet)
// Reset ping timeout on any packet, incoming data is a good sign of
// other side's liveness
this.resetPingTimeout(this.server.options.pingInterval + this.server.options.pingTimeout * 2)
switch (packet.type) {
case PacketTypes.PING:
this._packet({
type: PacketTypes.PONG,
data: packet.data
})
break
case PacketTypes.PONG:
this.schedulePing()
break
case PacketTypes.UPGRADE:
break
case PacketTypes.MESSAGE:
this.ondecoded(packet)
break
case PacketTypes.CLOSE:
this.onclose()
break
default:
console.log(`client ${this.id} reciver unknow packet type: ${packet.type}`)
}
} else {
console.debug(`packet received with closed client ${this.id}`)
}
}
/**
* Called upon transport considered closed.
* Possible reasons: `ping timeout`, `client error`, `parse error`,
* `transport error`, `server close`, `transport close`
*/
onClose(reason, description?: string) {
// if ("closed" !== this.conn.readyState) {
clearTimeout(this.pingIntervalTimer)
clearTimeout(this.pingTimeoutTimer)
clearInterval(this.checkIntervalTimer)
this.checkIntervalTimer = null
clearTimeout(this.upgradeTimeoutTimer)
// this.emit("close", reason, description)
// }
}
/**
* Pings client every `this.pingInterval` and expects response
* within `this.pingTimeout` or closes connection.
*
* @api private
*/
schedulePing() {
clearTimeout(this.pingIntervalTimer)
this.pingIntervalTimer = setTimeout(() => {
this.resetPingTimeout(this.server.options.pingTimeout)
process.nextTick(() => this._packet({ type: PacketTypes.PING }))
}, this.server.options.pingInterval)
}
/**
* Resets ping timeout.
*
* @api private
*/
resetPingTimeout(timeout: number) {
clearTimeout(this.pingTimeoutTimer)
this.pingTimeoutTimer = setTimeout(() => {
if (this.conn.readyState === "closed") return
this.onclose("ping timeout")
}, timeout)
}
}

View File

@ -1,9 +0,0 @@
export enum ServerEvent {
detect = 'detect',
connect = 'connect',
connection = 'connection',
message = 'message',
error = 'error',
disconnecting = 'disconnecting',
disconnect = 'disconnect',
}

View File

@ -1,677 +0,0 @@
import { EventEmitter } from 'events'
import { ServerEvent } from './constants'
import { Namespace } from './namespace'
import { Client } from './client'
import { Parser } from './parser'
import { Socket } from './socket'
import { Adapter } from './adapter'
import { Transport } from '../transport'
import { ParentNamespace } from './parent-namespace'
interface EngineOptions {
/**
* how many ms without a pong packet to consider the connection closed
* @default 5000
*/
pingTimeout: number
/**
* how many ms before sending a new ping packet
* @default 25000
*/
pingInterval: number
/**
* how many ms before an uncompleted transport upgrade is cancelled
* @default 10000
*/
upgradeTimeout: number
/**
* how many bytes or characters a message can be, before closing the session (to avoid DoS).
* @default 1e5 (100 KB)
*/
maxHttpBufferSize: number
/**
* A function that receives a given handshake or upgrade request as its first parameter,
* and can decide whether to continue or not. The second argument is a function that needs
* to be called with the decided information: fn(err, success), where success is a boolean
* value where false means that the request is rejected, and err is an error code.
*/
// allowRequest: (
// req: http.IncomingMessage,
// fn: (err: string | null | undefined, success: boolean) => void
// ) => void
/**
* the low-level transports that are enabled
* @default ["polling", "websocket"]
*/
// transports: Transport[]
/**
* whether to allow transport upgrades
* @default true
*/
allowUpgrades: boolean
/**
* parameters of the WebSocket permessage-deflate extension (see ws module api docs). Set to false to disable.
* @default false
*/
perMessageDeflate: boolean | object
/**
* parameters of the http compression for the polling transports (see zlib api docs). Set to false to disable.
* @default true
*/
httpCompression: boolean | object
/**
* what WebSocket server implementation to use. Specified module must
* conform to the ws interface (see ws module api docs). Default value is ws.
* An alternative c++ addon is also available by installing uws module.
*/
wsEngine: string
/**
* an optional packet which will be concatenated to the handshake packet emitted by Engine.IO.
*/
initialPacket: any
/**
* configuration of the cookie that contains the client sid to send as part of handshake response headers. This cookie
* might be used for sticky-session. Defaults to not sending any cookie.
* @default false
*/
// cookie: CookieSerializeOptions | boolean
/**
* the options that will be forwarded to the cors module
*/
// cors: CorsOptions
}
interface AttachOptions {
/**
* name of the path to capture
* @default "/engine.io"
*/
path: string
/**
* destroy unhandled upgrade requests
* @default true
*/
destroyUpgrade: boolean
/**
* milliseconds after which unhandled requests are ended
* @default 1000
*/
destroyUpgradeTimeout: number
}
interface EngineAttachOptions extends EngineOptions, AttachOptions { }
interface ServerOptions extends EngineAttachOptions {
event?: EventEmitter
root?: string
/**
* name of the path to capture
* @default "/socket.io"
*/
path: string
/**
* whether to serve the client files
* @default true
*/
serveClient: boolean
/**
* the adapter to use
* @default the in-memory adapter (https://github.com/socketio/socket.io-adapter)
*/
adapter: any
/**
* the parser to use
* @default the default parser (https://github.com/socketio/socket.io-parser)
*/
parser: any
/**
* how many ms before a client without namespace is closed
* @default 45000
*/
connectTimeout: number
}
interface WebSocketServer extends EventEmitter {
close(): void
}
class Server {
public readonly sockets: Namespace
/**
* @private
*/
_parser: Parser
private readonly encoder
/**
* @private
*/
_nsps: Map<string, Namespace>
private parentNsps: Map<
| string
| RegExp
| ((
name: string,
query: object,
fn: (err: Error, success: boolean) => void
) => void),
ParentNamespace
> = new Map();
private _adapter: Adapter
// private _serveClient: boolean;
private eio
private engine: { ws: any }
private _path: string
private clientPathRegex: RegExp
/**
* @private
*/
_connectTimeout: number
options: Partial<ServerOptions>
private websocketServer: WebSocketServer
private allClients: Map<string, Client>
constructor(instance: any, options: Partial<ServerOptions>) {
if (!instance) { throw new Error('instance can\'t be undefiend!') }
this.options = Object.assign({
event: new EventEmitter(),
path: '/socket.io',
root: root + '/wwwroot',
serveClient: false,
connectTimeout: 45000,
wsEngine: process.env.EIO_WS_ENGINE || "ws",
pingTimeout: 5000,
pingInterval: 25000,
upgradeTimeout: 10000,
maxHttpBufferSize: 1e6,
transports: 'websocket',
allowUpgrades: true,
httpCompression: {
threshold: 1024
},
cors: false
}, options)
this.initServerConfig()
this.sockets = this.of('/')
this.selectServerImpl(instance)
this.initServer()
}
/**
* Sets/gets whether client code is being served.
*
* @param {Boolean} v - whether to serve client code
* @return {Server|Boolean} self when setting or value when getting
* @public
*/
public serveClient(v: boolean): Server
public serveClient(): boolean
public serveClient(v?: boolean): Server | boolean {
throw new Error("Method not implemented.")
}
/**
* Executes the middleware for an incoming namespace not already created on the server.
*
* @param {String} name - name of incoming namespace
* @param {Object} auth - the auth parameters
* @param {Function} fn - callback
*
* @private
*/
_checkNamespace(
name: string,
auth: object,
fn: (nsp: Namespace) => void
) {
// if (this.parentNsps.size === 0) return fn(false)
// const keysIterator = this.parentNsps.keys()
// const run = () => {
// let nextFn = keysIterator.next()
// if (nextFn.done) {
// return fn(false)
// }
// nextFn.value(name, auth, (err, allow) => {
// if (err || !allow) {
// run()
// } else {
// fn(this.parentNsps.get(nextFn.value).createChild(name))
// }
// })
// }
fn(undefined)
}
/**
* Sets the client serving path.
*
* @param {String} v pathname
* @return {Server|String} self when setting or value when getting
* @public
*/
path(): string
path(v: string): Server
path(v?: any): string | Server {
if (!arguments.length) return this._path
this._path = v.replace(/\/$/, "")
const escapedPath = this._path.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")
this.clientPathRegex = new RegExp(
"^" +
escapedPath +
"/socket\\.io(\\.min|\\.msgpack\\.min)?\\.js(\\.map)?$"
)
return this
}
/**
* Set the delay after which a client without namespace is closed
* @param v
* @public
*/
public connectTimeout(v: number): Server
public connectTimeout(): number
public connectTimeout(v?: number): Server | number {
if (v === undefined) return this._connectTimeout
this._connectTimeout = v
return this
}
/**
* Sets the adapter for rooms.
*
* @param {Adapter} v pathname
* @return {Server|Adapter} self when setting or value when getting
* @public
*/
public adapter(): any
public adapter(v: any)
public adapter(v?): Server | any {
if (!arguments.length) return this._adapter
this._adapter = v
for (const nsp of this._nsps.values()) {
nsp._initAdapter()
}
return this
}
// /**
// * Attaches socket.io to a server or port.
// *
// * @param {http.Server|Number} srv - server or port
// * @param {Object} opts - options passed to engine.io
// * @return {Server} self
// * @public
// */
// public listen(srv: http.Server, opts?: Partial<ServerOptions>): Server
// public listen(srv: number, opts?: Partial<ServerOptions>): Server
// public listen(srv: any, opts: Partial<ServerOptions> = {}): Server {
// return this.attach(srv, opts)
// }
// /**
// * Attaches socket.io to a server or port.
// *
// * @param {http.Server|Number} srv - server or port
// * @param {Object} opts - options passed to engine.io
// * @return {Server} self
// * @public
// */
// public attach(srv: http.Server, opts?: Partial<ServerOptions>): Server
// public attach(port: number, opts?: Partial<ServerOptions>): Server
// public attach(srv: any, opts: Partial<ServerOptions> = {}): Server {
// if ("function" == typeof srv) {
// const msg =
// "You are trying to attach socket.io to an express " +
// "request handler function. Please pass a http.Server instance."
// throw new Error(msg)
// }
// // handle a port as a string
// if (Number(srv) == srv) {
// srv = Number(srv)
// }
// if ("number" == typeof srv) {
// debug("creating http server and binding to %d", srv)
// const port = srv
// srv = http.createServer((req, res) => {
// res.writeHead(404)
// res.end()
// })
// srv.listen(port)
// }
// // set engine.io path to `/socket.io`
// opts.path = opts.path || this._path
// this.initEngine(srv, opts)
// return this
// }
// /**
// * Initialize engine
// *
// * @param srv - the server to attach to
// * @param opts - options passed to engine.io
// * @private
// */
// private initEngine(srv: http.Server, opts: Partial<EngineAttachOptions>) {
// // initialize engine
// debug("creating engine.io instance with opts %j", opts)
// this.eio = engine.attach(srv, opts)
// // attach static file serving
// if (this._serveClient) this.attachServe(srv)
// // Export http server
// this.httpServer = srv
// // bind to engine events
// this.bind(this.eio)
// }
// /**
// * Attaches the static file serving.
// *
// * @param {Function|http.Server} srv http server
// * @private
// */
// private attachServe(srv) {
// debug("attaching client serving req handler")
// const evs = srv.listeners("request").slice(0)
// srv.removeAllListeners("request")
// srv.on("request", (req, res) => {
// if (this.clientPathRegex.test(req.url)) {
// this.serve(req, res)
// } else {
// for (let i = 0; i < evs.length; i++) {
// evs[i].call(srv, req, res)
// }
// }
// })
// }
// /**
// * Handles a request serving of client source and map
// *
// * @param {http.IncomingMessage} req
// * @param {http.ServerResponse} res
// * @private
// */
// private serve(req: http.IncomingMessage, res: http.ServerResponse) {
// const filename = req.url.replace(this._path, "")
// const isMap = dotMapRegex.test(filename)
// const type = isMap ? "map" : "source"
// // Per the standard, ETags must be quoted:
// // https://tools.ietf.org/html/rfc7232#section-2.3
// const expectedEtag = '"' + clientVersion + '"'
// const etag = req.headers["if-none-match"]
// if (etag) {
// if (expectedEtag == etag) {
// debug("serve client %s 304", type)
// res.writeHead(304)
// res.end()
// return
// }
// }
// debug("serve client %s", type)
// res.setHeader("Cache-Control", "public, max-age=0")
// res.setHeader(
// "Content-Type",
// "application/" + (isMap ? "json" : "javascript")
// )
// res.setHeader("ETag", expectedEtag)
// if (!isMap) {
// res.setHeader("X-SourceMap", filename.substring(1) + ".map")
// }
// Server.sendFile(filename, req, res)
// }
// /**
// * @param filename
// * @param req
// * @param res
// * @private
// */
// private static sendFile(
// filename: string,
// req: http.IncomingMessage,
// res: http.ServerResponse
// ) {
// const readStream = createReadStream(
// path.join(__dirname, "../client-dist/", filename)
// )
// const encoding = accepts(req).encodings(["br", "gzip", "deflate"])
// const onError = err => {
// if (err) {
// res.end()
// }
// }
// switch (encoding) {
// case "br":
// res.writeHead(200, { "content-encoding": "br" })
// readStream.pipe(createBrotliCompress()).pipe(res)
// pipeline(readStream, createBrotliCompress(), res, onError)
// break
// case "gzip":
// res.writeHead(200, { "content-encoding": "gzip" })
// pipeline(readStream, createGzip(), res, onError)
// break
// case "deflate":
// res.writeHead(200, { "content-encoding": "deflate" })
// pipeline(readStream, createDeflate(), res, onError)
// break
// default:
// res.writeHead(200)
// pipeline(readStream, res, onError)
// }
// }
// /**
// * Binds socket.io to an engine.io instance.
// *
// * @param {engine.Server} engine engine.io (or compatible) server
// * @return {Server} self
// * @public
// */
// public bind(engine): Server {
// this.engine = engine
// this.engine.on("connection", this.onconnection.bind(this))
// return this
// }
/**
* Called with each incoming transport connection.
*
* @param {engine.Socket} conn
* @return {Server} self
* @private
*/
private onconnection(conn): Server {
console.debug(`incoming connection with id ${conn.id}`)
let client = new Client(this, conn)
this.allClients.set(conn.id, client)
return this
}
// of(nsp: string): Namespace {
// if (!this._nsps.has(nsp)) {
// console.debug(`create Namespace ${nsp}`)
// this._nsps.set(nsp, new Namespace(this, nsp))
// }
// return this._nsps.get(nsp)
// }
/**
* Looks up a namespace.
*
* @param {String|RegExp|Function} name nsp name
* @param {Function} [fn] optional, nsp `connection` ev handler
* @public
*/
public of(
name:
| string
| RegExp
| ((
name: string,
query: object,
fn: (err: Error, success: boolean) => void
) => void),
fn?: (socket: Socket) => void
) {
if (typeof name === "function" || name instanceof RegExp) {
const parentNsp = new ParentNamespace(this)
console.debug(`initializing parent namespace ${parentNsp.name}`)
if (typeof name === "function") {
this.parentNsps.set(name, parentNsp)
} else {
this.parentNsps.set(
(nsp, conn, next) => next(null, (name as RegExp).test(nsp)),
parentNsp
)
}
if (fn) {
// @ts-ignore
parentNsp.on("connect", fn)
}
return parentNsp
}
if (String(name)[0] !== "/") name = "/" + name
let nsp = this._nsps.get(name)
if (!nsp) {
console.debug(`initializing namespace ${name}`)
nsp = new Namespace(this, name)
this._nsps.set(name, nsp)
}
if (fn) nsp.on("connect", fn)
return nsp
}
close(fn?: () => void): void {
this.clients.length
for (const client of this.allClients.values()) {
client._disconnect()
}
// this.engine.close()
this.websocketServer.close()
// if (this.httpServer) {
// this.httpServer.close(fn)
// } else {
fn && fn()
// }
}
on(event: "connection", listener: (socket: Socket) => void): Namespace
on(event: "connect", listener: (socket: Socket) => void): Namespace
on(event: string, listener: Function): Namespace
on(event: any, listener: any): Namespace {
return this.sockets.on(event, listener)
}
to(room: string): Namespace {
return this.sockets.to(room)
}
in(room: string): Namespace {
return this.sockets.in(room)
}
use(fn: (socket: Socket, fn: (err?: any) => void) => void): Namespace {
return this.sockets.use(fn)
}
emit(event: string, ...args: any[]): Namespace {
// @ts-ignore
return this.sockets.emit(event, ...args)
}
send(...args: any[]): Namespace {
return this.sockets.send(...args)
}
write(...args: any[]): Namespace {
return this.sockets.write(...args)
}
clients(...args: any[]): Namespace {
return this.sockets.clients(args[0])
}
compress(...args: any[]): Namespace {
return this.sockets.compress(args[0])
}
// ===============================
private initServerConfig() {
this.allClients = new Map()
this._nsps = new Map()
this.connectTimeout(this.options.connectTimeout || 45000)
this._parser = this.options.parser || new Parser()
this.adapter(this.options.adapter || Adapter)
}
private selectServerImpl(instance: any) {
let WebSocketServerImpl = undefined
if (instance.class.name.startsWith('io.netty.channel')) {
WebSocketServerImpl = require("../netty").NettyWebSocketServer
} else {
WebSocketServerImpl = require("../tomcat").TomcatWebSocketServer
}
this.websocketServer = new WebSocketServerImpl(instance, this.options)
}
private initServer() {
this.websocketServer.on(ServerEvent.connect, (transport: Transport) => {
this.onconnection(transport)
})
this.websocketServer.on(ServerEvent.message, (transport: Transport, text) => {
if (this.allClients.has(transport.id)) {
let client = this.allClients.get(transport.id)
client.onPacket(this._parser.decode(text))
} else {
console.error(`unknow transport ${transport.id} reciver message ${text}`)
}
})
this.websocketServer.on(ServerEvent.disconnect, (transport: Transport, reason) => {
if (this.allClients.has(transport.id)) {
this.allClients.get(transport.id).onclose(reason)
this.allClients.delete(transport.id)
} else {
console.error(`unknow transport ${transport?.id} disconnect cause ${reason}`)
}
})
this.websocketServer.on(ServerEvent.error, (transport: Transport, cause) => {
if (this.allClients.has(transport?.id)) {
let client = this.allClients.get(transport?.id)
if (client.listeners(ServerEvent.error).length) {
client.emit(ServerEvent.error, cause)
} else {
console.error(`client ${client.id} cause error: ${cause}`)
console.ex(cause)
}
} else {
console.error(`unknow transport ${transport?.id} cause error: ${cause}`)
console.ex(cause)
}
})
}
}
/**
* Expose main namespace (/).
*/
const emitterMethods = Object.keys(EventEmitter.prototype).filter(function (
key
) {
return typeof EventEmitter.prototype[key] === "function"
})
emitterMethods.forEach(function (fn) {
Server.prototype[fn] = function () {
return this.sockets[fn].apply(this.sockets, arguments)
}
})
export {
Server,
Socket,
Client,
Namespace,
ServerOptions
}

View File

@ -1,242 +0,0 @@
import { EventEmitter } from 'events'
import { Client } from './client'
import { ServerEvent } from './constants'
import { RESERVED_EVENTS, Socket } from './socket'
import { Adapter, Room, SocketId } from './adapter'
import { Server } from './index'
import { Packet } from './packet'
import { PacketTypes, SubPacketTypes } from './types'
export interface ExtendedError extends Error {
data?: any
}
export class Namespace extends EventEmitter {
public readonly name: string
public readonly sockets: Map<SocketId, Socket>
public adapter: Adapter
/** @private */
readonly server: Server
json: Namespace
/** @private */
_fns: Array<
(socket: Socket, next: (err: ExtendedError) => void) => void
> = [];
/** @private */
_rooms: Set<Room>
/** @private */
_flags: any = {}
/** @private */
_ids: number = 0
constructor(server: Server, name: string) {
super()
this.server = server
this.name = name + ''
this._initAdapter()
// =======================
this.sockets = new Map()
this._rooms = new Set()
}
_initAdapter() {
// @ts-ignore
this.adapter = new (this.server.adapter())(this)
}
/**
* Sets up namespace middleware.
*
* @return {Namespace} self
* @public
*/
public use(
fn: (socket: Socket, next: (err?: ExtendedError) => void) => void
): Namespace {
this._fns.push(fn)
return this
}
/**
* Executes the middleware for an incoming client.
*
* @param {Socket} socket - the socket that will get added
* @param {Function} fn - last fn call in the middleware
* @private
*/
private run(socket: Socket, fn: (err: ExtendedError) => void) {
const fns = this._fns.slice(0)
if (!fns.length) return fn(null)
function run(i) {
fns[i](socket, function (err) {
// upon error, short-circuit
if (err) return fn(err)
// if no middleware left, summon callback
if (!fns[i + 1]) return fn(null)
// go on to next
run(i + 1)
})
}
run(0)
}
to(name: string): Namespace {
this._rooms.add(name)
return this
}
in(name: string): Namespace {
return this.to(name)
}
_add(client: Client, query?: any, fn?: (socket: Socket) => void) {
const socket = new Socket(this, client, query || {})
console.debug(`client ${client.id} adding socket ${socket.id} to nsp ${this.name}`)
this.run(socket, err => {
process.nextTick(() => {
if ("open" == client.conn.readyState) {
if (err)
return socket._error({
message: err.message,
data: err.data
})
// track socket
this.sockets.set(socket.id, socket)
// it's paramount that the internal `onconnect` logic
// fires before user-set events to prevent state order
// violations (such as a disconnection before the connection
// logic is complete)
socket._onconnect()
// !!! at java multi thread need direct callback socket
if (fn) fn(socket)
// fire user-set events
super.emit(ServerEvent.connect, socket)
super.emit(ServerEvent.connection, socket)
} else {
console.debug(`next called after client ${client.id} was closed - ignoring socket`)
}
})
})
return socket
}
/**
* Removes a client. Called by each `Socket`.
*
* @private
*/
_remove(socket: Socket): void {
if (this.sockets.has(socket.id)) {
console.debug(`namespace ${this.name} remove socket ${socket.id}`)
this.sockets.delete(socket.id)
} else {
console.debug(`namespace ${this.name} ignoring remove for ${socket.id}`)
}
}
emit(event: string, ...args: any[]): boolean {
if (RESERVED_EVENTS.has(event)) {
throw new Error(`"${event}" is a reserved event name`)
}
// set up packet object
var packet = {
type: PacketTypes.MESSAGE,
sub_type: (this._flags.binary !== undefined ? this._flags.binary : this.hasBin(args)) ? SubPacketTypes.BINARY_EVENT : SubPacketTypes.EVENT,
name: event,
data: args
}
if ('function' == typeof args[args.length - 1]) {
throw new Error('Callbacks are not supported when broadcasting')
}
var rooms = new Set(this._rooms)
var flags = Object.assign({}, this._flags)
// reset flags
this._rooms.clear()
this._flags = {}
this.adapter.broadcast(packet, {
rooms: new Set(rooms),
flags: flags
})
// @ts-ignore
return this
}
send(...args: any[]): Namespace {
this.emit('message', ...args)
return this
}
write(...args: any[]): Namespace {
return this.send(...args)
}
/**
* Gets a list of clients.
*
* @return {Namespace} self
* @public
*/
public allSockets(): Promise<Set<SocketId>> {
if (!this.adapter) {
throw new Error("No adapter for this namespace, are you trying to get the list of clients of a dynamic namespace?")
}
const rooms = new Set(this._rooms)
this._rooms.clear()
return this.adapter.sockets(rooms)
}
/**
* Sets the compress flag.
*
* @param {Boolean} compress - if `true`, compresses the sending data
* @return {Namespace} self
* @public
*/
public compress(compress: boolean): Namespace {
this._flags.compress = compress
return this
}
/**
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
* receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle).
*
* @return {Namespace} self
* @public
*/
public get volatile(): Namespace {
this._flags.volatile = true
return this
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
*
* @return {Namespace} self
* @public
*/
public get local(): Namespace {
this._flags.local = true
return this
}
hasBin(args: any[]) {
return false
}
clients(fn: (sockets: Socket[]) => Namespace): Namespace {
return fn(Object.values(this.sockets))
}
close() {
this.removeAllListeners(ServerEvent.connect)
this.removeAllListeners(ServerEvent.connection)
Object.values(this.sockets).forEach(socket => socket.disconnect(false))
}
}

View File

@ -1,11 +0,0 @@
import { PacketTypes, SubPacketTypes } from './types'
export interface Packet {
type: PacketTypes;
sub_type?: SubPacketTypes;
nsp?: string;
id?: number;
name?: string;
data?: any;
attachments?: any;
}

View File

@ -1,40 +0,0 @@
import { Namespace } from "./namespace"
export class ParentNamespace extends Namespace {
private static count: number = 0;
private children: Set<Namespace> = new Set();
constructor(server) {
super(server, "/_" + ParentNamespace.count++)
}
_initAdapter() { }
public emit(...args: any[]): boolean {
this.children.forEach(nsp => {
nsp._rooms = this._rooms
nsp._flags = this._flags
nsp.emit.apply(nsp, args as any)
})
this._rooms.clear()
this._flags = {}
return true
}
createChild(name) {
const namespace = new Namespace(this.server, name)
namespace._fns = this._fns.slice(0)
this.listeners("connect").forEach(listener =>
// @ts-ignore
namespace.on("connect", listener)
)
this.listeners("connection").forEach(listener =>
// @ts-ignore
namespace.on("connection", listener)
)
this.children.add(namespace)
this.server._nsps.set(name, namespace)
return namespace
}
}

View File

@ -1,164 +0,0 @@
import { EventEmitter } from 'events'
import { Packet } from "./packet"
import { PacketTypes, SubPacketTypes } from "./types"
export class Parser extends EventEmitter {
encode(packet: Packet): string {
let origin = JSON.stringify(packet)
// first is type
let str = '' + packet.type
if (packet.type == PacketTypes.PONG) {
if (packet.data) { str += packet.data };
return str
}
if (packet.sub_type != undefined) {
str += packet.sub_type
}
// attachments if we have them
if ([SubPacketTypes.BINARY_EVENT, SubPacketTypes.BINARY_ACK].includes(packet.sub_type)) {
str += packet.attachments + '-'
}
// if we have a namespace other than `/`
// we append it followed by a comma `,`
if (packet.nsp && '/' !== packet.nsp) {
str += packet.nsp + ','
}
// immediately followed by the id
if (null != packet.id) {
str += packet.id
}
if (packet.sub_type == SubPacketTypes.EVENT) {
if (packet.name == undefined) { throw new Error(`SubPacketTypes.EVENT name can't be empty!`) }
packet.data = [packet.name, ...packet.data]
}
// json data
if (null != packet.data) {
let payload = this.tryStringify(packet.data)
if (payload !== false) {
str += payload
} else {
return '4"encode error"'
}
}
console.trace(`encoded ${origin} as ${str}`)
return str
}
tryStringify(str: any) {
try {
return JSON.stringify(str)
} catch (e) {
return false
}
}
decode(str: string): Packet {
let i = 0
// ignore parse binary
// if ((frame.getByte(0) == 'b' && frame.getByte(1) == '4')
// || frame.getByte(0) == 4 || frame.getByte(0) == 1) {
// return parseBinary(head, frame);
// }
// look up type
let p: Packet = {
type: Number(str.charAt(i))
}
if (null == PacketTypes[p.type]) {
return this.error('unknown packet type ' + p.type)
}
// if str empty return
if (str.length == i + 1) {
return p
}
// if is ping packet read data and return
if (PacketTypes.PING == p.type) {
p.data = str.substr(++i)
return p
}
// look up sub type
p.sub_type = Number(str.charAt(++i))
if (null == PacketTypes[p.sub_type]) {
return this.error('unknown sub packet type ' + p.type)
}
// look up attachments if type binary
if ([SubPacketTypes.BINARY_ACK, SubPacketTypes.BINARY_EVENT].includes(p.sub_type)) {
let buf = ''
while (str.charAt(++i) !== '-') {
buf += str.charAt(i)
if (i == str.length) break
}
if (buf != `${Number(buf)}` || str.charAt(i) !== '-') {
return this.error('Illegal attachments')
}
p.attachments = Number(buf)
}
// look up namespace (if any)
if ('/' === str.charAt(i + 1)) {
p.nsp = ''
while (++i) {
let c = str.charAt(i)
if (',' === c) break
p.nsp += c
if (i === str.length) break
}
} else {
p.nsp = '/'
}
// handle namespace query
if (p.nsp.indexOf('?') !== -1) {
p.nsp = p.nsp.split('?')[0]
}
// look up id
let next = str.charAt(i + 1)
if ('' !== next && !isNaN(Number(next))) {
let id = ''
while (++i) {
let c = str.charAt(i)
if (null == c || isNaN(Number(c))) {
--i
break
}
id += str.charAt(i)
if (i === str.length) break
}
p.id = Number(id)
}
// ignore binary packet
if (p.sub_type == SubPacketTypes.BINARY_EVENT) {
return this.error('not support binary parse...')
}
// look up json data
if (str.charAt(++i)) {
let payload = this.tryParse(str.substr(i))
let isPayloadValid = payload !== false && (p.sub_type == SubPacketTypes.ERROR || Array.isArray(payload))
if (isPayloadValid) {
p.name = payload[0]
p.data = payload.slice(1)
} else {
return this.error('invalid payload ' + str.substr(i))
}
}
console.trace(`decoded ${str} as ${JSON.stringify(p)}`)
return p
}
tryParse(str: string) {
try {
return JSON.parse(str)
} catch (e) {
return false
}
}
error(error: string): Packet {
return {
type: PacketTypes.MESSAGE,
sub_type: SubPacketTypes.ERROR,
data: 'parser error: ' + error
}
}
}

View File

@ -1,491 +0,0 @@
import { EventEmitter } from 'events'
import { Packet } from './packet'
import { PacketTypes, SubPacketTypes } from './types'
import { Client } from './client'
import { Namespace } from './namespace'
import * as querystring from 'querystring'
import { ServerEvent } from './constants'
import { Adapter, BroadcastFlags, Room, SocketId } from './adapter'
import { Server } from 'index'
export const RESERVED_EVENTS = new Set([
"connect",
"connect_error",
"disconnect",
"disconnecting",
// EventEmitter reserved events: https://nodejs.org/api/events.html#events_event_newlistener
"newListener",
"removeListener"
])
/**
* The handshake details
*/
export interface Handshake {
/**
* The headers sent as part of the handshake
*/
headers: object
/**
* The date of creation (as string)
*/
time: string
/**
* The ip of the client
*/
address: string
/**
* Whether the connection is cross-domain
*/
xdomain: boolean
/**
* Whether the connection is secure
*/
secure: boolean
/**
* The date of creation (as unix timestamp)
*/
issued: number
/**
* The request URL string
*/
url: string
/**
* The query object
*/
query: any
/**
* The auth object
*/
auth: any
}
export class Socket extends EventEmitter {
nsp: Namespace
public readonly id: SocketId
public readonly handshake: Handshake
public connected: boolean
public disconnected: boolean
private readonly server: Server
private readonly adapter: Adapter
client: Client
private acks: Map<number, () => void>
fns: any[]
private flags: BroadcastFlags = {};
private _rooms: Set<Room> = new Set();
private _anyListeners: Array<(...args: any[]) => void>
constructor(nsp: Namespace, client: Client, auth = {}) {
super()
this.nsp = nsp
this.server = nsp.server
this.adapter = this.nsp.adapter
this.id = nsp.name !== '/' ? nsp.name + '#' + client.id : client.id
this.client = client
this.acks = new Map()
this.connected = true
this.disconnected = false
this.handshake = this.buildHandshake(auth)
this.fns = []
this.flags = {}
this._rooms = new Set()
}
emit(event: string, ...args: any[]): boolean {
let packet: Packet = {
type: PacketTypes.MESSAGE,
sub_type: (this.flags.binary !== undefined ? this.flags.binary : this.hasBin(args)) ? SubPacketTypes.BINARY_EVENT : SubPacketTypes.EVENT,
name: event,
data: args
}
// access last argument to see if it's an ACK callback
if (typeof args[args.length - 1] === "function") {
if (this._rooms.size || this.flags.broadcast) {
throw new Error("Callbacks are not supported when broadcasting")
}
// console.debug("emitting packet with ack id %d", this.nsp._ids)
this.acks.set(this.nsp._ids, args.pop())
packet.id = this.nsp._ids++
}
const rooms = new Set(this._rooms)
const flags = Object.assign({}, this.flags)
// reset flags
this._rooms.clear()
this.flags = {}
if (rooms.size || flags.broadcast) {
this.adapter.broadcast(packet, {
except: new Set([this.id]),
rooms: rooms,
flags: flags
})
} else {
// dispatch packet
this.packet(packet, flags)
}
return true
}
to(name: Room): Socket {
this._rooms.add(name)
return this
}
in(room: string): Socket {
return this.to(room)
}
use(fn: (packet: Packet, next: (err?: any) => void) => void): Socket {
throw new Error("Method not implemented.")
}
send(...args: any[]): Socket {
this.emit("message", ...args)
return this
}
write(...args: any[]): Socket {
return this.send(...args)
}
public join(rooms: Room | Array<Room>): Promise<void> | void {
console.debug(`join room ${rooms}`)
return this.adapter.addAll(
this.id,
new Set(Array.isArray(rooms) ? rooms : [rooms])
)
}
/**
* Leaves a room.
*
* @param {String} room
* @return a Promise or nothing, depending on the adapter
* @public
*/
public leave(room: string): Promise<void> | void {
console.debug(`leave room ${room}`)
return this.adapter.del(this.id, room)
}
/**
* Leave all rooms.
*
* @private
*/
private leaveAll(): void {
this.adapter.delAll(this.id)
}
/**
* Called by `Namespace` upon successful
* middleware execution (ie: authorization).
* Socket is added to namespace array before
* call to join, so adapters can access it.
*
* @private
*/
_onconnect(): void {
console.debug(`socket ${this.id} connected - writing packet`)
this.join(this.id)
this.packet({ type: PacketTypes.MESSAGE, sub_type: SubPacketTypes.CONNECT, data: { sid: this.id } })
}
_onpacket(packet: Packet) {
switch (packet.sub_type) {
// 2
case SubPacketTypes.EVENT:
this.onevent(packet)
break
// 5
case SubPacketTypes.BINARY_EVENT:
this.onevent(packet)
break
// 3
case SubPacketTypes.ACK:
this.onack(packet)
break
// 6
case SubPacketTypes.BINARY_ACK:
this.onack(packet)
break
// 1
case SubPacketTypes.DISCONNECT:
this.ondisconnect()
break
// 4
case SubPacketTypes.ERROR:
this._onerror(new Error(packet.data))
}
}
onevent(packet: Packet) {
if (null != packet.id) {
console.trace(`attaching ack ${packet.id} callback to client ${this.id} event`)
this.dispatch(packet, this.ack(packet.id))
} else {
this.dispatch(packet)
}
}
ack(id: number) {
let sent = false
return (...args: any[]) => {
if (sent) return
this.packet({
id: id,
type: PacketTypes.MESSAGE,
sub_type: this.hasBin(args) ? SubPacketTypes.BINARY_ACK : SubPacketTypes.ACK,
data: args
})
sent = true
}
}
onack(packet: Packet) {
let ack = this.acks.get(packet.id)
if ('function' == typeof ack) {
console.trace(`calling ack ${packet.id} on socket ${this.id} with ${packet.data}`)
ack.apply(this, packet.data)
this.acks.delete(packet.id)
} else {
console.trace(`bad ack ${packet.id} on socket ${this.id}`)
}
}
/**
* Called upon client disconnect packet.
*
* @private
*/
private ondisconnect(): void {
console.debug(`socket ${this.id} got disconnect packet`)
this._onclose("client namespace disconnect")
}
/**
* Handles a client error.
*
* @private
*/
_onerror(err): void {
if (this.listeners("error").length) {
super.emit("error", err)
} else {
console.error(`Missing error handler on 'socket(${this.id})'.`)
console.error(err.stack)
}
}
/**
* Called upon closing. Called by `Client`.
*
* @param {String} reason
* @throw {Error} optional error object
*
* @private
*/
_onclose(reason: string) {
if (!this.connected) return this
console.debug(`closing socket ${this.id} - reason: ${reason} connected: ${this.connected}`)
super.emit(ServerEvent.disconnecting, reason)
this.leaveAll()
this.nsp._remove(this)
this.client._remove(this)
this.connected = false
this.disconnected = true
super.emit(ServerEvent.disconnect, reason)
}
/**
* Produces an `error` packet.
*
* @param {Object} err - error object
*
* @private
*/
_error(err) {
this.packet({ type: PacketTypes.MESSAGE, sub_type: SubPacketTypes.ERROR, data: err })
}
disconnect(close?: boolean): Socket {
if (!this.connected) return this
if (close) {
this.client._disconnect()
} else {
this.packet({ type: PacketTypes.MESSAGE, sub_type: SubPacketTypes.DISCONNECT })
this._onclose('server namespace disconnect')
}
return this
}
compress(compress: boolean): Socket {
throw new Error("Method not implemented.")
}
/**
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
* receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle).
*
* @return {Socket} self
* @public
*/
public get volatile(): Socket {
this.flags.volatile = true
return this
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to every sockets but the
* sender.
*
* @return {Socket} self
* @public
*/
public get broadcast(): Socket {
this.flags.broadcast = true
return this
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
*
* @return {Socket} self
* @public
*/
public get local(): Socket {
this.flags.local = true
return this
}
/**
* A reference to the request that originated the underlying Engine.IO Socket.
*
* @public
*/
public get request(): any {
return this.client.request
}
/**
* A reference to the underlying Client transport connection (Engine.IO Socket object).
*
* @public
*/
public get conn() {
return this.client.conn
}
/**
* @public
*/
public get rooms(): Set<Room> {
return this.adapter.socketRooms(this.id) || new Set()
}
/**
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
* callback.
*
* @param listener
* @public
*/
public onAny(listener: (...args: any[]) => void): Socket {
this._anyListeners = this._anyListeners || []
this._anyListeners.push(listener)
return this
}
/**
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
* callback. The listener is added to the beginning of the listeners array.
*
* @param listener
* @public
*/
public prependAny(listener: (...args: any[]) => void): Socket {
this._anyListeners = this._anyListeners || []
this._anyListeners.unshift(listener)
return this
}
/**
* Removes the listener that will be fired when any event is emitted.
*
* @param listener
* @public
*/
public offAny(listener?: (...args: any[]) => void): Socket {
if (!this._anyListeners) {
return this
}
if (listener) {
const listeners = this._anyListeners
for (let i = 0; i < listeners.length; i++) {
if (listener === listeners[i]) {
listeners.splice(i, 1)
return this
}
}
} else {
this._anyListeners = []
}
return this
}
/**
* Returns an array of listeners that are listening for any event that is specified. This array can be manipulated,
* e.g. to remove listeners.
*
* @public
*/
public listenersAny() {
return this._anyListeners || []
}
// ==========================================
buildHandshake(auth): Handshake {
let requestUri = this.request.uri()
let headers = {}
let nativeHeaders = this.request.headers()
nativeHeaders.forEach(function (header) {
headers[header.getKey()] = header.getValue()
})
return {
headers: headers,
time: new Date() + '',
address: this.conn.remoteAddress + '',
xdomain: !!headers['origin'],
secure: false,
issued: +new Date(),
url: requestUri,
query: querystring.parse(requestUri.indexOf('?') != -1 ? requestUri.split('?')[1] : ''),
auth
}
}
packet(packet: Packet, opts: any = { preEncoded: false }) {
if (!opts.preEncoded) {
packet.nsp = this.nsp.name
opts.compress = false !== opts.compress
}
try {
this.client._packet(packet, opts)
} catch (error) {
this._onerror(error)
}
}
dispatch(packet: Packet, ack?: () => void) {
if (ack) { this.acks.set(packet.id, ack) }
super.emit(packet.name, ...packet.data, ack)
}
private hasBin(obj: any) {
return false
}
}

View File

@ -1,18 +0,0 @@
export enum PacketTypes {
OPEN,
CLOSE,
PING,
PONG,
MESSAGE,
UPGRADE,
NOOP,
}
export enum SubPacketTypes {
CONNECT,
DISCONNECT,
EVENT,
ACK,
ERROR,
BINARY_EVENT,
BINARY_ACK
}

View File

@ -0,0 +1,279 @@
import { EventEmitter } from "events"
export type SocketId = string
export type Room = string
export interface BroadcastFlags {
volatile?: boolean
compress?: boolean
local?: boolean
broadcast?: boolean
binary?: boolean
}
export interface BroadcastOptions {
rooms: Set<Room>
except?: Set<SocketId>
flags?: BroadcastFlags
}
export class Adapter extends EventEmitter {
public rooms: Map<Room, Set<SocketId>> = new Map();
public sids: Map<SocketId, Set<Room>> = new Map();
private readonly encoder
/**
* In-memory adapter constructor.
*
* @param {Namespace} nsp
*/
constructor(readonly nsp: any) {
super()
this.encoder = nsp.server.encoder
}
/**
* To be overridden
*/
public init(): Promise<void> | void { }
/**
* To be overridden
*/
public close(): Promise<void> | void { }
/**
* Adds a socket to a list of room.
*
* @param {SocketId} id the socket id
* @param {Set<Room>} rooms a set of rooms
* @public
*/
public addAll(id: SocketId, rooms: Set<Room>): Promise<void> | void {
if (!this.sids.has(id)) {
this.sids.set(id, new Set())
}
for (const room of rooms) {
this.sids.get(id).add(room)
if (!this.rooms.has(room)) {
this.rooms.set(room, new Set())
this.emit("create-room", room)
}
if (!this.rooms.get(room).has(id)) {
this.rooms.get(room).add(id)
this.emit("join-room", room, id)
}
}
}
/**
* Removes a socket from a room.
*
* @param {SocketId} id the socket id
* @param {Room} room the room name
*/
public del(id: SocketId, room: Room): Promise<void> | void {
if (this.sids.has(id)) {
this.sids.get(id).delete(room)
}
this._del(room, id)
}
private _del(room, id) {
if (this.rooms.has(room)) {
const deleted = this.rooms.get(room).delete(id)
if (deleted) {
this.emit("leave-room", room, id)
}
if (this.rooms.get(room).size === 0) {
this.rooms.delete(room)
this.emit("delete-room", room)
}
}
}
/**
* Removes a socket from all rooms it's joined.
*
* @param {SocketId} id the socket id
*/
public delAll(id: SocketId): void {
if (!this.sids.has(id)) {
return
}
for (const room of this.sids.get(id)) {
this._del(room, id)
}
this.sids.delete(id)
}
/**
* Broadcasts a packet.
*
* Options:
* - `flags` {Object} flags for this packet
* - `except` {Array} sids that should be excluded
* - `rooms` {Array} list of rooms to broadcast to
*
* @param {Object} packet the packet object
* @param {Object} opts the options
* @public
*/
public broadcast(packet: any, opts: BroadcastOptions): void {
const flags = opts.flags || {}
const basePacketOpts = {
preEncoded: true,
volatile: flags.volatile,
compress: flags.compress
}
packet.nsp = this.nsp.name
const encodedPackets = this.encoder.encode(packet)
const packetOpts = encodedPackets.map(encodedPacket => {
if (typeof encodedPacket === "string") {
return {
...basePacketOpts,
wsPreEncoded: "4" + encodedPacket // "4" being the "message" packet type in Engine.IO
}
} else {
return basePacketOpts
}
})
this.apply(opts, socket => {
for (let i = 0; i < encodedPackets.length; i++) {
socket.client.writeToEngine(encodedPackets[i], packetOpts[i])
}
})
}
/**
* Gets a list of sockets by sid.
*
* @param {Set<Room>} rooms the explicit set of rooms to check.
*/
public sockets(rooms: Set<Room>): Promise<Set<SocketId>> {
const sids = new Set<SocketId>()
this.apply({ rooms }, socket => {
sids.add(socket.id)
})
return Promise.resolve(sids)
}
/**
* Gets the list of rooms a given socket has joined.
*
* @param {SocketId} id the socket id
*/
public socketRooms(id: SocketId): Set<Room> | undefined {
return this.sids.get(id)
}
/**
* Returns the matching socket instances
*
* @param opts - the filters to apply
*/
public fetchSockets(opts: BroadcastOptions): Promise<any[]> {
const sockets = []
this.apply(opts, socket => {
sockets.push(socket)
})
return Promise.resolve(sockets)
}
/**
* Makes the matching socket instances join the specified rooms
*
* @param opts - the filters to apply
* @param rooms - the rooms to join
*/
public addSockets(opts: BroadcastOptions, rooms: Room[]): void {
this.apply(opts, socket => {
socket.join(rooms)
})
}
/**
* Makes the matching socket instances leave the specified rooms
*
* @param opts - the filters to apply
* @param rooms - the rooms to leave
*/
public delSockets(opts: BroadcastOptions, rooms: Room[]): void {
this.apply(opts, socket => {
rooms.forEach(room => socket.leave(room))
})
}
/**
* Makes the matching socket instances disconnect
*
* @param opts - the filters to apply
* @param close - whether to close the underlying connection
*/
public disconnectSockets(opts: BroadcastOptions, close: boolean): void {
this.apply(opts, socket => {
socket.disconnect(close)
})
}
private apply(opts: BroadcastOptions, callback: (socket) => void): void {
const rooms = opts.rooms
const except = this.computeExceptSids(opts.except)
if (rooms.size) {
const ids = new Set()
for (const room of rooms) {
if (!this.rooms.has(room)) continue
for (const id of this.rooms.get(room)) {
if (ids.has(id) || except.has(id)) continue
const socket = this.nsp.sockets.get(id)
if (socket) {
callback(socket)
ids.add(id)
}
}
}
} else {
for (const [id] of this.sids) {
if (except.has(id)) continue
const socket = this.nsp.sockets.get(id)
if (socket) callback(socket)
}
}
}
private computeExceptSids(exceptRooms?: Set<Room>) {
const exceptSids = new Set()
if (exceptRooms && exceptRooms.size > 0) {
for (const room of exceptRooms) {
if (this.rooms.has(room)) {
this.rooms.get(room).forEach(sid => exceptSids.add(sid))
}
}
}
return exceptSids
}
/**
* Send a packet to the other Socket.IO servers in the cluster
* @param packet - an array of arguments, which may include an acknowledgement callback at the end
*/
public serverSideEmit(packet: any[]): void {
throw new Error(
"this adapter does not support the serverSideEmit() functionality"
)
}
}

View File

@ -0,0 +1,78 @@
import { isBinary } from "./is-binary"
/**
* Replaces every Buffer | ArrayBuffer | Blob | File in packet with a numbered placeholder.
*
* @param {Object} packet - socket.io event packet
* @return {Object} with deconstructed packet and list of buffers
* @public
*/
export function deconstructPacket(packet) {
const buffers = []
const packetData = packet.data
const pack = packet
pack.data = _deconstructPacket(packetData, buffers)
pack.attachments = buffers.length // number of binary 'attachments'
return { packet: pack, buffers: buffers }
}
function _deconstructPacket(data, buffers) {
if (!data) return data
if (isBinary(data)) {
const placeholder = { _placeholder: true, num: buffers.length }
buffers.push(data)
return placeholder
} else if (Array.isArray(data)) {
const newData = new Array(data.length)
for (let i = 0; i < data.length; i++) {
newData[i] = _deconstructPacket(data[i], buffers)
}
return newData
} else if (typeof data === "object" && !(data instanceof Date)) {
const newData = {}
for (const key in data) {
if (data.hasOwnProperty(key)) {
newData[key] = _deconstructPacket(data[key], buffers)
}
}
return newData
}
return data
}
/**
* Reconstructs a binary packet from its placeholder packet and buffers
*
* @param {Object} packet - event packet with placeholders
* @param {Array} buffers - binary buffers to put in placeholder positions
* @return {Object} reconstructed packet
* @public
*/
export function reconstructPacket(packet, buffers) {
packet.data = _reconstructPacket(packet.data, buffers)
packet.attachments = undefined // no longer useful
return packet
}
function _reconstructPacket(data, buffers) {
if (!data) return data
if (data && data._placeholder) {
return buffers[data.num] // appropriate buffer (should be natural order anyway)
} else if (Array.isArray(data)) {
for (let i = 0; i < data.length; i++) {
data[i] = _reconstructPacket(data[i], buffers)
}
} else if (typeof data === "object") {
for (const key in data) {
if (data.hasOwnProperty(key)) {
data[key] = _reconstructPacket(data[key], buffers)
}
}
}
return data
}

View File

@ -0,0 +1,316 @@
import EventEmitter = require("events")
import { deconstructPacket, reconstructPacket } from "./binary"
import { isBinary, hasBinary } from "./is-binary"
// const debug = require("debug")("socket.io-parser")
/**
* Protocol version.
*
* @public
*/
export const protocol: number = 5
export enum PacketType {
CONNECT,
DISCONNECT,
EVENT,
ACK,
CONNECT_ERROR,
BINARY_EVENT,
BINARY_ACK,
}
export interface Packet {
type: PacketType
nsp: string
data?: any
id?: number
attachments?: number
}
/**
* A socket.io Encoder instance
*/
export class Encoder {
/**
* Encode a packet as a single string if non-binary, or as a
* buffer sequence, depending on packet type.
*
* @param {Object} obj - packet object
*/
public encode(obj: Packet) {
console.trace("encoding packet", JSON.stringify(obj))
if (obj.type === PacketType.EVENT || obj.type === PacketType.ACK) {
if (hasBinary(obj)) {
obj.type =
obj.type === PacketType.EVENT
? PacketType.BINARY_EVENT
: PacketType.BINARY_ACK
return this.encodeAsBinary(obj)
}
}
return [this.encodeAsString(obj)]
}
/**
* Encode packet as string.
*/
private encodeAsString(obj: Packet) {
// first is type
let str = "" + obj.type
// attachments if we have them
if (
obj.type === PacketType.BINARY_EVENT ||
obj.type === PacketType.BINARY_ACK
) {
str += obj.attachments + "-"
}
// if we have a namespace other than `/`
// we append it followed by a comma `,`
if (obj.nsp && "/" !== obj.nsp) {
str += obj.nsp + ","
}
// immediately followed by the id
if (null != obj.id) {
str += obj.id
}
// json data
if (null != obj.data) {
str += JSON.stringify(obj.data)
}
console.trace("encoded", JSON.stringify(obj), "as", str)
return str
}
/**
* Encode packet as 'buffer sequence' by removing blobs, and
* deconstructing packet into object with placeholders and
* a list of buffers.
*/
private encodeAsBinary(obj: Packet) {
const deconstruction = deconstructPacket(obj)
const pack = this.encodeAsString(deconstruction.packet)
const buffers = deconstruction.buffers
buffers.unshift(pack) // add packet info to beginning of data list
return buffers // write all the buffers
}
}
/**
* A socket.io Decoder instance
*
* @return {Object} decoder
*/
export class Decoder extends EventEmitter {
private reconstructor: BinaryReconstructor
constructor() {
super()
}
/**
* Decodes an encoded packet string into packet JSON.
*
* @param {String} obj - encoded packet
*/
public add(obj: any) {
let packet
if (typeof obj === "string") {
packet = this.decodeString(obj)
if (
packet.type === PacketType.BINARY_EVENT ||
packet.type === PacketType.BINARY_ACK
) {
// binary packet's json
this.reconstructor = new BinaryReconstructor(packet)
// no attachments, labeled binary but no binary data to follow
if (packet.attachments === 0) {
super.emit("decoded", packet)
}
} else {
// non-binary full packet
super.emit("decoded", packet)
}
} else if (isBinary(obj) || obj.base64) {
// raw binary data
if (!this.reconstructor) {
throw new Error("got binary data when not reconstructing a packet")
} else {
packet = this.reconstructor.takeBinaryData(obj)
if (packet) {
// received final buffer
this.reconstructor = null
super.emit("decoded", packet)
}
}
} else {
throw new Error("Unknown type: " + obj)
}
}
/**
* Decode a packet String (JSON data)
*
* @param {String} str
* @return {Object} packet
*/
private decodeString(str): Packet {
let i = 0
// look up type
const p: any = {
type: Number(str.charAt(0)),
}
if (PacketType[p.type] === undefined) {
throw new Error("unknown packet type " + p.type)
}
// look up attachments if type binary
if (
p.type === PacketType.BINARY_EVENT ||
p.type === PacketType.BINARY_ACK
) {
const start = i + 1
while (str.charAt(++i) !== "-" && i != str.length) { }
const buf = str.substring(start, i)
if (buf != Number(buf) || str.charAt(i) !== "-") {
throw new Error("Illegal attachments")
}
p.attachments = Number(buf)
}
// look up namespace (if any)
if ("/" === str.charAt(i + 1)) {
const start = i + 1
while (++i) {
const c = str.charAt(i)
if ("," === c) break
if (i === str.length) break
}
p.nsp = str.substring(start, i)
} else {
p.nsp = "/"
}
// look up id
const next = str.charAt(i + 1)
if ("" !== next && Number(next) == next) {
const start = i + 1
while (++i) {
const c = str.charAt(i)
if (null == c || Number(c) != c) {
--i
break
}
if (i === str.length) break
}
p.id = Number(str.substring(start, i + 1))
}
// look up json data
if (str.charAt(++i)) {
const payload = tryParse(str.substr(i))
if (Decoder.isPayloadValid(p.type, payload)) {
p.data = payload
} else {
throw new Error("invalid payload")
}
}
console.trace("decoded", str, "as", p)
return p
}
private static isPayloadValid(type: PacketType, payload: any): boolean {
switch (type) {
case PacketType.CONNECT:
return typeof payload === "object"
case PacketType.DISCONNECT:
return payload === undefined
case PacketType.CONNECT_ERROR:
return typeof payload === "string" || typeof payload === "object"
case PacketType.EVENT:
case PacketType.BINARY_EVENT:
return Array.isArray(payload) && payload.length > 0
case PacketType.ACK:
case PacketType.BINARY_ACK:
return Array.isArray(payload)
}
}
/**
* Deallocates a parser's resources
*/
public destroy() {
if (this.reconstructor) {
this.reconstructor.finishedReconstruction()
}
}
}
function tryParse(str) {
try {
return JSON.parse(str)
} catch (e) {
return false
}
}
/**
* A manager of a binary event's 'buffer sequence'. Should
* be constructed whenever a packet of type BINARY_EVENT is
* decoded.
*
* @param {Object} packet
* @return {BinaryReconstructor} initialized reconstructor
*/
class BinaryReconstructor {
private reconPack
private buffers: Array<Buffer | ArrayBuffer> = [];
constructor(readonly packet: Packet) {
this.reconPack = packet
}
/**
* Method to be called when binary data received from connection
* after a BINARY_EVENT packet.
*
* @param {Buffer | ArrayBuffer} binData - the raw binary data received
* @return {null | Object} returns null if more binary data is expected or
* a reconstructed packet object if all buffers have been received.
*/
public takeBinaryData(binData) {
this.buffers.push(binData)
if (this.buffers.length === this.reconPack.attachments) {
// done with buffer list
const packet = reconstructPacket(this.reconPack, this.buffers)
this.finishedReconstruction()
return packet
}
return null
}
/**
* Cleans up binary packet reconstruction variables.
*/
public finishedReconstruction() {
this.reconPack = null
this.buffers = []
}
}

View File

@ -0,0 +1,65 @@
const withNativeArrayBuffer: boolean = typeof ArrayBuffer === "function"
const isView = (obj: any) => {
return typeof ArrayBuffer.isView === "function"
? ArrayBuffer.isView(obj)
: obj.buffer instanceof ArrayBuffer
}
const toString = Object.prototype.toString
const withNativeBlob = false
// typeof Blob === "function" ||
// (typeof Blob !== "undefined" &&
// toString.call(Blob) === "[object BlobConstructor]")
const withNativeFile = false
// typeof File === "function" ||
// (typeof File !== "undefined" &&
// toString.call(File) === "[object FileConstructor]")
/**
* Returns true if obj is a Buffer, an ArrayBuffer, a Blob or a File.
*
* @private
*/
export function isBinary(obj: any) {
return (
(withNativeArrayBuffer && (obj instanceof ArrayBuffer || isView(obj)))
// || (withNativeBlob && obj instanceof Blob) || (withNativeFile && obj instanceof File)
)
}
export function hasBinary(obj: any, toJSON?: boolean) {
if (!obj || typeof obj !== "object") {
return false
}
if (Array.isArray(obj)) {
for (let i = 0, l = obj.length; i < l; i++) {
if (hasBinary(obj[i])) {
return true
}
}
return false
}
if (isBinary(obj)) {
return true
}
if (
obj.toJSON &&
typeof obj.toJSON === "function" &&
arguments.length === 1
) {
return hasBinary(obj.toJSON(), true)
}
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key) && hasBinary(obj[key])) {
return true
}
}
return false
}

View File

@ -0,0 +1,320 @@
// import type { BroadcastFlags, Room, SocketId } from "socket.io-adapter"
import type { BroadcastFlags, Room, SocketId } from "../socket.io-adapter"
import { Handshake, RESERVED_EVENTS, Socket } from "./socket"
// import { PacketType } from "socket.io-parser"
import { PacketType } from "../socket.io-parser"
// import type { Adapter } from "socket.io-adapter"
import type { Adapter } from "../socket.io-adapter"
import type {
EventParams,
EventNames,
EventsMap,
TypedEventBroadcaster,
} from "./typed-events"
export class BroadcastOperator<EmitEvents extends EventsMap>
implements TypedEventBroadcaster<EmitEvents>
{
constructor(
private readonly adapter: Adapter,
private readonly rooms: Set<Room> = new Set<Room>(),
private readonly exceptRooms: Set<Room> = new Set<Room>(),
private readonly flags: BroadcastFlags = {}
) { }
/**
* Targets a room when emitting.
*
* @param room
* @return a new BroadcastOperator instance
* @public
*/
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> {
const rooms = new Set(this.rooms)
if (Array.isArray(room)) {
room.forEach((r) => rooms.add(r))
} else {
rooms.add(room)
}
return new BroadcastOperator(
this.adapter,
rooms,
this.exceptRooms,
this.flags
)
}
/**
* Targets a room when emitting.
*
* @param room
* @return a new BroadcastOperator instance
* @public
*/
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return this.to(room)
}
/**
* Excludes a room when emitting.
*
* @param room
* @return a new BroadcastOperator instance
* @public
*/
public except(room: Room | Room[]): BroadcastOperator<EmitEvents> {
const exceptRooms = new Set(this.exceptRooms)
if (Array.isArray(room)) {
room.forEach((r) => exceptRooms.add(r))
} else {
exceptRooms.add(room)
}
return new BroadcastOperator(
this.adapter,
this.rooms,
exceptRooms,
this.flags
)
}
/**
* Sets the compress flag.
*
* @param compress - if `true`, compresses the sending data
* @return a new BroadcastOperator instance
* @public
*/
public compress(compress: boolean): BroadcastOperator<EmitEvents> {
const flags = Object.assign({}, this.flags, { compress })
return new BroadcastOperator(
this.adapter,
this.rooms,
this.exceptRooms,
flags
)
}
/**
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
* receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle).
*
* @return a new BroadcastOperator instance
* @public
*/
public get volatile(): BroadcastOperator<EmitEvents> {
const flags = Object.assign({}, this.flags, { volatile: true })
return new BroadcastOperator(
this.adapter,
this.rooms,
this.exceptRooms,
flags
)
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
*
* @return a new BroadcastOperator instance
* @public
*/
public get local(): BroadcastOperator<EmitEvents> {
const flags = Object.assign({}, this.flags, { local: true })
return new BroadcastOperator(
this.adapter,
this.rooms,
this.exceptRooms,
flags
)
}
/**
* Emits to all clients.
*
* @return Always true
* @public
*/
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean {
if (RESERVED_EVENTS.has(ev)) {
throw new Error(`"${ev}" is a reserved event name`)
}
// set up packet object
const data = [ev, ...args]
const packet = {
type: PacketType.EVENT,
data: data,
}
if ("function" == typeof data[data.length - 1]) {
throw new Error("Callbacks are not supported when broadcasting")
}
this.adapter.broadcast(packet, {
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
})
return true
}
/**
* Gets a list of clients.
*
* @public
*/
public allSockets(): Promise<Set<SocketId>> {
if (!this.adapter) {
throw new Error(
"No adapter for this namespace, are you trying to get the list of clients of a dynamic namespace?"
)
}
return this.adapter.sockets(this.rooms)
}
/**
* Returns the matching socket instances
*
* @public
*/
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> {
return this.adapter
.fetchSockets({
rooms: this.rooms,
except: this.exceptRooms,
})
.then((sockets) => {
return sockets.map((socket) => {
if (socket instanceof Socket) {
// FIXME the TypeScript compiler complains about missing private properties
return socket as unknown as RemoteSocket<EmitEvents>
} else {
return new RemoteSocket(this.adapter, socket as SocketDetails)
}
})
})
}
/**
* Makes the matching socket instances join the specified rooms
*
* @param room
* @public
*/
public socketsJoin(room: Room | Room[]): void {
this.adapter.addSockets(
{
rooms: this.rooms,
except: this.exceptRooms,
},
Array.isArray(room) ? room : [room]
)
}
/**
* Makes the matching socket instances leave the specified rooms
*
* @param room
* @public
*/
public socketsLeave(room: Room | Room[]): void {
this.adapter.delSockets(
{
rooms: this.rooms,
except: this.exceptRooms,
},
Array.isArray(room) ? room : [room]
)
}
/**
* Makes the matching socket instances disconnect
*
* @param close - whether to close the underlying connection
* @public
*/
public disconnectSockets(close: boolean = false): void {
this.adapter.disconnectSockets(
{
rooms: this.rooms,
except: this.exceptRooms,
},
close
)
}
}
/**
* Format of the data when the Socket instance exists on another Socket.IO server
*/
interface SocketDetails {
id: SocketId
handshake: Handshake
rooms: Room[]
data: any
}
/**
* Expose of subset of the attributes and methods of the Socket class
*/
export class RemoteSocket<EmitEvents extends EventsMap>
implements TypedEventBroadcaster<EmitEvents>
{
public readonly id: SocketId
public readonly handshake: Handshake
public readonly rooms: Set<Room>
public readonly data: any
private readonly operator: BroadcastOperator<EmitEvents>
constructor(adapter: Adapter, details: SocketDetails) {
this.id = details.id
this.handshake = details.handshake
this.rooms = new Set(details.rooms)
this.data = details.data
this.operator = new BroadcastOperator(adapter, new Set([this.id]))
}
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean {
return this.operator.emit(ev, ...args)
}
/**
* Joins a room.
*
* @param {String|Array} room - room or array of rooms
* @public
*/
public join(room: Room | Room[]): void {
return this.operator.socketsJoin(room)
}
/**
* Leaves a room.
*
* @param {String} room
* @public
*/
public leave(room: Room): void {
return this.operator.socketsLeave(room)
}
/**
* Disconnects this client.
*
* @param {Boolean} close - if `true`, closes the underlying connection
* @return {Socket} self
*
* @public
*/
public disconnect(close = false): this {
this.operator.disconnectSockets(close)
return this
}
}

View File

@ -0,0 +1,331 @@
// import { Decoder, Encoder, Packet, PacketType } from "socket.io-parser"
import { Decoder, Encoder, Packet, PacketType } from "../socket.io-parser"
// import debugModule = require("debug")
import url = require("url")
// import type { IncomingMessage } from "http"
import type { Server } from "./index"
import type { Namespace } from "./namespace"
import type { EventsMap } from "./typed-events"
import type { Socket } from "./socket"
// import type { SocketId } from "socket.io-adapter"
import type { SocketId } from "../socket.io-adapter"
import type { Socket as EngineIOSocket } from '../engine.io/socket'
// const debug = debugModule("socket.io:client");
interface WriteOptions {
compress?: boolean
volatile?: boolean
preEncoded?: boolean
wsPreEncoded?: string
}
export class Client<
ListenEvents extends EventsMap,
EmitEvents extends EventsMap,
ServerSideEvents extends EventsMap
> {
public readonly conn: EngineIOSocket
/**
* @private
*/
readonly id: string
private readonly server: Server<ListenEvents, EmitEvents, ServerSideEvents>
private readonly encoder: Encoder
private readonly decoder: any
private sockets: Map<
SocketId,
Socket<ListenEvents, EmitEvents, ServerSideEvents>
> = new Map()
private nsps: Map<
string,
Socket<ListenEvents, EmitEvents, ServerSideEvents>
> = new Map()
private connectTimeout: NodeJS.Timeout
/**
* Client constructor.
*
* @param server instance
* @param conn
* @package
*/
constructor(
server: Server<ListenEvents, EmitEvents, ServerSideEvents>,
conn: EngineIOSocket
) {
this.server = server
this.conn = conn
this.encoder = server.encoder
this.decoder = new server._parser.Decoder()
this.id = conn.id
this.setup()
}
/**
* @return the reference to the request that originated the Engine.IO connection
*
* @public
*/
public get request(): any/** IncomingMessage */ {
return this.conn.request
}
/**
* Sets up event listeners.
*
* @private
*/
private setup() {
console.debug(`socket.io client setup conn ${this.conn.id}`)
this.onclose = this.onclose.bind(this)
this.ondata = this.ondata.bind(this)
this.onerror = this.onerror.bind(this)
this.ondecoded = this.ondecoded.bind(this)
// @ts-ignore
this.decoder.on("decoded", this.ondecoded)
this.conn.on("data", this.ondata)
this.conn.on("error", this.onerror)
this.conn.on("close", this.onclose)
this.connectTimeout = setTimeout(() => {
if (this.nsps.size === 0) {
console.debug(`no namespace joined yet, close the client ${this.id}`)
this.close()
} else {
console.debug(`the client ${this.id} has already joined a namespace, nothing to do`)
}
}, this.server._connectTimeout)
}
/**
* Connects a client to a namespace.
*
* @param {String} name - the namespace
* @param {Object} auth - the auth parameters
* @private
*/
private connect(name: string, auth: object = {}): void {
if (this.server._nsps.has(name)) {
console.debug(`socket.io client ${this.id} connecting to namespace ${name}`)
return this.doConnect(name, auth)
}
this.server._checkNamespace(
name,
auth,
(
dynamicNspName:
| Namespace<ListenEvents, EmitEvents, ServerSideEvents>
| false
) => {
if (dynamicNspName) {
console.debug(`dynamic namespace ${dynamicNspName} was created`)
this.doConnect(name, auth)
} else {
console.debug(`creation of namespace ${name} was denied`)
this._packet({
type: PacketType.CONNECT_ERROR,
nsp: name,
data: {
message: "Invalid namespace",
},
})
}
}
)
}
/**
* Connects a client to a namespace.
*
* @param name - the namespace
* @param {Object} auth - the auth parameters
*
* @private
*/
private doConnect(name: string, auth: object): void {
const nsp = this.server.of(name)
// @java-patch multi thread need direct callback socket
const socket = nsp._add(this, auth, (socket) => {
this.sockets.set(socket.id, socket)
this.nsps.set(nsp.name, socket)
if (this.connectTimeout) {
clearTimeout(this.connectTimeout)
this.connectTimeout = undefined
}
})
}
/**
* Disconnects from all namespaces and closes transport.
*
* @private
*/
_disconnect(): void {
for (const socket of this.sockets.values()) {
socket.disconnect()
}
this.sockets.clear()
this.close()
}
/**
* Removes a socket. Called by each `Socket`.
*
* @private
*/
_remove(socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>): void {
if (this.sockets.has(socket.id)) {
const nsp = this.sockets.get(socket.id)!.nsp.name
this.sockets.delete(socket.id)
this.nsps.delete(nsp)
} else {
console.debug("ignoring remove for", socket.id)
}
// @java-patch disconnect client when no live socket
process.nextTick(() => {
if (this.sockets.size == 0) {
this.onclose('no live socket')
}
})
}
/**
* Closes the underlying connection.
*
* @private
*/
private close(): void {
console.debug(`client ${this.id} clise - reason: forcing transport close`)
if ("open" === this.conn.readyState) {
console.debug("forcing transport close")
this.conn.close()
this.onclose("forced server close")
}
}
/**
* Writes a packet to the transport.
*
* @param {Object} packet object
* @param {Object} opts
* @private
*/
_packet(packet: Packet | any[], opts: WriteOptions = {}): void {
if (this.conn.readyState !== "open") {
console.debug(`client ${this.id} ignoring packet write ${JSON.stringify(packet)}`)
return
}
const encodedPackets = opts.preEncoded
? (packet as any[]) // previous versions of the adapter incorrectly used socket.packet() instead of writeToEngine()
: this.encoder.encode(packet as Packet)
for (const encodedPacket of encodedPackets) {
this.writeToEngine(encodedPacket, opts)
}
}
private writeToEngine(
encodedPacket: String | Buffer,
opts: WriteOptions
): void {
if (opts.volatile && !this.conn.transport.writable) {
console.debug(`client ${this.id} volatile packet is discarded since the transport is not currently writable`)
return
}
this.conn.write(encodedPacket, opts)
}
/**
* Called with incoming transport data.
*
* @private
*/
private ondata(data): void {
// try/catch is needed for protocol violations (GH-1880)
try {
this.decoder.add(data)
} catch (e) {
this.onerror(e)
}
}
/**
* Called when parser fully decodes a packet.
*
* @private
*/
private ondecoded(packet: Packet): void {
if (PacketType.CONNECT === packet.type) {
if (this.conn.protocol === 3) {
const parsed = url.parse(packet.nsp, true)
this.connect(parsed.pathname!, parsed.query)
} else {
this.connect(packet.nsp, packet.data)
}
} else {
const socket = this.nsps.get(packet.nsp)
if (socket) {
process.nextTick(function () {
socket._onpacket(packet)
})
} else {
console.debug(`client ${this.id} no socket for namespace ${packet.nsp}.`)
}
}
}
/**
* Handles an error.
*
* @param {Object} err object
* @private
*/
private onerror(err): void {
for (const socket of this.sockets.values()) {
socket._onerror(err)
}
this.conn.close()
}
/**
* Called upon transport close.
*
* @param reason
* @private
*/
private onclose(reason: string): void {
console.debug(`client ${this.id} close with reason ${reason}`)
// ignore a potential subsequent `close` event
this.destroy()
// `nsps` and `sockets` are cleaned up seamlessly
for (const socket of this.sockets.values()) {
socket._onclose(reason)
}
this.sockets.clear()
this.decoder.destroy() // clean up decoder
}
/**
* Cleans up event listeners.
* @private
*/
private destroy(): void {
this.conn.removeListener("data", this.ondata)
this.conn.removeListener("error", this.onerror)
this.conn.removeListener("close", this.onclose)
// @ts-ignore
this.decoder.removeListener("decoded", this.ondecoded)
if (this.connectTimeout) {
clearTimeout(this.connectTimeout)
this.connectTimeout = undefined
}
}
}

View File

@ -0,0 +1,825 @@
// import http = require("http");
// import { createReadStream } from "fs";
// import { createDeflate, createGzip, createBrotliCompress } from "zlib";
// import accepts = require("accepts");
// import { pipeline } from "stream";
// import path = require("path");
import engine = require("../engine.io")
import { Client } from './client'
import { EventEmitter } from 'events'
import { ExtendedError, Namespace, ServerReservedEventsMap } from "./namespace"
import { ParentNamespace } from './parent-namespace'
// import { Adapter, Room, SocketId } from "socket.io-adapter"
import { Adapter, Room, SocketId } from "../socket.io-adapter"
// import * as parser from "socket.io-parser";
import * as parser from "../socket.io-parser"
// import type { Encoder } from "socket.io-parser";
import type { Encoder } from "../socket.io-parser"
// import debugModule from "debug";
import { Socket } from './socket'
// import type { CookieSerializeOptions } from "cookie";
// import type { CorsOptions } from "cors";
import type { BroadcastOperator, RemoteSocket } from "./broadcast-operator"
import {
EventsMap,
DefaultEventsMap,
EventParams,
StrictEventEmitter,
EventNames,
} from "./typed-events"
import type { Socket as EngineIOSocket } from '../engine.io/socket'
// const clientVersion = require("../package.json").version
// const dotMapRegex = /\.map/
// type Transport = "polling" | "websocket";
type ParentNspNameMatchFn = (
name: string,
auth: { [key: string]: any },
fn: (err: Error | null, success: boolean) => void
) => void
type AdapterConstructor = typeof Adapter | ((nsp: Namespace) => Adapter)
interface EngineOptions {
/**
* how many ms without a pong packet to consider the connection closed
* @default 5000
*/
pingTimeout: number
/**
* how many ms before sending a new ping packet
* @default 25000
*/
pingInterval: number
/**
* how many ms before an uncompleted transport upgrade is cancelled
* @default 10000
*/
upgradeTimeout: number
/**
* how many bytes or characters a message can be, before closing the session (to avoid DoS).
* @default 1e5 (100 KB)
*/
maxHttpBufferSize: number
/**
* A function that receives a given handshake or upgrade request as its first parameter,
* and can decide whether to continue or not. The second argument is a function that needs
* to be called with the decided information: fn(err, success), where success is a boolean
* value where false means that the request is rejected, and err is an error code.
*/
// allowRequest: (
// req: http.IncomingMessage,
// fn: (err: string | null | undefined, success: boolean) => void
// ) => void
/**
* the low-level transports that are enabled
* @default ["polling", "websocket"]
*/
// transports: Transport[]
/**
* whether to allow transport upgrades
* @default true
*/
allowUpgrades: boolean
/**
* parameters of the WebSocket permessage-deflate extension (see ws module api docs). Set to false to disable.
* @default false
*/
perMessageDeflate: boolean | object
/**
* parameters of the http compression for the polling transports (see zlib api docs). Set to false to disable.
* @default true
*/
httpCompression: boolean | object
/**
* what WebSocket server implementation to use. Specified module must
* conform to the ws interface (see ws module api docs). Default value is ws.
* An alternative c++ addon is also available by installing uws module.
*/
wsEngine: string
/**
* an optional packet which will be concatenated to the handshake packet emitted by Engine.IO.
*/
initialPacket: any
/**
* configuration of the cookie that contains the client sid to send as part of handshake response headers. This cookie
* might be used for sticky-session. Defaults to not sending any cookie.
* @default false
*/
// cookie: CookieSerializeOptions | boolean
/**
* the options that will be forwarded to the cors module
*/
// cors: CorsOptions
/**
* whether to enable compatibility with Socket.IO v2 clients
* @default false
*/
allowEIO3: boolean
}
interface AttachOptions {
/**
* name of the path to capture
* @default "/engine.io"
*/
path: string
/**
* destroy unhandled upgrade requests
* @default true
*/
destroyUpgrade: boolean
/**
* milliseconds after which unhandled requests are ended
* @default 1000
*/
destroyUpgradeTimeout: number
}
interface EngineAttachOptions extends EngineOptions, AttachOptions { }
interface ServerOptions extends EngineAttachOptions {
/**
* name of the path to capture
* @default "/socket.io"
*/
path: string
/**
* whether to serve the client files
* @default true
*/
serveClient: boolean
/**
* the adapter to use
* @default the in-memory adapter (https://github.com/socketio/socket.io-adapter)
*/
adapter: any
/**
* the parser to use
* @default the default parser (https://github.com/socketio/socket.io-parser)
*/
parser: any
/**
* how many ms before a client without namespace is closed
* @default 45000
*/
connectTimeout: number
}
export class Server<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap
> extends StrictEventEmitter<
ServerSideEvents,
EmitEvents,
ServerReservedEventsMap<ListenEvents, EmitEvents, ServerSideEvents>
> {
public readonly sockets: Namespace<
ListenEvents,
EmitEvents,
ServerSideEvents
>
/**
* A reference to the underlying Engine.IO server.
*
* Example:
*
* <code>
* const clientsCount = io.engine.clientsCount;
* </code>
*
*/
public engine: any
/** @private */
readonly _parser: typeof parser
/** @private */
readonly encoder: Encoder
/**
* @private
*/
_nsps: Map<string, Namespace<ListenEvents, EmitEvents, ServerSideEvents>> =
new Map();
private parentNsps: Map<
ParentNspNameMatchFn,
ParentNamespace<ListenEvents, EmitEvents, ServerSideEvents>
> = new Map();
private _adapter?: AdapterConstructor
private _serveClient: boolean
private opts: Partial<EngineOptions>
private eio
private _path: string
private clientPathRegex: RegExp
/**
* @private
*/
_connectTimeout: number
// private httpServer: http.Server
constructor(srv: any, opts: Partial<ServerOptions> = {}) {
super()
if (!srv) { throw new Error('srv can\'t be undefiend!') }
// if (
// "object" === typeof srv &&
// srv instanceof Object &&
// !(srv as Partial<http.Server>).listen
// ) {
// opts = srv as Partial<ServerOptions>
// srv = undefined
// }
this.path(opts.path || "/socket.io")
this.connectTimeout(opts.connectTimeout || 45000)
this.serveClient(false !== opts.serveClient)
this._parser = opts.parser || parser
this.encoder = new this._parser.Encoder()
this.adapter(opts.adapter || Adapter)
this.sockets = this.of('/')
// if (srv) this.attach(srv as http.Server);
this.attach(srv, this.opts)
}
/**
* Sets/gets whether client code is being served.
*
* @param v - whether to serve client code
* @return self when setting or value when getting
* @public
*/
public serveClient(v: boolean): this
public serveClient(): boolean
public serveClient(v?: boolean): this | boolean
public serveClient(v?: boolean): this | boolean {
if (!arguments.length) return this._serveClient
this._serveClient = v!
return this
}
/**
* Executes the middleware for an incoming namespace not already created on the server.
*
* @param name - name of incoming namespace
* @param auth - the auth parameters
* @param fn - callback
*
* @private
*/
_checkNamespace(
name: string,
auth: { [key: string]: any },
fn: (
nsp: Namespace<ListenEvents, EmitEvents, ServerSideEvents> | false
) => void
): void {
if (this.parentNsps.size === 0) return fn(false)
const keysIterator = this.parentNsps.keys()
const run = () => {
const nextFn = keysIterator.next()
if (nextFn.done) {
return fn(false)
}
nextFn.value(name, auth, (err, allow) => {
if (err || !allow) {
run()
} else {
const namespace = this.parentNsps
.get(nextFn.value)!
.createChild(name)
// @ts-ignore
this.sockets.emitReserved("new_namespace", namespace)
fn(namespace)
}
})
}
run()
}
/**
* Sets the client serving path.
*
* @param {String} v pathname
* @return {Server|String} self when setting or value when getting
* @public
*/
public path(v: string): this
public path(): string
public path(v?: string): this | string
public path(v?: string): this | string {
if (!arguments.length) return this._path
this._path = v!.replace(/\/$/, "")
const escapedPath = this._path.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")
this.clientPathRegex = new RegExp(
"^" +
escapedPath +
"/socket\\.io(\\.min|\\.msgpack\\.min)?\\.js(\\.map)?$"
)
return this
}
/**
* Set the delay after which a client without namespace is closed
* @param v
* @public
*/
public connectTimeout(v: number): this
public connectTimeout(): number
public connectTimeout(v?: number): this | number
public connectTimeout(v?: number): this | number {
if (v === undefined) return this._connectTimeout
this._connectTimeout = v
return this
}
/**
* Sets the adapter for rooms.
*
* @param v pathname
* @return self when setting or value when getting
* @public
*/
public adapter(): AdapterConstructor | undefined
public adapter(v: AdapterConstructor): this
public adapter(
v?: AdapterConstructor
): AdapterConstructor | undefined | this {
if (!arguments.length) return this._adapter
this._adapter = v
for (const nsp of this._nsps.values()) {
nsp._initAdapter()
}
return this
}
/**
* Attaches socket.io to a server or port.
*
* @param srv - server or port
* @param opts - options passed to engine.io
* @return self
* @public
*/
public listen(
srv: any,//http.Server | number,
opts: Partial<ServerOptions> = {}
): this {
throw Error('Unsupport listen at MiaoScript Engine!')
//return this.attach(srv, opts)
}
/**
* Attaches socket.io to a server or port.
*
* @param srv - server or port
* @param opts - options passed to engine.io
* @return self
* @public
*/
public attach(
srv: any,//http.Server | number,
opts: Partial<ServerOptions> = {}
): this {
// if ("function" == typeof srv) {
// const msg =
// "You are trying to attach socket.io to an express " +
// "request handler function. Please pass a http.Server instance."
// throw new Error(msg)
// }
// // handle a port as a string
// if (Number(srv) == srv) {
// srv = Number(srv)
// }
// if ("number" == typeof srv) {
// debug("creating http server and binding to %d", srv)
// const port = srv
// srv = http.createServer((req, res) => {
// res.writeHead(404)
// res.end()
// })
// srv.listen(port)
// }
// merge the options passed to the Socket.IO server
Object.assign(opts, this.opts)
// set engine.io path to `/socket.io`
opts.path = opts.path || this._path
this.initEngine(srv, opts)
return this
}
/**
* Initialize engine
*
* @param srv - the server to attach to
* @param opts - options passed to engine.io
* @private
*/
private initEngine(srv: any, opts: Partial<EngineAttachOptions>) {
// // initialize engine
console.debug("creating engine.io instance with opts", JSON.stringify(opts))
this.eio = engine.attach(srv, opts)
// // attach static file serving
// if (this._serveClient) this.attachServe(srv)
// // Export http server
// this.httpServer = srv
// bind to engine events
this.bind(this.eio)
}
// /**
// * Attaches the static file serving.
// *
// * @param srv http server
// * @private
// */
// private attachServe(srv: http.Server): void {
// debug("attaching client serving req handler")
// const evs = srv.listeners("request").slice(0)
// srv.removeAllListeners("request")
// srv.on("request", (req, res) => {
// if (this.clientPathRegex.test(req.url)) {
// this.serve(req, res)
// } else {
// for (let i = 0; i < evs.length; i++) {
// evs[i].call(srv, req, res)
// }
// }
// })
// }
// /**
// * Handles a request serving of client source and map
// *
// * @param req
// * @param res
// * @private
// */
// private serve(req: http.IncomingMessage, res: http.ServerResponse): void {
// const filename = req.url!.replace(this._path, "")
// const isMap = dotMapRegex.test(filename)
// const type = isMap ? "map" : "source"
// // Per the standard, ETags must be quoted:
// // https://tools.ietf.org/html/rfc7232#section-2.3
// const expectedEtag = '"' + clientVersion + '"'
// const weakEtag = "W/" + expectedEtag
// const etag = req.headers["if-none-match"]
// if (etag) {
// if (expectedEtag === etag || weakEtag === etag) {
// debug("serve client %s 304", type)
// res.writeHead(304)
// res.end()
// return
// }
// }
// debug("serve client %s", type)
// res.setHeader("Cache-Control", "public, max-age=0")
// res.setHeader(
// "Content-Type",
// "application/" + (isMap ? "json" : "javascript")
// )
// res.setHeader("ETag", expectedEtag)
// Server.sendFile(filename, req, res)
// }
// /**
// * @param filename
// * @param req
// * @param res
// * @private
// */
// private static sendFile(
// filename: string,
// req: http.IncomingMessage,
// res: http.ServerResponse
// ): void {
// const readStream = createReadStream(
// path.join(__dirname, "../client-dist/", filename)
// )
// const encoding = accepts(req).encodings(["br", "gzip", "deflate"])
// const onError = (err: NodeJS.ErrnoException | null) => {
// if (err) {
// res.end()
// }
// }
// switch (encoding) {
// case "br":
// res.writeHead(200, { "content-encoding": "br" })
// readStream.pipe(createBrotliCompress()).pipe(res)
// pipeline(readStream, createBrotliCompress(), res, onError)
// break
// case "gzip":
// res.writeHead(200, { "content-encoding": "gzip" })
// pipeline(readStream, createGzip(), res, onError)
// break
// case "deflate":
// res.writeHead(200, { "content-encoding": "deflate" })
// pipeline(readStream, createDeflate(), res, onError)
// break
// default:
// res.writeHead(200)
// pipeline(readStream, res, onError)
// }
// }
/**
* Binds socket.io to an engine.io instance.
*
* @param {engine.Server} engine engine.io (or compatible) server
* @return {Server} self
* @public
*/
public bind(engine): Server {
console.debug('engine.io', engine.constructor.name, 'bind to socket.io')
this.engine = engine
this.engine.on("connection", this.onconnection.bind(this))
return this
}
/**
* Called with each incoming transport connection.
*
* @param {engine.Socket} conn
* @return {Server} self
* @private
*/
private onconnection(conn: EngineIOSocket): Server {
console.debug(`socket.io index incoming connection with id ${conn.id}`)
let client = new Client(this, conn)
if (conn.protocol === 3) {
// @ts-ignore
client.connect("/")
}
return this
}
/**
* Looks up a namespace.
*
* @param {String|RegExp|Function} name nsp name
* @param fn optional, nsp `connection` ev handler
* @public
*/
public of(
name: string | RegExp | ParentNspNameMatchFn,
fn?: (socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>) => void
): Namespace<ListenEvents, EmitEvents, ServerSideEvents> {
if (typeof name === "function" || name instanceof RegExp) {
const parentNsp = new ParentNamespace(this)
console.debug(`initializing parent namespace ${parentNsp.name}`)
if (typeof name === "function") {
this.parentNsps.set(name, parentNsp)
} else {
this.parentNsps.set(
(nsp, conn, next) => next(null, (name as RegExp).test(nsp)),
parentNsp
)
}
if (fn) {
// @ts-ignore
parentNsp.on("connect", fn)
}
return parentNsp
}
if (String(name)[0] !== "/") name = "/" + name
let nsp = this._nsps.get(name)
if (!nsp) {
console.debug("initializing namespace", name)
nsp = new Namespace(this, name)
this._nsps.set(name, nsp)
if (name !== "/") {
// @ts-ignore
this.sockets.emitReserved("new_namespace", nsp)
}
}
if (fn) nsp.on("connect", fn)
return nsp
}
/**
* Closes server connection
*
* @param [fn] optional, called as `fn([err])` on error OR all conns closed
* @public
*/
public close(fn?: (err?: Error) => void): void {
for (const socket of this.sockets.sockets.values()) {
socket._onclose("server shutting down")
}
this.engine.close()
// if (this.httpServer) {
// this.httpServer.close(fn)
// } else {
fn && fn()
// }
}
/**
* Sets up namespace middleware.
*
* @return self
* @public
*/
public use(
fn: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>,
next: (err?: ExtendedError) => void
) => void
): this {
this.sockets.use(fn)
return this
}
/**
* Targets a room when emitting.
*
* @param room
* @return self
* @public
*/
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return this.sockets.to(room)
}
/**
* Targets a room when emitting.
*
* @param room
* @return self
* @public
*/
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return this.sockets.in(room)
}
/**
* Excludes a room when emitting.
*
* @param name
* @return self
* @public
*/
public except(name: Room | Room[]): BroadcastOperator<EmitEvents> {
return this.sockets.except(name)
}
/**
* Sends a `message` event to all clients.
*
* @return self
* @public
*/
public send(...args: EventParams<EmitEvents, "message">): this {
this.sockets.emit("message", ...args)
return this
}
/**
* Sends a `message` event to all clients.
*
* @return self
* @public
*/
public write(...args: EventParams<EmitEvents, "message">): this {
this.sockets.emit("message", ...args)
return this
}
/**
* Emit a packet to other Socket.IO servers
*
* @param ev - the event name
* @param args - an array of arguments, which may include an acknowledgement callback at the end
* @public
*/
public serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
ev: Ev,
...args: EventParams<ServerSideEvents, Ev>
): boolean {
return this.sockets.serverSideEmit(ev, ...args)
}
/**
* Gets a list of socket ids.
*
* @public
*/
public allSockets(): Promise<Set<SocketId>> {
return this.sockets.allSockets()
}
/**
* Sets the compress flag.
*
* @param compress - if `true`, compresses the sending data
* @return self
* @public
*/
public compress(compress: boolean): BroadcastOperator<EmitEvents> {
return this.sockets.compress(compress)
}
/**
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
* receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle).
*
* @return self
* @public
*/
public get volatile(): BroadcastOperator<EmitEvents> {
return this.sockets.volatile
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
*
* @return self
* @public
*/
public get local(): BroadcastOperator<EmitEvents> {
return this.sockets.local
}
/**
* Returns the matching socket instances
*
* @public
*/
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> {
return this.sockets.fetchSockets()
}
/**
* Makes the matching socket instances join the specified rooms
*
* @param room
* @public
*/
public socketsJoin(room: Room | Room[]): void {
return this.sockets.socketsJoin(room)
}
/**
* Makes the matching socket instances leave the specified rooms
*
* @param room
* @public
*/
public socketsLeave(room: Room | Room[]): void {
return this.sockets.socketsLeave(room)
}
/**
* Makes the matching socket instances disconnect
*
* @param close - whether to close the underlying connection
* @public
*/
public disconnectSockets(close: boolean = false): void {
return this.sockets.disconnectSockets(close)
}
}
/**
* Expose main namespace (/).
*/
const emitterMethods = Object.keys(EventEmitter.prototype).filter(function (
key
) {
return typeof EventEmitter.prototype[key] === "function"
})
emitterMethods.forEach(function (fn) {
Server.prototype[fn] = function () {
return this.sockets[fn].apply(this.sockets, arguments)
}
})
export { Socket, ServerOptions, Namespace, BroadcastOperator, RemoteSocket }

View File

@ -0,0 +1,407 @@
import { Socket } from "./socket"
import type { Server } from "./index"
import {
EventParams,
EventNames,
EventsMap,
StrictEventEmitter,
DefaultEventsMap,
} from "./typed-events"
import type { Client } from "./client"
// import debugModule from "debug"
// import type { Adapter, Room, SocketId } from "socket.io-adapter"
import type { Adapter, Room, SocketId } from "../socket.io-adapter"
import { BroadcastOperator, RemoteSocket } from "./broadcast-operator"
// const debug = debugModule("socket.io:namespace");
export interface ExtendedError extends Error {
data?: any
}
export interface NamespaceReservedEventsMap<
ListenEvents extends EventsMap,
EmitEvents extends EventsMap,
ServerSideEvents extends EventsMap
> {
connect: (socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>) => void
connection: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>
) => void
}
export interface ServerReservedEventsMap<
ListenEvents,
EmitEvents,
ServerSideEvents
> extends NamespaceReservedEventsMap<
ListenEvents,
EmitEvents,
ServerSideEvents
> {
new_namespace: (
namespace: Namespace<ListenEvents, EmitEvents, ServerSideEvents>
) => void
}
export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set<
keyof ServerReservedEventsMap<never, never, never>
>(<const>["connect", "connection", "new_namespace"])
export class Namespace<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap
> extends StrictEventEmitter<
ServerSideEvents,
EmitEvents,
NamespaceReservedEventsMap<ListenEvents, EmitEvents, ServerSideEvents>
> {
public readonly name: string
public readonly sockets: Map<
SocketId,
Socket<ListenEvents, EmitEvents, ServerSideEvents>
> = new Map();
public adapter: Adapter
/** @private */
readonly server: Server<ListenEvents, EmitEvents, ServerSideEvents>
/** @private */
_fns: Array<
(
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>,
next: (err?: ExtendedError) => void
) => void
> = [];
/** @private */
_ids: number = 0;
/**
* Namespace constructor.
*
* @param server instance
* @param name
*/
constructor(
server: Server<ListenEvents, EmitEvents, ServerSideEvents>,
name: string
) {
super()
this.server = server
this.name = name
this._initAdapter()
}
/**
* Initializes the `Adapter` for this nsp.
* Run upon changing adapter by `Server#adapter`
* in addition to the constructor.
*
* @private
*/
_initAdapter() {
// @ts-ignore
this.adapter = new (this.server.adapter()!)(this)
}
/**
* Sets up namespace middleware.
*
* @return self
* @public
*/
public use(
fn: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>,
next: (err?: ExtendedError) => void
) => void
): this {
this._fns.push(fn)
return this
}
/**
* Executes the middleware for an incoming client.
*
* @param socket - the socket that will get added
* @param fn - last fn call in the middleware
* @private
*/
private run(
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>,
fn: (err: ExtendedError | null) => void
) {
const fns = this._fns.slice(0)
if (!fns.length) return fn(null)
function run(i: number) {
fns[i](socket, function (err) {
// upon error, short-circuit
if (err) return fn(err)
// if no middleware left, summon callback
if (!fns[i + 1]) return fn(null)
// go on to next
run(i + 1)
})
}
run(0)
}
/**
* Targets a room when emitting.
*
* @param room
* @return self
* @public
*/
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).to(room)
}
/**
* Targets a room when emitting.
*
* @param room
* @return self
* @public
*/
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).in(room)
}
/**
* Excludes a room when emitting.
*
* @param room
* @return self
* @public
*/
public except(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).except(room)
}
/**
* Adds a new client.
*
* @return {Socket}
* @private
*/
_add(
client: Client<ListenEvents, EmitEvents, ServerSideEvents>,
query,
fn?: (socket: Socket) => void
): Socket<ListenEvents, EmitEvents, ServerSideEvents> {
const socket = new Socket(this, client, query || {})
console.debug(`socket.io namespace client ${client.id} adding socket ${socket.id} to nsp ${this.name}`)
this.run(socket, err => {
process.nextTick(() => {
if ("open" == client.conn.readyState) {
if (err) {
if (client.conn.protocol === 3) {
return socket._error(err.data || err.message)
} else {
return socket._error({
message: err.message,
data: err.data,
})
}
}
// track socket
this.sockets.set(socket.id, socket)
console.debug(`socket.io namespace ${this.name} track client ${client.id} socket ${socket.id}`)
// it's paramount that the internal `onconnect` logic
// fires before user-set events to prevent state order
// violations (such as a disconnection before the connection
// logic is complete)
socket._onconnect()
// @java-patch multi thread need direct callback socket
if (fn) fn(socket)
// fire user-set events
this.emitReserved("connect", socket)
this.emitReserved("connection", socket)
} else {
console.debug(`next called after client ${client.id} was closed - ignoring socket`)
}
})
})
return socket
}
/**
* Removes a client. Called by each `Socket`.
*
* @private
*/
_remove(socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>): void {
if (this.sockets.has(socket.id)) {
console.debug(`namespace ${this.name} remove socket ${socket.id}`)
this.sockets.delete(socket.id)
} else {
console.debug(`namespace ${this.name} ignoring remove for ${socket.id}`)
}
}
/**
* Emits to all clients.
*
* @return Always true
* @public
*/
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean {
return new BroadcastOperator<EmitEvents>(this.adapter).emit(ev, ...args)
}
/**
* Sends a `message` event to all clients.
*
* @return self
* @public
*/
public send(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args)
return this
}
/**
* Sends a `message` event to all clients.
*
* @return self
* @public
*/
public write(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args)
return this
}
/**
* Emit a packet to other Socket.IO servers
*
* @param ev - the event name
* @param args - an array of arguments, which may include an acknowledgement callback at the end
* @public
*/
public serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
ev: Ev,
...args: EventParams<ServerSideEvents, Ev>
): boolean {
if (RESERVED_EVENTS.has(ev)) {
throw new Error(`"${ev}" is a reserved event name`)
}
args.unshift(ev)
this.adapter.serverSideEmit(args)
return true
}
/**
* Called when a packet is received from another Socket.IO server
*
* @param args - an array of arguments, which may include an acknowledgement callback at the end
*
* @private
*/
_onServerSideEmit(args: [string, ...any[]]) {
super.emitUntyped.apply(this, args)
}
/**
* Gets a list of clients.
*
* @return self
* @public
*/
public allSockets(): Promise<Set<SocketId>> {
return new BroadcastOperator(this.adapter).allSockets()
}
/**
* Sets the compress flag.
*
* @param compress - if `true`, compresses the sending data
* @return self
* @public
*/
public compress(compress: boolean): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).compress(compress)
}
/**
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
* receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle).
*
* @return self
* @public
*/
public get volatile(): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).volatile
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
*
* @return self
* @public
*/
public get local(): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).local
}
/**
* Returns the matching socket instances
*
* @public
*/
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> {
return new BroadcastOperator(this.adapter).fetchSockets()
}
/**
* Makes the matching socket instances join the specified rooms
*
* @param room
* @public
*/
public socketsJoin(room: Room | Room[]): void {
return new BroadcastOperator(this.adapter).socketsJoin(room)
}
/**
* Makes the matching socket instances leave the specified rooms
*
* @param room
* @public
*/
public socketsLeave(room: Room | Room[]): void {
return new BroadcastOperator(this.adapter).socketsLeave(room)
}
/**
* Makes the matching socket instances disconnect
*
* @param close - whether to close the underlying connection
* @public
*/
public disconnectSockets(close: boolean = false): void {
return new BroadcastOperator(this.adapter).disconnectSockets(close)
}
public close() {
RESERVED_EVENTS.forEach(event => this.removeAllListeners(event as any))
this.server._nsps.delete(this.name)
// @java-patch close all socket when namespace close
this.sockets.forEach(socket => socket._onclose(`namepsace ${this.name} close`))
}
}

View File

@ -0,0 +1,72 @@
import { Namespace } from "./namespace"
import type { Server } from "./index"
import type {
EventParams,
EventNames,
EventsMap,
DefaultEventsMap,
} from "./typed-events"
// import type { BroadcastOptions } from "socket.io-adapter"
import type { BroadcastOptions } from "../socket.io-adapter"
export class ParentNamespace<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap
> extends Namespace<ListenEvents, EmitEvents, ServerSideEvents> {
private static count: number = 0;
private children: Set<Namespace<ListenEvents, EmitEvents, ServerSideEvents>> = new Set();
constructor(server: Server<ListenEvents, EmitEvents, ServerSideEvents>) {
super(server, "/_" + ParentNamespace.count++)
}
_initAdapter() {
const broadcast = (packet: any, opts: BroadcastOptions) => {
this.children.forEach((nsp) => {
nsp.adapter.broadcast(packet, opts)
})
}
// @ts-ignore FIXME is there a way to declare an inner class in TypeScript?
this.adapter = { broadcast }
}
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean {
this.children.forEach((nsp) => {
nsp.emit(ev, ...args)
})
return true
}
// public emit(...args: any[]): boolean {
// this.children.forEach(nsp => {
// nsp._rooms = this._rooms
// nsp._flags = this._flags
// nsp.emit.apply(nsp, args as any)
// })
// this._rooms.clear()
// this._flags = {}
// return true
// }
createChild(
name: string
): Namespace<ListenEvents, EmitEvents, ServerSideEvents> {
const namespace = new Namespace(this.server, name)
namespace._fns = this._fns.slice(0)
this.listeners("connect").forEach((listener) =>
namespace.on("connect", listener)
)
this.listeners("connection").forEach((listener) =>
namespace.on("connection", listener)
)
this.children.add(namespace)
this.server._nsps.set(name, namespace)
return namespace
}
}

View File

@ -0,0 +1,727 @@
import { Packet, PacketType } from "../socket.io-parser"
import url = require("url")
// import debugModule from "debug"
import type { Server } from "./index"
import {
EventParams,
EventNames,
EventsMap,
StrictEventEmitter,
DefaultEventsMap,
} from "./typed-events"
import type { Client } from "./client"
import type { Namespace, NamespaceReservedEventsMap } from "./namespace"
// import type { IncomingMessage, IncomingHttpHeaders } from "http"
import type {
Adapter,
BroadcastFlags,
Room,
SocketId,
} from "socket.io-adapter"
// import base64id from "base64id"
import type { ParsedUrlQuery } from "querystring"
import { BroadcastOperator } from "./broadcast-operator"
// const debug = debugModule("socket.io:socket");
type ClientReservedEvents = "connect_error"
export interface SocketReservedEventsMap {
disconnect: (reason: string) => void
disconnecting: (reason: string) => void
error: (err: Error) => void
}
// EventEmitter reserved events: https://nodejs.org/api/events.html#events_event_newlistener
export interface EventEmitterReservedEventsMap {
newListener: (
eventName: string | Symbol,
listener: (...args: any[]) => void
) => void
removeListener: (
eventName: string | Symbol,
listener: (...args: any[]) => void
) => void
}
export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set<
| ClientReservedEvents
| keyof NamespaceReservedEventsMap<never, never, never>
| keyof SocketReservedEventsMap
| keyof EventEmitterReservedEventsMap
>(<const>[
"connect",
"connect_error",
"disconnect",
"disconnecting",
"newListener",
"removeListener",
])
/**
* The handshake details
*/
export interface Handshake {
/**
* The headers sent as part of the handshake
*/
headers: any//IncomingHttpHeaders
/**
* The date of creation (as string)
*/
time: string
/**
* The ip of the client
*/
address: string
/**
* Whether the connection is cross-domain
*/
xdomain: boolean
/**
* Whether the connection is secure
*/
secure: boolean
/**
* The date of creation (as unix timestamp)
*/
issued: number
/**
* The request URL string
*/
url: string
/**
* The query object
*/
query: ParsedUrlQuery
/**
* The auth object
*/
auth: { [key: string]: any }
}
export class Socket<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap
> extends StrictEventEmitter<
ListenEvents,
EmitEvents,
SocketReservedEventsMap
> {
public readonly id: SocketId
public readonly handshake: Handshake
/**
* Additional information that can be attached to the Socket instance and which will be used in the fetchSockets method
*/
public data: any = {};
public connected: boolean
public disconnected: boolean
private readonly server: Server<ListenEvents, EmitEvents, ServerSideEvents>
private readonly adapter: Adapter
private acks: Map<number, () => void> = new Map();
private fns: Array<(event: Array<any>, next: (err?: Error) => void) => void> =
[];
private flags: BroadcastFlags = {};
private _anyListeners?: Array<(...args: any[]) => void>
/**
* Interface to a `Client` for a given `Namespace`.
*
* @param {Namespace} nsp
* @param {Client} client
* @param {Object} auth
* @package
*/
constructor(
readonly nsp: Namespace<ListenEvents, EmitEvents, ServerSideEvents>,
readonly client: Client<ListenEvents, EmitEvents, ServerSideEvents>,
auth: object
) {
super()
this.nsp = nsp
this.server = nsp.server
this.adapter = this.nsp.adapter
// if (client.conn.protocol === 3) {
// // @ts-ignore
this.id = nsp.name !== "/" ? nsp.name + "#" + client.id : client.id
// } else {
// this.id = base64id.generateId() // don't reuse the Engine.IO id because it's sensitive information
// }
this.client = client
this.acks = new Map()
this.connected = true
this.disconnected = false
this.handshake = this.buildHandshake(auth)
}
buildHandshake(auth): Handshake {
return {
headers: this.request.headers,
time: new Date() + "",
address: this.conn.remoteAddress,
xdomain: !!this.request.headers.origin,
// @ts-ignore
secure: !!this.request.connection.encrypted,
issued: +new Date(),
url: this.request.url!,
query: url.parse(this.request.url!, true).query,
auth,
}
}
/**
* Emits to this client.
*
* @return Always returns `true`.
* @public
*/
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean {
if (RESERVED_EVENTS.has(ev)) {
throw new Error(`"${ev}" is a reserved event name`)
}
const data: any[] = [ev, ...args]
const packet: any = {
type: PacketType.EVENT,
data: data,
}
// access last argument to see if it's an ACK callback
if (typeof data[data.length - 1] === "function") {
console.trace("emitting packet with ack id %d", this.nsp._ids)
this.acks.set(this.nsp._ids, data.pop())
packet.id = this.nsp._ids++
}
const flags = Object.assign({}, this.flags)
this.flags = {}
this.packet(packet, flags)
return true
}
/**
* Targets a room when broadcasting.
*
* @param room
* @return self
* @public
*/
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return this.newBroadcastOperator().to(room)
}
/**
* Targets a room when broadcasting.
*
* @param room
* @return self
* @public
*/
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return this.newBroadcastOperator().in(room)
}
/**
* Excludes a room when broadcasting.
*
* @param room
* @return self
* @public
*/
public except(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return this.newBroadcastOperator().except(room)
}
/**
* Sends a `message` event.
*
* @return self
* @public
*/
public send(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args)
return this
}
/**
* Sends a `message` event.
*
* @return self
* @public
*/
public write(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args)
return this
}
/**
* Writes a packet.
*
* @param {Object} packet - packet object
* @param {Object} opts - options
* @private
*/
private packet(
packet: Omit<Packet, "nsp"> & Partial<Pick<Packet, "nsp">>,
opts: any = {}
): void {
packet.nsp = this.nsp.name
opts.compress = false !== opts.compress
this.client._packet(packet as Packet, opts)
}
/**
* Joins a room.
*
* @param {String|Array} rooms - room or array of rooms
* @return a Promise or nothing, depending on the adapter
* @public
*/
public join(rooms: Room | Array<Room>): Promise<void> | void {
console.debug(`join room ${rooms}`)
return this.adapter.addAll(
this.id,
new Set(Array.isArray(rooms) ? rooms : [rooms])
)
}
/**
* Leaves a room.
*
* @param {String} room
* @return a Promise or nothing, depending on the adapter
* @public
*/
public leave(room: string): Promise<void> | void {
console.debug(`leave room ${room}`)
return this.adapter.del(this.id, room)
}
/**
* Leave all rooms.
*
* @private
*/
private leaveAll(): void {
this.adapter.delAll(this.id)
}
/**
* Called by `Namespace` upon successful
* middleware execution (ie: authorization).
* Socket is added to namespace array before
* call to join, so adapters can access it.
*
* @private
*/
_onconnect(): void {
console.debug(`socket ${this.id} connected - writing packet`)
this.join(this.id)
if (this.conn.protocol === 3) {
this.packet({ type: PacketType.CONNECT })
} else {
this.packet({ type: PacketType.CONNECT, data: { sid: this.id } })
}
}
/**
* Called with each packet. Called by `Client`.
*
* @param {Object} packet
* @private
*/
_onpacket(packet: Packet): void {
console.trace("got packet", JSON.stringify(packet))
switch (packet.type) {
case PacketType.EVENT:
this.onevent(packet)
break
case PacketType.BINARY_EVENT:
this.onevent(packet)
break
case PacketType.ACK:
this.onack(packet)
break
case PacketType.BINARY_ACK:
this.onack(packet)
break
case PacketType.DISCONNECT:
this.ondisconnect()
break
case PacketType.CONNECT_ERROR:
this._onerror(new Error(packet.data))
}
}
/**
* Called upon event packet.
*
* @param {Packet} packet - packet object
* @private
*/
private onevent(packet: Packet): void {
const args = packet.data || []
console.trace("emitting event", JSON.stringify(args))
if (null != packet.id) {
console.trace("attaching ack callback to event")
args.push(this.ack(packet.id))
}
if (this._anyListeners && this._anyListeners.length) {
const listeners = this._anyListeners.slice()
for (const listener of listeners) {
listener.apply(this, args)
}
}
this.dispatch(args)
}
/**
* Produces an ack callback to emit with an event.
*
* @param {Number} id - packet id
* @private
*/
private ack(id: number): () => void {
const self = this
let sent = false
return function () {
// prevent double callbacks
if (sent) return
const args = Array.prototype.slice.call(arguments)
console.trace("sending ack", JSON.stringify(args))
self.packet({
id: id,
type: PacketType.ACK,
data: args,
})
sent = true
}
}
/**
* Called upon ack packet.
*
* @private
*/
private onack(packet: Packet): void {
const ack = this.acks.get(packet.id!)
if ("function" == typeof ack) {
console.trace(`socket ${this.id} calling ack ${packet.id} with ${packet.data}`)
ack.apply(this, packet.data)
this.acks.delete(packet.id!)
} else {
console.debug(`socket ${this.id} bad ack`, packet.id)
}
}
/**
* Called upon client disconnect packet.
*
* @private
*/
private ondisconnect(): void {
console.debug(`socket ${this.id} got disconnect packet`)
this._onclose("client namespace disconnect")
}
/**
* Handles a client error.
*
* @private
*/
_onerror(err: Error): void {
if (this.listeners("error").length) {
this.emitReserved("error", err)
} else {
console.error("Missing error handler on `socket`.")
console.error(err.stack)
}
}
/**
* Called upon closing. Called by `Client`.
*
* @param {String} reason
* @throw {Error} optional error object
*
* @private
*/
_onclose(reason: string): this | undefined {
if (!this.connected) return this
console.debug(`closing socket ${this.id} - reason: ${reason}`)
this.emitReserved("disconnecting", reason)
this.leaveAll()
this.nsp._remove(this)
this.client._remove(this)
this.connected = false
this.disconnected = true
this.emitReserved("disconnect", reason)
return
}
/**
* Produces an `error` packet.
*
* @param {Object} err - error object
*
* @private
*/
_error(err): void {
this.packet({ type: PacketType.CONNECT_ERROR, data: err })
}
/**
* Disconnects this client.
*
* @param {Boolean} close - if `true`, closes the underlying connection
* @return {Socket} self
*
* @public
*/
public disconnect(close = false): this {
if (!this.connected) return this
if (close) {
this.client._disconnect()
} else {
this.packet({ type: PacketType.DISCONNECT })
this._onclose("server namespace disconnect")
}
return this
}
/**
* Sets the compress flag.
*
* @param {Boolean} compress - if `true`, compresses the sending data
* @return {Socket} self
* @public
*/
public compress(compress: boolean): this {
this.flags.compress = compress
return this
}
/**
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
* receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle).
*
* @return {Socket} self
* @public
*/
public get volatile(): this {
this.flags.volatile = true
return this
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to every sockets but the
* sender.
*
* @return {Socket} self
* @public
*/
public get broadcast(): BroadcastOperator<EmitEvents> {
return this.newBroadcastOperator()
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
*
* @return {Socket} self
* @public
*/
public get local(): BroadcastOperator<EmitEvents> {
return this.newBroadcastOperator().local
}
/**
* Dispatch incoming event to socket listeners.
*
* @param {Array} event - event that will get emitted
* @private
*/
private dispatch(event: [eventName: string, ...args: any[]]): void {
console.trace("dispatching an event", JSON.stringify(event))
this.run(event, (err) => {
process.nextTick(() => {
if (err) {
return this._onerror(err)
}
if (this.connected) {
super.emitUntyped.apply(this, event)
} else {
console.debug("ignore packet received after disconnection")
}
})
})
}
/**
* Sets up socket middleware.
*
* @param {Function} fn - middleware function (event, next)
* @return {Socket} self
* @public
*/
public use(
fn: (event: Array<any>, next: (err?: Error) => void) => void
): this {
this.fns.push(fn)
return this
}
/**
* Executes the middleware for an incoming event.
*
* @param {Array} event - event that will get emitted
* @param {Function} fn - last fn call in the middleware
* @private
*/
private run(
event: [eventName: string, ...args: any[]],
fn: (err: Error | null) => void
): void {
const fns = this.fns.slice(0)
if (!fns.length) return fn(null)
function run(i: number) {
fns[i](event, function (err) {
// upon error, short-circuit
if (err) return fn(err)
// if no middleware left, summon callback
if (!fns[i + 1]) return fn(null)
// go on to next
run(i + 1)
})
}
run(0)
}
/**
* A reference to the request that originated the underlying Engine.IO Socket.
*
* @public
*/
public get request(): any /** IncomingMessage */ {
return this.client.request
}
/**
* A reference to the underlying Client transport connection (Engine.IO Socket object).
*
* @public
*/
public get conn() {
return this.client.conn
}
/**
* @public
*/
public get rooms(): Set<Room> {
return this.adapter.socketRooms(this.id) || new Set()
}
/**
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
* callback.
*
* @param listener
* @public
*/
public onAny(listener: (...args: any[]) => void): this {
this._anyListeners = this._anyListeners || []
this._anyListeners.push(listener)
return this
}
/**
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
* callback. The listener is added to the beginning of the listeners array.
*
* @param listener
* @public
*/
public prependAny(listener: (...args: any[]) => void): this {
this._anyListeners = this._anyListeners || []
this._anyListeners.unshift(listener)
return this
}
/**
* Removes the listener that will be fired when any event is emitted.
*
* @param listener
* @public
*/
public offAny(listener?: (...args: any[]) => void): this {
if (!this._anyListeners) {
return this
}
if (listener) {
const listeners = this._anyListeners
for (let i = 0; i < listeners.length; i++) {
if (listener === listeners[i]) {
listeners.splice(i, 1)
return this
}
}
} else {
this._anyListeners = []
}
return this
}
/**
* Returns an array of listeners that are listening for any event that is specified. This array can be manipulated,
* e.g. to remove listeners.
*
* @public
*/
public listenersAny() {
return this._anyListeners || []
}
private newBroadcastOperator(): BroadcastOperator<EmitEvents> {
const flags = Object.assign({}, this.flags)
this.flags = {}
return new BroadcastOperator(
this.adapter,
new Set<Room>(),
new Set<Room>([this.id]),
flags
)
}
}

View File

@ -0,0 +1,180 @@
import { EventEmitter } from "events"
/**
* An events map is an interface that maps event names to their value, which
* represents the type of the `on` listener.
*/
export interface EventsMap {
[event: string]: any
}
/**
* The default events map, used if no EventsMap is given. Using this EventsMap
* is equivalent to accepting all event names, and any data.
*/
export interface DefaultEventsMap {
[event: string]: (...args: any[]) => void
}
/**
* Returns a union type containing all the keys of an event map.
*/
export type EventNames<Map extends EventsMap> = keyof Map & (string | symbol)
/** The tuple type representing the parameters of an event listener */
export type EventParams<
Map extends EventsMap,
Ev extends EventNames<Map>
> = Parameters<Map[Ev]>
/**
* The event names that are either in ReservedEvents or in UserEvents
*/
export type ReservedOrUserEventNames<
ReservedEventsMap extends EventsMap,
UserEvents extends EventsMap
> = EventNames<ReservedEventsMap> | EventNames<UserEvents>
/**
* Type of a listener of a user event or a reserved event. If `Ev` is in
* `ReservedEvents`, the reserved event listener is returned.
*/
export type ReservedOrUserListener<
ReservedEvents extends EventsMap,
UserEvents extends EventsMap,
Ev extends ReservedOrUserEventNames<ReservedEvents, UserEvents>
> = FallbackToUntypedListener<
Ev extends EventNames<ReservedEvents>
? ReservedEvents[Ev]
: Ev extends EventNames<UserEvents>
? UserEvents[Ev]
: never
>
/**
* Returns an untyped listener type if `T` is `never`; otherwise, returns `T`.
*
* This is a hack to mitigate https://github.com/socketio/socket.io/issues/3833.
* Needed because of https://github.com/microsoft/TypeScript/issues/41778
*/
type FallbackToUntypedListener<T> = [T] extends [never]
? (...args: any[]) => void
: T
/**
* Interface for classes that aren't `EventEmitter`s, but still expose a
* strictly typed `emit` method.
*/
export interface TypedEventBroadcaster<EmitEvents extends EventsMap> {
emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean
}
/**
* Strictly typed version of an `EventEmitter`. A `TypedEventEmitter` takes type
* parameters for mappings of event names to event data types, and strictly
* types method calls to the `EventEmitter` according to these event maps.
*
* @typeParam ListenEvents - `EventsMap` of user-defined events that can be
* listened to with `on` or `once`
* @typeParam EmitEvents - `EventsMap` of user-defined events that can be
* emitted with `emit`
* @typeParam ReservedEvents - `EventsMap` of reserved events, that can be
* emitted by socket.io with `emitReserved`, and can be listened to with
* `listen`.
*/
export abstract class StrictEventEmitter<
ListenEvents extends EventsMap,
EmitEvents extends EventsMap,
ReservedEvents extends EventsMap = {}
>
extends EventEmitter
implements TypedEventBroadcaster<EmitEvents>
{
/**
* Adds the `listener` function as an event listener for `ev`.
*
* @param ev Name of the event
* @param listener Callback function
*/
on<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
ev: Ev,
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
): this {
return super.on(ev, listener)
}
/**
* Adds a one-time `listener` function as an event listener for `ev`.
*
* @param ev Name of the event
* @param listener Callback function
*/
once<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
ev: Ev,
listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>
): this {
return super.once(ev, listener)
}
/**
* Emits an event.
*
* @param ev Name of the event
* @param args Values to send to listeners of this event
*/
emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean {
return super.emit(ev, ...args)
}
/**
* Emits a reserved event.
*
* This method is `protected`, so that only a class extending
* `StrictEventEmitter` can emit its own reserved events.
*
* @param ev Reserved event name
* @param args Arguments to emit along with the event
*/
protected emitReserved<Ev extends EventNames<ReservedEvents>>(
ev: Ev,
...args: EventParams<ReservedEvents, Ev>
): boolean {
return super.emit(ev, ...args)
}
/**
* Emits an event.
*
* This method is `protected`, so that only a class extending
* `StrictEventEmitter` can get around the strict typing. This is useful for
* calling `emit.apply`, which can be called as `emitUntyped.apply`.
*
* @param ev Event name
* @param args Arguments to emit along with the event
*/
protected emitUntyped(ev: string, ...args: any[]): boolean {
return super.emit(ev, ...args)
}
/**
* Returns the listeners listening to an event.
*
* @param event Event name
* @returns Array of listeners subscribed to `event`
*/
listeners<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(
event: Ev
): ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>[] {
return super.listeners(event) as ReservedOrUserListener<
ReservedEvents,
ListenEvents,
Ev
>[]
}
}

View File

@ -1,22 +1,24 @@
import { Transport } from '../transport'
import { WebSocketClient } from '../server/client'
export class TomcatClient extends Transport {
export class TomcatClient extends WebSocketClient {
private session: javax.websocket.Session
constructor(server: any, session: javax.websocket.Session) {
super(server)
this.remoteAddress = session + ''
this.request = {
uri: () => `${session.getRequestURI()}`,
headers: () => []
}
this._id = session.getId() + ''
constructor(session: javax.websocket.Session) {
super()
this.id = session.getId() + ''
this.session = session
}
doSend(text: string) {
Java.synchronized(() => this.session.getBasicRemote().sendText(text), this.session)()
send(text: string, opts?: any, callback?: (err?: Error) => void) {
Java.synchronized(() => {
try {
this.session.getBasicRemote().sendText(text)
callback?.()
} catch (error) {
callback?.(error)
}
}, this.session)()
}
doClose() {
close() {
this.session.close()
}
}

View File

@ -1,60 +1,61 @@
import { EventEmitter } from 'events'
import { JavaServerOptions, WebSocketServer } from '../server'
import { Request } from '../server/request'
import { ServerOptions } from '../socket-io'
import { ServerEvent } from '../socket-io/constants'
import { ProxyBeanName } from './constants'
import { TomcatClient } from './client'
import { ProxyBeanName } from './constants'
const ThreadPoolExecutor = Java.type('java.util.concurrent.ThreadPoolExecutor')
type TomcatWebSocketSession = javax.websocket.Session
class TomcatWebSocketServer extends EventEmitter {
private beanFactory: any
class TomcatWebSocketServer extends WebSocketServer {
private executor: any
private clients: Map<string, any>
constructor(beanFactory: any, options: ServerOptions) {
super()
this.clients = new Map()
this.beanFactory = beanFactory
constructor(beanFactory: any, options: JavaServerOptions) {
super(beanFactory, options)
}
protected initialize(): void {
this.initThreadPool()
try { this.beanFactory.destroySingleton(ProxyBeanName) } catch (error) { }
try { this.instance.destroySingleton(ProxyBeanName) } catch (error) { }
let NashornWebSocketServerProxy = Java.extend(Java.type("pw.yumc.MiaoScript.websocket.WebSocketProxy"), {
onOpen: (session: TomcatWebSocketSession) => {
let cid = `${session?.getId()}`
let tomcatClient = new TomcatClient(this, session)
this.clients.set(cid, tomcatClient)
this.emit(ServerEvent.connect, tomcatClient)
this.onconnect(session)
},
onMessage: (session: TomcatWebSocketSession, message: string) => {
let cid = `${session?.getId()}`
if (this.clients.has(cid)) {
this.executor.execute(() => this.emit(ServerEvent.message, this.clients.get(cid), message))
} else {
console.error(`unknow client ${session} reciver message ${message}`)
}
this.onmessage(session, message)
},
onClose: (session: TomcatWebSocketSession, reason: any) => {
let cid = `${session?.getId()}`
if (this.clients.has(cid)) {
this.emit(ServerEvent.disconnect, this.clients.get(cid), reason)
} else {
console.error(`unknow client ${session} disconnect cause ${reason}`)
}
this.ondisconnect(session, reason)
},
onError: (session: TomcatWebSocketSession, error: Error) => {
let cid = `${session?.getId()}`
if (this.clients.has(cid)) {
this.emit(ServerEvent.error, this.clients.get(cid), error)
} else {
console.error(`unknow client ${session} cause error ${error}`)
console.ex(error)
}
this.onerror(session, error)
},
})
this.beanFactory.registerSingleton(ProxyBeanName, new NashornWebSocketServerProxy())
this.instance.registerSingleton(ProxyBeanName, new NashornWebSocketServerProxy())
}
protected getId(session) {
return session?.getId() + ''
}
protected getRequest(session) {
let request = new Request(session.getRequestURI(), "GET")
request.connection = {
remoteAddress: ''
}
return request
}
protected getSocket(session) {
return new TomcatClient(session)
}
protected doClose() {
this.instance.destroySingleton(ProxyBeanName)
this.executor.shutdown()
}
private initThreadPool() {
const ThreadPoolTaskExecutor = Java.type('org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor')
this.executor = new ThreadPoolTaskExecutor()
@ -66,11 +67,6 @@ class TomcatWebSocketServer extends EventEmitter {
this.executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy())
this.executor.initialize()
}
close() {
this.clients.forEach(client => client.close())
this.beanFactory.destroySingleton(ProxyBeanName)
this.executor.shutdown()
}
}
export {

View File

@ -1,35 +0,0 @@
import { EventEmitter } from 'events'
export abstract class Transport extends EventEmitter {
protected _id: string
server: any
readyState: 'opening' | 'open' | 'closing' | 'closed'
remoteAddress: string
upgraded: boolean
request: any
constructor(server: any) {
super()
this.server = server
this.readyState = 'open'
this.upgraded = true
}
get id() {
return this._id
}
send(text: string) {
if (this.readyState == 'open') {
this.doSend(text)
} else {
console.debug(`send message ${text} to close client ${this._id}`)
}
}
close() {
if ("closed" === this.readyState || "closing" === this.readyState) { return }
this.doClose()
this.readyState = 'closed'
}
abstract doSend(text: string)
abstract doClose()
}