websockets_如何设计一个使用RxJx,Node和WebSockets控制对象动画的分布式系统

websockets

In my previous article, How to think reactively and animate moving objects using RxJs, I described how to build a MobileObject class that simulates the movement of an object subject to accelerations imposed on it by an external controller.

在我的上一篇文章“ 如何使用RxJsReact性地思考和动画化移动的对象”中 ,我描述了如何构建MobileObject类来模拟对象的运动,该对象受到外部控制器施加的加速度的作用。

Now I want to show you a simple distributed system that allows a Controller app to remotely control the movement of a MobileObject. A second remote app, the Monitor, shows the movement of the object on a two-dimensional plan. At the center of the system lays a MobileObjectServer, which is the place where the MobileObjects live.

现在,我想向您展示一个简单的分布式系统,该系统允许Controller应用程序远程控制MobileObject的移动 第二个远程应用程序Monitor可以在二维平面上显示对象的运动。 在系统的中心放置一个MobileObjectServer ,它是MobileObjects所在的地方。

The goal of this article is to explain how Reactive thinking can progressively produce a design which maps the requirements very naturally and produces a neat solution. We will end up solving the problem subscribing to just ONE Observable.

本文的目的是解释React式思维如何逐步产生出一种设计,该设计可以非常自然地映射需求并提供一种简洁的解决方案。 我们最终将解决仅订阅ONE Observable的问题

We’ll focus on the server part, which is the most intriguing from this standpoint.

从服务器角度来看,我们将重点介绍服务器部分。

For the implementation, we’ll use RxJs and TypeScript. The server runs on Node. All the components communicate using Web-Sockets.

对于实现,我们将使用RxJs和TypeScript。 服务器在节点上运行。 所有组件都使用Web套接字进行通信。

The full code base, comprised of the Server Controller and Monitor, can be found here.

包含服务器控制器和监视器的完整代码库可以在此处找到。

分布式系统的架构 (Schema of the distributed system)

The logical schema of the distributed system is represented in the following diagram:

下图表示了分布式系统的逻辑模式:

At the center lays the MobileObjectServer where the instances of the MobileObjets run. Each MobileObject is controlled by its Controller, that is a Web app through which we can issue commands (like accelerate, brake) to the MobileObject. The movement of all MobileObjects can be seen on one or more Monitors. Each Monitor is again a Web app.

在中心奠定了在MobileObjets的情况下运行MobileObjectServer。 每个MobileObject都由其Controller进行控制Controller是一个Web应用程序,通过它我们可以向MobileObject发出命令(如加速,制动)。 所有MobileObjects的运动都可以在一个或多个Monitor上看到。 每个监视器也是一个Web应用程序。

The following diagram shows a sample interaction flow between one Controller, one Monitor, and the MobileObjectServer.

下图显示了一个控制器 ,一个监视器MobileObjectServer之间的示例交互流。

服务器事件方面的要求 (The Server Requirements in terms of events)

We can express the requirements for the server part of our distributed system in terms of events:

我们可以根据事件表达对分布式系统服务器部分的要求:

  • Event1 — when a Controller connects => create a MobileObject

    Event1 —当控制器连接时=>创建MobileObject

  • Event2 — when a Controller receives a command => forward the command to the MobileObject controlled by the Controller

    Event2-控制器收到命令时=>将命令转发到由控制者控制的 MobileObject

  • Event3 — when a Controller disconnects => delete the MobileObject controlled by the Controller

    EVENT3 -当一个控制器断开=>删除吨他MobileObje CT用t来控制他控制 LER

  • Event4 — when a Monitor connects => start sending dynamics data of all running MobileObjects to the newly connected Monitor

    EVENT4 -当监控器连接=>开始发送动态的所有runni 纳克MobileObje CTS数据到新连接ED莫尼 TOR

  • Event5 — when a MobileObject is added => start sending its dynamics data to all the Monitors connected

    Event5 -当添加MobileObject =>开始发送其数据动力学对所有吨他Monito RS连接

  • Event6 — when a Monitor disconnects => stop sending the streams of dynamics data for all MobileObjects to that Monitor

    Event6 —当监视器断开连接时=>停止向Moni Tor 处的所有 MobileObject动态数据流发送

Reactive thinking will produce a design which naturally maps the requirements expressed in this way.

React性思维将产生一个设计,该设计自然地映射以这种方式表达的需求。

组成服务器的元素 (The elements composing the server)

The server component of the distributed application is made up of two main elements:

分布式应用程序的服务器组件由两个主要元素组成:

  • the MobileObject class, which implements the dynamic movement logic using RxJs Observables — this has been described in detail here

    MobileObject类,该类使用RxJs Observables实现动态运动逻辑- 此处已对其进行了详细描述

  • the MobileObjectServer class, which manages the web-socket protocol, receiving commands from the Controller and sending out to the Monitors all information about the dynamics of MobileObject. This implementation has been inspired by this article from Luis Aviles.

    MobileObjectServer该类管理Web套接字协议,从Controller接收命令并向Monitor发送有关MobileObject动态的所有信息 Luis Aviles的 这篇文章启发了这种实现。

MobileObject API (MobileObject APIs)

Let’s have a brief overview of the MobileObject class — all details can be found here while the code can be found in this repository.

让我们简要概述一下MobileObject类-所有详细信息都可以在此处找到而代码可以在此存储库中找到。

The MobileObject offers two families of APIs.

MobileObject提供两个API系列。

The first one is the set of methods through which an external Controller can issue commands that affect the dynamics of the object (for example, accelerate, brake).

第一个是一组方法,外部控制器可以通过这些方法发出影响对象动态性的命令(例如,加速,制动)。

The second are streams of readonly data which communicate to external clients, the Monitors, the relevant data about the dynamic behaviour of the object (that is, its position and velocity over time).

第二个是只读数据流,这些数据流与外部客户端Monitors进行通信,这些数据与对象的动态行为(即对象的位置和速度随时间的变化)有关。

In order to move an instance of a MobileObject, a Controller has to turn it on (with the turnOn() method), apply the desired acceleration (with the methods accelerateX(acc: number) and accelerateY(acc: number)), and then maybe brake (with the method brake()).

为了移动MobileObject的实例, 控制器必须将其打开(使用turnOn()方法),应用所需的加速度(使用方法turnOn() accelerateX(acc: number)accelerateY(acc: number) ),以及然后可能会刹车(使用brake()方法)。

When a Monitor connects to the MobileObjectServer, the MobileObjectServer subscribes to the dynamicsObs and the observable of the MobileObjects running in the server. It then starts sending the data related to their movement to the connected Monitors.

Monitor连接到MobileObjectServer时MobileObjectServer订阅服务器中运行的dynamicsObs和可观察对象的MobileObject 。 然后,它开始将与它们的运动有关的数据发送到连接的Monitor

For the purpose of this article, this is all you need to know about the MobileObject.

就本文而言,这就是有关MobileObject的所有知识

套接字作为可观察对象 (Sockets as Observables)

The MobileObjectServer starts doing something when a client, either a Controller or a Monitor, opens a websocket connection. Over the course of time, the MobileObjectServer can receive many requests to open a connection from many clients.

当客户端(无论是Controller还是Monitor )打开websocket连接时, MobileObjectServer开始执行操作。 随着时间的流逝, MobileObjectServer可以从许多客户端收到许多打开连接的请求。

This looks like an Observable of sockets. This is how to obtain it using the socket.io library:

这看起来像是一个可观察的套接字。 这是使用socket.io库获取它的方法:

import { Server } from 'http';

import { Observable } from 'rxjs';
import { Observer } from 'rxjs';

import * as socketIoServer from 'socket.io';

import {SocketObs} from './socket-obs';

export function sockets(httpServer: Server, port) {
    httpServer.listen(port, () => {
        console.log('Running server on port %s', port);
    });
    return new Observable<SocketObs>(
        (subscriber: Observer<SocketObs>) => {
            socketIoServer(httpServer).on('connect', 
                socket => {
                    console.log('client connected');
                    subscriber.next(new SocketObs(socket));
                }
            );
        }
    );
}

Via the function sockets, we create an Observable of SocketObs (we will see the implementation of this class later). Any time the websocket server receives a connect request and creates a new socket, the Observable returned by this function emits an instance of SocketObs which wraps the socket just created.

通过函数sockets ,我们创建一个Observable的SocketObs (我们将在稍后看到此类的实现)。 每当websocket服务器接收到连接请求并创建新的套接字时 ,此函数返回的Observable都会发出SocketObs实例,该实例包装刚刚创建的套接字

套接字上的消息为可观察对象 (Messages over sockets as Observables)

Sockets can be used to send messages from the client to the server and vice versa. With the socket.io library, we can send messages using the emit method.

套接字可用于将消息从客户端发送到服务器,反之亦然。 使用socket.io库,我们可以使用emit方法发送消息。

SocketIO.Socket.emit(event: string, …args: any[]): SocketIO.Socket

SocketIO.Socket.emit(event: string, …args: any[]): SocketIO.Socket

The parameter event can be seen as an identifier of the type of message we want to send. The …args parameters can be used to send data specific to a single message.

可以将参数event视为我们要发送的消息类型的标识符。 …args参数可用于发送特定于单个消息的数据。

Whoever is interested in a certain type of message (or event, to use the socket.io terminology) can start listening on the socket using the method on.

对某种类型的消息(或事件,使用socket.io术语)感兴趣的人可以使用on方法开始在套接字上侦听。

SocketIO.Emitter.on(event: string, fn: Function): SocketIO.Emitter

SocketIO.Emitter.on(event: string, fn: Function): SocketIO.Emitter

Again, the sequences of messages received by the Receiver look like Observables. This is how we can create Observables that actually emit any time a message of a certain type is received.

同样,接收方接收到的消息序列看起来像是可观察的。 这就是我们如何创建可观察到的对象,该对象在接收到某种类型的消息时会实际发出。

The onMessageType method is the one that does the trick. It returns an Observable, which emits any time a message of type messageType is received.

onMessageType方法就是解决问题的方法。 它返回一个Observable,它在接收到messageType类型的消息时发出。

import { Observable, Observer } from 'rxjs';

export class SocketObs {
    constructor(private socket: SocketIO.Socket) {}
    
    onMessageType(messageType): Observable<any> {
        return new Observable<any>((observer: Observer<any>) => {
            this.socket.on(messageType, data => observer.next(data));
        });
    }
}

In this way, sockets events, or messages as we call them here, have been transformed into Observables. These are going to be the foundations of our design.

这样,套接字事件或我们在此处称为消息的消息已转换为Observable。 这些将成为我们设计的基础。

确定客户的性质 (Determine the nature of the Client)

There are two types of clients which can connect with the MobileObjectServer. One is the Controller and one is the Monitor. The MobileObjectServer first needs to determine which type of client it is going to deal with on a specific socket.

可以与MobileObjectServer连接的客户端有两种 一种是控制器 ,一种是监视器 。 首先, MobileObjectServer需要确定它将在特定套接字上处理的客户端类型。

The way we have chosen to implement such logic is to have the Controller and the Monitor send different message types as their first message.

我们选择实现这种逻辑的方法是让ControllerMonitor发送不同的消息类型作为它们的第一条消息。

  • Controller sends a message of type BIND_CONTROLLER

    控制器发送BIND_CONTROLLER类型的消息

  • Monitor sends a message of type BIND_MONITOR

    监视器发送类型为BIND_MONITOR的消息

Depending on the type of the first message received on a socket, the MobileObjectServer is able to identify whether it is communicating with a Controller or a Monitor.

根据在套接字上收到的第一条消息的类型, MobileObjectServer能够识别它是与Controller还是Monitor进行通信。

As soon as a socket is created, the MobileObjectServer has to start listening to both types of messages, BIND_CONTROLLER and BIND_MONITOR. The first to occur will win. It is a race between the two Observables which map the two different types of messages.

创建套接字后, MobileObjectServer必须开始侦听两种类型的消息,即BIND_CONTROLLER和BIND_MONITOR。 最早发生的将获胜。 这是两个可观察对象之间的race ,它们映射了两种不同类型的消息。

Such logic has to be repeated any time a new socket is created, that is any time the Observable returned by the function sockets emits. Therefore, we need to merge all the events that win the race. We need to use the mergeMap operator, which merges all the events raised by the Observables involved, and flatten the results into a new Observable (mergeMap was formerly know as flatMap).

每当创建新套接字时,即函数sockets返回的Observable发出时,都必须重复这种逻辑。 因此,我们需要合并赢得比赛的所有事件。 我们需要使用mergeMap运算符,该运算符将所涉及的Observable引发的所有事件合并,并将结果展平到新的Observable中( mergeMap以前称为flatMap )。

The code to obtain this result is the following:

获得此结果的代码如下:

startSocketServer(httpServer: Server) {
    sockets(httpServer, this.port).pipe(
        mergeMap(socket =>
            race(
                socket.onMessageType(MessageType.BIND_MONITOR),
                socket.onMessageType(MessageType.BIND_CONTROLLER)
            )
        )
    )
    .subscribe();
}

Now that we know how to differentiate Controllers and Monitors, we can focus on what to do in these two cases.

既然我们知道了如何区分控制器监视器 ,那么我们可以专注于这两种情况下的操作。

与监视器相关的事件 (Events relevant for a Monitor)

A Monitor shows the movement of all MobileObjects which are running on the MobileObjectServer. So the MobileObjectServer has to send the right information to the monitors at the right times. Let’s see first what those times are, that is which are the relevant events that the MobileObjectServer has to be aware of in order to fulfill its job.

一个监视器显示其在MobileObjectServer运行的所有MobileObjects的运动。 因此, MobileObjectServer必须在正确的时间将正确的信息发送到监视器。 首先让我们看看这些时间是什么,即MobileObjectServer为了完成其工作必须注意的相关事件。

添加和删​​除MobileObjects (Adding and removing MobileObjects)

The first relevant events are:

第一个相关事件是:

  • a MobileObject has been added => the MobileObject is shown on the Monitor

    一个MobileObject已添加=>此MobileObject被示出在T 他莫尼 TOR

  • a MobileObject has been removed => the MobileObject is removed from the Monitor

    一个MobileObject已被移除=>此MobileObject从吨他莫尼 TOR除去

MobileObjects are added or removed over time, so such events can be modeled with two Observables:

