Outline
TGrid
supports WebSocket protocol.
With TGrid
, you can easily develop WebSocket system under the RPC (Remote Procedure Call) concept.
By the way, when you're developing WebSocket server natively only with TGrid
, you have to construct and open the WebSocket server through WebSocketServer class. Also, you have to access to the WebSocket server with manual WebSocketConnector composition.
Instead, if you develop the WebSocket server with NestJS
, client can easily interact with the WebSocket server by SDK (Software Development Kit) library generated by Nestia
. Also, you can make both http and websocket operations to the NestJS controllers, so that makes the server compatible on both protocols.
Therefore, when you develop WebSocket server, I recommend to use NestJS
with TGrid
for the best development experience.
Native Classes
WebSocketServer
import { Driver, WebSocketServer } from "tgrid";
import { ICalcConfig } from "./interfaces/ICalcConfig";
import { ICalcEventListener } from "./interfaces/ICalcEventListener";
import { CompositeCalculator } from "./providers/CompositeCalculator";
import { ScientificCalculator } from "./providers/ScientificCalculator";
import { SimpleCalculator } from "./providers/SimpleCalculator";
import { StatisticsCalculator } from "./providers/StatisticsCalculator";
export const webSocketServerMain = async () => {
const server: WebSocketServer<
ICalcConfig,
| CompositeCalculator
| SimpleCalculator
| StatisticsCalculator
| ScientificCalculator,
ICalcEventListener
> = new WebSocketServer();
await server.open(37_000, async (acceptor) => {
// LIST UP PROPERTIES
const config: ICalcConfig = acceptor.header;
const listener: Driver<ICalcEventListener> = acceptor.getDriver();
// ACCEPT OR REJECT
if (acceptor.path === "/composite")
await acceptor.accept(new CompositeCalculator(config, listener));
else if (acceptor.path === "/simple")
await acceptor.accept(new SimpleCalculator(config, listener));
else if (acceptor.path === "/statistics")
await acceptor.accept(new StatisticsCalculator(config, listener));
else if (acceptor.path === "/scientific")
await acceptor.accept(new ScientificCalculator(config, listener));
else {
await acceptor.reject(1002, `WebSocket API endpoint not found.`);
return;
}
// PING REPEATEDLY TO KEEP CONNECTION
acceptor.ping(15_000);
});
return server;
};
Available only in the NodeJS.
WebSocketServer
is a class which can open an websocket server. Clients connecting to the WebSocketServer
would communicate with this websocket server through WebSocketAcceptor
objects with RPC (Remote Procedure Call) concept.
To open the websocket server, call the WebSocketServer.open()
method with target port number, and your custom callback function which would be called whenever a WebSocketAcceptor
has been newly created by a new client's connection.
Also, when declaring WebSocketServer
type, you have to specify three generic arguments; Header
, Provider
and Remote
. Those generic arguments would be propagated to the WebSocketAcceptor
, so that WebSocketAcceptor
would have the same generic arguments, too.
For reference, the first Header
type repersents an initial data from the remote client after the connection. I recommend utilize it as an activation tool for security enhancement. The second generic argument Provider
represents a provider from server to client, and the other Remote
means a provider from the remote client to server.
- Above example case:
Header
:ICalcConfig
typeProvider
: Server is providing one of below to the clientSimpleCalculator
StatisticsCalculator
ScientificCalculator
Remote
: Client is providingICalcEventListener
to the server
WebSocketAcceptor
import { Driver, WebSocketAcceptor, WebSocketServer } from "tgrid";
import { ICalcConfig } from "./interfaces/ICalcConfig";
import { ICalcEventListener } from "./interfaces/ICalcEventListener";
import { CompositeCalculator } from "./providers/CompositeCalculator";
import { ScientificCalculator } from "./providers/ScientificCalculator";
import { SimpleCalculator } from "./providers/SimpleCalculator";
import { StatisticsCalculator } from "./providers/StatisticsCalculator";
export const webSocketServerMain = async () => {
const server: WebSocketServer<
ICalcConfig,
| CompositeCalculator
| SimpleCalculator
| StatisticsCalculator
| ScientificCalculator,
ICalcEventListener
> = new WebSocketServer();
await server.open(
37_000,
async (
acceptor: WebSocketAcceptor<
ICalcConfig,
| CompositeCalculator
| SimpleCalculator
| StatisticsCalculator
| ScientificCalculator,
ICalcEventListener
>,
) => {
// LIST UP PROPERTIES
const config: ICalcConfig = acceptor.header;
const listener: Driver<ICalcEventListener> = acceptor.getDriver();
// ACCEPT OR REJECT
if (acceptor.path === "/composite")
await acceptor.accept(new CompositeCalculator(config, listener));
else if (acceptor.path === "/simple")
await acceptor.accept(new SimpleCalculator(config, listener));
else if (acceptor.path === "/statistics")
await acceptor.accept(new StatisticsCalculator(config, listener));
else if (acceptor.path === "/scientific")
await acceptor.accept(new ScientificCalculator(config, listener));
else {
await acceptor.reject(1002, `WebSocket API endpoint not found.`);
return;
}
// PING REPEATEDLY TO KEEP CONNECTION
acceptor.ping(15_000);
},
);
return server;
};
Available only in the NodeJS.
The WebSocketAcceptor
is a communicator class interacting with the remote websocket client through (RPC)(Remote Procedure Call) concept, created by the WebSocketServer
class whenever a remote client connects to the websocket server.
When the closure function being called by the connection of a remote client, you can determine whether to accept the client's connection or not, reading the WebSocketAcceptor.header
or WebSocketAcceptor.path
properties. If you've decided to accept the connection, call the WebSocketAcceptor.accept()
method with Provider
instance. Otherwise, reject it through the WebSocketAcceptor.reject()
method.
Also, don't forget to closing
the connection after your business logic has been completed to clean up the resources. Otherwise the closing must be performed by the remote client, you can wait the remote client's closing signal by the WebSocketAcceptor.join()
method.
Ping
If client comes from web browser, the connection would be closed automatically after a certain period of time if there's no signal. In the Google Chrome case, it automatically closes the connection after 60 seconds.
To make the connection alive forcibly, you can ping a signal to the remote client repeatedly in the specified interval by calling the WebSocketAcceptor.ping()
method. Therefore, when developing WebSocket server application, consider to calling the WebSocketAcceptor.ping()
method after accepting the connection.
WebSocketConnector
import { Driver, WebSocketConnector } from "tgrid";
import { ICalcConfig } from "../interfaces/ICalcConfig";
import { ICalcEvent } from "../interfaces/ICalcEvent";
import { ICalcEventListener } from "../interfaces/ICalcEventListener";
import { ICompositeCalculator } from "../interfaces/ICompositeCalculator";
export const webSocketClientMain = async () => {
const stack: ICalcEvent[] = [];
const listener: ICalcEventListener = {
on: (evt: ICalcEvent) => stack.push(evt),
};
const connector: WebSocketConnector<
ICalcConfig,
ICalcEventListener,
ICompositeCalculator
> = new WebSocketConnector(
{ precision: 2 }, // header
listener, // provider for remote server
);
await connector.connect("ws://127.0.0.1:37000/composite");
const remote: Driver<ICompositeCalculator> = connector.getDriver();
console.log(
await driver.plus(10, 20), // returns 30
await driver.multiplies(3, 4), // returns 12
await driver.divides(5, 3), // returns 1.67
await driver.scientific.sqrt(2), // returns 1.41
await driver.statistics.mean(1, 3, 9), // returns 4.33
);
await connector.close();
console.log(stack);
};
Terminal$ npm start 30 12 1.67 1.41 4.33 [ { type: 'plus', input: [ 10, 20 ], output: 30 }, { type: 'multiplies', input: [ 3, 4 ], output: 12 }, { type: 'divides', input: [ 5, 3 ], output: 1.67 }, { type: 'sqrt', input: [ 2 ], output: 1.41 }, { type: 'mean', input: [ 1, 3, 9 ], output: 4.33 } ]
Available in both Browser/NodeJS.
Web Socket Connector.
The WebSocketConnector
is a communicator class which connects to a websocket server and interacts with it through RPC (Remote Procedure Call) concept.
You can connect to the websocket server using WebSocketConnector.connect()
method. The interaction would be started if the server accepts your connection by calling the WebSocketAcceptor.accept()
method. If the remote server rejects your connection through WebSocketAcceptor.reject()
method, the exception would be thrown.
After the connection, don't forget to closing the connection, if your business logics have been completed, to clean up the resources. Otherwise, the closing must be performed by the remote websocket server, you can wait the remote server's closing signal through the WebSocketConnector.join()
method.
Also, when declaring this WebSocketConnector
type, you've to define three generic arguments; Header
, Provider
and Remote
. Those generic arguments must be same with the ones defined in the target WebSocketServer
and WebSocketAcceptor
classes (Provider
and Remote
must be reversed).
For reference, the first Header
type repersents an initial data from the remote client after the connection. I recommend utilize it as an activation tool for security enhancement. The second generic argument Provider
represents a provider from client to server, and the other Remote
means a provider from the remote server to client.
- Above example case:
Header
:ICalcConfig
typeProvider
: Client is providingICalcEventListener
to the serverRemote
: Server is providingISimpleCalculator
to the client
Demonstration
You can run it on Playground Website (opens in a new tab), or local machine.
git clone https://github.com/samchon/tgrid.example.websocket
npm install
npm start
NestJS Integration
If you develop websocket application, I recommend integrate TGrid
with NestJS
.
It's because you can manage WebSocket API endpoints much effectively and easily by NestJS
controller patterns. Also, you can make your server to support both HTTP and WebSocket protocols at the same time. NestJS controllers are compatible with both HTTP and WebSocket operations.
In the client side, you also can take advantages of automatically generated SDK (Software Development Kit) library for the client developers. With the SDK, client developers no more need to write the WebSocket connection and RPC (Remote Procedure Call) codes manually, so that the client development becomes much easier and safer.
Bootstrap
import { WebSocketAdaptor } from "@nestia/core";
import { INestApplication } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
import { CalculateModule } from "./calculate.module";
export const bootstrap = async (): Promise<INestApplication> => {
const app: INestApplication = await NestFactory.create(CalculateModule);
await WebSocketAdaptor.upgrade(app);
await app.listen(37_000, "0.0.0.0");
return app;
};
To utilize TGrid
in the NestJS
, upgrade the NestJS application like above.
Just call the WebSocketAdaptor.upgrade()
(opens in a new tab) method, then you can utilize TGrid
in the NestJS
server.
About detailed setup or more detailed informations, please refer below docs:
NestJS Controller
import { TypedRoute, WebSocketRoute } from "@nestia/core";
import { Controller } from "@nestjs/common";
import { Driver, WebSocketAcceptor } from "tgrid";
import { ICalcConfig } from "./api/interfaces/ICalcConfig";
import { ICalcEventListener } from "./api/interfaces/ICalcEventListener";
import { ICompositeCalculator } from "./api/interfaces/ICompositeCalculator";
import { IScientificCalculator } from "./api/interfaces/IScientificCalculator";
import { ISimpleCalculator } from "./api/interfaces/ISimpleCalculator";
import { IStatisticsCalculator } from "./api/interfaces/IStatisticsCalculator";
import { CompositeCalculator } from "./providers/CompositeCalculator";
import { ScientificCalculator } from "./providers/ScientificCalculator";
import { SimpleCalculator } from "./providers/SimpleCalculator";
import { StatisticsCalculator } from "./providers/StatisticsCalculator";
@Controller("calculate")
export class CalculateController {
/**
* Health check API (HTTP GET).
*/
@TypedRoute.Get("health")
public health(): string {
return "Health check OK";
}
/**
* Prepare a composite calculator.
*/
@WebSocketRoute("composite")
public async composite(
@WebSocketRoute.Acceptor()
acceptor: WebSocketAcceptor<
ICalcConfig,
ICompositeCalculator,
ICalcEventListener
>,
@WebSocketRoute.Header() header: ICalcConfig,
@WebSocketRoute.Driver() listener: Driver<ICalcEventListener>
): Promise<void> {
const provider: CompositeCalculator = new CompositeCalculator(
header,
listener
);
await acceptor.accept(provider); // ACCEPT CONNECTION
acceptor.ping(15_000); // PING REPEATEDLY TO KEEP CONNECTION
}
/**
* Prepare a simple calculator.
*/
@WebSocketRoute("simple")
public async simple(
@WebSocketRoute.Acceptor()
acceptor: WebSocketAcceptor<
ICalcConfig, // header
ISimpleCalculator, // provider for remote client
ICalcEventListener // provider from remote client
>
): Promise<void> {
const header: ICalcConfig = acceptor.header;
const listener: Driver<ICalcEventListener> = acceptor.getDriver();
const provider: SimpleCalculator = new SimpleCalculator(header, listener);
await acceptor.accept(provider); // ACCEPT CONNECTION
acceptor.ping(15_000); // PING REPEATEDLY TO KEEP CONNECTION
}
/**
* Prepare a scientific calculator.
*/
@WebSocketRoute("scientific")
public async scientific(
@WebSocketRoute.Acceptor()
acceptor: WebSocketAcceptor<
ICalcConfig,
IScientificCalculator,
ICalcEventListener
>
): Promise<void> {
const header: ICalcConfig = acceptor.header;
const listener: Driver<ICalcEventListener> = acceptor.getDriver();
const provider: ScientificCalculator = new ScientificCalculator(
header,
listener
);
await acceptor.accept(provider); // ACCEPT CONNECTION
acceptor.ping(15_000); // PING REPEATEDLY TO KEEP CONNECTION
}
/**
* Prepare a statistics calculator.
*/
@WebSocketRoute("statistics")
public async statistics(
@WebSocketRoute.Acceptor()
acceptor: WebSocketAcceptor<
ICalcConfig,
IStatisticsCalculator,
ICalcEventListener
>
): Promise<void> {
const header: ICalcConfig = acceptor.header;
const listener: Driver<ICalcEventListener> = acceptor.getDriver();
const provider: IStatisticsCalculator = new StatisticsCalculator(
header,
listener
);
await acceptor.accept(provider); // ACCEPT CONNECTION
acceptor.ping(15_000); // PING REPEATEDLY TO KEEP CONNECTION
}
}
From now on, you can define WebSocket API operations like above.
Just import and attach the @WebSocketRoute()
decorator function to the target controller methods.
Note that, don't forget to define @WebSocketRoute.Acceptor()
decorated parameter with WebSocketAcceptor
type. It's because the websocket server must determine whether to WebSocketAcceptor.accept()
the client's connection or WebSocketAcceptor.reject()
it.
Also, when declaring the WebSocketAcceptor
type, ou have to specify three generic arguments; Header
, Provider
and Remote
. Those generic arguments would be propagated to the automatically generated Software Development Kit for the client, so that the client developers will utilize the same generic types what you've defined (Provider
and Remote
must be reversed).
For reference, the first Header
type repersents an initial data from the remote client after the connection. I recommend utilize it as an activation tool for security enhancement. The second generic argument Provider
represents a provider from server to client, and the other Remote
means a provider from the remote client to server.
You can find more detailed informations about @WebSocketRoute()
:
Software Development Kit
/**
* @packageDocumentation
* @module api.functional.calculate
* @nestia Generated by Nestia - https://github.com/samchon/nestia
*/
//================================================================
import type { IConnection, Primitive } from "@nestia/fetcher";
import { PlainFetcher } from "@nestia/fetcher/lib/PlainFetcher";
import { WebSocketConnector } from "tgrid";
import type { Driver } from "tgrid";
import type { ICalcConfig } from "../../interfaces/ICalcConfig";
import type { ICalcEventListener } from "../../interfaces/ICalcEventListener";
import type { ICompositeCalculator } from "../../interfaces/ICompositeCalculator";
import type { IScientificCalculator } from "../../interfaces/IScientificCalculator";
import type { ISimpleCalculator } from "../../interfaces/ISimpleCalculator";
import type { IStatisticsCalculator } from "../../interfaces/IStatisticsCalculator";
/**
* Health check API (HTTP GET).
*
* @controller CalculateController.health
* @path GET /calculate/health
* @nestia Generated by Nestia - https://github.com/samchon/nestia
*/
export async function health(connection: IConnection): Promise<health.Output> {
return PlainFetcher.fetch(connection, {
...health.METADATA,
path: health.path(),
});
}
export namespace health {
export type Output = Primitive<string>;
export const METADATA = {
method: "GET",
path: "/calculate/health",
request: null,
response: {
type: "application/json",
encrypted: false,
},
status: null,
} as const;
export const path = () => "/calculate/health";
}
/**
* Prepare a composite calculator.
*
* @controller CalculateController.composite
* @path /calculate/composite
* @nestia Generated by Nestia - https://github.com/samchon/nestia
*/
export async function composite(
connection: IConnection<composite.Header>,
provider: composite.Provider,
): Promise<composite.Output> {
const connector: WebSocketConnector<
composite.Header,
composite.Provider,
composite.Listener
> = new WebSocketConnector(connection.headers ?? ({} as any), provider);
await connector.connect(
`${connection.host.endsWith("/") ? connection.host.substring(0, connection.host.length - 1) : connection.host}${composite.path()}`,
);
const driver: Driver<composite.Listener> = connector.getDriver();
return {
connector,
driver,
};
}
export namespace composite {
export type Output = {
connector: WebSocketConnector<Header, Provider, Listener>;
driver: Driver<Listener>;
};
export type Header = ICalcConfig;
export type Provider = ICalcEventListener;
export type Listener = ICompositeCalculator;
export const path = () => "/calculate/composite";
}
/**
* Prepare a simple calculator.
*
* @controller CalculateController.simple
* @path /calculate/simple
* @nestia Generated by Nestia - https://github.com/samchon/nestia
*/
export async function simple(
connection: IConnection<simple.Header>,
provider: simple.Provider,
): Promise<simple.Output> {
const connector: WebSocketConnector<
simple.Header,
simple.Provider,
simple.Listener
> = new WebSocketConnector(connection.headers ?? ({} as any), provider);
await connector.connect(
`${connection.host.endsWith("/") ? connection.host.substring(0, connection.host.length - 1) : connection.host}${simple.path()}`,
);
const driver: Driver<simple.Listener> = connector.getDriver();
return {
connector,
driver,
};
}
export namespace simple {
export type Output = {
connector: WebSocketConnector<Header, Provider, Listener>;
driver: Driver<Listener>;
};
export type Header = ICalcConfig;
export type Provider = ICalcEventListener;
export type Listener = ISimpleCalculator;
export const path = () => "/calculate/simple";
}
/**
* Prepare a scientific calculator.
*
* @controller CalculateController.scientific
* @path /calculate/scientific
* @nestia Generated by Nestia - https://github.com/samchon/nestia
*/
export async function scientific(
connection: IConnection<scientific.Header>,
provider: scientific.Provider,
): Promise<scientific.Output> {
const connector: WebSocketConnector<
scientific.Header,
scientific.Provider,
scientific.Listener
> = new WebSocketConnector(connection.headers ?? ({} as any), provider);
await connector.connect(
`${connection.host.endsWith("/") ? connection.host.substring(0, connection.host.length - 1) : connection.host}${scientific.path()}`,
);
const driver: Driver<scientific.Listener> = connector.getDriver();
return {
connector,
driver,
};
}
export namespace scientific {
export type Output = {
connector: WebSocketConnector<Header, Provider, Listener>;
driver: Driver<Listener>;
};
export type Header = ICalcConfig;
export type Provider = ICalcEventListener;
export type Listener = IScientificCalculator;
export const path = () => "/calculate/scientific";
}
/**
* Prepare a statistics calculator.
*
* @controller CalculateController.statistics
* @path /calculate/statistics
* @nestia Generated by Nestia - https://github.com/samchon/nestia
*/
export async function statistics(
connection: IConnection<statistics.Header>,
provider: statistics.Provider,
): Promise<statistics.Output> {
const connector: WebSocketConnector<
statistics.Header,
statistics.Provider,
statistics.Listener
> = new WebSocketConnector(connection.headers ?? ({} as any), provider);
await connector.connect(
`${connection.host.endsWith("/") ? connection.host.substring(0, connection.host.length - 1) : connection.host}${statistics.path()}`,
);
const driver: Driver<statistics.Listener> = connector.getDriver();
return {
connector,
driver,
};
}
export namespace statistics {
export type Output = {
connector: WebSocketConnector<Header, Provider, Listener>;
driver: Driver<Listener>;
};
export type Header = ICalcConfig;
export type Provider = ICalcEventListener;
export type Listener = IStatisticsCalculator;
export const path = () => "/calculate/statistics";
}
npx nestia sdk
When you run npx nestia sdk
command, SDK (Software Development Kit) library be generated.
Above file is one of the SDK library corresponding to the CalculateController
class we've seen in the previous NestJS Controller section. Client developers can utilize the automatically generated SDK functions to connect to the WebSocket server, and interact it type safely. Also, HTTP operation is compatible with the WebSocket operation.
Let's see how client developer utilizes the SDK library in the next section.
You can find more detailed informations about SDK generator:
Client Application
import api from "./api";
import { ICalcEvent } from "./api/interfaces/ICalcEvent";
import { ICalcEventListener } from "./api/interfaces/ICalcEventListener";
export const testCalculateSdk = async () => {
//----
// HTTP PROTOCOL
//---
// CALL HEALTH CHECK API
console.log(
await api.functional.calculate.health({
host: "http://127.0.0.1:37000",
})
);
//----
// WEBSOCKET PROTOCOL
//---
// PROVIDER FOR WEBSOCKET SERVER
const stack: ICalcEvent[] = [];
const listener: ICalcEventListener = {
on: (evt: ICalcEvent) => stack.push(evt),
};
// DO CONNECT
const { connector, driver } = await api.functional.calculate.composite(
{
host: "ws://127.0.0.1:37000",
headers: {
precision: 2,
},
},
listener
);
// CALL FUNCTIONS OF REMOTE SERVER
console.log(
await driver.plus(10, 20), // returns 30
await driver.multiplies(3, 4), // returns 12
await driver.divides(5, 3), // returns 1.67
await driver.scientific.sqrt(2), // returns 1.41
await driver.statistics.mean(1, 3, 9) // returns 4.33
);
// TERMINATE
await connector.close();
console.log(stack);
};
Terminal$ npm start [Nest] 4328 - 05/15/2024, 3:19:50 AM LOG [NestFactory] Starting Nest application... [Nest] 4328 - 05/15/2024, 3:19:50 AM LOG [InstanceLoader] CalculateModule dependencies initialized +5ms [Nest] 4328 - 05/15/2024, 3:19:50 AM LOG [RoutesResolver] CalculateController {/calculate}: +5ms [Nest] 4328 - 05/15/2024, 3:19:50 AM LOG [NestApplication] Nest application successfully started +2ms Health check OK 30 12 1.67 1.41 4.33 [ { type: 'plus', input: [ 10, 20 ], output: 30 }, { type: 'multiplies', input: [ 3, 4 ], output: 12 }, { type: 'divides', input: [ 5, 3 ], output: 1.67 }, { type: 'sqrt', input: [ 2 ], output: 1.41 }, { type: 'mean', input: [ 1, 3, 9 ], output: 4.33 } ]
Do import the SDK, and enjoy the type-safe and easy-to-use RPC.
Looking at the above code, the client application is calling a function of the automatically generated SDK (Software Development Kit) library, so that connecting to the websocket server, and starting interaction through RPC (Remote Procedure Call) concept with Driver
<ICompositeCalculator
> instance.
Doesn't the "SDK based development" seems much easier and safer than the previous Natives Classes > WebSocketConnector
case? This is the reason why I've recommended to combine with the NestJS
when using websocket protocol based network system.
For reference, return type of SDK function is a pair WebSocketConnector
and Driver
<ICompositeCalculator
> instances, but it would be actually returned only when the websocket server accepts your connection. Otherwise, the websocket server rejects your connection, an exception would be thrown.
Also, don't forget to closing the connection, if your business logics have been completed, to clean up the resources. Otherwise, the closing must be performed by the remote websocket server, you can wait the remote server's closing signal through the WebSocketConnector.join()
method.
Demonstration
You can run it on Playground Website (opens in a new tab), or local machine.
git clone https://github.com/samchon/tgrid.example.nestjs
npm install
npm start