zoukankan      html  css  js  c++  java
  • RUDP之二 —— Sending and Receiving Packets

    原文链接

    原文:http://gafferongames.com/networking-for-game-programmers/sending-and-receiving-packets/

    Sending and Receiving Packets

    介绍

    大家好,我是Glenn Fiedler,欢迎阅读我的网上电子书《游戏程序的网络设计》第二章。

    在前一章我们讨论了在电脑之间发送数据的选择,并且决定用UDP而不用TCP。我们选择UDP以便我们的数据能够准时到达而不必等待数据包重发。

    现在我将给你展示怎么使用UDP来发送和接收数据。

    BSD sockets

           对于大多数现代平台,你有某种基本的基于BSD套接字的套接字层可用。

           BSD套接字使用如下函数操作比如“socket”,“bind”, “sendto” 和“recvfrom”。你当然可以直接使用这些函数,但这样会使你的代码保持平台独立性较困难,因为在每个平台这些都会有稍许差别。

       所以尽管我首先会给你展示BSD套接字的例子来展示基本的套接字的用法,我们将不会长久地直接使用BSD套接字。反之,我们将封装所有的基本套接字功能,我们会把它们抽象到一个类里面,让你更简单地写出平台独立的套接字代码。

    Platform specifics

           首先,让我们设置一个宏让我们发现我们当前的平台,以便我们在平台间做出细微的改变:

               // platform detection
     
        #define PLATFORM_WINDOWS  1
        #define PLATFORM_MAC      2
        #define PLATFORM_UNIX     3
     
        #if defined(_WIN32)
        #define PLATFORM PLATFORM_WINDOWS
        #elif defined(__APPLE__)
        #define PLATFORM PLATFORM_MAC
        #else
        #define PLATFORM PLATFORM_UNIX
        #endif

    现在让我们引入需要的套接字头文件。因为这些头文件也是平台特殊的,我们将使用平台#define来引入不同平台不同的头文件

    #if PLATFORM == PLATFORM_WINDOWS
     
            #include <winsock2.h>
     
        #elif PLATFORM == PLATFORM_MAC || PLATFORM == PLATFORM_UNIX
     
            #include <sys/socket.h>
            #include <netinet/in.h>
            #include <fcntl.h>
     
        #endif

    套接字构建在基于unix的系统库的标准平台,所以我们不得不作一些额外的连接。然而,在Windows系统上我们需要链接到winsock的library得到套接字的功能。

    这里有一个简单的技巧来做到这一点,并且不需要更改您的项目或makefile

        #if PLATFORM == PLATFORM_WINDOWS
        #pragma comment( lib, "wsock32.lib" )
        #endif

    我喜欢这么使用,因为我超级懒,当然你总是可以在你的工程或者makefile中进行链接,如果你喜欢的话。

    Initializing the socket layer

    大部分类unix平台(包括macosx)不需要任何特定的步骤来初始化这个套接字层,然而Windows如果你跳过这一步,你的套接字则不能正常工作。你必须调用“WSAStartup”函数来初始化你的套接字层在你使用任何套接字前,并使用“WSACleanup”来关闭。

    让我们来添加新函数

         inline bool InitializeSockets()
        {
            #if PLATFORM == PLATFORM_WINDOWS
            WSADATA WsaData;
            return WSAStartup( MAKEWORD(2,2), &WsaData ) == NO_ERROR;
            #else
            return true;
            #endif
        }
     
        inline void ShutdownSockets()
        {
            #if PLATFORM == PLATFORM_WINDOWS
            WSACleanup();
            #endif
        }

    现在我们有一个平台独立的方式来初始化套接字层。如果平台不要求初始化套接字,那么这个函数就不会做任何事。

    Creating a socket

    现在是创建UDP套接字,这里是如何做:

        int handle = socket( AF_INET, SOCK_DGRAM, IPPROTO_UDP );
        if ( handle <= 0 )
        {
            printf( "failed to create socket
    " );
            return false;
        }

    接下来绑定套接字到一个端口号(比如30000)。每个套接字绑定到一个不同的端口号上,因为一个包到达的端口号决定哪个套接字来发送它。不要使用低于1024的端口号,因为这些是保留给系统使用的。

    特别注意的是,如果你不关心你的套接字绑定到哪个端口上,你可以使用“0”作为你的端口,系统会自动给你分配一个空闲的端口号。

        sockaddr_in address;
        address.sin_family = AF_INET;
        address.sin_addr.s_addr = INADDR_ANY;
        address.sin_port = htons( (unsigned short) port );
     
        if ( bind( handle, (const sockaddr*) &address, sizeof(sockaddr_in) ) < 0 )
        {
            printf( "failed to bind socket
    " );
            return false;
        }

    现在套接字已经准备好发送和接收数据了。

    但是为什么会在代码前神秘地调用“htons”?这是一个帮助函数将一个16bit的整型转换为从主机字节序(小或高位优先)转换为网络字节序(高位优先)。这是必须地要求每当你直接设置套接字整数类型的结构成员。

    你会看到htons和它的32位整数大小的近亲函数htonl多次使用在本文中, 所以留意,你会知道是怎么回事。

    Setting the socket as non-blocking

    默认设置套接字设置为阻塞模式。这就意味着如果你使用”recvfrom”不读取套接字,这个函数将不会返回直到有可用的数据包。这并不适合我们的目的。视频游戏是实时程序,模拟在30或60帧每秒,他们不能只是呆在那里,等待一个数据包到达。

    这个解决方案能在你创建套接字后,使你的套接字变为非阻塞模式。一旦这么做了,”recvfrom’函数立即返回即使没有可用的数据包,返回值会告诉你稍后再读取数据包。

    这里是如何将一个套接字转为非阻塞模式。

            #if PLATFORM == PLATFORM_MAC || PLATFORM == PLATFORM_UNIX
            int nonBlocking = 1;
            if ( fcntl( handle, F_SETFL, O_NONBLOCK, nonBlocking ) == -1 )
            {
                printf( "failed to set non-blocking socket
    " );
                return false;
            }
            #elif PLATFORM == PLATFORM_WINDOWS
            DWORD nonBlocking = 1;
            if ( ioctlsocket( handle, FIONBIO, &nonBlocking ) != 0 )
            {
                printf( "failed to set non-blocking socket
    " );
                return false;
            }
            #endif

    当你看到上面这些,windows没有提供”fcntl”函数,所以我们使用”ioctlsocket”函数来代替。

    Sendingpackets

    UDP是一个无连接模式的传输协议,所以每次发包前你必须指定目的地址。你可以使用一个UDP套接字来发送数据包到任意数量的不同的IP地址。在UDP的另一端并没有一台计算机你正在连接。

    这里是如何发送数据包到指定的地址:

    int sent_bytes = sendto( handle, (const char*)packet_data, packet_size,
                                 0, (sockaddr*)&address, sizeof(sockaddr_in) );
     
        if ( sent_bytes != packet_size )
        {
            printf( "failed to send packet: return value = %d
    ", sent_bytes );
            return false;
        }

    注意!“sendto”函数的返回值表明本机数据包是否发送成功。但它并没有告诉你这个数据包是否被目的计算机接收。UDP没有任何方式知道这个数据包是否到达了目的地。

    在上面的代码中,我们传入了一个参数“sockaddr_in”的结构作为目的地址。我们如何来设置这个结构体呢?

    比如我们打算发送数据到207.45.186.98:30000

    以如下形式开始我们的地址

        unsigned int a = 207;
        unsigned int b = 45;
        unsigned int c = 186;
        unsigned int d = 98;
        unsigned short port = 30000;

    我们还有一些工作要做,完成sendto的形式要求。

    unsigned int destination_address = ( a << 24 ) | ( b << 16 ) | ( c << 8 ) | d;
        unsigned short destination_port = port;
     
        sockaddr_in address;
        address.sin_family = AF_INET;
        address.sin_addr.s_addr = htonl( destination_address );
        address.sin_port = htons( destination_port );

    正如你所看见,我们结合a,b,c,d(范围是0,255)值到一个单独的整型,每个字节的整数都是相应的输入值。我们接下来用整型地址和端口号初始化“sockaddr_in”结构,确保我们的地址与端口号通过使用“htonl”和“htons”函数由主机字节序转为网络字节序。

    特例:如果你想向自己发送数据包,并不需要查询你的本机IP,就用回环地址127.0.0.1,数据包就会送到你的本机上。

    Receivingpackets

    一旦你端口上已经绑定了一个UDP套接字,任何发到你IP和端口号的UDP数据包就会放在队列中。接收数据包只是循环调用 “recvfrom”,直到它失败表明没有更多的包留在队列。

    因为UDP是无连接传输模式,数据包可以来自从任意数量的不同的电脑。每次你调用“recvfrom”接收数据包,都会得到发送者的IP地址和端口号,所以你可以知道这个数据包来自哪里。

       这里是怎么循环接收所有入站的数据包

         while ( true )
        {
            unsigned char packet_data[256];
            unsigned int maximum_packet_size = sizeof( packet_data );
     
            #if PLATFORM == PLATFORM_WINDOWS
            typedef int socklen_t;
            #endif
     
            sockaddr_in from;
            socklen_t fromLength = sizeof( from );
     
            int received_bytes = recvfrom( socket, (char*)packet_data, maximum_packet_size,
                                       0, (sockaddr*)&from, &fromLength );
     
            if ( received_bytes <= 0 )
                break;
     
            unsigned int from_address = ntohl( from.sin_addr.s_addr );
            unsigned int from_port = ntohs( from.sin_port );
     
            // process received packet
        }

    如果在队列中的数据包大于你的接收缓冲区就会被悄悄丢掉。所以如果你有256个字节的缓冲区来接收数据包像上面的代码所示,但有人发给你300字节的包,这个300字节的包就会被丢掉。你不可能收到300字节的前256个字节。

    因为是你自己编写你自己的游戏网络协议,在实际工作中,这没有任何问题,只是要确保你的接收缓冲足够大,超过你代码中最大的发送数据包。

    Destroying a socket

    在大多数类似UNIX的平台, 是文件句柄,所以你可以使用标准的“close”函数来关闭套接字,一旦你停止使用它们。然而,Windows平台下有点不同,所以我们用“closesocket”函数来代替。

        #if PLATFORM == PLATFORM_MAC || PLATFORM == PLATFORM_UNIX
        close( socket );
        #elif PLATFORM == PLATFORM_WINDOWS
        closesocket( socket );
        #endif

    Socket class

    所以我们已经实现了所有的基本操作:创建一个套接字,绑定到端口,设置为非阻塞模式,发送和接收数据,销毁套接字。

    但是你已经注意到大多数这些操作都因为平台不同,而有稍微差别。当你每次完成套接字的某些操作时你需要记住使用#ifdef 并指定特定的平台,这很麻烦。

    我们可以通过包装我们的套接字函数到类内来解决这个问题。我们还可以添加一个”Address”类来简单地指定网络地址。这样我们在每次收发数据时,可以避免手动的编码和解码“sockaddr_in”结构。

    这里是我们的套接字类:

    class Socket
        {
        public:
     
            Socket();
            ~Socket();
            bool Open( unsigned short port );
            void Close();
            bool IsOpen() const;
            bool Send( const Address & destination, const void * data, int size );
            int Receive( Address & sender, void * data, int size );
     
        private:
     
            int handle;
        };

    这里是地址类:

        class Address
        {
        public:
     
            Address();
            Address( unsigned char a, unsigned char b, unsigned char c, unsigned char d, unsigned short port );
            Address( unsigned int address, unsigned short port );
            unsigned int GetAddress() const;
            unsigned char GetA() const;
            unsigned char GetB() const;
            unsigned char GetC() const;
            unsigned char GetD() const;
            unsigned short GetPort() const;
            bool operator == ( const Address & other ) const;
            bool operator != ( const Address & other ) const;
     
        private:
     
            unsigned int address;
            unsigned short port;
        };

    这里是你如何使用这些类来收发数据:

    // create socket
     
        const int port = 30000;
        Socket socket;
        if ( !socket.Open( port ) )
        {
            printf( "failed to create socket!
    " );
            return false;
        }
     
        // send a packet
     
        const char data[] = "hello world!";
        socket.Send( Address(127,0,0,1,port), data, sizeof( data ) );
     
        // receive packets
     
        while ( true )
        {
            Address sender;
            unsigned char buffer[256];
            int bytes_read = socket.Receive( sender, buffer, sizeof( buffer ) );
            if ( !bytes_read )
                break;
            // process packet
        }

    正如你看到的一样,这比直接使用BSD套接字简单多了。另好的是,这段代码几乎可用在所有的平台上,因为所有平台细节的处理都包含在你的socket和 address 类中。

    Conclusion

    现在我们有平台独立的方式来收发UDP数据包。

    UDP是无连接传模式,我想创建一个事例程序来证明这点。所以,我写了个简单的例子,它从文本文件中读取IP地址,然后每秒发送一个数据包到这些地址。每次这个程序收到一个数据包,就会打印出来这个包来自哪里,并打印出包的大小。

    你可以很容易的设置它,这样在本地机器上你就有大量的节点发送数据包到对方,传递不同的端口号码到应用程序的多个实例,像这样:

         > Node 30000
        > Node 30001
        > Node 30002
        etc...

    然后每个节点将尝试发送数据包到对方节点,它像一个迷你点对点的设置

    我在MacOSX开发了这个程序,但是你应该能够在任何类unix系统或Windows很容易编译,所以让我知道你是否有任何修改来兼容不同主机。一旦你尝试稍稍修改事例程序,那么将会有更有趣的事发生。在下一章中,我将展示给你怎么基于UDP协议建立一个虚拟连接,加入和超时退出。

  • 相关阅读:
    调用外部 DLL 中的函数(显示调用)
    模式窗体与非模式窗体
    使用PChar和string类型时的内存分配技术
    保密卡程序的编写
    Dll 使用 PChar 参数的小例子
    delphi动态创建组件的颜色
    Dll 模式窗口与非模式窗口
    调用外部 DLL 中的函数(隐式调用)
    内核读写只读内存方法总结[Delphi描述][转帖]
    delphi资源文件制作及使用详解
  • 原文地址:https://www.cnblogs.com/weizhxa/p/5728238.html
Copyright © 2011-2022 走看看