随着时间的推移会添加或删除MobileObject ,因此可以使用两个Observables来建模此类事件:

  • an Observable which emits when a MobileObject is added

    添加MobileObject时发出的Observable

  • an Observable which emits when a MobileObject is removed

    移除MobileObject时发出的Observable

Once a Monitor is connected, the MobileObjectServer starts being interested in both of those Observables, so it has to merge them:

连接Monitor后MobileObjectServer就开始对这两个Observable都感兴趣,因此必须merge它们:

Similar to what we have seen before, we need to repeat such logic any time a Monitor is added. Therefore we need to mergeMap all the Observables which are the result of the merge of the ‘mobile object added’ Observable with the ‘mobile object removed’ Observable.

与我们之前看到的类似, 每次添加Monitor时,我们都需要重复这种逻辑。 因此,我们需要mergeMap所有Observable的mergeMap ,这是“添加的移动对象” Observable与“删除的移动对象” Observable merge的结果。

This is the code to obtain an Observable which emits any time a MobileObject has to be added to or removed from every Monitor:

这是获取Observable的代码,该代码在必须将MobileObject添加到每个Monitor或从每个Monitor移除MobileObject时发出

import {sockets} from './socket-io-observable';
import {SocketObs} from './socket-obs';

class MobileObjectServer {
    private mobileObjectAdded = new Subject<{mobObj: MobileObject, mobObjId: string}>();
    private mobileObjectRemoved = new Subject<string>();

    startSocketServer(httpServer: Server) {
        sockets(httpServer, this.port).pipe(
            mergeMap(socket =>
                race(
                    socket.onMessageType(MessageType.BIND_MONITOR)
                    .pipe(
                        map(() => (socketObs: SocketObs) => this.handleMonitorObs(socketObs))
                    ),
                    socket.onMessageType(MessageType.BIND_CONTROLLER)
                    // something will be added here soon to make this logic work
                )
                .pipe(
                    mergeMap(handler => handler(socket))
                )
            )
        )
        .subscribe();
    }

    handleMonitorObs(socket: SocketObs) {
        const mobObjAdded = this.mobileObjectAdded;
        const mobObjRemoved = this.mobileObjectRemoved;
        return merge(mobObjAdded, mobObjRemoved);
    }
}

We have introduced a few things with this code which are worth commenting on here.

我们用这段代码介绍了一些东西,在这里值得一提。

We have created the MobileObjectServer class, which will be the place where we will code all our server logic from now on.

我们已经创建了MobileObjectServer类,从现在开始,它将是我们对所有服务器逻辑进行编码的地方。

The method handleMonitorsObs, which we are going to enrich later on, returns simply the merge of two Observables, mobileObjectAdded and mobileObjectRemoved, which are Subjects. This is the “inner” merge shown in the picture above.

我们稍后将要充实的handleMonitorsObs方法handleMonitorsObs返回两个Observable的merge ,这两个对象是mobileObjectRemovedmobileObjectAddedmobileObjectRemoved 。 这是上图所示的“内部” merge

Subjects are Observables, and therefore can be merged as we do here. But Subjects are also Observers, so we can emit events through them. As we will see later in the code, there will be a time when we will use these Subjects to emit the events their names suggest.

主题是可观察的,因此可以像我们在此处那样进行合并。 但是主题也是观察者,因此我们可以通过它们发出事件。 正如我们将在代码的后面看到的那样,有时会有一段时间我们将使用这些Subject来发出其名称所暗示的事件。

The last point is related to the code we have added in the startSocketServer method:

最后一点与我们在startSocketServer方法中添加的代码有关:

race(
   socket.onMessageType(MessageType.BIND_MONITOR)
   .pipe(
      map(() => (sObs: SocketObs) => this.handleMonitorObs(sObs))
   ),
   socket.onMessageType(MessageType.BIND_CONTROLLER)
   // something will be added here soon to make this logic work
)
.pipe(
   mergeMap(handler => handler(socket))
)

This is basically a way to say: any time a BIND_MONITOR message is received, return the function

这基本上是一种表达方式:只要收到BIND_MONITOR消息,就返回该函数

(socketObs: SocketObs) => this.handleMonitorObs(socketObs)

which will be executed within the mergeMap operator piped into the result of the race function. This mergeMap operator is the external mergeMap shown in the picture above.

它将在通过管道mergeMaprace函数结果中的mergeMap运算符中执行。 该mergeMap运算符是上图所示的外部mergeMap

Another way to read the code is the following: any event corresponding to a message of type BIND_MONITOR gets transformed by the logic of

读取代码的另一种方法是:与BIND_MONITOR类型的消息相对应的任何事件都将由

mergeMap(() => this.handleMonitorObs(socket))

where socket is the instance of type SocketsObs emitted by the race function.

其中socketrace函数发出的SocketsObs类型的实例。

Soon we will add something similar for the BIND_CONTROLLER case to make this whole logic work.

不久,我们将为BIND_CONTROLLER情况添加类似的内容,以使整个逻辑正常工作。

