Last active
June 27, 2020 09:51
-
-
Save crosstyan/a99cab87d3dcdea98ddba8e62da23d7d to your computer and use it in GitHub Desktop.
A modified version of https://gist.github.com/crosstyan/8036093d1dbdc1106352a435e95466a3. Combine Express and two websocket server using node http
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import WebSocket from 'ws' | |
import mongodb, { ObjectID } from 'mongodb' | |
import * as mongoose from 'mongoose' | |
import { prop, getModelForClass } from '@typegoose/typegoose' | |
import * as querystring from 'querystring' | |
import { v4 as uuid } from 'uuid' | |
import Ajv from 'ajv' | |
import express from 'express' | |
import http, { Server } from 'http' | |
import url from 'url' | |
import { Socket } from 'net' | |
//use typescript-json-schema (https://github.com/YousefED/typescript-json-schema) | |
//to compile the interface to JSON Schema | |
const postSchema = require('./PostSchema.json') | |
//A valid request example | |
// { | |
// "nickname":"rabbit", | |
// "content":"little rabbit", | |
// "threadId":"507f1f77bcf86cd799439011" | |
// } | |
//Connect to ws://127.0.0.1:8081/thread?name=yourGroupname | |
//to test the server | |
/** | |
* @TJS-additionalProperties false | |
*/ | |
export interface Post { | |
id?: string | |
nickname: string | |
content:string | |
token?: string | |
threadId:string | |
} | |
class PostClass implements Post{ | |
@prop() | |
public id?: string | |
@prop({required:true}) | |
public nickname!: string | |
@prop({required:true}) | |
public content!:string | |
@prop() | |
public token?: string | |
@prop({required:true}) | |
public threadId!:string | |
} | |
class ErrorMsg { | |
errorMap: Record<number, string> = {} | |
toString(code: number): string { | |
if (this.errorMap[code] != undefined) { | |
const msg={ | |
code: code, | |
description:this.errorMap[code] | |
} | |
return JSON.stringify(msg) | |
} else { | |
const msg={ | |
code: 622, | |
description:"Unknown error" | |
} | |
return JSON.stringify(msg) | |
} | |
} | |
} | |
interface WsSessionGroup{ | |
id?: ObjectID | |
name?:string | |
wsSessions: WebSocket[] | |
//Or use a map with uuid | |
//wsSessions:Record<string,WebSocket> | |
} | |
class WsHub{ | |
wsSessionGroupMap: Record<string, WsSessionGroup> = {} | |
//wsSessionGroupMap:Map<string,WsSessionGroup> = new Map | |
addSession(groupName:string,session:WebSocket) { | |
if (this.wsSessionGroupMap[groupName]==undefined){ | |
const sessionGroup = new ThreadSessionGroup | |
sessionGroup.name = groupName | |
sessionGroup.wsSessions.push(session) | |
this.wsSessionGroupMap[groupName] = sessionGroup | |
//If you wanna use uuid map | |
// const uid = uuid() | |
// this.wsSessionGroupMap[groupName].wsSessions[uid]=session | |
console.log(`New group "${groupName}" created`) | |
} | |
else { | |
this.wsSessionGroupMap[groupName].wsSessions.push(session) | |
console.log(`Old group "${groupName}" connected`) | |
} | |
} | |
deleteSession(groupName:string,session:WebSocket):void { | |
const sessionList = this.wsSessionGroupMap[groupName].wsSessions | |
sessionList.splice(sessionList.indexOf(session), 1) | |
console.log(`A member from group "${groupName}" disconnected`) | |
if (sessionList.length == 0) { | |
delete this.wsSessionGroupMap[groupName] | |
console.log(`Delete empty group "${groupName}"`) | |
} | |
} | |
broadcastGroup(groupName:string, message: WebSocket.Data):void { | |
const targetGroup = this.wsSessionGroupMap[groupName] | |
targetGroup?.wsSessions.forEach((session) => { | |
if (session.readyState === WebSocket.OPEN) { | |
session.send(message) | |
} | |
}) | |
console.log(`"${message}" from "${groupName}"`) | |
} | |
} | |
class ThreadSessionGroup implements WsSessionGroup{ | |
id?: ObjectID | |
name?:string | |
wsSessions: WebSocket[] =[] | |
} | |
function parsePost(wsData:WebSocket.Data) :PostClass|null{ | |
const wsDataStr = wsData.toString() | |
const ajv = new Ajv({allErrors:true}) | |
try { | |
const wsDataJson = JSON.parse(wsDataStr) | |
const isValid = ajv.validate(postSchema,wsDataJson) | |
if (isValid) { | |
return wsDataJson as PostClass | |
} else { | |
return null | |
} | |
} catch{ | |
return null | |
} | |
} | |
//Express Web Server | |
//const EXPRESS_PORT=8080 | |
const app = express() | |
app.get('/', (req,res) => { | |
res.send("hello world") | |
}) | |
// app.listen(EXPRESS_PORT, () => { | |
// console.log(`Express is listening ${EXPRESS_PORT}`) | |
// }) | |
//Error Parse | |
const errorMsg = new ErrorMsg | |
errorMsg.errorMap = { | |
400: "Bad request", | |
404: "Not found", | |
} | |
//MongoDB | |
// const mongoURL = "mongodb://localhost:27017/wschat" | |
// mongoose.connect(mongoURL, { useNewUrlParser: true }) | |
// const db=mongoose.connection | |
// db.on('error', console.error.bind(console, 'error when connecting to mongodb')) | |
// db.once('open', () => { //Do something when db connection on. | |
// const PostModel=getModelForClass(Post) | |
// }) | |
//Server | |
const PORT=8081 | |
const httpServer = http.createServer() | |
httpServer.on('request', app) //app=express() is a callback function | |
httpServer.on('upgrade', (req:http.IncomingMessage, socket:Socket, head:Buffer) => { | |
const pathname = url.parse(req.url!).pathname; | |
if (pathname === '/board') { | |
boardWs.handleUpgrade(req, socket, head, (webSocketConn) => { | |
boardWs.emit('connection', webSocketConn,req) | |
}); | |
} else if (pathname === '/thread') { | |
threadWs.handleUpgrade(req, socket, head, (webSocketConn) => { | |
threadWs.emit('connection', webSocketConn,req); | |
}); | |
} else { | |
socket.destroy(); | |
} | |
}) | |
httpServer.listen(PORT, () => { | |
console.log(`Listening ${PORT}`) | |
}) | |
//Websocket Board Server | |
//Stupid you can't do that! A http server can only bind a websocket server. | |
//Actually you can | |
const boardHub = new WsHub | |
const boardWs = new WebSocket.Server({ | |
noServer:true | |
}) | |
boardWs.on('connection', (webSocketConn, req) => { | |
webSocketConn.send("Hello World") | |
}) | |
//Websocket Thread Server | |
const threadHub = new WsHub | |
const threadWs = new WebSocket.Server({ | |
noServer:true | |
}) | |
threadWs.on('connection', (webSocketConn, req) => { | |
threadApp(webSocketConn,req) | |
}) | |
function threadApp(webSocketConn: WebSocket, req: http.IncomingMessage):void { | |
const reParam=req.url?.match(/(?<=\?).+/) | |
const urlParam = querystring.decode(reParam![0]) | |
const sessionNameInURL = urlParam.name?.toString()! | |
if ('name' in urlParam) { | |
threadHub.addSession(sessionNameInURL,webSocketConn) | |
} | |
else { | |
webSocketConn.close(1003,"Params aren't correct. ") | |
} | |
webSocketConn.on('message', (message) => { | |
const messageParsed = parsePost(message) | |
if (messageParsed != null) { | |
const messageStr = JSON.stringify(messageParsed) | |
threadHub.broadcastGroup(sessionNameInURL,messageStr) | |
} else { | |
webSocketConn.send(errorMsg.toString(400)) | |
} | |
}) | |
webSocketConn.on('close', () => { | |
threadHub.deleteSession(sessionNameInURL, webSocketConn) | |
}) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment