精通
英语
和
开源
,
擅长
开发
与
培训
,
胸怀四海
第一信赖
锐英源精品原创,禁止全文或局部转载,禁止任何形式的非法使用,侵权必究。
锐英源软件擅长开源定制开发,本文对应的开源项目代码集成到锐英源和上市公司合作项目里,进行了商业级升级开发。想通过开源学习高难技术细节,也请关注锐英源,头条号:“软件技术及人才和养生”,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来为自己获取一个由适当的根授权机构签名的免费证书。您可以在本地主机上使用此证书(但您的浏览器会给您证书警告)。