处理MobileObject动态观察 (Handle MobileObject dynamics Observables)

Let’s consider one Monitor which connects to the MobileObjectServer. After the connection, a couple of MobileObjects are added to the MobileObjectServer.

让我们考虑一个监视器它连接到MobileObjectServer。 连接之后,将两个MobileObject添加到MobileObjectServer

Now for each MobileObject, we have to start considering the dynamics Observables they offer as part of their APIs. These Observables emit, at regular intervals of time, data about the dynamics (position and velocity) of the MobileObject. If mobileObject stores a reference to a MobileObject, we can obtain its dynamics Observable via mobileObject.dynamicsObs (see MobileObject APIs).

现在,对于每个MobileObject,我们必须开始考虑它们作为API的一部分提供的动态Observable。 这些Observable会定期发出有关MobileObject动力学(位置和速度)的数据 。 如果mobileObject存储到MobileObject一个参考,我们可以得到它的动力通过可观察mobileObject.dynamicsObs (见MobileObject的API)。

First we have to transform each event representing the fact that a MobileObject has been added into the series of events emitted by its dynamicsObs. Then we mergeMap all these series into a new single Observable which emits all dynamic events for all MobileObjects which are added.

首先,我们必须转换表示将MobileObject添加到其dynamicsObs发出的一系列事件中的事实的每个事件。 然后,我们mergeMap所有这些系列合并到新的单个Observable中,该Observable会为添加的所有MobileObject发出所有动态事件。

Then we apply all this jazz to all the Monitors which connect to the MobileObjectServer. So we end up with a new Observable which emits dynamics data for all Monitors and all MobileObjects (plus all events related to the fact that a MobileObject has been removed).

然后,我们将所有这些爵士乐应用于连接到MobileObjectServer的所有Monitor 因此,我们最终得到了一个新的Observable,它为所有Monitor和所有MobileObjects (以及与删除MobileObject的事实有关的所有事件)发出动态数据。

Per each time interval, we have groups of four events related to the emission of data about the dynamics of our MobileObjects. Why? This makes sense if we think that we have two Monitors and two MobileObjects. Each MobileObject has to send its dynamics data to each Monitor per every time interval. Therefore it is correct to see four events per each time interval.

每个时间间隔,我们有四个事件组,这些事件与有关MobileObjects动态的数据的发射有关。 为什么? 如果我们认为我们有两个Monitors和两个MobileObjects ,这是有道理的 。 每个MobileObject必须在每个时间间隔将其动态数据发送到每个Monitor 。 因此,每个时间间隔看到四个事件是正确的。

Once this is clear, the code is very simple:

明确说明后,代码非常简单:

import {sockets} from './socket-io-observable';
import {SocketObs} from './socket-obs';

class MobileObjectServer {
    private mobileObjectAdded = new Subject<{mobObj: MobileObject, mobObjId: string}>();
    private mobileObjectRemoved = new Subject<string>();


    startSocketServer(httpServer: Server) {
        sockets(httpServer, this.port).pipe(
            mergeMap(socket =>
                race(
                    socket.onMessageType(MessageType.BIND_MONITOR)
                    .pipe(
                        map(() => (socketObs: SocketObs) => this.handleMonitorObs(socketObs))
                    ),
                    socket.onMessageType(MessageType.BIND_CONTROLLER)
                    // something will be added here soon to make this logic work
                )
                .pipe(
                    mergeMap(handler => handler(socket))
                )
            )
        )
        .subscribe();
    }

    handleMonitorObs(socket: SocketObs) {
        const mobObjAdded = this.mobileObjectAdded
                              .pipe(
                                mergeMap(data => data.mobileObject.dynamicsObs)
                              );
        const mobObjRemoved = this.mobileObjectRemoved;
        return merge(mobObjAdded, mobObjRemoved);
    }

}

We have just introduced one simple change. We changed the handleMonitorObs method to add the mergeMap operator. This transforms the mobileObjectAdded Observable so that the new Observable emits the dynamics data we are looking for.

我们刚刚介绍了一个简单的更改。 我们更改了handleMonitorObs方法以添加mergeMap运算符。 这将转换mobileObjectAdded Observable,以便新的Observable发出我们正在寻找的动态数据。

The rest has remained untouched.

其余的保持不变。

到目前为止的总结 (Summary so far)

What have we done so far? We have just transformed Observables to obtain new Observables which emit all the events MobileObjectServer is interested in when it has to deal with a Monitor. Nothing else.

到目前为止,我们做了什么? 我们刚刚对Observables进行了转换,以获取新的Observables,这些Observables发出MobileObjectServer在必须处理Monitor时感兴趣的所有事件。 没有其他的。

You can see how these transformations are reflected in the code in the following image:

您可以在下图中看到这些转换如何反映在代码中:

The only thing we need to do now is to add the desired side effects to the relevant events. This will eventually allow us to achieve what we want, that is to communicate to the Monitor the right information at the right time.

