前文我们实现了WebSocket消息的读取和发送:C#实现WebSocket服务器:(02)消息帧分析和代码实现
但是我们所有的逻辑都是写在OnWebSocket方法的,并不方便扩展,现在我们封装一个Messager
抽象类,封装消息的读取和发送。
Messager类封装
我们将在类中暴露一些必要的方法。
下面我们用消息
这个词,而不是帧
,因为一条消息可能是由多个帧(例如Text帧+N个Continuation帧)组成的。
方法 | 说明 |
---|---|
Accept | 在OnWebSocket方法中,调用具体Messager的Accept方法,开始接收客户端的消息 |
OnConnected | 客户端新连接 |
OnDisconnected | 客户端断开连接 |
OnNewFrame | 服务器收到新帧,主要用于调试了展示用,尽量不要在这里处理业务逻辑 |
OnText | 服务器收到文本消息 |
OnBinary | 服务器收到二进制消息 |
Close | 关闭WebSocket,服务端会向客户端发送Close帧,服务端收到客户端的Close响应后,真正关闭连接 |
Send | 发送消息给客户端,实现了多个重载,用来发送不同类型的消息 |
Ping | 发送Ping帧给客户端,客户端会响应一个Pong帧给服务器 |
其他方法和帧处理我们都不暴露出来了,程序内部会自动处理。 | |
例如:服务器收到Ping帧会自动响应Pong帧给客户端、Close帧也会自动处理。 | |
Github上有具体的代码实现:https://github.com/sometiny/http/blob/main/src/WebSocket/Messager.cs | |
我们就不拷贝过来了,占篇幅。 |
Messager类的使用
Messager类为抽象类,不能直接实例化,需要我们实现自己的的Messager类,继承Messager抽象类即可。
下面实现了一个Messager用来作测试用。
这里也可以找到源码:https://github.com/sometiny/http/blob/main/demo/WebSocketMessager.cs
/// <summary>
/// 实现一个Messager类
/// </summary>
public class MyMessager : Messager
{
private EndPoint _remoteEndPoint = null;
public MyMessager(Stream stream) : base(stream) {
//获取客户端的连接信息
if(stream is BufferedNetworkStream networkStream)
{
_remoteEndPoint = networkStream.BaseSocket.RemoteEndPoint ;
}
}
/// <summary>
/// 客户端新连接
/// </summary>
protected override void OnConnected()
{
Console.WriteLine($"{DateTime.Now:HH:mm:ss} > 新客户端连接:{_remoteEndPoint}");
}
/// <summary>
/// 客户端断开连接
/// </summary>
protected override void OnDisconnected()
{
Console.WriteLine($"{DateTime.Now:HH:mm:ss} > 连接断开:{_remoteEndPoint}");
}
/// <summary>
/// 接收到新的帧,仅展示下
/// </summary>
/// <param name="frame"></param>
protected override void OnNewFrame(Frame frame)
{
var color = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.DarkYellow;
Console.WriteLine($"{DateTime.Now:HH:mm:ss} > 接收到新帧;帧类型:{frame.OpCode},结束帧:{frame.Fin},携带掩码:{frame.Mask},长度:{frame.PayloadLength}");
Console.ForegroundColor = color;
}
/// <summary>
/// 收到Text消息时的实现
/// 里面定义两个特使的消息:close和ping,用来测试服务器主动发送Close和Ping帧。
/// </summary>
/// <param name="payload">完整消息</param>
protected override void OnText(string payload)
{
Console.WriteLine($"{DateTime.Now:HH:mm:ss} > 文本数据:{payload}");
if(payload == "close")
{
Send($"服务器接收到close指令,关闭连接。");
Close();
return;
}
if (payload == "ping")
{
Send($"服务器接收到ping指令,发送ping。");
Ping();
return;
}
Send($"服务器接收到文本数据:{payload}");
}
/// <summary>
/// 接收到二进制消息
/// 二进制消息是通过流来读取的,不像Text,直接一股脑读取全部消息。
/// </summary>
/// <param name="inputStream">输入流</param>
protected override void OnBinary(Stream inputStream)
{
//为了测试,我们把二进制消息读取到字节数组。
byte[] payload = StreamUtils.ReadAllBytes(inputStream);
Console.WriteLine($"{DateTime.Now:HH:mm:ss} > 二进制数据,长度:{payload.Length}");
Send($"服务器接收到二进制数据,长度:{payload.Length}");
}
}
测试
我们使用上面实现的MyMessager
进行测试。
实例化一个服务器,使用MyMessager
来接收和发送消息。
public class HttpServer : HttpServerBase
{
public HttpServer() : base()
{
//设置根目录
WebRoot = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "web"));
}
protected override void OnWebSocket(HttpRequest request, Stream stream)
{
//使用自己实现的Messager类处理业务。
//可以根据请求的Url不同去实例化不同的Messager,处理不同的业务逻辑。
new MyMessager(stream).Accept();
}
}
启动服务器,浏览器访问:http://127.0.0.1:4189/websocket.html
1、连接服务器后,分别发送:hello world!
和ping

控制台输出的黄色部分,可以看到浏览器发送给服务器的各种帧。
我们在测试代码里面对内容为ping
的文本消息进行了特殊处理,服务器收到这个消息后会主动给客户端发送Ping帧,然后客户端回复一个Pong帧。
2、我们再向服务器发送close
文本。
可以看到,服务器接收到close文本后,向客户端发送了Close帧,浏览器回复给服务器一个Close帧。
这就是Close帧的逻辑:一方发送Close帧后,对方需要回复一个Close帧。
服务器收到回复的Close帧后,就关闭连接了,不会再响应任何数据给客户端。
3、连接服务器后,直接点击关闭
按钮。
这时候,浏览器主动发送Close帧给服务器。
服务器收到Close帧后,向浏览器回复了一个Close帧,我们在Messager里面对这个帧设置了状态码和原因。
所以浏览器在红色部分显示了我们设置的信息。
总结
目前为止,从握手,到帧分析,到逻辑封装都已经完成。
握手和帧的分析和读取是关键。
消息处理的封装只是为了方便业务逻辑的实现,业务层只要关心必要的接口即可,必要但不是必须的。
是不是可以着手实现一个聊天室了?