• 关于 Source Engine 2007 网络通信的分析


    最近在写自己的游戏引擎,主要是参考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[ 4 ];

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

    enum
    {
        NS_CLIENT = 0,    // 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 = 0; 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, 0, &from, &fromlen );
        if ( ret > 0 )
        {
            // convert the data to netpacket_t ...
            // ...
        }
    }

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

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

  • 相关阅读:
    在Html中使用echarts图表
    html+css模拟微信对话
    解决React 的<img >src使用require的方式图片显示不出来,展示的是[object Module]的问题
    easygui入门
    python安装easygui
    关于gcc、make和CMake的区别
    FreeRTOS使用心得。
    C/C++整数输出位不足前补0方法
    AngularJS前端分页 + PageHelper后端分页
    AngularJS常见指令
  • 原文地址:https://www.cnblogs.com/crsky/p/10760229.html
Copyright © 2020-2023  润新知