欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

服务器开发- Asp.Net Core中的websocket,并封装一个简单的中间件

程序员文章站 2022-07-07 11:50:19
先拉开msdn的文档,大致读一遍 (https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/websockets)websocket是一个协...

先拉开msdn的文档,大致读一遍 (https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/websockets)

 websocket 是一个协议,支持通过 tcp 连接建立持久的双向信道。 它可用于聊天、股票报价和游戏等应用程序,以及 web 应用程序中需要实时功能的任何情景。

使用方法

  • 安装 microsoft.aspnetcore.websockets 包。
  • 配置中间件。
  • 接受 websocket 请求。
  • 发送和接收消息。

如果是创建的asp.net core项目,默认会有一个all的包,里面默认带了websocket的包。所以,添加的时候,注意看一下

然后就是配置websocket的中间件

app.usewebsockets();

如果需要更细致的配置websocket,msdn文档上也提供了一种配置缓冲区大小和ping的option

var websocketoptions = new websocketoptions()
{
    keepaliveinterval = timespan.fromseconds(120),  //向客户端发送“ping”帧的频率,以确保代理保持连接处于打开状态
    receivebuffersize = 4 * 1024   //用于接收数据的缓冲区的大小。 只有高级用户才需要对其进行更改,以便根据数据大小调整性能。
};
app.usewebsockets(websocketoptions);

接受 websocket 请求

在请求生命周期后期(例如在 configure 方法或 mvc 操作的后期),检查它是否是 websocket 请求并接受 websocket 请求。

该示例来自 configure 方法的后期。

app.use(async (context, next) =>
{
    if (context.request.path == "/ws")
    {
        if (context.websockets.iswebsocketrequest)
        {
            websocket websocket = await context.websockets.acceptwebsocketasync();
            await echo(context, websocket);
        }
        else
        {
            context.response.statuscode = 400;
        }
    }
    else
    {
        await next();
    }

});

websocket 请求可以来自任何 url,但此示例代码只接受 /ws 的请求

