最近在写自己的游戏引擎,主要是参考Quake和GoldSrc和SourceEngine2007,其中SourceEngine2007代码比较新一些。

对比了这几个引擎的代码,前两者代码比较简单,基于C代码风格编写的,SourceEngine2007则是基于C++代码风格对前两者的代码进行了一些封装和升级。

下文贴出的代码我会适当删除一些细节以保证良好的可读性。

首先“新建游戏”之后会进入 Host_NewGame 这个函数,这个函数负责启动游戏服务端,里面调用 NET_ListenSocket 开始监听端口。

void Host_NewGame()
{
NET_ListenSocket( NS_SERVER ); // activated server TCP socket
}

看到那行注释我还以为它用TCP,后来发现事情不对劲。

跟进 NET_ListenSocket 看看。

void NET_ListenSocket( int sock )
{
netsocket_t* netsock = &net_sockets[ sock ]; netsock->hUDP = NET_OpenSocket( net_interface, netsock->nPort ); struct sockaddr_in address; NET_StringToSockaddr( net_interface, &address ); address.sin_port = NET_HostToNetShort( netsock->nPort ); bind( netsock->hUDP, &address, sizeof( address ) );
}

首先它从 net_sockets 这个数组里取出了一个 netsocket_t ,这个数组是个全局变量。

// the 4 sockets, Server, Client, HLTV, Matchmaking
static netsocket_t net_sockets[ ];

这说明引擎内部固定使用四个 Socket 进行通信,索引取值如下:

enum
{
NS_CLIENT = , // client socket
NS_SERVER, // server socket
NS_HLTV,
NS_MATCHMAKING,
MAX_SOCKETS
};

回到 NET_ListenSocket 函数,接下来调用 NET_OpenSocket 创建一个 Socket 并将句柄保存到 netsock->hUDP 里。

接着调用 bind 就完成了 UDP 端口的监听。

最后 net_sockets[ NS_SERVER ] 就设置好了。

服务端 Socket 创建好了,怎么接收数据包呢?我们接着看。

void CBaseServer::RunFrame()
{
NET_ProcessSocket( NS_SERVER, this );
}

服务端创建好之后,游戏主循环就会不停地调用 CBaseServer::RunFrame 这个函数。

在 CBaseServer::RunFrame 里,调用 NET_ProcessSocket 这个函数来处理上面创建的服务端Socket。

void NET_ProcessSocket( int sock, IConnectionlessPacketHandler* handler )
{
while ( ( packet = NET_GetPacket ( sock, buffer ) ) != NULL )
{
// check for connectionless packet
if ( packet->header_byte == CONNECTIONLESS_HEADER )
{
handler->ProcessConnectionlessPacket( packet );
continue;
} // check for packets from connected clients
CNetChan* netchan = NET_FindNetChannel( sock, packet->from );
if ( netchan )
{
netchan->ProcessPacket( packet );
}
}
}

可以看到它内部有一个循环,循环调用 NET_GetPacket 读取 Socket 接收到的所有数据包。

  • ConnectionlessPacket

你可以看到这里数据包分成两路处理,第一种是 ConnectionlessPacket ,翻译过来就是 无连接 数据包。

这种数据包是用来接收一些客户端请求的,比方说:PING,请求服务端信息,请求游戏信息,等等…

最最最重要的是,客户端尝试连接到服务端的时候,也是通过这种数据包进行握手(交互)协议的。

接收到这种数据包之后,就会调用 handler->ProcessConnectionlessPacket( packet ) 来把数据包交给 上层的 CBaseServer 来处理。

那个 handler 就是 上面传入 NET_ProcessSocket  的 this。

  • NetChannel

这是另一种处理数据包的方式,主要用来跟已经连接的客户端进行数据交互。

例如把实体数据下发给客户端,或者接收客户端上传的数据。

首先我们关注 NET_FindNetChannel 这个函数。

CNetChan* NET_FindNetChannel( int sock, netadr_t& adr )
{
for ( int i = ; i < s_NetChannels.Count(); i++ )
{
CNetChan* chan = s_NetChannels[i]; // sockets must match
if ( sock != chan->GetSocket() )
continue; // and the IP:Port address
if ( adr.CompareAdr( chan->GetRemoteAddress() ) )
return chan;
} return NULL;
}

它遍历一个 s_NetChannels 数组,寻找一个特定的 NetChannel 并返回

我们看一下这个数组的定义:

static CUtlVector<CNetChan*> s_NetChannels;

可以看到它是个全局变量,也就是说有某处地方创建了 NetChannel 并保存在这个数组里。

但我们优先关注一下 NetChannel 到底是干什么的,以及它保存了什么数据。

