MMORPG游戏核心技术之网络通信-接收篇(一)
序言
时间如梭,岁月是一把杀猪刀,一晃眼从一个偏偏少年,已经变成了一位标准大叔了。在游戏行业滚摸爬打已经快9年了。之前一直梦想着做一款家喻户晓的游戏。惭愧,一直到现在还没有实现自己的梦想。我还在自己的梦想的路上,与大家共勉之。
不得不说的事,经历了十年的游戏进化史,经典的MMORPG模式已经被改的面目全非,十个开发商九个都在估量着换个美工即得一款新的网游,再也找不回当年玩《传奇》与《奇迹》的一丝感觉,也怀念大学的时候省着钱买月卡玩游戏的日子。悲哀,悲剧!
然而令大家欣慰的是客户端技术日益变化,3D网游逐渐向高画质,高细节,多操作方向发展,玩家们已经能享受更好的视觉体验,更无与伦比的操作。相信在灿烂的2016年,会出现更多好的游戏。
本文以unity引擎为例,阐述大型MMORPG游戏中遇见的核心技术。
正文
网络通信,大家听起来肯定都不陌生,在我认识中很多人使用protobuf作为网络消息协议,网上关于protobuf的介绍很多,这里我不在做进一步描述,当然也有使用skynet和sproto协议。我下面要介绍的是如何使用c#完成自定义的协议。因为unity开发脚本最好的语言就是C#了,至少C#是我见过最优美的语言。本架构使用的是流套接字,协议为tcp协议。(想了解TCP协议、UDP协议、流套接字、数据报套接字、原始套接字的同学可以自行了解)
1、性能好,效率高
2、完全可控
3、纯原生态C#,可读性
无(重新写一套还是需要一点点时间的!)
函数介绍
Socket类:Socket 类为网络通信提供了一套丰富的方法和属性。
BeginReceive方法:socket类提供的异步接送消息的方法。
EndReceive方法:socket类提供的异步接收消息完之后回调的方法。
Stream类:Stream 是所有流的抽象基类。 流是字节序列的抽象,例如文件、输入/输出设备、进程中通信管道或 TCP/IP 套接字。
对,我们需要就是这么几个类还有函数!So easy!
Start函数:实例化Socket。使用的是流套接字,协议使用Tcp长连接。大家可能看见了有一个m_bUseWenSocket字段,这个字段是用来控制是否使用Unity5.0新加的功能WebGl的通信的。关于这一段,之后我会在新的章节进行介绍。需要了解的朋友也可以留言!
1 2 3 4 5 6 7 8 9 10 11 12 | public void Start( bool bClient) { if (!GameSet.m_bUseWebSocket) { m_socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); m_nSend = new NetAsynSend(m_socketClient, m_bClient ? ( uint )4096 : ( uint )65536); //接收的可能多一些 m_nRece = new NetAsynRecevice(m_socketClient); m_nSend.Start(); m_nRece.Start(); } } |
DispatchServerMsg函数:从NetAsynRecevice中获取需要处理的NetMssage消息,并且将消息保存到列表m_listRecevice中。之后回调消息中的Onrecv()方法进行逻辑处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | private void DispatchServerMsg( float fTime, float fDTime) { m_nRece.GetReceviceMsg( ref m_listRecevice); foreach (NetMessage msg in m_listRecevice) { if (msg != null ) { if (!msg.OnRecv()) { Debug.LogWarning( "消息处理失败----" + msg.m_uID.ToString()); } } } m_listRecevice.Clear(); } |
ReceviceMessage函数:调用BeginReceive,这里最大接收的长度为SocketStream.DEFAULTSOCKETMAXBUFFER,也就是8192,为什么是8192?我也给不出最好的理由,就跟服务器为什么在接收线程开CPU*2 + 2一样,我只能在同屏多人,消息量特别大的时候,测试,这个数字的效率是最好的。关于为什么使用BeginRecive,而不使用Recive?异步嘛,不堵塞!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | private void __ReceviceMessage() { //接受数据 if (!GameSet.m_bUseWebSocket) { if (m_socket != null ) { m_socket.BeginReceive(m_packStream.m_bytes, 0, SocketStream.DEFAULTSOCKETMAXBUFFER, SocketFlags.None, new AsyncCallback( this .ReceviceEnd), null ); } } else { int len = m_webSocket.Recv( ref m_packStream.m_bytes); if (len != 0) { //填充消息 m_socketStream.Fill(m_packStream, len); //产生消息 m_socketStream.GenMsg(m_listMsg); } } } |
ReceviceEnd函数:BeginReceive的回调,功能只有二个:一、将m_packStream.m_bytes接收到的数据保存到存储区。二、将存储区的数据解析成NetMessage,并且保留m_listMsg中。这里有个字段m_bRecing开关,再接收的时候设置为true,接收完之后设置为false,以保证只能接受同一消息.
1 2 3 4 5 6 7 8 9 10 | private void ReceviceEnd(IAsyncResult ar) { int num = m_socket.EndReceive(ar); //填充消息 m_socketStream.Fill(m_packStream, num); //产生消息 m_socketStream.GenMsg(m_listMsg); //处理完消息再接受消息 m_bRecing = false ; } |
Fill函数:将_packStream.m_bytes接收到的数据保存到存储区的具体实现。函数中存在一个存储概念。m_Head为存储消息的头位置,m_Tail为存储消息的尾位置,m_BufferLen为整个消息的存储长度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | public bool Fill(SocketStream stream, int recLen) { int nFree = 0; if (m_Head <= m_Tail) { // H T LEN=10 // 0123456789 // abcd...... nFree = m_m_BufferLen - m_Tail; if (nFree >= recLen) { //直接填充数据 m_steam.Seek(m_Tail); //尾部写入数据 m_steam.Write( ref stream.m_bytes, recLen); m_Tail += recLen; } else { if (!ReSize(recLen + 1)) return false ; m_steam.Write( ref stream.m_bytes, recLen); m_Tail += recLen; } } else { //这种清空应该不会出现 Debug.LogError( string .Format( "socket通信出现错误!!{0},{1}" , m_Head, m_Tail)); // T H LEN=10 // 0123456789 // abcd...efg } return true ; }
放松篇 |