---------- 20200417 https://www.codeproject.com/Articles/1063910/WebSocket-Server-in-Csharp https://github.com/ninjasource/Ninja.WebSockets Step by step guide: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers WebSocket 握手 http 例如: example.com/chat (網站 example.com, URI=/chat) port 80, 443 or any port. URI 可做為應用軟體的分類. 客户端握手请求 GET example: GET /chat HTTP/1.1 Host: example.com:8000 Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13 及其他的 User-Agent, Referer, Cookie. Server 可回應 400 Bad Request, 并立即关闭套接字. 403 Forbidden 檢查瀏覽器發送來的 Origin 請求標頭 Sec-WebSocket-Version, 例如 v13 可檢查瀏覽器發送來的 Origin 請求標頭. 服务器握手响应 当服务器收到握手请求时,它应该发回一个特殊的响应,表明协议将从HTTP变为WebSocket。 看起来像这样(记住每个请求头以 \r\n结尾,并在最后一个之后放置一个额外的 \r\n): HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Accept 参数很有趣,它需要服务器通过客户端发送的Sec-WebSocket-Key 计算出来。 怎样计算呢, 把客户发送的 Sec-WebSocket-Key 和 "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" (这个叫做 "魔法值")连接起来,把结果用SHA-1编码,再用base64编码一次,就可以了 一旦服务器发送这个请求头,握手就完成了,你可以开始交换数据! 服务端可以在发送握手回复前发送其他请求头,诸如Set-Cookie,请求认证或通过状态码重定向。 跟踪客户端 这并不直接与WebSocket协议相关,但是在这里值得一提的是:你的服务器将不得不跟踪客户的套接字,所以你不会再和已经完成握手的客户握手。 同一个客户端IP地址可以尝试连接多次(但是如果客户端尝试过多的连接,服务器可以拒绝它们以免遭拒绝服务攻击)。 交换数据帧 客户端或服务端都可以在任何时间点发送数据——这就是WebSocket的魅力。然而,从这些被称为“帧”的数据中提取信息就不是十分愉快的体验了。尽管所有的帧都遵从相同的格式规范,从客户端发送到服务端的数据都被 异或加密(用一个32位的key)格式化 格式 每个数据帧(从客户端到服务器,反之亦然)遵循相同的格式: Frame format: 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+ Masking-Key 掩码明确告知我们消息是否经过格式化。 从客户端来的消息必须经过格式化,所以你的服务器必须要求这个掩码是1 如果客户端发送了没有格式化的消息,你的服务器应该断开连接. 当向客户端发送帧时,不要对其进行掩码,也不要设置掩码位。 注意:即使使用安全套接字,也必须屏蔽消息。 RSV1-3可以忽略,它们是用于扩展的 opcode 操作码字段定义了如何解释有效负载数据: 0x0表示延续, 0x1表示文本(总是用UTF-8编码), 0x2表示二进制,以及其他所谓的“控制代码”,稍后将对此进行讨论。 在这个版本的WebSockets中,0x3到0x7和0xB到0xF没有任何意义。 FIN位告诉我们这是不是系列的最后一条消息。 如果是0,那么服务器将继续侦听消息的更多部分; 否则,服务器应该考虑传递的消息。不仅仅是这样 解码有效载荷长度 要读取有效负载数据,您必须知道何时停止读取。这就是为什么有效载荷长度很重要。不幸的是,这有点复杂。要阅读它,请遵循以下步骤: 1. 读取9-15(包括)位并将其解析为无符号整型。 如果长度小于等于125,那么就是长度; 你就完成了。 如果是126,到第二步。 如果是127,到步骤3。 2. 读取下面的16位,并将其解释为无符号整型。你就完成了。 3. 读取接下来的64位,并将其解释为无符号整型(最重要的位必须为0)。 读取和解密数据 如果设置了掩码位(对于客户机到服务器的消息应该是这样),则读取接下来的4个字节(32位); 这是掩蔽键。一旦有效负载长度和掩蔽键被解码,您就可以继续从套接字读取字节数。 让我们调用已编码的数据和密钥掩码。 要获得解码,可以通过编码的八位元(字节,即文本数据的字符)和XOR八位元(i模4)掩码的第四个八位元进行循环。 在伪代码中(恰好是有效的JavaScript): var DECODED = ""; for (var i = 0; i < ENCODED.length; i++) { DECODED[i] = ENCODED[i] ^ MASK[i % 4]; } 消息帧 FIN和操作码字段一起工作,以发送分裂为独立帧的消息。这称为消息碎片。片段只能在操作码0x0到0x2上可用。 回想一下,操作码告诉了帧应该做什么。如果是0x1,有效载荷就是文本。如果是0x2,有效载荷就是二进制数据。但是,如果是0x0,则该帧是一个延续帧。这意味着服务器应该将帧的有效负载连接到从该客户机接收到的最后一个帧。下面是一个粗略的示意图,其中服务器对发送文本消息的客户机做出反应。第一个消息在单个帧中发送,而第二个消息跨三个帧发送。FIN和操作码的详细信息只显示给客户: Client: FIN=1, opcode=0x1, msg="hello" Server: (process complete message immediately) Hi. Client: FIN=0, opcode=0x1, msg="and a" Server: (listening, new message containing text started) Client: FIN=0, opcode=0x0, msg="happy new" Server: (listening, payload concatenated to previous message) Client: FIN=1, opcode=0x0, msg="year!" Server: (process complete message) Happy new year to you too! 注意,第一个框架包含一个完整的消息(具有FIN=1和opcode!=0x0),因此服务器可以根据需要进行处理或响应。客户机发送的第二帧具有文本有效负载(opcode=0x1),但是整个消息还没有到达(FIN=0)。该消息的所有剩余部分都用延续帧(opcode=0x0)发送,消息的最终帧用FIN=1标记。Section 5.4 of the spec描述了消息帧。 Pings和Pongs:WebSockets的心跳 在经过握手之后的任意时刻里,无论客户端还是服务端都可以选择发送一个ping给另一方。 当ping消息收到的时候,接受的一方必须尽快回复一个pong消息。 例如,可以使用这种方式来确保客户端还是连接状态。 一个ping 或者 pong 都只是一个常规的帧, 只是这个帧是一个控制帧。Ping消息的opcode字段值为 0x9,pong消息的opcode值为 0xA 。当你获取到一个ping消息的时候,回复一个跟ping消息有相同载荷数据的pong消息 (对于ping和pong,最大载荷长度位125)。 你也有可能在没有发送ping消息的情况下,获取一个pong消息,当这种情况发生的时候忽略它。 如果在你有机会发送一个pong消息之前,你已经获取了超过一个的ping消息,那么你只发送一个pong消息。 关闭连接 客户端或服务器端都可以通过发送一个带有指定控制序列的控制帧以开始关闭连接握手(参见章节5.5.1)。对端收到这个控制帧会回复一个关闭帧,关闭发起端关闭连接。任何在关闭连接后接收到的数据都会被丢弃。