class CNetChan
{
public:
void Setup( int sock, netadr_t* adr, char* name, INetChannelHandler* handler )
void ProcessPacket( netpacket_t* packet ); // NS_SERVER or NS_CLIENT index, depending on channel.
int m_Socket;
// Address this channel is talking to.
netadr_t remote_address;
// who registers and processes messages
INetChannelHandler* m_MessageHandler
}

首先它有一个成员 m_Socket 用来标识NetChannel是属于服务端还是客户端的。

然后还有个 remote_address 用来保存远程主机的网络地址。

注意了!NetChannel是服务端客户端都能使用的,所以对于服务端来说remote_address就是那些已连接的客户端的网络地址,

对于客户端来说remote_address就是已连上的服务端的网络地址。

我们再看s_NetChannels,很显然对于服务端来说,里面应该保存着一些用于跟客户端通信的NetChannel。

现在再看回 NET_ProcessSocket 函数里调用 NET_FindNetChannel 的地方,其实就是为了找出这个数据包是来自哪个客户端发来的。

确定了是哪个客户端发来的之后,自然是要处理这个数据包了。

netchan->ProcessPacket( packet );

数据包在 NetChannel 里处理成一种 Message 之后,就会把 Message 交给 handler 来处理。

void CNetChan::ProcessPacket( netpacket_t* packet )
{
m_MessageHandler->PacketStart(); m_MessageHandler->ProcessMessages( &msg ); m_MessageHandler->PacketEnd();
}

现在再回到上上面,我们现在好像还不知道 s_NetChannels 里的那些 NetChannel 是哪里的哪个谁创建的。

于是我找到了一个函数:

bool CBaseServer::ConnectClient( netadr_t& adr, int protocol, char* name, char* password )
{
if ( !CheckInfo() )
{
RejectConnection( "Bad Connection" );
return false;
} // get a free client slot
CBaseClient* client = GetFreeClient();
if ( !client )
{
RejectConnection( "Server Full" );
} // create network channel
INetChannel* netchan = NET_CreateNetChannel( NS_SERVER, adr, client ); // make sure client is reset and clear
client->Connect( name, netchan );
}

当一个客户端尝试连接到服务端的时候,服务端就会调用 CBaseServer::ConnectClient 来处理这个客户端。

如果再看 CBaseServer::ConnectClient 的上一层就是:

bool CBaseServer::ProcessConnectionlessPacket( netpacket_t* packet )
{
bf_read msg = packet->message; char cmd = msg.ReadChar(); switch ( cmd )
{
case C2S_CONNECT:
{
ConnectClient( packet->from, name, password );
}
}
}

所以 CBaseServer::ConnectClient 实际上就是处理客户端连接请求的函数。

我们再仔细看看它,如果客户端通过重重验证,那么服务端就会为客户端安排一个槽位,

如果没位置了,那就Sorry了。

安排好位置之后。就会调用 NET_CreateNetChannel 来给这个客户端创建一个 NetChannel 了。

CNetChan* NET_CreateNetChannel( int sock, netadr_t& address, INetChannelHandler* handler )
{
// create new channel
chan = new CNetChan(); s_NetChannels.AddToTail( chan ); chan->Setup( sock, address, handler ); return chan;
}

这个函数里创建一个 NetChannel 并保存到 s_Channels 里。

注意那个 handler ,就是上面的 CBaseClient*  所以 NetChannel 就会把 Message 交给 CBaseClient 来处理了。

NetChannel 小结:

  • 处理来自 remote_address 的数据包
  • 向 remote_address 发送数据包

事情到这已经差不多结束了。但我喜欢追究细节,所以我们回去看看 NET_GetPacket 是怎么干活的。

netpacket_t* NET_GetPacket( int sock )
{
// then check UDP data
netpacket_t* packet = NET_ReceiveDatagram( sock, &inpacket ); // ... return packet;
}

还有下层

bool NET_ReceiveDatagram ( int sock, netpacket_t* packet )
{
// Each socket has its own netpacket to allow multithreading
netpacket_t& inpacket = net_packets[sock]; int net_socket = net_sockets[sock].hUDP; int ret = recvfrom(net_socket, packet->data, NET_MAX_MESSAGE, , &from, &fromlen );
if ( ret > )
{
// convert the data to netpacket_t ...
// ...
}
}

就是调用 recvfrom 来读取数据了。

发送的部分比较简单,都是调用一两个函数就直接 sento 或者 send 发送到指定地址而已。

