排序混乱

正如实践所表明的那样,出现问题的绝大部分不是因为解决方案本身,而是因为系统组件之间的通信是如何发生的。 如果系统组件之间的通信混乱,那么,由于您没有尝试很好地编写单个组件,因此整个系统将发生故障。


注意事项 里面的自行车。


问题或问题陈述


不久前,它碰巧在一家公司的项目上工作,该公司将CRM,ERM系统和衍生产品之类的乐趣带给了大众。 此外,该公司发布了一种相当全面的产品,从收银机软件到呼叫中心,可以租用最多200人的话务员。


我本人致力于呼叫中心的前端应用程序。


不难想象,正是来自所有系统组件的信息才流入操作员的应用程序。 而且,如果我们考虑到它不是一个单一的操作员,而是一个经理和管理员的事实,那么您可以想象应用程序应该“消化”并相互关联的通信和信息量。


当该项目已经启动甚至运行稳定时,系统的透明性问题就出现了。


这就是重点。 有很多组件,它们都使用其数据源。 但是几乎所有这些组件都曾经被编写为独立产品。 那不是作为整个系统的组成部分,而是作为单独的销售决定。 结果,它们之间没有单一的(系统)API,也没有共同的通信标准。


我会解释。 某些组件发送JSON,“某人”发送带有键的行:里面的值,“某人”通常发送二进制文件,并随便使用它。 但是,呼叫中心的最终应用程序必须完全掌握并以某种方式处理它。 嗯,最重要的是,系统中没有链接可以识别出数据格式/结构已更改。 如果某个组件昨天发送了JSON,而今天决定发送二进制文件-没人会看到这一点。 仅最终应用程序将按预期开始崩溃。


很快就变得清楚了(因为我周围的人,而不是我,因为我在设计阶段就谈到了这个问题),组件之间缺乏“统一的通信语言”会导致严重的问题。


最简单的情况是客户端要求更改某些数据集。 例如,他们将任务分给年轻人,后者“持有”用于处理商品/服务数据库的组件。 他完成了自己的工作,实现了新的数据集,对他而言,混蛋,一切正常。 但是,更新之后的第二天……哦……呼叫中心中的应用程序突然开始无法正常工作。


您可能已经猜到了。 我们的英雄不仅更改了数据集,还更改了其组件发送到系统的数据结构。 结果,呼叫中心应用程序根本无法再使用此组件,并且其他依赖项沿链飞行。


他们开始考虑我们实际上想走的路。 因此,我们为可能的解决方案制定了以下要求:


首先 ,数据结构中的任何更改应立即在系统中“突出显示”。 如果有人在某处进行了更改,而这些更改与系统的预期不兼容,则在组件测试阶段应发生一个错误,该错误已被更改。


第二个 。 不仅应在编译期间检查数据类型,还应在运行时检查数据类型。


第三 。 由于大量具有完全不同技能水平的人员在组件上工作,因此描述语言应该更简单。


第四 。 无论采用哪种解决方案,都应尽可能方便地使用它。 如果可能,IDE应尽可能突出显示。


首先想到的是实现protobuf。 简单,易读且容易。 严格的数据键入。 这似乎是医生命令的。 但可惜,并非所有protobuf语法看起来都很简单。 此外,即使是已编译的协议也需要附加的库,但是protobuf不支持Javascript,这是社区工作的结果。 总的来说,他们拒绝了。


然后出现了用JSON描述协议的想法。 好吧,有多少容易?


好吧,然后我辞职了。 在这个职位上本该完成的,因为在我离开后,没有其他人开始特别关注这个问题。


但是,考虑到几个个人项目,组件之间的通信问题再次发挥了全部潜力,我决定自己开始实施该想法。 下面将讨论什么。


因此,我提请您注意ceres项目,其中包括:


  • 协议生成器
  • 提供者
  • 客户
  • 实施运输

协议书


我们的任务是做到:


  • 在系统中设置消息结构很容易。
  • 确定所有消息字段的数据类型很容易。
  • 可以定义辅助实体并引用它们。
  • 当然,IDE会突出显示所有这些内容

我认为,以一种完全自然的方式,作为将协议转换成的语言,Typescript被选择为非纯Javascript。 也就是说,协议生成器所做的只是将JSON转换为Typescript。


要描述系统中可用的消息,您只需知道什么是JSON。 我敢肯定,没有人有任何问题。


除了提供Hello World之外,我还提供了不少讲究的示例-聊天。


{ "Events": { "NewMessage": { "message": "ChatMessage" }, "UsersListUpdated": { "users": "Array<User>" } }, "Requests": { "GetUsers": {}, "AddUser": { "user": "User" } }, "Responses": { "UsersList": { "users": "Array<User>" }, "AddUserResult": { "error?": "asciiString" } }, "ChatMessage": { "nickname": "asciiString", "message": "utf8String", "created": "datetime" }, "User": { "nickname": "asciiString" }, "version": "0.0.1" } 

