目录
介绍
背景
使用代码
网络套接字协议
服务器握手
客户端握手
读和写
兴趣点
使用SSL——安全Web套接字
设置WebSocketsCmd为启动项目。
NEW——完全重构以.NetStandard 2.0为目标(参见上面的GitHub链接)
NEW ——更好地记录SSL错误
NEW——添加SSL支持。保护您的连接
NEW——添加了C#客户端支持。重构以获得更好的API
没有外部库。完全独立。
很多Web Socket示例都是针对旧的Web Socket版本的,并且包含用于回退通信的复杂代码(和外部库)。所有现代浏览器都至少支持第13版的Web Socket协议,所以我不想让向后兼容性支持复杂化。这是C#中Web套接字协议的基本实现,不涉及外部库。您可以使用标准HTML5 JavaScript或C#客户端进行连接。
这个应用程序提供基本的HTML页面以及处理WebSocket连接。这可能看起来令人困惑,但它允许您向客户端发送建立Web套接字连接所需的HTML,并且还允许您共享相同的端口。但是,HttpConnection是非常初级的。我敢肯定它有一些明显的安全问题。它只是为了让这个演示更容易运行。用你自己的替换它或不要使用它。
Web Sockets没有什么神奇之处。该规范易于遵循,无需使用特殊库。有一次,我甚至考虑以某种方式与Node.js进行通信,但这不是必需的。规范可能有点繁琐,但这可能是为了保持低开销。以下链接提供了一些很好的建议:
分步指南:
官方Web Socket规范:
C#中一些有用的东西:
替代C#实现:
首次运行此应用程序时,您应该会收到一条Windows防火墙警告弹出消息。只需接受警告并添加自动防火墙规则即可。每当一个新应用程序侦听端口(此应用程序确实如此)时,您都会收到此消息,它会指出恶意应用程序可能通过您的网络发送和接收不需要的数据。所有代码都在那里供您查看,因此您可以相信这个项目中发生的事情。
放置断点的好地方是WebServer类中的函数HandleAsyncConnection。请注意,这是一个多线程服务器,因此如果这变得混乱,您可能需要冻结线程。控制台输出打印线程id以使事情变得更容易。如果您想跳过所有管道,那么另一个好的起点是WebSocketConnection类中的Respond函数。如果您对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:
///
/// Combines the key supplied by the client with a guid and
/// returns the sha1 hash of the combination
///
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为自己获取一个由适当的根授权签署的免费证书。您可以在本地主机上使用此证书(但您的浏览器会给您证书警告)。
https://www.codeproject.com/Articles/1063910/WebSocket-Server-in-Csharp