关于 Source Engine 2007 网络通信的分析的更多相关文章

  1. 百度贴吧客户端(Android)网络通信行为分析

    百度贴吧安卓客户端网络通信行为分析 本文由CSDN-蚍蜉撼青松[主页:http://blog.csdn.net/howeverpf]原创,转载请注明出处! 一.实验环境与结果概述 1.1 实验环境   ...

  2. 源码分析 ucosii/source 任务源码详细分析

    分析源码: 得先学会读文档, 函数前边的 note :是了解该程序员的思想的途径.不得不重视 代码前边的  Notes,了解思想后,然后在分析代码时看他是如何具体实现的. 1. ucosii/sour ...

  3. Delta3D Open Source Engine

    在看<游戏编程精粹6>时看到了这个引擎,彩插7是这个引擎的Editor,我一看就是Qt写的,我太熟悉Qt的界面了,呵呵.Editor非常强悍!有类似3dmax的4个视图 下载这个看下吧,里 ...

  4. docker容器网络通信原理分析

    概述 自从docker容器出现以来,容器的网络通信就一直是大家关注的焦点,也是生产环境的迫切需求.而容器的网络通信又可以分为两大方面:单主机容器上的相互通信和跨主机的容器相互通信.而本文将分别针对这两 ...

  5. docker容器网络通信原理分析(转)

    概述 自从docker容器出现以来,容器的网络通信就一直是大家关注的焦点,也是生产环境的迫切需求.而容器的网络通信又可以分为两大方面:单主机容器上的相互通信和跨主机的容器相互通信.而本文将分别针对这两 ...

  6. Win7 无法安装Office source engine 足够的权限安装系统服务怎么办

    运行CMD,输入命令:sc delete ose 重试即可.

  7. poj3264 - Balanced Lineup(RMQ_ST)

    Balanced Lineup Time Limit: 5000MS   Memory Limit: 65536K Total Submissions: 45243   Accepted: 21240 ...

  8. POJ_3273_Monthly_Expense_(二分,最小化最大值)

    描述 http://poj.org/problem?id=3273 共n个月,给出每个月的开销.将n个月划分成m个时间段,求m个时间段中开销最大的时间段的最小开销值. Monthly Expense ...

  9. POJ_3280_Cheapest_Palindrome_(动态规划)

    描述 http://poj.org/problem?id=3280 n 种小写字母构成长度为 m 的串,现在要通过增删字母使串回文,给出每种字母增和删的费用,求最小花费. Cheapest Palin ...

随机推荐

  1. 如何用python获取文件中的某一行——python小技巧

    很多人有的时候只需要获取文章中的固定的一行,那么我知道这一行,我需要怎么样去获取呢 可能会有人说读取这一行,如果这一行是已什么开头的就读出来, 其实还有一种办法,就是我知道文件的路径.知道我要取的行数 ...

  2. nginx Location正则表达式

    1. Location正则表达式 1.1. location的作用 location指令的作用是根据用户请求的URI来执行不同的应用,也就是根据用户请求的网站URL进行匹配,匹配成功即进行相关的操作. ...

  3. 树莓派修改VNC分辨率

    1.打开终端输入 sudo raspi-config 2. 选择 Advanced Options  再选择 Resolution 3.选择想要的分辨率,确定 4.重启

  4. 关于python中的tkinter模块

    python2.7和python3.6中的tkinter是两个包,不会自动升级,假如在fedora28做开发的话, 错误:用import Tkinter /import tkinter /import ...

  5. elementUI实现前端分页

    按照他的文档来写分页,最主要的是el-table里面展示的数据怎么处理 <el-table :data="AllCommodityList.slice((currentPage-1)* ...

  6. 2018NOIP爆0记第二弹之day1

    出门进了电梯 白底黑字的告示上只有一句话 善待你一生. 湖上的白天鹅和白鹭远远厮混成一点,抱着玻璃杯里装着的小菊花,又慢悠悠溜达去了实验楼. t1 原本写过原题,结果考场上死去活来也只搞出了个nlog ...

  7. SSO流程

    SSO SSO又名单点登录,用户只需要登录一次就可以访问权限范围内的所有应用子系统.举个简单的例子,你在百度首页登录成功之后,你再访问百度百科.百度知道.百度贴吧等网站也会处于登录状态了,这就是一个单 ...

  8. 新手Python第三天(函数)

    Python 函数的创建 def func2(): print('haha') # 函数的返回值 # 函数的返回值,没有定义返回None, # 有一个返回值返回这个object(可以返回一个函数对象) ...

  9. Cocos2dx源码赏析(3)之事件分发

    Cocos2dx源码赏析(3)之事件分发 这篇,继续从源码的角度赏析下Cocos2dx引擎的另一模块事件分发处理机制.引擎的版本是3.14.同时,也是学习总结的过程,希望通过这种方式来加深对Cocos ...

  10. Go单元测试注意事项及测试单个方法和整个文件的命令

    Go程序开发过程中免不了要对所写的单个业务方法进行单元测试,Go提供了 "testing" 包可以实现单元测试用例的编写,不过想要正确编写单元测试需要注意以下三点: Go文件名必须 ...