MMORPG游戏核心技术-网络通信(发送篇)(二)
序言:
上一篇文章介绍了自定网络消息的接收篇,需要了解的朋友可以通过下面地址了解:
http://gad.qq.com/article/detail/7160380
关于网络通信,这里需要说一点题外话,这也是最近跟同行讨论比较多的问题,关于同步的问题。
1、关于网络出现卡顿的同步问题,当客户端网络出现卡顿,之后恢复正常同时接受大量服务器消息时候的处理。建议:将客户端的表现一个状态一个状态的追赶过去,别直接跳过状态,负责客户端会出现各种表现的异常,比如:穿越障碍,空中飞行等状态。当接收到的消息量巨大的时候,瞬间产生的命令太多。也可以对部分命令进行过滤。
2、到底是以客户端为准,还是以服务器为准。建议:根据项目组的需要,进行一个合理的取舍,如果项目组需要更好的客户端表现,以客户端为准;如果项目组需要根精确的计算,以服务器为准。当然,就算以客户端为准,也需要对客户端进行合理效验,负责外挂就太疯狂了。
3、最需要的还是进行大量测试。然后保证通信的稳定。一套稳定的架构是游戏的基础。
正文:
这一章主要给大家介绍自定网络通信的发送篇,网络消息发送,就是将客户端需要发送的消息发送到服务器,然后等待服务器处理完之后返回处理结果。在整个发送篇中
函数介绍
Socket类:Socket 类为网络通信提供了一套丰富的方法和属性。
BeginSend方法:socket类提供的异步发送消息的方法。
SendedEnd方法:socket类提供的异步发送消息完之后回调的方法。
Stream类:Stream 是所有流的抽象基类。 流是字节序列的抽象,例如文件、输入/输出设备、进程中通信管道或 TCP/IP 套接字。
整套类架构
流程
NetRunTime类
Start函数:实例化socket定义。定义NetAsynSend最大的发送消息的长度(4096);
1 2 3 4 5 6 7 8 9 10 11 12 | public void Start() { if (!GameSet.m_bUseWebSocket) { m_socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); m_nSend = new NetAsynSend(m_socketClient, 4096); //接收的可能多一些 m_nRece = new NetAsynRecevice(m_socketClient); m_nSend.Start(); m_nRece.Start(); } } |
SendMessage函数:外部接口调用,发送消息的时候统一掉这个函数。将需要发送的消息保存到m_listSend中。
1 2 3 4 5 6 7 8 | public void SendMessage(NetMessage msg) { if (!IsConnected()) { return ; } m_listSend.Add(msg); } |
DisPatchServerSendMsg函数:将需要发送消息的m_listSend发送给NetAsynSend进行处理。
1 2 3 4 5 6 7 8 9 10 11 12 | private void DisPatchServerSendMsg( float fTime, float fDTime) { if (m_nSend.SendMessage( ref m_listSend)) { m_listSend.Clear(); m_lastSendMessage = 0; } else { Debug.Log( "消息发送失败!" ); } } |
NetAsynSend类
SendMessage函数:将NetRunTime中的消息列表缓存到NetAsynSend中,如果再发送状态,将等到发送完成之后再继续发送。
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 | public bool SendMessage(ref BetterList listMsg) { if (listMsg. Count == 0) { return false; } var e = listMsg.GetEnumerator(); while (e.MoveNext()) { m_listMsg.Add(e.Current); } if (!GameSet.m_bUseWebSocket) { if (m_bSending) { //下帧在发送 return false; } } //有消息才需要发送 if (m_listMsg. Count > 0) { SendToSever(); } return true; } |
SendToSever函数:将发送的状态设置为true,调用SocketOutputStream中将NetMessage消息转换了二进制,开始发送消息!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | private void SendToSever() { m_bSending = true ; if (NetRunTime.Inst.IsConnected()) { //如果还没有建立连接试着建立连接 int iMsgNums = m_outPutStream.MakeBuffer(m_listMsg); if (iMsgNums > 0) { if (!GameSet.m_bUseWebSocket) { if (m_socket != null ) { m_socket.BeginSend(m_outPutStream.m_steam.GetBuffer(), 0, m_outPutStream.m_steam.GetOffset(), SocketFlags.None, new AsyncCallback(SendedEnd), null ); } } else { m_webSocket.Send(m_outPutStream.m_steam.GetBuffer(), m_outPutStream.m_steam.GetOffset()); m_outPutStream.m_steam.Reset(); } } } } |
SocketOutputStream类
MakeBuffer函数:将NetMessage消息转换到消息发送存储区m_stream中。这里需要注意的是如果消息的发送长度超过了4096。将对消息进行保留,放置到下一帧发送。
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 35 36 37 38 39 40 41 42 | public int MakeBuffer(BetterList lstMsg) { int iFreeBuffer = ( int )m_m_BufferLen; int i = 0; for (; i < lstMsg.Count; i++) { //计算发送NetMessage消息的长度 lstMsg[i].MakeMessage(); //这一帧发送的消息长度已经达到最大,下一帧再发送 if (iFreeBuffer - lstMsg[i].m_uMsgLenght <= 0) { //TODO:以后扩展为可以支持任意大小的缓冲 //Debug.LogWarning("消息缓冲区不够了。消息" + m_listDelegates[i].m_uID.ToString() + "隔帧发送"); } else { //获取流的偏移位置 int nOffset = m_steam.GetOffset(); //NetMessage消息里面对数据转换为Btye int nMsgLen = lstMsg[i].ToByte( ref m_steam); //判断消息的偏移数据是否与消息的实际长度一致 int nWriteLen = m_steam.GetOffset() - nOffset; if (nWriteLen != nMsgLen) { Debug.LogError( string .Format( "{0}消息时 消息长度为{1} 实际发送的消息长度却是{2}" , (NetMessageDefine)lstMsg[i].m_uID, nMsgLen, nWriteLen)); } iFreeBuffer -= nMsgLen; } } if (i < lstMsg.Count) { for ( int j = 0; j < i ; j++) { lstMsg.RemoveAt(0); } } else { lstMsg.Clear(); } return i; } |
总结
到止,自定义消息的架构基本介绍完毕。整个架构用到了5个类:NetRunTime(消息运行类)、NetAsynReveice(异步消息接收类)、NetAsynSend(异步消息发送类)、SocketInputStream(异步接收消息存储区)、SocketOutputStream(异步发送消息存储区)。对应每个类的描述都有在文章中做了详细的描述。存在疑问的伙伴,欢迎留言进行讨论。