现在我们唯一要做的就是将所需的副作用添加到相关事件中。 这最终将使我们能够实现我们想要的目标,即在正确的时间向监控器传达正确的信息。

But before moving to side effects, let’s cover what MobileObjectServer needs to do when interacting with a Controller, the other client in our distributed system.

但是在谈到 副作用之前,让我们介绍一下MobileObjectServer在与Controller (我们的分布式系统中的另一个客户端)进行交互时需要做什么。

与控制器相关的事件 (Events relevant for a Controller)

When a Controller connects to the MobileObjectServer there are fewer things that the server needs to care about. At least there are fewer nested relevant events happening.

Controller连接到MobileObjectServer时 ,服务器需要关心的事情就更少了。 至少发生的嵌套相关事件更少。

The things that the MobileObjectServer needs to care about are:

MobileObjectServer需要关心的事情是:

  • A Controller has connected, which in our simple logic means that we have to create a brand new MobileObject

    控制器已连接,按照我们的简单逻辑,这意味着我们必须创建一个全新的MobileObject

  • The Controller has sent commands for its MobileObject

    控制器已为其MobileObject发送命令

  • The Controller has disconnected. In our implementation, this means that we somehow have to delete the MobileObject controlled by the Controller (we have a 1 to 1 relationship between MobileObject and its Controller)

    控制器已断开连接。 在我们的实现中,这意味着我们必须以某种方式删除由Controller控制MobileObject (我们在MobileObject及其Controller之间具有1对1的关系)

We already know the first event: it is the one emitted by the Observable returned by socket.onMessageType(BIND_CONTROLLER).

我们已经知道第一个事件:它是由socket.onMessageType(BIND_CONTROLLER)返回的Observable发出的事件。

Commands are sent by the Controller to the MobileObjectServer in the form of messages. So we can create an Observable of commands received over a certain socket (received from a certain Controller) since each Controller has its own socket. We do this by simply using the onMessageType method of SocketObs

控制器以消息形式将命令发送到MobileObjectServer 。 因此,我们可以创建一个通过某个套接字(从某个Controller接收到)接收的命令的Observable,因为每个Controller都有自己的套接字。 我们只需使用SocketObsonMessageType方法即可完成此SocketObs

socket.onMessageType(CONTROLLER_COMMAND)

SocketObs also offers a method, onDisconnect, which returns an Observable that emits when the socket is disconnected. This is what we need in order to deal with the third event.

SocketObs还提供了onDisconnect方法,该方法返回一个套接字断开连接时发出的Observable。 这是我们处理第三事件所需要的。

Since we are dealing with more than one Controller potentially connecting to the MobileObjectServer, it should not surprise you to learn that we need to mergeMap the result of the merge. This is the same type of transformation we have already done a few times.

因为我们面对的是一个以上的控制器可能连接到MobileObjectServer,它不应该令你感到惊讶地得知,我们需要mergeMap的结果merge 。 这是我们已经进行过几次的相同类型的转换。

The code should be no surprise as well.

该代码也应该不足为奇。

startSocketServer(httpServer: Server) {
        sockets(httpServer, this.port).pipe(
            mergeMap(socket =>
                race(
                    socket.onMessageType(MessageType.BIND_MONITOR)
                    .pipe(
                        map(() => (socketObs: SocketObs) => this.handleMonitorObs(socketObs))
                    ),
                    socket.onMessageType(MessageType.BIND_CONTROLLER)
                    .pipe(
                        map(() => (socketObs: SocketObs) => this.handleControllerObs(socketObs))
                    ),
                )
                .pipe(
                    mergeMap(handler => handler(socket))
                )
            )
        )
        .subscribe();
}

handleMonitorObs(socket: SocketObs) {
        const mobObjAdded = this.mobileObjectAdded
                              .pipe(
                                mergeMap(data => data.mobileObject.dynamicsObs)
                              );
        const mobObjRemoved = this.mobileObjectRemoved;
        return merge(mobObjAdded, mobObjRemoved);
}

handleControllerObs(socket: SocketObs) {
        const commands = socket.onMessageType(MessageType.CONTROLLER_COMMAND);
        const disconnect = socket.onDisconnect();

        return merge(commands, disconnect);
}

We have simply added an handleControllerObs method that deals with commands received and the disconnect of a Controller. We apply the mergeMap transformation to it as we have already done with handleMonitorObs.

我们仅添加了一个handleControllerObs方法,该方法处理接收到的命令断开控制器的连接 。 我们应用mergeMap改造它,因为我们已经做了与handleMonitorObs

应用于控制器的转换摘要 (Summary of the transformations applied to Controllers)

The following diagram illustrates all transformations we have applied starting from the Observable that emits when a Controller connects.

下图说明了我们从Controller连接时发出的Observable开始应用的所有转换。

最终的观察 (The Final Observable)

