sta/websocket-sharp
GitHub: sta/websocket-sharp
websocket-sharp 是一个 C# 编写的 WebSocket 协议库,提供完整的客户端与服务器实现,解决 .NET 环境下的实时双向通信需求。
Stars: 6069 | Forks: 1684

## 欢迎使用 websocket-sharp!
websocket-sharp 支持:
- [RFC 6455](#supported-websocket-specifications)
- [WebSocket 客户端](#websocket-client)和[服务器](#websocket-server)
- [逐消息压缩](#per-message-compression)扩展
- [安全连接](#secure-connection)
- [HTTP 身份验证](#http-authentication)
- [查询字符串、Origin 头、Cookies 和用户头](#query-string-origin-header-cookies-and-user-headers)
- [通过 HTTP 代理服务器连接](#connecting-through-the-http-proxy-server)
- .NET Framework **3.5** 或更高版本的 .NET Framework(包含兼容环境,如 [Mono])
## 分支
- [master] 用于正式发布版本。
- [hybi-00] 用于较旧的 [draft-ietf-hybi-thewebsocketprotocol-00]。不再维护。
- [draft75] 用于更旧的 [draft-hixie-thewebsocketprotocol-75]。不再维护。
## 构建
websocket-sharp 被构建为单个程序集,**websocket-sharp.dll**。
websocket-sharp 是使用 [MonoDevelop] 开发的。因此,一种简单的构建方式是在 MonoDevelop 中打开 **websocket-sharp.sln**,并使用任意构建配置(例如 `Debug`)对 **websocket-sharp project** 运行构建。
## 安装
### 自行构建
您应该将您的 websocket-sharp.dll(例如 `/path/to/websocket-sharp/bin/Debug/websocket-sharp.dll`)添加到您项目的库引用中。
如果您想在您的 [Unity] 项目中使用该 dll,您应该在 **Unity Editor** 中将其添加到您项目的任意文件夹中(例如 `Assets/Plugins`)。
### NuGet Gallery
websocket-sharp 已在 [NuGet Gallery] 上提供,目前仍为 **预发布** 版本。
- [NuGet Gallery: websocket-sharp]
您可以使用 NuGet Package Manager,在 Package Manager Console 中执行以下命令,将 websocket-sharp 添加到您的项目中。
```
PM> Install-Package WebSocketSharp -Pre
```
## 用法
### WebSocket 客户端
```
using System;
using WebSocketSharp;
namespace Example
{
public class Program
{
public static void Main (string[] args)
{
using (var ws = new WebSocket ("ws://dragonsnest.far/Laputa")) {
ws.OnMessage += (sender, e) =>
Console.WriteLine ("Laputa says: " + e.Data);
ws.Connect ();
ws.Send ("BALUS");
Console.ReadKey (true);
}
}
}
}
```
#### 步骤 1
所需的命名空间。
```
using WebSocketSharp;
```
`WebSocket` 类存在于 `WebSocketSharp` 命名空间中。
#### 步骤 2
使用要连接的 WebSocket URL 创建 `WebSocket` 类的新实例。
```
var ws = new WebSocket ("ws://example.com");
```
`WebSocket` 类继承了 `System.IDisposable` 接口,因此您可以使用 `using` 语句来创建它。
```
using (var ws = new WebSocket ("ws://example.com")) {
...
}
```
当控制流离开 `using` 块时,这将使用状态码 `1001`(going away)**关闭** WebSocket 连接。
#### 步骤 3
设置 `WebSocket` 的事件。
##### WebSocket.OnOpen 事件
当 WebSocket 连接建立时,将触发此事件。
```
ws.OnOpen += (sender, e) => {
...
};
```
`e` 会传入 `System.EventArgs.Empty`,因此您不需要使用它。
##### WebSocket.OnMessage 事件
当 `WebSocket` 实例接收到消息时,将触发此事件。
```
ws.OnMessage += (sender, e) => {
...
};
```
`e` 会传入一个 `WebSocketSharp.MessageEventArgs` 实例。
如果您想获取消息数据,您应该访问 `e.Data` 或 `e.RawData` 属性。
`e.Data` 属性返回一个 `string`,因此它主要用于获取**文本**消息数据。
`e.RawData` 属性返回一个 `byte[]`,因此它主要用于获取**二进制**消息数据。
```
if (e.IsText) {
// Do something with e.Data.
...
return;
}
if (e.IsBinary) {
// Do something with e.RawData.
...
return;
}
```
此外,如果您想通过此事件通知已接收到 **ping**,您应该将 `WebSocket.EmitOnPing` 属性设置为 `true`。
```
ws.EmitOnPing = true;
ws.OnMessage += (sender, e) => {
if (e.IsPing) {
// Do something to notify that a ping has been received.
...
return;
}
};
```
##### WebSocket.OnError 事件
当 `WebSocket` 实例发生错误时,将触发此事件。
```
ws.OnError += (sender, e) => {
...
};
```
`e` 会传入一个 `WebSocketSharp.ErrorEventArgs` 实例。
如果您想获取错误消息,您应该访问 `e.Message` 属性。
`e.Message` 属性返回一个表示错误消息的 `string`。
如果错误是由异常引起的,`e.Exception` 属性将返回一个表示错误原因的 `System.Exception` 实例。
##### WebSocket.OnClose 事件
当 WebSocket 连接已关闭时,将触发此事件。
```
ws.OnClose += (sender, e) => {
...
};
```
`e` 会传入一个 `WebSocketSharp.CloseEventArgs` 实例。
如果您想获取关闭的原因,您应该访问 `e.Code` 或 `e.Reason` 属性。
`e.Code` 属性返回一个表示关闭状态码的 `ushort`。
`e.Reason` 属性返回一个表示关闭原因的 `string`。
#### 步骤 4
连接到 WebSocket 服务器。
```
ws.Connect ();
```
如果您想异步连接到服务器,您应该使用 `WebSocket.ConnectAsync ()` 方法。
#### 步骤 5
向 WebSocket 服务器发送数据。
```
ws.Send (data);
```
`WebSocket.Send` 方法已被重载。
您可以使用 `WebSocket.Send (string)`、`WebSocket.Send (byte[])`、`WebSocket.Send (System.IO.FileInfo)` 或 `WebSocket.Send (System.IO.Stream, int)` 方法来发送数据。
如果您想异步发送数据,您应该使用 `WebSocket.SendAsync` 方法。
```
ws.SendAsync (data, completed);
```
此外,如果您想在发送完成后执行某些操作,您应该将 `completed` 设置为任意的 `Action` 委托。
#### 步骤 6
关闭 WebSocket 连接。
```
ws.Close (code, reason);
```
如果您想显式关闭连接,您应该使用 `WebSocket.Close` 方法。
`WebSocket.Close` 方法已被重载。
您可以使用 `WebSocket.Close ()`、`WebSocket.Close (ushort)`、`WebSocket.Close (WebSocketSharp.CloseStatusCode)`、`WebSocket.Close (ushort, string)` 或 `WebSocket.Close (WebSocketSharp.CloseStatusCode, string)` 方法来关闭连接。
如果您想异步关闭连接,您应该使用 `WebSocket.CloseAsync` 方法。
### WebSocket 服务器
```
using System;
using WebSocketSharp;
using WebSocketSharp.Server;
namespace Example
{
public class Laputa : WebSocketBehavior
{
protected override void OnMessage (MessageEventArgs e)
{
var msg = e.Data == "BALUS"
? "Are you kidding?"
: "I'm not available now.";
Send (msg);
}
}
public class Program
{
public static void Main (string[] args)
{
var wssv = new WebSocketServer ("ws://dragonsnest.far");
wssv.AddWebSocketService ("/Laputa");
wssv.Start ();
Console.ReadKey (true);
wssv.Stop ();
}
}
}
```
#### 步骤 1
所需的命名空间。
```
using WebSocketSharp.Server;
```
`WebSocketBehavior` 和 `WebSocketServer` 类存在于 `WebSocketSharp.Server` 命名空间中。
#### 步骤 2
创建继承 `WebSocketBehavior` 类的类。
例如,如果您想提供 Echo 服务,
```
using System;
using WebSocketSharp;
using WebSocketSharp.Server;
public class Echo : WebSocketBehavior
{
protected override void OnMessage (MessageEventArgs e)
{
Send (e.Data);
}
}
```
如果您想提供聊天服务,
```
using System;
using WebSocketSharp;
using WebSocketSharp.Server;
public class Chat : WebSocketBehavior
{
private string _suffix;
public Chat ()
{
_suffix = String.Empty;
}
public string Suffix {
get {
return _suffix;
}
set {
_suffix = value ?? String.Empty;
}
}
protected override void OnMessage (MessageEventArgs e)
{
Sessions.Broadcast (e.Data + _suffix);
}
}
```
您可以通过创建继承 `WebSocketBehavior` 类的类来定义任何 WebSocket 服务的行为。
如果您重写 `WebSocketBehavior.OnMessage (MessageEventArgs)` 方法,当服务中某个会话使用的 `WebSocket` 接收到消息时,将会调用该方法。
如果您重写 `WebSocketBehavior.OnOpen ()`、`WebSocketBehavior.OnError (ErrorEventArgs)` 和 `WebSocketBehavior.OnClose (CloseEventArgs)` 方法,则当 `WebSocket` 的各个事件(`OnOpen`、`OnError` 和 `OnClose`)触发时,将会分别调用它们。
`WebSocketBehavior.Send` 方法可以向服务中某个会话的客户端发送数据。
如果您想获取服务中的会话,您应该访问 `WebSocketBehavior.Sessions` 属性(返回一个 `WebSocketSharp.Server.WebSocketSessionManager`)。
`WebSocketBehavior.Sessions.Broadcast` 方法可以向服务中的每个客户端发送数据。
#### 步骤 3
创建 `WebSocketServer` 类的新实例。
```
var wssv = new WebSocketServer (4649);
wssv.AddWebSocketService ("/Echo");
wssv.AddWebSocketService ("/Chat");
wssv.AddWebSocketService ("/ChatWithNyan", s => s.Suffix = " Nyan!");
```
您可以使用 `WebSocketServer.AddWebSocketService (string)` 或 `WebSocketServer.AddWebSocketService (string, Action)` 方法,通过指定的行为和服务的绝对路径,将任何 WebSocket 服务添加到您的 `WebSocketServer` 中。
`TBehavior` 的类型必须继承 `WebSocketBehavior` 类,并且必须具有公共的无参构造函数。
因此,您可以使用上面步骤 2 中的类来添加该服务。
如果您在不带端口号的情况下创建 `WebSocketServer` 类的新实例,它会将端口号设置为 **80**。因此,必须使用 root 权限运行。
```
$ sudo mono example2.exe
```
#### 步骤 4
启动 WebSocket 服务器。
```
wssv.Start ();
```
#### 步骤 5
停止 WebSocket 服务器。
```
wssv.Stop ();
```
### 带有 WebSocket 的 HTTP 服务器
我修改了 **[Mono]** 中的 `System.Net.HttpListener`、`System.Net.HttpListenerContext` 及其他一些类,以创建一个允许接受 WebSocket 握手请求的 HTTP 服务器。
因此,websocket-sharp 提供了 `WebSocketSharp.Server.HttpServer` 类。
您可以使用 `HttpServer.AddWebSocketService (string)` 或 `HttpServer.AddWebSocketService (string, Action)` 方法,通过指定的行为和服务的路径,将任何 WebSocket 服务添加到您的 `HttpServer` 中。
```
var httpsv = new HttpServer (4649);
httpsv.AddWebSocketService ("/Echo");
httpsv.AddWebSocketService ("/Chat");
httpsv.AddWebSocketService ("/ChatWithNyan", s => s.Suffix = " Nyan!");
```
了解更多信息,请查看 **[Example3]**?
### WebSocket 扩展
#### 逐消息压缩
websocket-sharp 支持 [Per-message Compression][rfc7692] 扩展(但不支持带有 [context take over] 的扩展)。
作为 WebSocket 客户端,如果您想启用此扩展,您应该在调用连接方法之前,将 `WebSocket.Compression` 属性设置为某种压缩方法。
```
ws.Compression = CompressionMethod.Deflate;
```
然后,客户端将在发送给服务器的握手请求中包含以下标头。
```
Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover
```
如果服务器支持此扩展,它将返回包含相应值的相同标头。
因此,最终当客户端在握手响应中接收到该标头时,此扩展将可用。
#### 忽略扩展
作为 WebSocket 服务器,如果您想忽略客户端请求的扩展,您应该在您的 `WebSocketBehavior` 构造函数或其初始化过程中将 `WebSocketBehavior.IgnoreExtensions` 属性设置为 `true`,如下所示。
```
wssv.AddWebSocketService (
"/Chat",
s => s.IgnoreExtensions = true // To ignore the extensions requested from a client.
);
```
如果设置为 `true`,服务将不会在其握手响应中返回 Sec-WebSocket-Extensions 标头。
我认为当您在连接服务器时遇到错误,并希望排除扩展作为错误原因时,这会很有用。
### 安全连接
websocket-sharp 支持基于 **SSL/TLS** 的安全连接。
作为 WebSocket 客户端,您应该使用带有 **wss** 方案的 WebSocket URL 来创建 `WebSocket` 类的新实例。
```
var ws = new WebSocket ("wss://example.com");
```
如果您想对服务器证书设置自定义验证,您应该将 `WebSocket.SslConfiguration.ServerCertificateValidationCallback` 属性设置为此验证对应的回调。
```
ws.SslConfiguration.ServerCertificateValidationCallback =
(sender, certificate, chain, sslPolicyErrors) => {
// Do something to validate the server certificate.
...
return true; // If the server certificate is valid.
};
```
默认的回调始终返回 `true`。
作为 WebSocket 服务器,您应该创建一个带有安全连接相关设置的 `WebSocketServer` 或 `HttpServer` 类的新实例,如下所示。
```
var wssv = new WebSocketServer (5963, true);
wssv.SslConfiguration.ServerCertificate = new X509Certificate2 (
"/path/to/cert.pfx", "password for cert.pfx"
);
```
### HTTP 身份验证
websocket-sharp 支持 [HTTP Authentication (Basic/Digest)][rfc2617]。
作为 WebSocket 客户端,您应该在调用连接方法之前,使用 `WebSocket.SetCredentials (string, string, bool)` 方法为 HTTP 身份验证设置一对用户名和密码。
```
ws.SetCredentials ("nobita", "password", preAuth);
```
如果 `preAuth` 为 `true`,客户端将在第一次握手请求中向服务器发送用于 Basic 身份验证的凭据。
否则,它将在第二次握手请求中向服务器发送用于 Basic 或 Digest(由第一次握手请求的未授权响应决定)身份验证的凭据。
作为 WebSocket 服务器,您应该在调用启动方法之前,设置 HTTP 身份验证方案、领域(realm)以及任何用于查找用户凭据的函数,如下所示。
```
wssv.AuthenticationSchemes = AuthenticationSchemes.Basic;
wssv.Realm = "WebSocket Test";
wssv.UserCredentialsFinder = id => {
var name = id.Name;
// Return user name, password, and roles.
return name == "nobita"
? new NetworkCredential (name, "password", "gunfighter")
: null; // If the user credentials are not found.
};
```
如果您想提供 Digest 身份验证,您应该进行如下设置。
```
wssv.AuthenticationSchemes = AuthenticationSchemes.Digest;
```
### 查询字符串、Origin 标头、Cookies 和用户标头
#### 查询字符串
作为 WebSocket 客户端,如果您想在握手请求中发送查询字符串,您应该使用包含 [Query] string 参数的 WebSocket URL 来创建 `WebSocket` 类的新实例。
```
var ws = new WebSocket ("ws://example.com/?name=nobita");
```
作为 WebSocket 服务器,如果您想获取握手请求中包含的查询字符串,您应该访问 `WebSocketBehavior.QueryString` 属性,如下所示。
```
public class Chat : WebSocketBehavior
{
private string _name;
...
protected override void OnOpen ()
{
_name = QueryString["name"];
}
...
}
```
#### Origin 标头
作为 WebSocket 客户端,如果您想在握手请求中发送 Origin 标头,您应该在调用连接方法之前,将 `WebSocket.Origin` 属性设置为作为 [Origin] 标头的允许值。
```
ws.Origin = "http://example.com";
```
作为 WebSocket 服务器,如果您想验证 Origin 标头,您应该使用您的 `WebSocketBehavior` 为其设置验证逻辑,例如,使用 `WebSocketServer.AddWebSocketService (string, Action)` 方法并进行初始化,如下所示。
```
wssv.AddWebSocketService (
"/Chat",
s => {
s.OriginValidator =
val => {
// Check the value of the Origin header, and return true if valid.
Uri origin;
return !val.IsNullOrEmpty ()
&& Uri.TryCreate (val, UriKind.Absolute, out origin)
&& origin.Host == "example.com";
};
}
);
```
#### Cookies
作为 WebSocket 客户端,如果您想在握手请求中发送 Cookies,您应该在调用连接方法之前,使用 `WebSocket.SetCookie (WebSocketSharp.Net.Cookie)` 方法设置任意 cookie。
```
ws.SetCookie (new Cookie ("name", "nobita"));
```
作为 WebSocket 服务器,如果您想对 Cookies 作出响应,您应该使用您的 `WebSocketBehavior` 为其设置响应操作,例如,使用 `WebSocketServer.AddWebSocketService (string, Action)` 方法并进行初始化,如下所示。
```
wssv.AddWebSocketService (
"/Chat",
s => {
s.CookiesResponder =
(reqCookies, resCookies) => {
foreach (var cookie in reqCookies) {
cookie.Expired = true;
resCookies.Add (cookie);
}
};
}
);
```
#### 用户标头
作为 WebSocket 客户端,如果您想在握手请求中发送用户标头,您应该在调用连接方法之前,使用 `WebSocket.SetUserHeader (string, string)` 方法设置任意用户定义的标头。
```
ws.SetUserHeader ("RequestForID", "ID");
```
如果您想获取握手响应中包含的用户标头,您应该在握手完成后访问 `WebSocket.HandshakeResponseHeaders` 属性。
```
var id = ws.HandshakeResponseHeaders["ID"];
```
作为 WebSocket 服务器,如果您想对用户标头作出响应,您应该使用您的 `WebSocketBehavior` 为其设置响应操作,例如,使用 `WebSocketServer.AddWebSocketService (string, Action)` 方法并进行初始化,所示。
```
wssv.AddWebSocketService (
"/Chat",
s => {
s.UserHeadersResponder =
(reqHeaders, userHeaders) => {
var val = reqHeaders["RequestForID"];
if (!val.IsNullOrEmpty ())
userHeaders[val] = s.ID;
};
}
);
```
### 通过 HTTP 代理服务器连接
websocket-sharp 支持通过 HTTP 代理服务器进行连接。
如果您想通过 HTTP 代理服务器连接到 WebSocket 服务器,您应该在调用连接方法之前,使用 `WebSocket.SetProxy (string, string, string)` 方法设置代理服务器 URL,并在必要时设置用于代理服务器身份验证的一对用户名和密码。
```
var ws = new WebSocket ("ws://example.com");
ws.SetProxy ("http://localhost:3128", "nobita", "password");
```
我已经使用 **[Squid]** 测试了此功能。必须在 **squid.conf**(例如 `/etc/squid/squid.conf`)中禁用以下选项。
```
# 拒绝连接到除 SSL 端口以外的其他端口
#http_access deny CONNECT !SSL_ports
```
### 日志记录
`WebSocket` 类拥有自己的日志记录功能。
您可以将其与 `WebSocket.Log` 属性(返回一个 `WebSocketSharp.Logger`)一起使用。
因此,如果您想更改当前的日志记录级别(默认为 `WebSocketSharp.LogLevel.Error`),您应该将 `WebSocket.Log.Level` 属性设置为 `LogLevel` 枚举值中的任意一个。
```
ws.Log.Level = LogLevel.Debug;
```
上面的代码表示低于 `LogLevel.Debug` 的日志不会被输出。
如果您想输出日志,您应该使用任意输出方法。以下代码输出一条级别为 `LogLevel.Debug` 的日志。
```
ws.Log.Debug ("This is a debug message.");
```
`WebSocketServer` 和 `HttpServer` 类具有相同的日志记录功能。
## 示例
使用 websocket-sharp 的示例。
### 示例
[Example] 连接到由 [Example2] 或 [Example3] 执行的服务器。
### Example2
[Example2] 启动一个 WebSocket 服务器。
### Example3
[Example3] 启动一个允许接受 WebSocket 握手请求的 HTTP 服务器。
在 Example3 运行期间,您是否愿意使用您的 Web 浏览器访问 [http://localhost:4649](http://localhost:4649) 来进行 **WebSocket Echo Test**?
## 支持的 WebSocket 规范
websocket-sharp 支持 **RFC 6455**,它基于以下参考资料:
- [The WebSocket Protocol][rfc6455]
- [The WebSocket API][api]
- [Compression Extensions for WebSocket][rfc7692]
## 许可证
websocket-sharp 基于 [The MIT License] 提供。
标签:WebSocket, 依赖分析, 内核驱动, 开发库, 网络协议, 网络通信