精通
英语
和
开源
,
擅长
开发
与
培训
,
胸怀四海
第一信赖
锐英源精品原创,禁止全文或局部转载,禁止任何形式的非法使用,侵权必究。
锐英源软件擅长开源定制开发,本文对应的开源项目代码集成到锐英源和上市公司合作项目里,进行了商业级升级开发。想通过开源学习高难技术细节,也请关注锐英源,头条号:“软件技术及人才和养生”,QQ群:14372360。本文翻译自codeproject,是学习通信协议的好案例,想学习请联系,学习要点有
1、工厂模式。
2、派生。
3、WebSocket协议和HTTP协议。
新 - 完全重构目标 .NetStandard 2.0(参见上面的 GitHub 链接)
新 - 更好地记录 SSL 错误
新 - 添加了 SSL 支持。保护您的连接
新 - 添加了 C# 客户端支持。重构以获得更好的 API
没有外部库。完全独立。
许多 Web Socket 示例都是针对旧的 Web Socket 版本的,并且包含用于回退通信的复杂代码(和外部库)。所有现代浏览器都至少支持Web Socket 协议的第 13 版,因此我不想让向后兼容支持的事情复杂化。这是 C# 中 Web 套接字协议的基本实现,不涉及外部库。您可以使用标准 HTML5 JavaScript 或 C# 客户端进行连接。
此应用程序提供基本的 HTML 页面以及处理 WebSocket 连接。这可能看起来令人困惑,但它允许您向客户端发送他们建立 Web 套接字连接所需的 HTML,还允许您共享相同的端口。然而,这HttpConnection是非常简陋的。我敢肯定它有一些明显的安全问题。它只是为了让这个演示更容易运行。用你自己的替换它或不要使用它。
Web Sockets 没有什么神奇之处。该规范易于遵循,无需使用特殊库。有一次,我什至考虑以某种方式与 Node.js 进行通信,但这不是必需的。该规范可能有点繁琐,但这可能是为了保持较低的开销。这是我的第一篇 CodeProject 文章,我希望您会发现它很容易理解。以下链接提供了一些很好的建议:
分步指南:
官方 Web Socket 规范:
C# 中一些有用的东西:
替代 C# 实现:
首次运行此应用程序时,您应该会收到一条 Windows 防火墙警告弹出消息。只需接受警告并添加自动防火墙规则。每当一个新的应用程序在端口上侦听(这个应用程序就是这样做的)时,您都会收到这条消息,它会指出恶意应用程序可能通过您的网络发送和接收不需要的数据。所有代码都在那里供您查看,因此您可以相信这个项目中发生的事情。
放置断点的好地方是在函数WebServer的类中HandleAsyncConnection。请注意,这是一个多线程服务器,因此如果这变得混乱,您可能希望冻结线程。控制台输出打印线程 ID 以简化操作。如果您想跳过所有管道,那么另一个不错的起点是类Respond中的函数WebSocketConnection。如果您对 Web Sockets 的内部工作原理不感兴趣,只想使用它们,那么请看一下ChatWebSocketConnection类的OnTextFrame。见下文。
聊天网络套接字连接的实现如下:
internal class ChatWebSocketService : WebSocketService { private readonly IWebSocketLogger _logger; public ChatWebSocketService(NetworkStream networkStream, TcpClient tcpClient, string header, IWebSocketLogger logger) : base(networkStream, tcpClient, header, true, logger) { _logger = logger; } protected override void OnTextFrame(string text) { string response = "ServerABC: " + text; base.Send(response); } }
用于创建连接的工厂如下:
internal class ServiceFactory : IServiceFactory { public ServiceFactory(string webRoot, IWebSocketLogger logger) { _logger = logger; _webRoot = webRoot; } public IService CreateInstance(ConnectionDetails connectionDetails) { switch (connectionDetails.ConnectionType) { case ConnectionType.WebSocket: // you can support different kinds of web socket connections // using a different path if (connectionDetails.Path == "/chat") { return new ChatWebSocketService(connectionDetails.NetworkStream, connectionDetails.TcpClient, connectionDetails.Header, _logger); } break; case ConnectionType.Http: // this path actually refers to the // relative location of some HTML file or image return new HttpService(connectionDetails.NetworkStream, connectionDetails.Path, _webRoot, _logger); } return new BadRequestService (connectionDetails.NetworkStream, connectionDetails.Header, _logger); } }
用于连接的 HTML5 JavaScript:
// open the connection to the Web Socket server var CONNECTION = new WebSocket('ws://localhost/chat'); // Log messages from the server CONNECTION.onmessage = function (e) { console.log(e.data); }; CONNECTION.send('Hellow World');
但是,您也可以用 C# 编写自己的测试客户端。命令行应用程序中有一个示例。从命令行应用程序启动服务器和测试客户端:
private static void Main(string[] args) { IWebSocketLogger logger = new WebSocketLogger(); try { string webRoot = Settings.Default.WebRoot; int port = Settings.Default.Port; // used to decide what to do with incoming connections ServiceFactory serviceFactory = new ServiceFactory(webRoot, logger); using (WebServer server = new WebServer(serviceFactory, logger)) { server.Listen(port); Thread clientThread = new Thread(new ParameterizedThreadStart(TestClient)); clientThread.IsBackground = false; clientThread.Start(logger); Console.ReadKey(); } } catch (Exception ex) { logger.Error(null, ex); Console.ReadKey(); } }
测试客户端运行一个简短的自测以确保一切正常。此处测试打开和关闭握手。
关于协议的第一件事是,它本质上是一个基本的双工 TCP/IP 套接字连接。连接从客户端连接到远程服务器并将 HTTP 标头文本发送到该服务器开始。标头文本要求 Web 服务器将连接升级为 Web 套接字连接。这是作为握手完成的,其中 Web 服务器使用适当的 HTTP 文本标头进行响应,从那时起,客户端和服务器将使用 Web Socket 语言进行对话。
Regex webSocketKeyRegex = new Regex("Sec-WebSocket-Key: (.*)");
Regex webSocketVersionRegex = new Regex("Sec-WebSocket-Version: (.*)");// check the version. Support version 13 and above
const int WebSocketVersion = 13;
int secWebSocketVersion =
Convert.ToInt32(webSocketVersionRegex.Match(header).Groups[1].Value.Trim());
if (secWebSocketVersion < WebSocketVersion)
{
throw new WebSocketVersionNotSupportedException
(string.Format("WebSocket Version {0} not supported.
Must be {1} or above", secWebSocketVersion, WebSocketVersion));
} string secWebSocketKey = webSocketKeyRegex.Match(header).Groups[1].Value.Trim();
string setWebSocketAccept = base.ComputeSocketAcceptString(secWebSocketKey);
string response = ("HTTP/1.1 101 Switching Protocols\r\n"
+ "Connection: Upgrade\r\n"
+ "Upgrade: websocket\r\n"
+ "Sec-WebSocket-Accept: " + setWebSocketAccept); HttpHelper.WriteHttpHeader(response, networkStream);
注意:不要使用Environment.Newline,\r\n因为 HTTP 规范正在寻找回车换行符(两个特定的 ASCII 字符),而不是您的环境认为等效的任何字符。
这计算accept string:
/// <summary>
/// Combines the key supplied by the client with a guid and
/// returns the sha1 hash of the combination
/// </summary>
public static string ComputeSocketAcceptString(string secWebSocketKey)
{
// this is a guid as per the web socket spec
const string webSocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
string concatenated = secWebSocketKey + webSocketGuid;
byte[] concatenatedAsBytes = Encoding.UTF8.GetBytes(concatenated);
byte[] sha1Hash = SHA1.Create().ComputeHash(concatenatedAsBytes);
string secWebSocketAccept = Convert.ToBase64String(sha1Hash);
return secWebSocketAccept;
}
Uri uri = _uri;
WebSocketFrameReader reader = new WebSocketFrameReader();
Random rand = new Random();
byte[] keyAsBytes = new byte[16];
rand.NextBytes(keyAsBytes);
string secWebSocketKey = Convert.ToBase64String(keyAsBytes);
string handshakeHttpRequestTemplate = "GET {0} HTTP/1.1\r\n" +
"Host: {1}:{2}\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Origin: http://{1}:{2}\r\n" +
"Sec-WebSocket-Key: {3}\r\n" +
"Sec-WebSocket-Version: 13\r\n\r\n";
string handshakeHttpRequest = string.Format(handshakeHttpRequestTemplate,
uri.PathAndQuery, uri.Host, uri.Port, secWebSocketKey);
byte[] httpRequest = Encoding.UTF8.GetBytes(handshakeHttpRequest);
networkStream.Write(httpRequest, 0, httpRequest.Length);
执行握手后,服务器进入循环read。以下两个类将字节流转换为 Web 套接字帧,反之亦然:WebSocketFrameReader和WebSocketFrameWriter.
// from WebSocketFrameReader class public WebSocketFrame Read(Stream stream, Socket socket) { byte byte1; try { byte1 = (byte) stream.ReadByte(); } catch (IOException) { if (socket.Connected) { throw; } else { return null; } } // process first byte byte finBitFlag = 0x80; byte opCodeFlag = 0x0F; bool isFinBitSet = (byte1 & finBitFlag) == finBitFlag; WebSocketOpCode opCode = (WebSocketOpCode) (byte1 & opCodeFlag); // read and process second byte byte byte2 = (byte) stream.ReadByte(); byte maskFlag = 0x80; bool isMaskBitSet = (byte2 & maskFlag) == maskFlag; uint len = ReadLength(byte2, stream); byte[] payload; // use the masking key to decode the data if needed if (isMaskBitSet) { byte[] maskKey = BinaryReaderWriter.ReadExactly (WebSocketFrameCommon.MaskKeyLength, stream); payload = BinaryReaderWriter.ReadExactly((int) len, stream); // apply the mask key to the payload (which will be mutated) WebSocketFrameCommon.ToggleMask(maskKey, payload); } else { payload = BinaryReaderWriter.ReadExactly((int) len, stream); } WebSocketFrame frame = new WebSocketFrame(isFinBitSet, opCode, payload, true); return frame; }
// from WebSocketFrameWriter class public void Write(WebSocketOpCode opCode, byte[] payload, bool isLastFrame) { // best to write everything to a memory stream before we push it onto the wire // not really necessary but I like it this way using (MemoryStream memoryStream = new MemoryStream()) { byte finBitSetAsByte = isLastFrame ? (byte) 0x80 : (byte) 0x00; byte byte1 = (byte) (finBitSetAsByte | (byte) opCode); memoryStream.WriteByte(byte1); // NB, set the mask flag if we are constructing a client frame byte maskBitSetAsByte = _isClient ? (byte)0x80 : (byte)0x00; // depending on the size of the length we want to write it as a byte, ushort or ulong if (payload.Length < 126) { byte byte2 = (byte)(maskBitSetAsByte | (byte) payload.Length); memoryStream.WriteByte(byte2); } else if (payload.Length <= ushort.MaxValue) { byte byte2 = (byte)(maskBitSetAsByte | 126); memoryStream.WriteByte(byte2); BinaryReaderWriter.WriteUShort((ushort) payload.Length, memoryStream, false); } else { byte byte2 = (byte)(maskBitSetAsByte | 127); memoryStream.WriteByte(byte2); BinaryReaderWriter.WriteULong((ulong) payload.Length, memoryStream, false); } // if we are creating a client frame then we MUST mack the payload as per the spec if (_isClient) { byte[] maskKey = new byte[WebSocketFrameCommon.MaskKeyLength]; _random.NextBytes(maskKey); memoryStream.Write(maskKey, 0, maskKey.Length); // mask the payload WebSocketFrameCommon.ToggleMask(maskKey, payload); } memoryStream.Write(payload, 0, payload.Length); byte[] buffer = memoryStream.ToArray(); _stream.Write(buffer, 0, buffer.Length); } }
注意:客户端帧必须包含屏蔽的有效载荷数据。这样做是为了防止原始代理服务器缓存数据,认为它是静态 HTML。当然,使用 SSL 可以绕过代理问题,但该协议的作者无论如何都选择强制执行它。
代理服务器的问题:未配置为支持 Web 套接字的代理服务器将无法正常工作。我建议您使用传输层安全(使用 SSL 证书),如果您希望它在更广泛的互联网上工作,尤其是在公司内部。
要在演示中启用 SSL,您需要执行以下操作:
我建议您在尝试使用 JavaScript 客户端之前让演示聊天开始工作,因为很多事情都可能出错,而且演示会公开更多日志信息。WebSocketClient.ValidateServerCertificate如果您收到证书错误(如名称不匹配或过期),那么您始终可以通过使函数始终返回来禁用检查true。
如果您在创建证书时遇到问题,我强烈建议您使用它LetsEncrypt来为自己获取一个由适当的根授权机构签名的免费证书。您可以在本地主机上使用此证书(但您的浏览器会给您证书警告)。