【.NET】简单实现Websocket客户端和服务端通讯方式(原生开发方式和DotNetty方式)

2024-08-13 18:53:45 浏览数 (3)

前言:早上在一个群里看到一个小伙子的ID叫netty,就想到了dotnetty,于是就顺便想起写个dotnetty的入门文章好了。由于dotnetty不维护了,所以后面也提供了一个原生的开发方式(.NET CORE 3.1以及以上版本支持)

趁着台风要放假没啥玩的,就开始动手写一个吧!以下正文:

一、Dotnetty的方式(Dotnetty项目微软已经不维护了,但是还可以用)

1.1 创建一个服务端和一个客户端

1.2 在服务端,新增一个帧数处理类WebSocketFrameHandler,用来处理客户端请求和数据解析使用

代码语言:javascript复制
 
代码语言:javascript复制
// 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 服务器类,用来启动和关闭服务端

代码语言:javascript复制
// 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();
            }
        }
    }
代码语言:javascript复制

1.4 启动项里面进行启动

代码语言:javascript复制
static async Task Main(string[] args)
        {
            // 创建一个 WebSocket 服务器实例
            var server = new WebSocketServer();
            // 运行 WebSocket 服务器
            await server.RunServerAsync();
            // 等待用户输入,以保持程序运行
            Console.ReadLine();
        }
代码语言:javascript复制

1.5 客户端也新增一个WebSocket 帧处理器类

代码语言:javascript复制
  
代码语言:javascript复制
// 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 客户端类,用来连接服务端和收发消息

代码语言:javascript复制
// 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();
            }
        }
    }
代码语言:javascript复制

1.7 启动项进行启动

代码语言:javascript复制
static async Task Main(string[] args)
        {
            // 创建一个 WebSocket 客户端实例
            var client = new WebSocketClient();
            // 运行 WebSocket 客户端
            await client.RunClientAsync();
            Console.ReadLine();
        }
代码语言:javascript复制

1.8 服务端启动项里面加个死循环,一直给客户端发消息,并且不等待:

代码语言:javascript复制
Task.Run(() =>
            {
                int i = 0;
                while (true)
                {
                    i  ;
                    WebSocketFrameHandler.Broadcast(i.ToString());
                    Thread.Sleep(500);
                }
            });
代码语言:javascript复制

1.9 启动服务端,再启动客户端,查看效果:

二、原生方式

2.1 创建两个控制台项目,不引用任何包,保持干爽,才叫原生

2.2 服务端所有代码如下,包括代码注释。新建一个简单的 WebSocket 服务器示例,它可以接收和响应客户端消息,并定期向所有连接的客户端发送服务器的当前时间

代码语言:javascript复制
 
代码语言:javascript复制
// 用于存储所有连接的客户端
        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 连接。

代码语言:javascript复制
  
代码语言:javascript复制
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);
            }
        }
代码语言:javascript复制

2.4 启动服务端,并启动客户端,查看效果。可以看到,客户端可以收到服务端推送的消息,服务端也可以收到客户端的信息。

以上就是本文章的全部内容,感谢大佬们围观。如有帮助,欢迎点在转发在看,一键三连~也欢迎关注本公众号:Dotnet Dancer

0 人点赞