前言:早上在一个群里看到一个小伙子的ID叫netty,就想到了dotnetty,于是就顺便想起写个dotnetty的入门文章好了。由于dotnetty不维护了,所以后面也提供了一个原生的开发方式(.NET CORE 3.1以及以上版本支持)
趁着台风要放假没啥玩的,就开始动手写一个吧!以下正文:
一、Dotnetty的方式(Dotnetty项目微软已经不维护了,但是还可以用)
1.1 创建一个服务端和一个客户端
1.2 在服务端,新增一个帧数处理类WebSocketFrameHandler,用来处理客户端请求和数据解析使用
// WebSocket 帧处理器类
public class WebSocketFrameHandler : SimpleChannelInboundHandler<WebSocketFrame>
{
// 用于存储所有连接的客户端
private static readonly ConcurrentDictionary<IChannel, bool> Clients = new ConcurrentDictionary<IChannel, bool>();
// 当接收到 WebSocket 帧时的处理方法
protected override void ChannelRead0(IChannelHandlerContext ctx, WebSocketFrame msg)
{
try
{
// 如果是文本帧,则输出并广播
if (msg is TextWebSocketFrame textFrame)
{
Console.WriteLine($"接收到文本: {textFrame.Text()}");
Broadcast(textFrame.Text());
}
else if (msg is BinaryWebSocketFrame binaryFrame)
{
// 处理二进制帧(此处未实现具体逻辑)
}
// ... 其他帧类型的处理逻辑
}
catch (Exception ex)
{
// 输出异常信息
Console.WriteLine($"异常信息: {ex.Message}");
}
}
// 当有新的客户端连接时的处理方法
public override void ChannelActive(IChannelHandlerContext ctx)
{
// 将新连接的客户端添加到 Clients 集合中
Clients.TryAdd(ctx.Channel, true);
base.ChannelActive(ctx);
}
// 当客户端断开连接时的处理方法
public override void ChannelInactive(IChannelHandlerContext ctx)
{
// 从 Clients 集合中移除断开的客户端
bool removed;
Clients.TryRemove(ctx.Channel, out removed);
base.ChannelInactive(ctx);
}
// 广播消息到所有连接的客户端
public static void Broadcast(string message)
{
var frame = new TextWebSocketFrame(message);
foreach (var client in Clients.Keys)
{
var duplicateFrame = frame.RetainedDuplicate();
client.WriteAndFlushAsync(duplicateFrame).ContinueWith(t =>
{
if (t.IsFaulted)
{
// 输出发送失败的异常信息
Console.WriteLine(t.Exception?.Message);
}
// 释放帧资源
duplicateFrame.Release();
});
}
}
}
1.3 新建一个WebSocket 服务器类,用来启动和关闭服务端
// WebSocket 服务器类
public class WebSocketServer
{
// 异步运行 WebSocket 服务器的方法
public async Task RunServerAsync()
{
// 输出 WebSocket 服务开启的信息
Console.WriteLine("WebSocket 服务已开启...");
// 创建 bossGroup 和 workerGroup,用于处理网络事件
var bossGroup = new MultithreadEventLoopGroup(1);
var workerGroup = new MultithreadEventLoopGroup();
try
{
// 初始化服务器引导程序
var bootstrap = new ServerBootstrap();
bootstrap.Group(bossGroup, workerGroup)
.Channel<TcpServerSocketChannel>()
// 设置子通道的处理器
.ChildHandler(new ActionChannelInitializer<ISocketChannel>(channel =>
{
var pipeline = channel.Pipeline;
// 添加处理器到 pipeline
pipeline.AddLast(new HttpServerCodec());
pipeline.AddLast(new HttpObjectAggregator(65536));
pipeline.AddLast(new WebSocketServerProtocolHandler("/websocket"));
pipeline.AddLast(new WebSocketFrameHandler());
}));
// 绑定服务器到指定端口并开始监听
var channel = await bootstrap.BindAsync(18080);
Console.WriteLine("服务开始监听端口 18080...");
await channel.CloseCompletion;
}
finally
{
// 关闭 bossGroup 和 workerGroup
await bossGroup.ShutdownGracefullyAsync();
await workerGroup.ShutdownGracefullyAsync();
}
}
}
1.4 启动项里面进行启动
static async Task Main(string[] args)
{
// 创建一个 WebSocket 服务器实例
var server = new WebSocketServer();
// 运行 WebSocket 服务器
await server.RunServerAsync();
// 等待用户输入,以保持程序运行
Console.ReadLine();
}
1.5 客户端也新增一个WebSocket 帧处理器类
// WebSocket 帧处理器类
public class WebSocketFrameHandler : SimpleChannelInboundHandler<WebSocketFrame>
{
// 当接收到 WebSocket 帧时的处理方法
protected override void ChannelRead0(IChannelHandlerContext ctx, WebSocketFrame msg)
{
try
{
// 如果是文本帧,则输出接收到的信息
if (msg is TextWebSocketFrame textFrame)
{
Console.WriteLine($"接收到信息: {textFrame.Text()}");
}
// ... 其他帧类型的处理逻辑
}
catch (Exception ex)
{
// 输出异常信息
Console.WriteLine($"异常信息: {ex.Message}");
}
}
// 当触发用户事件时的处理方法
public override void UserEventTriggered(IChannelHandlerContext context, object evt)
{
base.UserEventTriggered(context, evt);
// 判断 WebSocket 握手事件
if (evt is WebSocketClientProtocolHandler.ClientHandshakeStateEvent handshakeStateEvent)
{
if (handshakeStateEvent == WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HandshakeIssued)
{
// 握手请求已发出
Console.WriteLine("客户端已发出握手请求");
}
else if (handshakeStateEvent == WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HandshakeComplete)
{
// 握手请求完成
Console.WriteLine("客户端握手请求完成");
// 发送一条消息到服务器
var frame = new TextWebSocketFrame($"我是客户端{DateTime.Now.Ticks}");
context.Channel.WriteAndFlushAsync(frame);
}
}
}
}
1.6 WebSocket 客户端类,用来连接服务端和收发消息
// WebSocket 客户端类
public class WebSocketClient
{
// 异步运行 WebSocket 客户端的方法
public async Task RunClientAsync()
{
// 输出客户端启动的信息
Console.WriteLine("客户端启动...");
// 创建事件循环组,用于处理网络事件
var group = new MultithreadEventLoopGroup();
try
{
// 初始化客户端引导程序
var bootstrap = new Bootstrap();
bootstrap.Group(group)
.Channel<TcpSocketChannel>()
// 设置通道的处理器
.Handler(new ActionChannelInitializer<ISocketChannel>(channel =>
{
var pipeline = channel.Pipeline;
// 添加处理器到 pipeline
pipeline.AddLast(new HttpClientCodec());
pipeline.AddLast(new HttpObjectAggregator(8192));
pipeline.AddLast(new WebSocketClientProtocolHandler(new Uri("ws://localhost:18080/websocket"), WebSocketVersion.V13, null, false, new DefaultHttpHeaders(), 8192));
pipeline.AddLast(new WebSocketFrameHandler());
}));
// 连接到服务器
var channel = await bootstrap.ConnectAsync(new IPEndPoint(IPAddress.Loopback, 18080));
Console.WriteLine("连接服务端...");
// 等待通道关闭
await channel.CloseCompletion;
}
finally
{
// 优雅地关闭事件循环组
await group.ShutdownGracefullyAsync();
}
}
}
1.7 启动项进行启动
static async Task Main(string[] args)
{
// 创建一个 WebSocket 客户端实例
var client = new WebSocketClient();
// 运行 WebSocket 客户端
await client.RunClientAsync();
Console.ReadLine();
}
1.8 服务端启动项里面加个死循环,一直给客户端发消息,并且不等待:
Task.Run(() =>
{
int i = 0;
while (true)
{
i++;
WebSocketFrameHandler.Broadcast(i.ToString());
Thread.Sleep(500);
}
});
1.9 启动服务端,再启动客户端,查看效果:
二、原生方式
2.1 创建两个控制台项目,不引用任何包,保持干爽,才叫原生
2.2 服务端所有代码如下,包括代码注释。新建一个简单的 WebSocket 服务器示例,它可以接收和响应客户端消息,并定期向所有连接的客户端发送服务器的当前时间
// 用于存储所有连接的客户端
private static ConcurrentDictionary<string, WebSocket> clients = new ConcurrentDictionary<string, WebSocket>();
static async Task Main(string[] args)
{
// 创建了一个 HttpListener 实例,设置它监听 http://localhost:18091/ 地址,并启动
var httpListener = new HttpListener();
httpListener.Prefixes.Add("http://localhost:18091/");
httpListener.Start();
Console.WriteLine("WebSocket服务启动地址:ws://localhost:18091/");
// 启动一个新的任务来推送消息,该任务会定期向所有连接的客户端发送消息
_ = PushMessagesToClients();
while (true)
{
var context = await httpListener.GetContextAsync();
if (context.Request.IsWebSocketRequest)
{
var webSocketContext = await context.AcceptWebSocketAsync(null);
var webSocket = webSocketContext.WebSocket;
var clientId = Guid.NewGuid().ToString();
// 将新的客户端添加到集合中
clients.TryAdd(clientId, webSocket);
var buffer = new byte[1024];
while (webSocket.State == WebSocketState.Open)
{
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
Console.WriteLine($"来自客户端 {clientId} 的消息: {message}");
// 根据客户端发送的消息来决定响应的内容
var response = Encoding.UTF8.GetBytes("服务端还活着");
await webSocket.SendAsync(new ArraySegment<byte>(response), WebSocketMessageType.Text, true, CancellationToken.None);
if (message == "100")
{
// 从集合中移除需要断开连接的客户端
clients.TryRemove(clientId, out _);
}
}
}
}
}
// 持续地向所有连接的客户端推送消息
private static async Task PushMessagesToClients()
{
while (true)
{
foreach (var client in clients)
{
if (client.Value.State == WebSocketState.Open)
{
var message = Encoding.UTF8.GetBytes($"服务器时间: {DateTime.Now}");
await client.Value.SendAsync(new ArraySegment<byte>(message), WebSocketMessageType.Text, true, CancellationToken.None);
}
}
await Task.Delay(1000);
}
}
2.3 创建一个简单的 WebSocket 客户端程序。该客户端会连接到指定的 WebSocket 服务器,并定期向服务器发送递增的数字消息。同时,它也会接收并打印来自服务器的任何消息。当完成所有操作后,客户端会关闭 WebSocket 连接。
static async Task Main(string[] args)
{
// 创建一个新的 WebSocket 客户端实例
using var client = new ClientWebSocket();
// 连接到指定的 WebSocket 服务器
await client.ConnectAsync(new Uri("ws://localhost:18091/"), CancellationToken.None);
// 启动一个新的任务,该任务会定期向服务器发送消息
_ = SendMessageToServer(client);
// 定义一个接收缓冲区
var receiveBuffer = new byte[1024];
// 当客户端的状态为打开时,持续接收来自服务器的消息
while (client.State == WebSocketState.Open)
{
var result = await client.ReceiveAsync(new ArraySegment<byte>(receiveBuffer), CancellationToken.None);
var receivedMessage = Encoding.UTF8.GetString(receiveBuffer, 0, result.Count);
// 打印接收到的消息
Console.WriteLine($"客户端接收到消息: {receivedMessage}");
}
// 当完成所有操作后,关闭 WebSocket 连接
await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
}
// 定时发送不同的消息给服务端的方法
private static async Task SendMessageToServer(ClientWebSocket client)
{
int i = 0;
int index = 0;
// 当客户端的状态为打开时,持续发送消息到服务器
while (client.State == WebSocketState.Open)
{
var message = i++.ToString();
var sendBuffer = Encoding.UTF8.GetBytes(message);
// 发送消息到服务器
await client.SendAsync(new ArraySegment<byte>(sendBuffer), WebSocketMessageType.Text, true, CancellationToken.None);
// 打印发送的消息
Console.WriteLine($"客户端发送指令: {message}");
index++;
await Task.Delay(5000);
}
}
2.4 启动服务端,并启动客户端,查看效果。可以看到,客户端可以收到服务端推送的消息,服务端也可以收到客户端的信息。
以上就是本文章的全部内容,感谢大佬们围观。如有帮助,欢迎点在转发在看,一键三连~也欢迎关注本公众号:Dotnet Dancer