If we put together the transformations we have done for both the Monitors and the Controllers, what we obtain is the following final Observable.

如果将对监视器控制器所做的转换放在一起我们将获得以下最终的可观察值。

Just by subscribing to this one final Observable, the whole tree of events gets unfolded.

只需订阅这一最后的Observable,事件的整个树就可以展开。

副作用 (Side effects)

The beautiful tree of events we have created by subscribing to the Final Observable does not do anything. But it does a good job of mapping the Events we identified while describing the requirements of the Server at the beginning of this article.

订阅《最终可观察者》,我们创建的美丽事件树没有任何作用。 但这很好地映射了我们在本文开头描述服务器需求时所标识的事件的映射。

Basically it tells us clearly when we have to do something.

基本上它清楚地告诉我们什么时候该做些什么

This something is what we call a side effect.

东西就是我们所说的副作用

When a Controller connects and disconnects, we respectively create or delete a MobileObject. As side effect of these actions is that we raise “MobileObject added” and “MobileObject deleted” events using the mobileObjectAdded and mobileObjectRemoved Subjects we introduces some paragraphs ago.

当Controller连接和断开连接时,我们分别创建或删除MobileObject 。 这些操作的副作用是,我们使用mobileObjectAdded介绍的段中的mobileObjectAddedmobileObjectRemoved主题引发了“ 添加MobileObject”“删除MobileObject”事件。

如何实现副作用 (How to implement side effects)

In RxJs there are different ways to implement side effects.

在RxJ中,有多种实现副作用的方法

Observers is one. We can add Observers while we subscribe using the tap operator (formerly know as do).

观察者是一个。 我们可以在使用tap运算符进行subscribe同时添加Observers(以前称为do )。

Another way is to inject them in any function we pass to any RxJs operator.

另一种方法是将它们注入我们传递给任何RxJs运算符的任何函数中。

We are mainly going to use tap, since it allows us to place side effects throughout the entire tree of events. But we are also going to place side effects directly inside functions we pass to RxJs operators.

我们主要将使用tap ,因为它使我们能够在整个事件树中放置副作用。 但是我们也将副作用直接传递给RxJs运算符。

The only place we do not put side effects is subscribe. The reason is that, given how we built it, the Final Observer emits many different types of events. Therefore subscribe, which works the same for all events, is not the right place to put behavior which depends on certain types of events.

我们唯一不产生副作用的地方就是subscribe 。 原因是,鉴于我们的构建方式,最终观察者会发出许多不同类型的事件。 因此,对所有事件都起作用的subscribe不是放置依赖于某些事件类型的行为的正确位置。

Hopefully at this point the code sort of speaks for itself.

希望在这一点上,代码本身能够说明一切。

最后但并非最不重要:Observables的完成 (Last but not least: completion of Observables)

There is one thing that we still need to do to complete our design: stop the streams of events, or complete the Observables, when either a Controller or a Monitor disconnects.

要完成设计,我们仍然需要做一件事:在ControllerMonitor断开连接时,停止事件流或完成Observable。

控制器断开连接时 (When a Controller disconnects)

When a Controller disconnects, we delete the MobileObject it controls. As part of the deletion, it is important to make sure that the MobileObjectServer stops sending dynamics data related to this MobileObject to the connected Monitors. This means that we must complete the following Observable:

当Controller断开连接时,我们将删除其控制的MobileObject 。 作为删除的一部分,重要的是确保MobileObjectServer停止将与此MobileObject相关的动态数据发送到连接的Monitor。 这意味着我们必须完成以下Observable:

mobObjInfo.mobObj.dynamicsObs
.pipe(
  tap(dynamics => socket.send(MessageType.DYNAMICS_INFO, dynamics)),
)

We can easily achieve this just using the takeUntil operator together with the mobileObjectRemoved Observable we already know:

我们只需将takeUntil运算符与我们已经知道的mobileObjectRemoved Observable一起轻松实现此目标:

mobObjInfo.mobObj.dynamicsObs
.pipe(
  tap(dynamics => socket.send(MessageType.DYNAMICS_INFO, dynamics)),
  takeUntil(this.mobileObjectRemoved.pipe(
    filter(id => id === mobObjInfo.mobObjId)
  ))
)

takeUntil ensures that an Observable completes when the Observable passed as a parameter to takeUntil emits.

takeUntil确保当Observable作为takeUntil的参数传递时,Observable完成。

mobileObjectRemoved emits every time a MobileObject is removed. What we want, though, is to stop sending dynamics info when a specific MobileObject, identified by its id, is removed. So we add the filter logic.

每次删除mobileObjectRemoved发出mobileObjectRemoved 。 但是,我们想要的是在删除由其ID标识的特定MobileObject时停止发送动态信息。 因此,我们添加了filter逻辑。

显示器断开连接时 (When a Monitor disconnects)

In this case, we can also use takeUntil.

在这种情况下,我们也可以使用takeUntil