一切都非常简单。 我们有几个NewMessage和UsersListUpdated事件。 以及几个UsersList和AddUserResult请求。 还有两个实体:ChatMessage和User。


如您所见,描述非常透明且易于理解。 关于规则的一些知识。


  • JSON中的对象将成为生成的协议中的类
  • 属性值是数据类型定义或对类(实体)的引用
  • 从生成协议的角度来看,嵌套对象将成为“嵌套”类,也就是说,嵌套对象将继承其父级的所有属性。

现在,您需要做的就是生成一个协议以开始使用它。


 npm install ceres.protocol -g ceres.protocol -s chat.protocol.json -o chat.protocol.ts -r 

结果,我们得到了Typescript生成的协议。 我们连接并使用:


图片

因此,该协议已经为开发人员提供了一些帮助:


  • IDE突出显示了协议中的内容。 IDE还突出显示了所有预期的属性。
  • Typescript,它肯定会告诉我们数据类型是否有问题。 当然,这是在开发阶段完成的,但是协议本身已经在运行时检查数据类型,并且如果检测到违规,则引发异常
  • 通常,您可以忘记验证。 该协议将进行所有必要的检查。
  • 生成的协议不需要任何其他库。 他需要工作的所有内容,已经包含在内。 而且非常方便。

是的,至少可以说,所生成协议的大小可能会让您感到惊讶。 但是,不要忘了压缩,生成的协议文件很适合自己使用。

现在我们可以“打包”邮件并发送


 import * as Protocol from '../../protocol/protocol.chat'; const message: Protocol.ChatMessage = new Protocol.ChatMessage({ nickname: 'noname', message: 'Hello World!', created: new Date() }); const packet: Uint8Array = message.stringify(); // Send packet somewhere 

在此处进行保留很重要,数据包将是一个字节数组,从流量负载的角度来看,这是非常好的且正确的,因为发送相同的JSON“成本”当然会更昂贵。 但是,该协议有一个窍门-在调试模式下,它将生成可读的JSON,以便开发人员可以“查看”流量并查看会发生什么。


这是在运行时直接完成的。


 import * as Protocol from '../../protocol/protocol.chat'; const message: Protocol.ChatMessage = new Protocol.ChatMessage({ nickname: 'noname', message: 'Hello World!', created: new Date() }); // Switch to debug mode Protocol.Protocol.state.debug(true); // Now packet will be present as JSON string const packet: string = message.stringify(); // Send packet somewhere 