(比如要测试websocket的连接,地址必须写上:ws://ip:端口/ws) 最后这个路径的ws是可以自己定义的,可以理解为mvc的路由,或者url地址,websocket第一次连接的时候,可以使用url传递参数

发送和接收消息

acceptwebsocketasync 方法将 tcp 连接升级到 websocket 连接,并提供 websocket 对象。 使用 websocket 对象发送和接收消息。

之前显示的接受 websocket 请求的代码将 websocket 对象传递给 echo 方法;此处为 echo 方法。 代码接收消息并立即发回相同的消息。 一直在循环中执行此操作,直到客户端关闭连接

private async task echo(httpcontext context, websocket websocket)
{
    var buffer = new byte[1024 * 4];
    websocketreceiveresult result = await websocket.receiveasync(new arraysegment<byte>(buffer), cancellationtoken.none);
    while (!result.closestatus.hasvalue)
    {
        await websocket.sendasync(new arraysegment<byte>(buffer, 0, result.count), result.messagetype, result.endofmessage, cancellationtoken.none);

        result = await websocket.receiveasync(new arraysegment<byte>(buffer), cancellationtoken.none);
    }
    await websocket.closeasync(result.closestatus.value, result.closestatusdescription, cancellationtoken.none);
}

如果在开始此循环之前接受 websocket,中间件管道会结束。 关闭套接字后,管道展开。 也就是说,如果接受 websocket ,请求会在管道中停止前进,就像点击 mvc 操作一样。 但是完成此循环并关闭套接字时,请求将在管道中后退。

如果要测试是否连上,那么可以自己写ws的客户端程序,当然也可以使用一些现成的工具辣

服务器开发- Asp.Net Core中的websocket,并封装一个简单的中间件

封装一个简单的中间件:

什么是中间件?msdn对此的解释是:

中间件是一种装配到应用程序管道以处理请求和响应的软件。 每个组件:

  • 选择是否将请求传递到管道中的下一个组件。
  • 可在调用管道中的下一个组件前后执行工作。

请求委托用于生成请求管道。 请求委托处理每个 http 请求。

使用 runmap 和 use 扩展方法来配置请求委托。 可将一个单独的请求委托并行指定为匿名方法(称为并行中间件),或在可重用的类中对其进行定义。 这些可重用的类和并行匿名方法即为中间件或中间件组件。 请求管道中的每个中间件组件负责调用管道中的下一个组件,或在适当情况下使链发生短路。

新建一个websocketextensions.cs的类

 public static class websocketextensions
    {
        public static iapplicationbuilder mapwebsocketmanager(this iapplicationbuilder app,pathstring path,websockethandler handler)
        {
            return app.map(path, (_app) => _app.usemiddleware<websocketmanagermiddleware>(handler));
        }
        public static iservicecollection addwebsocketmanager(this iservicecollection services)
        {
            services.addtransient<websocketconnectionmanager>();

            foreach (var type in assembly.getentryassembly().exportedtypes)
            {
                if (type.gettypeinfo().basetype == typeof(websockethandler))
                {
                    services.addsingleton(type);
                }
            }

            return services;
        }
    }

addwebsocketmanager这个方法主要是处理依赖注入的问题。通过反射把实现websockethandler的类,统统注入

管理websocket连接

public class websocketconnectionmanager
    {
        private concurrentdictionary<string, websocket> _sockets = new concurrentdictionary<string, websocket>();

        public int getcount()
        {
            return _sockets.count;
        }

        public websocket getsocketbyid(string id)
        {
            return _sockets.firstordefault(p => p.key == id).value;
        }

        public concurrentdictionary<string, websocket> getall()
        {
            return _sockets;
        }
        public websocket getwebsocket(string key)
        {
            websocket _socket;
            _sockets.trygetvalue(key, out _socket);
            return _socket;

        }

        public string getid(websocket socket)
        {
            return _sockets.firstordefault(p => p.value == socket).key;
        }
        public void addsocket(websocket socket,string key)
        {
            if (getwebsocket(key)!=null)
            {
                _sockets.tryremove(key, out websocket destorywebsocket);
            }
            _sockets.tryadd(key, socket);
            //string sid = createconnectionid();
            //while (!_sockets.tryadd(sid, socket))
            //{
            //    sid = createconnectionid();
            //}



        }

        public async task removesocket(string id)
        {
            try
            {
                websocket socket;

                _sockets.tryremove(id, out socket);


                await socket.closeoutputasync(websocketclosestatus.normalclosure, null, cancellationtoken.none);


            }
            catch (exception)
            {

            }

        }

        public async task closesocket(websocket socket)
        {
            await socket.closeoutputasync(websocketclosestatus.normalclosure, null, cancellationtoken.none);
        }

        private string createconnectionid()
        {
            return guid.newguid().tostring();
        }
    }

 

这里我把客户的连接的管理都封装到一个连接类里面,我的思路是

  • 我使用webapi来验证身份,走http协议的接口
  • 验证成功后,服务器给客户端返回一个token
  • 客户端通过websocket连接服务器的时候,需要带上上次返回的token,这样我就可以在连接字典里面判断出重复的socket(因为我试过在.net core里面如果多次连接,服务器不会走断开的事件,而是不断的出现多个socket对象)

websocketmanagermiddleware类的封装

public class websocketmanagermiddleware
    {
        private readonly requestdelegate _next;
        private websockethandler _websockethandler { get; set; }

        public websocketmanagermiddleware(requestdelegate next,
                                          websockethandler websockethandler)
        {
            _next = next;
            _websockethandler = websockethandler;
        }

        public async task invoke(httpcontext context)
        {
            if (!context.websockets.iswebsocketrequest)
                return;

            var socket = await context.websockets.acceptwebsocketasync();
            string key = context.request.query["key"];
            console.writeline("连接人:"+key);

            _websockethandler.onconnected(socket,key);

            await receive(socket, async (result, buffer) =>
            {
                if (result.messagetype == websocketmessagetype.text)
                {
                    await _websockethandler.receiveasync(socket, result, buffer);
                    return;
                }

                else if (result.messagetype == websocketmessagetype.close)
                {
                    await _websockethandler.ondisconnected(socket);
                    return;
                }

            });

            //todo - investigate the kestrel exception thrown when this is the last middleware
            //await _next.invoke(context);
        }

        private async task receive(websocket socket, action<websocketreceiveresult, byte[]> handlemessage)
        {
            try
            {
                var buffer = new byte[1024 * 4];

                while (socket.state == websocketstate.open)
                {
                    var result = await socket.receiveasync(buffer: new arraysegment<byte>(buffer),
                                                           cancellationtoken: cancellationtoken.none);

                    handlemessage(result, buffer);
                }
            }
            catch (exception ex)
            {
                gslog.e(ex.stacktrace);
            }

        }
    }

 

 invoke的时候,传递的key参数需要客户端验证身份后,传递进来:(ws://ip:端口/ws?key=xxx) 这样的格式来连接websocket

在这个类里面,我们主要处理三个事情

  • 如果客户端连接进来,那么我们把这个连接放到连接管理字典里面
  • 如果客户端断开连接,那么我们把这个连接冲连接管理字典里面移除
  • 如果是发送数据过来,那么我们就调用receiveasync方法,并把连接对象和数据传递进去

websockethandler类的封装

这个类主要关联游戏逻辑模块和websocket的一个纽带,我们封装的中间件,通过websockethandler把数据传递给实现这个类的子类

 public abstract class websockethandler
    {
        public websocketconnectionmanager websocketconnectionmanager { get; set; }

        public websockethandler(websocketconnectionmanager websocketconnectionmanager)
        {
            websocketconnectionmanager = websocketconnectionmanager;
        }

        public virtual void onconnected(websocket socket, string key)
        {
            //var serversocket = websocketconnectionmanager.getwebsocket(key);
            //if (serversocket != null)
            //{
            //    websocketconnectionmanager.addsocket();
            //    console.writeline("已经存在当前的连接,断开。。");
            //}
            websocketconnectionmanager.addsocket(socket, key);
        }

        public virtual async task ondisconnected(websocket socket)
        {
            console.writeline("socket 断开了");
            await websocketconnectionmanager.removesocket(websocketconnectionmanager.getid(socket));
        }

        public async task sendmessageasync(websocket socket, string message)
        {
            if (socket.state != websocketstate.open)
                return;
            var bytes = encoding.utf8.getbytes(message);
            await socket.sendasync(buffer: new arraysegment<byte>(array: bytes, offset: 0, count: bytes.length), messagetype: websocketmessagetype.text, endofmessage: true, cancellationtoken: cancellationtoken.none);
        }

        public async task sendmessageasync(string socketid, string message)
        {
            try
            {
                await sendmessageasync(websocketconnectionmanager.getsocketbyid(socketid), message);
            }
            catch (exception)
            {

            }

        }

        public async task sendmessagetoallasync(string message)
        {
            foreach (var pair in websocketconnectionmanager.getall())
            {
                if (pair.value.state == websocketstate.open)
                    await sendmessageasync(pair.value, message);
            }
        }
        /// <summary>
        /// 获取一些连接
        /// </summary>
        /// <param name="keys"></param>
        /// <returns></returns>
        public ienumerable<websocket> getsomewebsocket(string[] keys)
        {
            foreach (var key in keys)
            {
                yield return websocketconnectionmanager.getwebsocket(key);
            }
        }

        /// <summary>
        /// 给一堆人发消息
        /// </summary>
        /// <param name="websockets"></param>
        /// <param name="message"></param>
        /// <returns></returns>
        public async task sendmessagetosome(websocket[] websockets, string message)
        {
            websockets.tolist().foreach(async a => { await sendmessageasync(a, message); });
        }

        public abstract task receiveasync(websocket socket, websocketreceiveresult result, byte[] buffer);
    }

需要把一些必须要重写的方法定义为abstract  给子类重写

使用我们写好的websocket管理中间件

  public void configureservices(iservicecollection services)
        {
            services.addwebsocketmanager();
        }
 var websocketoptions = new websocketoptions()
            {
                keepaliveinterval = timespan.fromseconds(20),
                receivebuffersize = 4 * 1024
            };
            app.usewebsockets(websocketoptions);
            app.mapwebsocketmanager("/zhajinhua", serviceprovider.getservice<zjhgame>());

zjhgame这个类,必须实现 websockethandler,这样我们就能在zjhgame这个类,处理游戏逻辑

好了,大致就是这样的,毕竟我也没有开发游戏的经验,有错误的地方,希望大佬们能指出