We know when a Monitor disconnects because the socket, of type SocketObs, associated to it emits via the socket.onDisconnect() Observable. So what we need to do is stop sending dynamics info when socket.onDisconnect() emits.

我们知道,当一个显示器断开连接,因为socket ,类型SocketObs ,与之相关联的经发出socket.onDisconnect()可观察到。 因此,我们需要做的是在socket.onDisconnect()发出时停止发送动态信息。

So the final logic to govern the completion of the Observable is

因此,控制Observable完成的最终逻辑是

mobObjInfo.mobObj.dynamicsObs
.pipe(
  tap(dynamics => socket.send(MessageType.DYNAMICS_INFO, dynamics)),
  takeUntil(this.stopSendDynamics(socket, mobObjInfo.mobObjId))
)

where

哪里

private stopSendDynamics(socket: SocketObs, mobObjId: string){
  return merge(
            this.mobileObjectRemoved.pipe(
                                       filter(id => id === mobObjId)
                                     ),
            socket.onDisconnect()
  );
}

And this is how the core of the code implementing our logic looks:

这就是实现我们的逻辑的代码核心的样子:

import {sockets} from './socket-io-observable';
import {SocketObs} from './socket-obs';

class MobileObjectServer {
    private mobileObjectAdded = new Subject<{mobObj: MobileObject, mobObjId: string}>();
    private mobileObjectRemoved = new Subject<string>();


        public startSocketServer(httpServer: Server) {
        sockets(httpServer, this.port).pipe(
            mergeMap(socket =>
                race(
                    socket.onMessageType(MessageType.BIND_MONITOR)
                    .pipe(
                        map(() => (socketObs: SocketObs) => this.handleMonitorObs(socketObs))
                    ),
                    socket.onMessageType(MessageType.BIND_CONTROLLER)
                    .pipe(
                        map(() => (socketObs: SocketObs) => this.handleControllerObs(socketObs))
                    ),
                )
                .pipe(
                    mergeMap(handler => handler(socket)) 
                )
            )
        )
        .subscribe();
    }


    private handleMonitorObs(socket: SocketObs) {
        const mobObjAdded = this.mobileObjectAdded
                                .pipe(
                                    tap(mobObjInfo => socket.send(MessageType.MOBILE_OBJECT, mobObjInfo.mobObjId)),
                                    mergeMap(mobObjInfo => mobObjInfo.mobObj.dynamicsObs
                                                    .pipe(
                                                        tap(dynamics => socket.send(MessageType.DYNAMICS_INFO, dynamics)),
                                                        takeUntil(this.stopSendDynamicsInfo(socket, mobObjInfo.mobObjId))
                                                    )
                                    )
                                );
        const mobObjRemoved = this.mobileObjectRemoved
                                .pipe(
                                    tap(mobObjId => socket.send(MessageType.MOBILE_OBJECT_REMOVED, mobObjId)),
                                );
        return merge(mobObjAdded, mobObjRemoved);
    }

    private handleControllerObs(socket: SocketObs) {
        const {mobObj, mobObjId} = this.newMobileObject();
        
        this.mobileObjectAdded.next({mobObj, mobObjId});

        const commands = socket.onMessageType(MessageType.CONTROLLER_COMMAND)
                        .pipe(
                            tap(command  => this.execute(command, mobObj))
                        );

        const disconnect = socket.onDisconnect()
                        .pipe(
                            tap(() => this.mobileObjectRemoved.next(mobObjId)),
                        );

        return merge(commands, disconnect);
    }

    private stopSendDynamicsInfo(socket: SocketObs, mobObjId: string) {
        return merge(this.mobileObjectRemoved.pipe(filter(id => id === mobObjId)), socket.onDisconnect());
    }

}

结论 (Conclusion)

It has been a pretty long journey. We have seen some reasoning driven by Reactive Thinking and some implementations of this reasoning.

这是一段相当长的旅程。 我们已经看到了React式思维驱动的一些推理以及该推理的一些实现。

We started transforming WebSockets events into Observables. Then, applying incremental transformations, we ended up creating a single Observable that, once subscribed, unfolds all the events we are interested in.

我们开始将WebSockets事件转换为Observables。 然后,应用增量转换,最终创建了一个Observable,一旦订阅,它就会展开我们感兴趣的所有事件。

At this point, adding the side effects that allow us to achieve our goal has been straightforward.

在这一点上,添加使我们实现目标的副作用非常简单。

This mental process of design, which is incremental in itself, is the meaning I give to “Reactive Thinking”.

设计的这种心理过程本身就是增量的,这就是我赋予“React性思维”的含义。

The full code base, comprising Server Controller and Monitor, can be found here.

包括服务器控制器和监视器的完整代码库可在此处找到。

翻译自: https://www.freecodecamp.org/news/reactive-thinking-how-to-design-a-distributed-system-with-rxjs-websockets-and-node-57d772f89260/

websockets

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值