在服务器(或任何其他收件人)上,我们可以轻松地将邮件拆包:


 import * as Protocol from '../../protocol/protocol.chat'; const smth = Protocol.parse(packet); if (smth instanceof Error) { // Oops. Something wrong with this packet. } if (Protocol.ChatMessage.instanceOf(smth) === true) { // This is chat message } 

该协议支持所有主要数据类型:


型式价值观内容描述大小,字节
utf8StringUTF8编码的字符串X
asciiStringASCII字符串1个字符-1个字节
int8-128至1271个
int16-32768至327672
int32-2147483648至21474836474
uint80至2551个
uint160至655352
uint320至42949672954
float321.2x10 -38至3.4x10 384
float645.0x10 -3241.8x10 3088
布尔值1个

在协议中,这些数据类型称为原始数据。 但是,该协议的另一个功能是它允许您添加自己的数据类型(称为“其他数据类型”)。


例如,您可能已经注意到ChatMessage具有一个带有日期时间数据类型的创建字段。 在应用程序级别-此类型对应于Date ,并且协议内部将其存储为uint32 (并发送)。


将类型添加到协议中非常简单。 例如,如果我们要具有电子邮件数据类型,请对协议中的以下消息说:


 { "User": { "nickname": "asciiString", "address": "email" }, "version": "0.0.1" } 

您所需要做的就是为电子邮件类型编写一个定义。


 export const AdvancedTypes: { [key:string]: any} = { email: { // Binary type or primitive type binaryType : 'asciiString', // Initialization value. This value is used as default value init : '""', // Parse value. We should not do any extra decode operations with it parse : (value: string) => { return value; }, // Also we should not do any encoding operations with it serialize : (value: string) => { return value; }, // Typescript type tsType : 'string', // Validation function to valid value validate : (value: string) => { if (typeof value !== 'string'){ return false; } if (value.trim() === '') { // Initialization value is "''", so we allow use empty string. return true; } const validationRegExp = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/gi; return validationRegExp.test(value); }, } }; 

仅此而已。 通过生成协议,我们获得了对新电子邮件数据类型的支持。 当我们尝试使用错误的地址创建实体时,会出现错误


 const user: Protocol.User = new Protocol.User({ nickname: 'Brad', email: 'not_valid_email' }); console.log(user); 

喔...


 Error: Cannot create class of "User" due error(s): - Property "email" has wrong value; validation was failed with value "not_valid_email". 

因此,该协议根本不允许“不良”数据进入系统。


请注意,在定义新的数据类型时,我们指定了几个关键属性:


  • binaryType-对原始数据类型的引用,应用于存储,编码/解码数据。 在这种情况下,我们指示该地址是一个ascii字符串。
  • tsType是对Javascript类型的引用,即在Javascript环境中应如何表示数据类型。 在这种情况下,我们正在谈论字符串
  • 还值得注意的是,仅在生成协议时才需要定义新的数据类型。 在输出中,我们获得了已包含新数据类型的生成协议。

您可以在ceres.protocol上查看有关所有协议功能的详细信息。

提供者和客户


总的来说,协议本身可以用于组织通信。 但是,如果我们谈论的是浏览器和nodejs,则提供程序和客户端都可用。


顾客


创作


要创建客户端,您需要客户端和传输。


安装方式


 # Install consumer (client) npm install ceres.consumer --save # Install transport npm install ceres.consumer.browser.ws --save 

创作


 import Transport, { ConnectionParameters } from 'ceres.consumer.browser.ws'; import Consumer from 'ceres.consumer'; // Create transport const transport:Transport = new Transport(new ConnectionParameters({ host: 'http://localhost', port: 3005, wsHost: 'ws://localhost', wsPort: 3005, })); // Create consumer const consumer: Consumer = new Consumer(transport); 

客户端和提供者都是专门为协议设计的。 也就是说,它们仅适用于协议(ceres.protocol)。

大事记


创建客户端后,开发人员可以订阅事件


 import * as Protocol from '../../protocol/protocol.chat'; import Transport, { ConnectionParameters } from 'ceres.consumer.browser.ws'; import Consumer from 'ceres.consumer'; // Create transport const transport:Transport = new Transport(new ConnectionParameters({ host: 'http://localhost', port: 3005, wsHost: 'ws://localhost', wsPort: 3005, })); // Create consumer const consumer: Consumer = new Consumer(transport); // Subscribe to event consumer.subscribe(Protocol.Events.NewMessage, (message: Protocol.Events.NewMessage) => { console.log(`New message came: ${message.message}`); }).then(() => { console.log('Subscription to "NewMessage" is done'); }).catch((error: Error) => { console.log(`Fail to subscribe to "NewMessage" due error: ${error.message}`); }); 

请注意,只有在消息数据完全正确的情况下,客户端才会调用事件处理程序。 换句话说,我们的应用程序受到保护以防止错误数据,并且NewMessage事件处理程序将始终以Protocol.Events.NewMessage实例作为参数来调用。


当然,客户端可以生成事件。


 consumer.emit(new Protocol.Events.NewMessage({ message: 'This is new message' })).then(() => { console.log(`New message was sent`); }).catch((error: Error) => { console.log(`Fail to send message due error: ${error.message}`); }); 

请注意,我们没有在任何地方指定事件名称,我们只是使用协议中指向类的链接,或者传递它的实例。


我们还可以通过指定类型为{ [key: string]: string }的简单对象作为第二个参数,将消息发送给有限的一组收件人。 在ceres中,此对象称为query


 consumer.emit( new Protocol.Events.NewMessage({ message: 'This is new message' }), { location: "UK" } ).then(() => { console.log(`New message was sent`); }).catch((error: Error) => { console.log(`Fail to send message due error: ${error.message}`); }); 

因此,通过另外指示{ location: "UK" } ,我们可以确保只有已将自己的位置标识为英国的客户才能收到此消息。


要将客户端本身与特定查询相关联,您只需调用ref方法:


 consumer.ref({ id: '12345678', location: 'UK' }).then(() => { console.log(`Client successfully bound with query`); }); 

在将客户端与查询连接后,他就有机会接收“个人”或“组”消息。


咨询处


我们也可以提出要求


 consumer.request( new Protocol.Requests.GetUsers(), // Request Protocol.Responses.UsersList // Expected response ).then((response: Protocol.Responses.UsersList) => { console.log(`Available users: ${response.users}`); }).catch((error: Error) => { console.log(`Fail to get users list due error: ${error.message}`); }); 

值得注意的是,作为第二个参数,我们指定了预期的结果( Protocol.Responses.UsersList ),这意味着仅当响应是UsersList的实例时,我们的请求才会成功完成,在所有其他情况下,我们将“落入”赶上 同样,这可以确保我们避免处理不正确的数据。


客户本人也可以与可以处理请求的人员交谈。 为此,您只需要“确定”自己为该请求的“负责人”即可。


 function processRequestGetUsers(request: Protocol.Requests.GetUsers, callback: (error: Error | null, results : any ) => any) { // Get user list somehow const users: Protocol.User[] = []; // Prepare response const response = new Protocol.Responses.UsersList({ users: users }); // Send response callback(null, response); // Or send error // callback(new Error(`Something is wrong`)) }; consumer.listenRequest(Protocol.Requests.GetUsers, processRequestGetUsers, { location: "UK" }).then(() => { console.log(`Consumer starts listen request "GetUsers"`); }); 

注意,可选地,作为第三个参数,我们可以指定一个可用于标识客户端的查询对象。 因此,如果有人发送带有查询的查询 ,例如{ location: "RU" } ,那么我们的客户将不会收到这样的请求,因为他的查询{ location: "UK" }


查询可以包含无限数量的属性。 例如,您可以指定以下内容


 { location: "UK", type: "managers" } 

然后,除了完整的查询匹配外,我们还将成功处理以下查询:


 { location: "UK" } 


 { type: "managers" } 

提供者


创作


要创建提供程序(以及创建客户端),您需要提供程序和传输。


安装方式


 # Install provider npm install ceres.provider --save # Install transport npm install ceres.provider.node.ws --save 

创作


 import Transport, { ConnectionParameters } from 'ceres.provider.node.ws'; import Provider from 'ceres.provider'; // Create transport const transport:Transport = new Transport(new ConnectionParameters({ port: 3005 })); // Create provider const provider: Provider = new Provider(transport); 

从创建提供程序的那一刻起,它就可以接受来自客户端的连接。


大事记


与客户端一样,提供程序也可以“监听”消息并生成消息。


倾听


 // Subscribe to event provider.subscribe(Protocol.Events.NewMessage, (message: Protocol.Events.NewMessage) => { console.log(`New message came: ${message.message}`); }); 

产生


 provider.emit(new Protocol.Events.NewMessage({ message: 'This message from provider' })); 

咨询处


当然,提供者可以(并且应该)“监听”请求


 function processRequestGetUsers(request: Protocol.Requests.GetUsers, clientID: string, callback: (error: Error | null, results : any ) => any) { console.log(`Request from client ${clientId} was gotten.`); // Get user list somehow const users: Protocol.User[] = []; // Prepare response const response = new Protocol.Responses.UsersList({ users: users }); // Send response callback(null, response); // Or send error // callback(new Error(`Something is wrong`)) }; provider.listenRequest(Protocol.Requests.GetUsers, processRequestGetUsers).then(() => { console.log(`Consumer starts listen request "GetUsers"`); }); 

与客户端只有一个区别,提供者除请求正文外还将收到唯一的clientId ,该ID将自动分配给所有连接的客户端。


例子


实际上,我真的不想让您从文档摘录中感到无聊,我相信只要看一小段代码,对您来说就会更加容易和有趣。


您可以通过下载源代码并执行几个简单的操作来轻松安装聊天示例


客户端安装和启动


 cd chat/client npm install npm start 

客户端将在http:// localhost:3000上可用。 立即与客户端打开几个选项卡,以查看“通信”。


提供程序(服务器)的安装和启动


 cd chat/server npm install ts-node ./server.ts 

我确定您熟悉ts-node程序包,但如果不熟悉,它将允许您运行TS文件。 如果您不想安装,请编译服务器,然后运行JS文件。


 cd chat/server npm run build node ./build/server/server.js 

什么啊 再次?


由于从Protobuf到BMW的铁杆Joynr,已经有许多可行的解决方案,因此人们预想着为什么要发明另一辆自行车的问题,我只能说这对我很有趣。 整个项目都是在没有工作的情况下,仅靠个人主动完成的,这是我的业余时间。


这就是为什么您的反馈对我特别重要 。 为了以某种方式激励您,我可以保证,对于github上的每个星星,我都会抚摸它的仓鼠(从某种程度上来说,我不喜欢它)。 对于叉子,呃,我要抓他的pussiko ... brrrr。


仓鼠不是我的,儿子的仓鼠


另外,该项目将在几周内对我以前的同事(我在帖子开头提到并且对alfa版本感兴趣的人)进行测试。 目标是调试并在多个组件上运行。 我真的希望它能起作用。


链接和包装


该项目由两个存储库托管


  • ceres来源:ceres.provider,ceres.consumer和今天可用的所有交通工具。
  • ceres.protocol协议生成器源

NPM以下软件包可用



好又轻。

Source: https://habr.com/ru/post/zh-CN442380/


All Articles