服务器开发- Asp.Net Core中的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的客户端程序,当然也可以使用一些现成的工具辣
封装一个简单的中间件:
什么是中间件?msdn对此的解释是:
中间件是一种装配到应用程序管道以处理请求和响应的软件。 每个组件:
- 选择是否将请求传递到管道中的下一个组件。
- 可在调用管道中的下一个组件前后执行工作。
请求委托用于生成请求管道。 请求委托处理每个 http 请求。
使用 run、map 和 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这个类,处理游戏逻辑
好了,大致就是这样的,毕竟我也没有开发游戏的经验,有错误的地方,希望大佬们能指出