aboutsummaryrefslogtreecommitdiff
path: root/files/zh-cn/web/api/websockets_api/writing_websocket_server
diff options
context:
space:
mode:
authorPeter Bengtsson <mail@peterbe.com>2020-12-08 14:40:17 -0500
committerPeter Bengtsson <mail@peterbe.com>2020-12-08 14:40:17 -0500
commit33058f2b292b3a581333bdfb21b8f671898c5060 (patch)
tree51c3e392513ec574331b2d3f85c394445ea803c6 /files/zh-cn/web/api/websockets_api/writing_websocket_server
parent8b66d724f7caf0157093fb09cfec8fbd0c6ad50a (diff)
downloadtranslated-content-33058f2b292b3a581333bdfb21b8f671898c5060.tar.gz
translated-content-33058f2b292b3a581333bdfb21b8f671898c5060.tar.bz2
translated-content-33058f2b292b3a581333bdfb21b8f671898c5060.zip
initial commit
Diffstat (limited to 'files/zh-cn/web/api/websockets_api/writing_websocket_server')
-rw-r--r--files/zh-cn/web/api/websockets_api/writing_websocket_server/index.html273
1 files changed, 273 insertions, 0 deletions
diff --git a/files/zh-cn/web/api/websockets_api/writing_websocket_server/index.html b/files/zh-cn/web/api/websockets_api/writing_websocket_server/index.html
new file mode 100644
index 0000000000..dbc31715ae
--- /dev/null
+++ b/files/zh-cn/web/api/websockets_api/writing_websocket_server/index.html
@@ -0,0 +1,273 @@
+---
+title: 用C#来编写WebSocket服务器
+slug: Web/API/WebSockets_API/Writing_WebSocket_server
+tags:
+ - HTML5
+ - NeedsMarkupWork
+ - Tutorial
+ - WebSockets
+translation_of: Web/API/WebSockets_API/Writing_WebSocket_server
+---
+<h2 id="介绍">介绍</h2>
+
+<p>如果你想学习如何使用 WebSocket API,那么有一台服务器将会是非常有用的。在本文中,我将向你展示如何使用C#来写后端。你可以使用任何可用于后端开发的语言来做这个事<span style="line-height: 1.5;">, 但是,要为了使例子简明易懂,我选择微软的C#。</span></p>
+
+<p>此服务器符合 <a href="http://tools.ietf.org/html/rfc6455" title="http://tools.ietf.org/html/rfc6455">RFC 6455</a> 因此,因此它只处理来自 Chrome16,Firefox 11,IE 10 及更高版本的连接。</p>
+
+<h2 id="第一步">第一步</h2>
+
+<p>WebSockets 通过 <a href="http://en.wikipedia.org/wiki/Transmission_Control_Protocol" title="http://en.wikipedia.org/wiki/Transmission_Control_Protocol">TCP (传输控制协议)</a> 连接进行通信.。幸运的是, C# 中有一个 <a href="http://msdn.microsoft.com/en-us/library/system.net.sockets.tcplistener.aspx" title="http://msdn.microsoft.com/en-us/library/system.net.sockets.tcplistener.aspx">TcpListener </a>类。 它位于 <em>System.Net.Sockets</em> 的命名空间。</p>
+
+<div class="note">
+<p><span style="line-height: 1.572;">最好使用 <code>using</code></span><span style="line-height: 1.572;"> 关键字来包含命名空间,这样在你写代码的时候就不需要指定详细的命名空间。</span></p>
+</div>
+
+<h3 id="TcpListener">TcpListener</h3>
+
+<p>构造函数:</p>
+
+<pre class="brush: cpp">TcpListener(System.Net.IPAddress localaddr, int port)</pre>
+
+<p><code>localaddr</code> 是监听地址,  <code>port</code> 是监听端口.</p>
+
+<div class="note">
+<p><em>如果字符串创建 <code>IPAddress</code> 对象,请使用 Parse静态方法。</em></p>
+</div>
+
+<p><span style="line-height: 1.572;">方法</span><span style="line-height: 1.572;">:</span></p>
+
+<ul>
+ <li><code><span style="line-height: 1.572;">Start()</span></code></li>
+ <li><span style="line-height: 1.572;"><code>System.Net.Sockets.<a href="http://msdn.microsoft.com/en-us/library/system.net.sockets.tcpclient.aspx" title="http://msdn.microsoft.com/en-us/library/system.net.sockets.tcpclient.aspx">TcpClient</a> AcceptTcpClient()</code><br>
+ 等一个Tcp 连接, 并接受一个返回的TcpClient对象。</span></li>
+</ul>
+
+<p><span style="line-height: 1.572;">下面是基于服务端的实现:</span></p>
+
+<pre class="brush: cpp">​using System.Net.Sockets;
+using System.Net;
+using System;
+
+class Server {
+ public static void Main() {
+ TcpListener server = new TcpListener(IPAddress.Parse("127.0.0.1"), 80);
+
+ server.Start();
+ Console.WriteLine("Server has started on 127.0.0.1:80.{0}Waiting for a connection...", Environment.NewLine);
+
+ TcpClient client = server.AcceptTcpClient();
+
+ Console.WriteLine("A client connected.");
+ }
+}
+</pre>
+
+<h3 id="TcpClient"><span style="line-height: 1.572;">TcpClient</span></h3>
+
+<p>方法:</p>
+
+<ul>
+ <li><code>System.Net.Sockets.<a href="http://msdn.microsoft.com/en-us/library/system.net.sockets.networkstream.aspx" title="http://msdn.microsoft.com/en-us/library/system.net.sockets.networkstream.aspx">NetworkStream</a> GetStream()</code><br>
+ 获取一个通信通道的流,通道两边都具有读写能力。</li>
+</ul>
+
+<p>属性:</p>
+
+<ul>
+ <li><code>int Available</code><br>
+ 这个属性表示已经发送了多少个字节的数据。它的值为零,直到 <code>NetworkStream.DataAvailable</code> 为true。</li>
+</ul>
+
+<h3 id="NetworkStream">NetworkStream</h3>
+
+<p>方法:</p>
+
+<ul>
+ <li><code>Write(Byte[] buffer, int offset, int size)</code><br>
+ 根据buffer数组写入字节流,offset与size参数决定了消息的长度。</li>
+ <li><code><span class="brush: cpp" style="line-height: 1.572;">Read(Byte[] buffer, int offset, int size)</span></code><br>
+ 将字节流读取到 <code>buffer</code> 中。 <code>offset</code> 和 <code>size</code> 参数决定了消息的长度。</li>
+</ul>
+
+<p>让我们扩充一下我们的示例.</p>
+
+<pre class="brush: cpp">TcpClient client = server.AcceptTcpClient();
+
+Console.WriteLine("A client connected.");
+
+NetworkStream stream = client.GetStream();
+
+//enter to an infinite cycle to be able to handle every change in stream
+while (true) {
+ while (!stream.DataAvailable);
+
+ Byte[] bytes = new Byte[client.Available];
+
+ stream.Read(bytes, 0, bytes.Length);
+}</pre>
+
+<h2 id="握手">握手</h2>
+
+<p>当一个客户端连接到服务器时,它会发送一个GET请求将现在一个简单的HTTP请求升级为一个WebSocket请求。这被称为握手。</p>
+
+<p>下面是一段检测从客户端发来的GET请求的代码。需要注意的是,下面的程序在没有收到消息开头的3个有效字节前将处于阻塞状态。在生产环境下,应该考虑使用可用于替代的解决方案。</p>
+
+<pre class="brush: cpp">using System.Text;
+using System.Text.RegularExpressions;
+
+while(client.Available &lt; 3)
+{
+ // wait for enough bytes to be available
+}
+
+Byte[] bytes = new Byte[client.Available];
+
+stream.Read(bytes, 0, bytes.Length);
+
+//translate bytes of request to string
+String data = Encoding.UTF8.GetString(bytes);
+
+if (Regex.IsMatch(data, "^GET")) {
+
+} else {
+
+}</pre>
+
+<p>回应的消息很容易构造,但是可能会有一点难以理解。完整的关于服务器握手的解释可以在 <a href="RFC 6455, section 4.2.2">RFC 6455, section 4.2.2</a> 找到。从我们的目的出发,我们将构造一个简单的回应消息。</p>
+
+<p>你必须:</p>
+
+<ol>
+ <li>获取请求头中"Sec-WebSocket-Key"字段的值,这个字段值不能有任何的前导和后继空格字符</li>
+ <li>将它与"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"(一个 RFC 6455 中规定的特殊的 GUID )拼接起来</li>
+ <li>计算新的值的 SHA-1 和 Base64 哈希值</li>
+ <li>将哈希值写回到一个HTTP响应头,作为"Sec-WebSocket-Accept"字段的值</li>
+</ol>
+
+<pre class="brush: cpp"><em>
+</em>if (new System.Text.RegularExpressions.Regex("^GET").IsMatch(data))
+{
+    const string eol = "\r\n"; // HTTP/1.1 defines the sequence CR LF as the end-of-line marker
+
+    Byte[] response = Encoding.UTF8.GetBytes("HTTP/1.1 101 Switching Protocols" + eol
+        + "Connection: Upgrade" + eol
+        + "Upgrade: websocket" + eol
+        + "Sec-WebSocket-Accept: " + Convert.ToBase64String(
+            System.Security.Cryptography.SHA1.Create().ComputeHash(
+                Encoding.UTF8.GetBytes(
+                    new System.Text.RegularExpressions.Regex("Sec-WebSocket-Key: (.*)").Match(data).Groups[1].Value.Trim() + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
+                )
+            )
+        ) + eol
+        + eol);
+
+    stream.Write(response, 0, response.Length);
+}
+</pre>
+
+<h2 id="解密消息">解密消息</h2>
+
+<p>在一次成功的握手之后,客户端将向服务器发送加密后的消息</p>
+
+<p>如果我们发送了 "MDN",那么我们会得到下面这些字节:</p>
+
+<table>
+ <tbody>
+ <tr>
+ <td>129</td>
+ <td>131</td>
+ <td>61</td>
+ <td>84</td>
+ <td>35</td>
+ <td>6</td>
+ <td>112</td>
+ <td>16</td>
+ <td>109</td>
+ </tr>
+ </tbody>
+</table>
+
+<p>让我们看看这些字节意味着什么。</p>
+
+<p>第一个字节,当前值是129,是按位组成的,分解如下:</p>
+
+<table>
+ <thead>
+ <tr>
+ <th scope="col">FIN (Bit 0)</th>
+ <th scope="col">RSV1 (Bit 1)</th>
+ <th scope="col">RSV2 (Bit 2)</th>
+ <th scope="col">RSV3 (Bit 3)</th>
+ <th scope="col">Opcode (Bit 4:7)</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>1</td>
+ <td>0</td>
+ <td>0</td>
+ <td>0</td>
+ <td>0x1=0001</td>
+ </tr>
+ </tbody>
+</table>
+
+<ul>
+ <li>FIN 位:这个位表明是否整个消息都已经从客户端被发送出去。消息可能以多个帧的形式发送,但现在我们将情景考虑得简单一些。</li>
+ <li>RSV1, RSV2, RSV3:除非规定的扩展协议支持将它们赋为非0值,否则这些位必须为0。</li>
+ <li><span style="line-height: 1.572;">Opcode:</span><span style="line-height: 1.572;">这些位描述了接收的消息的类型。Opcode 0x1 意味着这是一条文本消息。</span><a href="http://tools.ietf.org/html/rfc6455#section-5.2" style="line-height: 1.572;" title="http://tools.ietf.org/html/rfc6455#section-5.2">Opcodes值的完整罗列</a></li>
+</ul>
+
+<p>第二个字节,当前值是131,是另一个按位组成的部分,分解如下:</p>
+
+<table>
+ <thead>
+ <tr>
+ <th scope="col">MASK (Bit 0)</th>
+ <th scope="col">Payload Length (Bit 1:7)</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>1</td>
+ <td>0x83=0000011</td>
+ </tr>
+ </tbody>
+</table>
+
+<ul>
+ <li>MASK 位:定义了是否"Payload data"进行了掩码计算。如果值设置为1,那么在Masking-Key字段中会有一个掩码密钥,并且它可以用来进行"Payload data"的去掩码计算。所有从客户端发到服务器的消息中此位都会被置1。</li>
+ <li>Payload Length:如果这个值在0与125之间,那么这个值就是消息的长度。如果这个值是126,那么接下来的2个字节(16位无符号整数)是消息长度。如果这个值是127,那么接下来的8个字节(64位无符号整数)是消息长度。</li>
+</ul>
+
+<div class="note">
+<p>因为在客户端到服务器的消息中第一位总是1,所以你可以将这个字节减去128去除 MASK 位。</p>
+</div>
+
+<p>需要注意的是MASK位在我们的消息中被置为1。这意味着接下来的4个字节(61, 84, 35, 6) 是用于解码消息的掩码字节。这些字节在每个消息中都不是固定不变的。</p>
+
+<p>剩下的字节是加密后的消息载荷。</p>
+
+<h3 id="解密算法">解密算法</h3>
+
+<p><em>D<sub>i</sub></em> = <em>E<sub>i</sub></em> XOR <em>M</em><sub>(<em>i</em> mod 4)</sub></p>
+
+<p>D 是解密后的消息数组, <em>E</em> 是被加密的消息数组,<em> M</em> 是掩码字节数组, <em>i</em> 是需要解密的消息字节的序号。</p>
+
+<p>C# 示例:</p>
+
+<pre class="brush: cpp">Byte[] decoded = new Byte[3];
+Byte[] encoded = new Byte[3] {112, 16, 109};
+Byte[] mask = new Byte[4] {61, 84, 35, 6};
+
+for (int i = 0; i &lt; encoded.Length; i++) {
+ decoded[i] = (Byte)(encoded[i] ^ mask[i % 4]);
+}</pre>
+
+<h2 id="有关文档">有关文档</h2>
+
+<ul>
+ <li><a href="/en-US/docs/WebSockets/Writing_WebSocket_servers">编写 WebSocket 服务器</a></li>
+</ul>
+
+<div id="cke_pastebin" style="position: absolute; top: 2209.23px; width: 1px; height: 1px; overflow: hidden; left: -1000px;"><em> </em></div>