Networking - worldscapes/engine GitHub Wiki
Networking layer is abstraction that provides customizable layer to transfer data between Server and Client (or different Clients).
It's not aware of data it transfers or any other layers, Engine itself uses this layer to connect different parts or instances. Networking layer is responsible both for translating Server data to Client and Client events to Server. It allows data to cycle though Engine.
Networking layer should ensure packet-loss and connection-loss security. Those features can be not needed in some protocols or implemented differently, so it should be done inside of Adapter.
Network Adapter is entity that actually implements networking process. Since in some Networking implementation parties can have equal relationship, Adapter is same API for Client and Server. If Adapter should have different features on Server and Client, it can be implemented for each party separately with slightly different features.
Networking layer transfers data using abstract protocol. Because of that, NetworkingAdapter API should support many connections (not only between Client and Server) directly to allow PTP protocols (like WebRTC).
Networking Adapter should be implemented generally for Server and Client. NetworkServer and NetworkClient are ones who define the difference between Server and Client.
Engine should not call send methods directly, instead it just uses abstract methods of Network Server and Client which do the job (like sendSnapshot()
)
Though API should allow to transport any data, Engine parts should mainly exchange Events with each other to notify about some action or event.
List of features to be implemented in networking layer:
- Custom network adapters injection
- Support of several active connections at once (WebRTC-like)
- Additional validation for messages both in client and server adapters
- Distinguishing different message types and handling them appropriately
- Customizable authentication strategies
It's important to defer ConnectionIds, PlayerIds
PlayerId is unique identifier for every Player that is playing the game. Every player has separate input and separate world state slice.
ConnectionId is assined to every established Connection to map network traffic accordingly.
The point is that several ConnectionId can resolve to one PlayerId.
Adapter is API object responsible for transporting data between parties. It provides API which is same for client and server.
The edge between Adapter and Network API is that Network implements high-level API object interaction and organizes data flow in Engine network part. Adapter implements lower-level features described below.
Though connection list depends on Adapter, connections should be known to Engine and so to Simulation, because there can be some user-specific logic on client side.
To keep Engine updated on new users, Network should notify it when new user connects or disconnects through some API.
- Data transfer protocol
- Connection establishment
- Automatic ping test (which updates current ping values on server every second)
- Packet loss safety
Event sent via network should be validated before actually applying them to world state. Mostly it's needed on Server when receiving user input, but can also be useful on Client side when having multiple connections.
Validation is the part that can be used by Engine user to customize application networking and security. It allows to check incoming packages and map them into custom or built in Events (for example, to pass Command to Simulation). It can be espacially powerful when using custom Command handlers in Simulation.
Basically, Validation implementation takes messages recieved from Client and Server to translate them into server-side events.
- Network security
- Custom Message reactions
- Separate Client event input validation and game world logic
Networking adapters are only responsible for interchanging data between Client and Server, data itself should be translated (transformed) into messages by separate abstraction layer. This layer takes data and converts it into different types of messages or decodes messages back to data.
It's needed to allow customization and optimization of networking messages.
Implementation is used both on Client and Server and should meet following criterias:
- Be pure so it works in any environment and doesn't create side effects
- Any plain object should be encoded and decoded back without changes
Translation is needed only in several adapter implementations and passed directly to it as dependency.
In different cases and implementations network messages can have different targets.
Server can target every client or some specific one for example. Clients can send message to server, but also to all other clients when working with multi-connection protocols.
This is why data is passed with target info (id) or rank to networking API. All needed connection data is contained inside networking so it will automatically find needed connection and use it.
Connection rank is label assigned to each connection. It's needed to define received package priorities and securely handle them.
Ranks help to abstractly work with multiple connections in network implementation.
Those ranks can play role when there are more then 2 parties in networking. In most cases server and client ranks are used, but Adapter implementation can define some custom ranks additionally.
On Client-side Engine can define different handlers for different rank package senders (when using WebRTC for example). This is needed to avoid cheating or breaking client world state.
Engine does not control how connection is established since it can greatly differ depending on exact networking protocol or implementation. This is why Adapter should only have methods for data transportation.
How connection establishes is defined by Adapter creator or it's constructor arguments. Engine just takes Adapter instance passed and uses it.
It's possible to inject different implementation on Client and Server in cases when they handle connection differently.
Sometimes Engine needs to get information about established connections. In this case Adapter implements special method which returns information about all connections. This information should include:
- Connection rank
- Unique connection id
-
onMessage: (messageInfo: any) => void
- callback that will be called to handle received messages -
sendMessage(target, messageData): void
- (all, by Id, by rank) getConnectionList(): ConnectionInfo[]
-
isReady(): Promise<void>
- resolved when Adapter is ready to use
// Server side
const serverAdapter = new WebsocketServerAdapter(
new WebsocketNetworkingAdapter({ port: 11112 }),
);
// Client side
const clientAdapter = new NetworkingClient(
new WebsocketNetworkingAdapter({ url: 'some-server-url', port: 11112 }),
);
// Server side
const serverAdapter = new LocalServerAdapter({});
// Client side
const clientAdapter = new LocalClientAdapter({
// Since client has direct link to server instance, they can bind to each other to pass data immidiately
server: serverAdapter
});