原文
原文很简单,以下为机翻
WIRER ON THE WIRE - SIGNALR协议的非正式描述
我已经看到询问有关SignalR协议的描述的问题出现了很多。哎呀,当我开始关注SignalR时,我也在寻找类似的东西。现在,差不多一年之后,在我从架构上重新设计了SignalR C#客户端并从头开始编写SignalR C ++客户端后,我想我可以非常准确地描述协议。所以,我们走了。
在我看来,SignalR使用的协议由两部分组成。第一部分与连接管理有关,即连接如何启动,停止,重新连接等。这部分包含一些非常复杂的部分(特别是在启动连接时),对于想要编写自己的客户端的人来说,这是最有趣的。 ,我相信,是少数)。我认为绝大多数用户实际感兴趣的第二部分是所有这些“H”,“A”,“我”等等.SignalR正在线上并写入日志。我将从第一部分开始,然后将描述第二部分。
免责声明:在某些情况下,我将讨论客户之间的差异。我只用工作SignalR .NET客户端,该SignalR C ++客户端和SignalR JavaScript客户端(在这种情况下“工作”是一种夸大其词 - 我只修复了一些错误并多次查看代码)。我知道其他SignalR客户端,如Java或Objective-C,但我没有尝试过它们,也没有看过代码,我不知道它们做了什么,它们是如何做的以及它们与下面的描述一致。
连接管理
SignalR使用HTTP(S)协议管理连接。操作由客户端发起,客户端发送包含请求的操作和公共参数子集的HTTP请求。可以使用GET或(当使用协议版本1.5时)POST方法发送请求。并非所有请求都需要所有参数。以下是SignalR请求中使用的参数及其描述:
- transport - 正在使用的传输的名称。有效值:webSockets,longPolling,serverSentEvents,foreverFrame
- clientProtocol - 客户端使用的协议版本。最新版本是1.5但是它仅由JavaScript客户端使用,因为强制将协议版本提升为1.5的更改仅与此客户端相关。.NET和C ++客户端目前使用1.4版。请注意,服务器旨在支持下层客户端(即使用以前版本协议的客户端),当前(2.2.0)版本支持1.2到1.5之间的协议版本
- connectionToken - 标识发件人的字符串。它在对negotiate请求的响应中返回。有关连接令牌的更多详细信息,请参阅此文档
- connectionData - 一个url编码的JSon数组,包含客户端订阅的集线器列表。例如,如果客户端订阅了两个集线器 - “my_hub”,“your_hub”,要发送的数组如下所示:[{"Name":"my_hub"},{"Name":"your_hub"}]并且在url-encoding之后它变为:
%5B%7B%22Name%22:%22my_hub%22%7D,%7B%22Name%22:%22your_hub%22%7D%5D - messageId - 最后收到的消息的ID。用于重新连接和 - 在使用longPolling传输时 - poll请求中
- groupsToken - 描述连接所属组的标记。用于重新连接
- queryString - 用户提供的任意查询字符串; 附加到所有请求
启动连接
启动连接是与SignalR客户端执行的连接管理相关的最复杂的任务。它需要发送三个请求到服务器- negotiate,connect和start。整个序列如下:
- 客户端发送negotiate请求。对协商请求的响应包含许多客户端配置设置
- 客户端通过发送connect请求来启动传输。该connect请求必须由服务器在响应返回的超时时间内完成negotiate请求。对新connect请求(即初始消息)的响应在新启动的传输上发送(即如果您使用webSockets传输,它将在新打开的websocket上发送,如果您使用serverSentEvents它将在新打开的事件流上发送,如果您使用longPolling它将作为对connect/ pollrequest 的回复发送)
- 一旦收到init消息,客户端就会发送启动请求。服务器通过响应{Response: Started}有效负载确认它收到了启动请求
您还可以在此处找到有关启动顺序的一些详细信息。
连接管理请求
以下是客户端发送以启动,停止和重新连接连接的请求列表。
»negotiate - 协商连接参数
必需参数:clientProtocol,connectionData(使用集线器时)
可选参数:queryString
样本请求:
HTTP://主机/ signalr /谈判clientProtocol = 1.5&connectionData =%5B%7B%22name%22%3A%22chat%22%7D%5D
样品回复:
{
"Url":"/signalr",
"ConnectionToken":"X97dw3uxW4NPPggQsYVcNcyQcuz4w2",
"ConnectionId":"05265228-1e2c-46c5-82a1-6a5bcc3f0143",
"KeepAliveTimeout":10.0,
"DisconnectTimeout":5.0,
"TryWebSockets":true,
"ProtocolVersion":"1.5",
"TransportConnectTimeout":30.0,
"LongPollDelay":0.0
}
- Url - SignalR端点的路径。目前尚未被客户使用。
- ConnectionToken - 服务器分配的连接令牌。有关详细信息,请参阅此文章。此值需要在每个后续请求中作为connectionToken参数的值发送
- ConnectionId- 连接的ID
- KeepAliveTimeout- 客户端在尝试重新连接之前应等待的时间(以秒为单位),如果它尚未收到保持活动消息。如果服务器配置为不发送保持活动消息,则此值为空。
- DisconnectTimeout - 如果连接消失,客户端应尝试重新连接的时间量。
- TryWebSockets- 服务器是否支持websockets
- ProtocolVersion- 用于通信的协议版本
- TransportConnectTimeout - 客户端应尝试使用给定传输连接到服务器的最长时间
»connect -开始传输
所需的参数:transport,clientProtocol,connectionToken,connectionData(使用集线器时)
可选参数:queryString
样本请求:
WSS://主机/ signalr /连接传输的WebSockets =&clientProtocol = 1.5&connectionToken = LkNk&connectionData =%5B%7B%22name%22%3A%22chat%22%7D%5D?
示例响应(也称为init消息):
{"C":"s-0,2CDDE7A|1,23ADE88|2,297B01B|3,3997404|4,33239B5","S":1,"M":[]}
备注:
该connect请求将启动运输。如果您使用webSockets传输,客户端将使用ws://或wss://方案打开websocket。如果您正在使用serverSentEvents传输,则客户端将打开事件流。对于longPolling传输,服务器将连接请求视为第一个轮询请求。connect使用新打开的通道发送对请求的响应,并且是包含该属性的JSon对象"S"设置为1(aka init messge)。然而,服务器不保证该消息是发送给客户端的第一个消息(例如,正在进行的广播将在服务器发送init消息之前发送到客户端。这在longPolling传输的情况下很有意思。因为对连接请求的响应将关闭挂起的连接请求,即使它不是init消息。在这种情况下,init消息将作为对后续轮询请求的响应发送。
»start -通知运输成功启动服务器
必需的参数:transport,clientProtocol,connectionToken,connectionData(使用集线器时)
可选参数:queryString
样品要求:
HTTP://主机/ signalr /启动运输=&的WebSockets clientProtocol = 1.5&connectionToken = LkNk&connectionData =%5B%7B%22name%22%3A%22chat%22%7D%5D
样品回复:
{"Response":"started"}
备注:
start在协议版本1.4中添加了请求,以使某些方案在服务器端可靠地运行。将此请求添加到启动序列会使客户端上的事情变得复杂,因为在客户端收到init消息之后但在收到对启动消息的响应之前有很多事情可能会出错(如连接丢失和客户端开始重新连接,用户停止连接等)。
»reconnect -当连接丢失发送到服务器和客户端被重新连接
所需的参数:transport,clientProtocol,connectionToken,connectionData(使用集线器时), ,messageId(groupsToken如果连接所属的组)
的可选参数:queryString
样本请求:
WS://主机/ signalr /重新连接运输=&的WebSockets clientProtocol = 1.4&connectionToken = AA- AQA&connectionData =%5B%7B%22Name%22:%22hubConnection%22%7D%5D&MESSAGEID = d-3104A0A8-H,0%7CL,0%7CM,2%7CK,0&groupsToken = AQ
样本响应:N / A
备注:
与connect请求类似,reconnect请求启动(重新启动)传输。对于longPolling从客户端角度来看的传输,它只是另一种形式的轮询,对于serverSentEvents传输,将打开一个新的事件流,为webSockets传输它将打开一个新的websocket。在messageId讲述什么是客户端收到的最后一条消息服务器和groupsToken通知客户端重新连接前属于什么组服务器。
»abort -停止连接
所需的参数:transport,clientProtocol,connectionToken,connectionData(使用集线器时)
可选参数:queryString
样本请求:
HTTP://主机/ signalr /中止运输= longPolling&clientProtocol = 1.5&connectionToken = QcnlM&connectionData =%5B%7B%22name%22%3A%22chathub%22%7D%5D
示例响应:空
备注:JavaScript和C ++客户端abort以一种消防方式发送请求并忽略所有错误。.NET客户端阻塞,直到收到响应或发生超时,除了花费更多时间,会导致一些问题(如此错误)。
»ping - ping服务器
必需参数:无
可选参数:queryString
示例请求:
HTTP://主机/ signalr /ping
样品回复:
{ "Response": "pong" }
备注:ping请求实际上不是“连接管理请求”。此请求的唯一目的是使ASP.NET会话保持活动状态。它仅由JavaScript客户端发送。
SignalR消息
在我们看看SignalR发送的消息之前,我们需要讨论不同的传输如何发送和接收消息。的webSockets运输是非常简单,因为它正在创建用于从服务器向客户端,并从客户端向服务器发送数据的全双工通信信道。设置通道后HTTP,在客户端停止(abort请求)或连接丢失且客户端尝试重新建立连接(reconnect请求)之前,不会再有其他请求。的serverSentEvents传输创建用于从服务器接收消息的事件流。如果客户端想要向服务器发送消息,它将创建sendHTTP POST请求并在请求正文中发送数据。该longPollingtransport创建一个长时间运行的HTTP请求,如果服务器有客户端消息,服务器将响应该请求。如果服务器未在配置的超时内发送任何数据(计算为对请求ConnectionTimeout的响应中收到的总和negotiate+ 10秒 - 默认为120秒),则当前轮询请求将关闭,客户端将启动新的轮询请求(这是为了防止代理关闭长时间运行的请求,这会导致不必要的重新连接)。发送消息的工作方式与serverSentEvents传输相同- send包含请求正文中的消息的HTTP请求将发送到服务器。以下是的描述send和poll要求。
»send - 将数据发送到服务器。由所使用的serverSentEvents和longPolling传输
所需的参数:transport,clientProtocol,connectionToken,connectionData(使用集线器时),数据(请求正文发送)
可选参数:queryString
样本请求:
HTTP://主机/ signalr /发送传输= longPolling&clientProtocol = 1.5&connectionToken = Ac5y5&connectionData =%5B%7B%22name%22%3A%22chathub%22%7D%5D
数据发送到请求体(url编码,请参阅下面的说明):
数据=%7B%22H%22%3A%22chathub%22%2C%22M%22%3A%22Send%22%2C%22A%22%3A%5B%22A%22%2C%22test + MSG%22%5D %2C%22I%22%3A0%7D
样品回复(见下面的说明):
{ "I" : 0 }
»poll - 启动(可能)长时间运行的轮询请求,服务器将使用该请求将数据发送到客户端。仅由所使用的longPolling传输
所需的参数:transport,clientProtocol,connectionToken,connectionData(使用集线器时),messageId(JavaScript的客户端发送messageId在请求体)
可选参数:queryString
样本请求:
HTTP://主机/ signalr /轮询传输= longPolling&clientProtocol = 1.5&connectionToken = A12 -FX&connectionData =%5B%7B%22name%22%3A%22chathub%22%7D%5D&MESSAGEID = d-53B8FCED-B%2C1%7CC%2C0%7CD%2C1
样品回复(见下面的说明):
{
"C":"d-53B8FCED-B,4|C,0|D,1",
"M":
[
{"H":"ChatHub","M":"broadcastMessage","A":["client","test msg1"]},
{"H":"ChatHub","M":"broadcastMessage","A":["client","test msg2"]},
{"H":"ChatHub","M":"broadcastMessage","A":["client","qwerty"]}
]
}
持久连接消息
用于持久连接的协议非常简单。发送到服务器的消息只是原始字符串。它们没有任何特定的格式.C#客户端有一个方便的Send()方法,它接受一个应该发送到服务器的对象,但所有这个方法只是将对象转换为JSon并调用Send()重载采取字符串。发送到客户端的消息更加结构化。它们是具有许多属性的JSon字符串。根据消息的目的,有效负载中可能存在不同的属性,或者消息可能没有属性(KeepAlive消息)。您可以在消息中找到的属性如下:
C - 所有非KeepAlive消息的消息ID
M - 包含实际数据的数组。
{"C":"d-9B7A6976-B,2|C,2","M":["Welcome!"]}
- S - 表示传输已初始化(也称为init消息)
{"C":"s-0,2CDDE7A|1,23ADE88|2,297B01B|3,3997404|4,33239B5","S":1,"M":[]}
- G - groups token - 表示组成员身份的加密字符串
{"C":"d-6CD4082D-B,0|C,2|D,0","G":"92OXaCStiSZGy5K83cEEt8aR2ocER=","M":[]}
T- 如果值是1客户端应该转换到重新连接状态并尝试重新连接到服务器(即发送reconnect请求)。1如果正在关闭或重新启动,则服务器正在发送一条消息,并将此属性设置为。longPolling仅适用于运输。
L - 重新建立轮询连接之间的延迟。longPolling仅适用于运输。仅由JavaScript客户端使用。可通过设置IConfigurationManager.LongPollDelay属性在服务器上进行配置。
{"C":"d-E9D15DD8-B,4|C,0|D,0","L":2000,
"M":[{"H":"ChatHub","M":"broadcastMessage","A":["C++","msg"]}]}
KeepAlive消息
KeepAlive消息是空对象JSon字符串(即{}),SignalR客户端可以使用它来检测网络问题。SignalR服务器将以配置的时间间隔发送保持活动消息。如果客户端在一段时间内没有从服务器收到任何消息(包括保持活动消息),它将尝试重新启动连接。请注意,并非所有客户端当前都支持基于网络活动重新启动连接(最值得注意的是SignalR C ++客户端不支持)。通过将KeepAlive服务器配置属性设置为,可以关闭服务器发送保持活动消息null。
集线器消息
Hubs API可以从服务器的客户端和客户端方法调用服务器方法。用于持久连接的协议不够丰富,无法表达RPC(远程过程调用)语义。但是,这并不意味着用于集线器连接的协议与用于持久连接的协议完全不同。相反,用于集线器连接的协议主要是用于持久连接的协议的扩展。
当客户端调用服务器方法时,它不再像持久连接那样发送*流字符串。相反,它发送一个JSon字符串,其中包含调用该方法所需的所有必要信息。以下是客户端发送以调用服务器方法的示例消息:
{"H":"chathub","M":"Send","A":["JS Client","Test message"],"I":0, "S":{"customProperty" : "abc"}}
有效负载具有以下属性:
- I- 调用标识符 - 允许将响应与请求匹配
- H- 集线器
- M的名称 - 方法的名称
- A- 参数(如果方法没有任何参数,则数组可以为空)
- S- 状态 - 包含其他自定义数据的字典(可选,当前C ++客户端不支持)
从服务器发送到客户端的消息可以是以下之一:
- 服务器方法调用的结果
- 调用客户端方法
- 进度信息
- 服务器端集线器方法调用结果
当调用服务器方法时,服务器通过向客户端发送调用id来返回调用已完成的确认,并且 - 如果方法返回值 - 返回值,或者 - 如果调用方法失败 - 则返回错误。有两种错误 - 一般错误和集线器错误。在一般的错误的情况下,响应仅包含一个错误消息,并且该错误由客户端变成一个通用异常- .NET客户端抛出InvalidOperationException,C ++的客户端抛出一个std::runtime_error和JavaScript客户机创建Error与Exception作为源。集线器错误包含布尔属性设置,true以指示它们是集线器错误,并且它们可能包含一些其他错误数据。集线器错误HubException由.NET客户端转换为asignalr::hub_exception由C ++客户端和JavaScript客户端创建一个Error源设置为HubException。以下是服务器方法调用的示例结果:
{"I":"0"}
void调用标识符已"0"成功完成的服务器方法。
"{"I":"0", "R":42}
返回"0"成功完成调用标识符的数字的服务器方法,并返回该值42。
{"I":"0", "E":"Error occurred"}
一种服务器方法,其调用标识符因"0"错误而失败"Error occurred"
{"I":"0","E":"Hub error occurred", "H":true, "D":{"ErrorNumber":42}}
一种服务器方法,其调用标识符因"0"集线器错误"Hub error occurred"而失败,并发送了一些其他错误数据。
以下是服务器方法调用结果中可以包含的完整属性列表:
- I- invocation Id(始终存在)
- R- 服务器方法返回的值(如果方法不为void,则显示)
- E- 错误消息
- H- true如果这是一个集线器错误
- D- 包含其他错误数据的对象(只能出现集线器错误)
- T- 堆栈跟踪(如果HubConfiguration.EnableDetailedErrors在服务器上打开了详细的错误报告(即属性))。请注意,没有任何客户端当前将堆栈跟踪传播给用户,但如果启用了跟踪,则会记录消息
- S- state - 包含其他自定义数据的字典(可选,当前C ++客户端不支持)
客户端集线器方法调用
要调用客户端方法,服务器会扩展用于持久连接的协议。不同之处在于,服务器不是在消息的消息部分中发送*流文本,而是发送一个JSon字符串,其中包含调用该方法所需的所有详细信息(如集线器和方法名称和参数)。以下是服务器发送的用于在客户端上调用hub方法的消息的示例:
{"C":"d- F430FB19", "M":[{"H":"my_hub", "M":"broadcast", "A":["Hi!", 1]}] }
正如您所看到的,消息ID或消息属性形式的“信封”与持久连接相同。从中心角度来看,有趣的部分是M财产的价值:
{"H":"my_hub", "M":"broadcast", "A":["Hi!", 1]}
此结构与客户端用于调用服务器中心方法的结构非常相似(除了没有调用ID,因为服务器不期望对此消息做出任何响应)。
- H- 集线器
- M的名称 - 集线器方法的名称
- A- 参数(如果方法没有任何参数,则数组可以为空)
- S- 状态 - 包含其他自定义数据的字典(可选,当前不支持(忽略) C ++客户端)
进展信息
从服务器发送到客户端的最后一种消息是进度消息。当服务器方法是长时间运行的方法时,服务器可以将关于方法的执行进度的信息发送到客户端。与客户端方法调用类似,进度信息嵌入在持久连接消息的消息部分中。整个消息如下所示:
{"C":"d-5E80A020-A,1|B,0|C,15|D,0", M:[{I:"P|1", "P":{"I":"0", "D":1}}] }
但进度消息本身看起来像这样:
{I:"P|1", "P":{"I":"0", "D":1}}
包含有关进度信息的结构包含两个属性:
I- 一种调用ID,但前缀为"P|"。仅供较旧的客户使用。
P - 包含有关进度的实际信息的对象
包含“真实”进度信息的对象具有以下属性:
I- 调用此进程消息适用于哪个调用的调用ID
D- 方法返回的进度数据
请注意,在服务器发送调用方法的实际结果之前,可能会有多个进度消息发送到客户端。
最近的协议修订
1.4 - start请求的介绍
1.5 - 现在可以使用该POST方法发送请求。在Chrome和IE浏览器中使用传输时,这有助于避免内存泄漏longPolling(错误2953)。仅在longPolling运输时由JS客户端使用。请注意,服务器检查请求主体的唯一属性是groupsToken和messageId
这就是它。SignalR协议并不是很复杂,但是一些警告和例外可能会使实现有点麻烦。