Sending and Receiving Packets

Sending and Receiving Packets

How to send and receive packets over UDP with BSD sockets

Posted by Glenn Fiedler on Friday,October 3,2008

Introduction

Hi, I’m Glenn Fiedler and welcome to Networking for Game Programmers.

在上一篇文章中,我们讨论了在计算机之间发送数据的可选项,并决定对时间关键型数据使用UDP而不是TCP。

在本文中,我将向您展示如何发送和接收UDP包。

BSD sockets

对于大多数现代平台,您都有一些基于BSD sockets的基本socket层。

BSD套接字使用“socket”、“bind”、“sendto”和“recvFrom”等简单函数进行操作。当然,如果愿意的话,您可以直接使用这些函数,但是很难保持代码与平台独立,因为每个平台略有不同。

因此,尽管我将首先向您展示BSD套接字示例代码来演示基本的套接字用法,但我们不会长时间直接使用BSD套接字。一旦我们介绍了所有基本的套接字功能,我们将把所有内容抽象成一组类,使您能够轻松地编写平台独立的套接字代码。

Platform specifics

首先,让我们设置一个define,这样我们可以检测当前平台是什么,并处理从一个平台到另一个平台的套接字之间的细微差异:

// 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

现在,我们让套接字包含适当的头文件。由于头文件是特定于平台的,因此我们将使用平台定义来基于平台包含不同的文件集:

#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库以获取套接字功能。

以下是一个简单的技巧,可以在不更改项目或生成文件的情况下完成此操作:

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

我喜欢这个把戏,因为我太懒了。如果愿意,您可以随时从项目或makefile链接。

Initializing the socket layer

大多数类Unix平台(包括MacOSX)不需要任何特定的步骤来初始化套接字层,但是Windows要求您进行一些操作以使套接字代码正常工作。

在调用任何套接字函数之前,必须调用“WSAStartup”来初始化套接字层,并在完成后调用“WSACleanup”来关闭。

让我们添加两个新函数:

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

void ShutdownSockets(){
    #if PLATFORM == PLATFORM_WINDOWS
    WSACleanup();
    #endif
}

现在我们有了一种独立于平台的方法来初始化套接字层。

###Creating a socket

是时候来创建socket了:

int handle = socket( AF_INET, 
                     SOCK_DGRAM, 
                     IPPROTO_UDP );

if ( handle <= 0 ){
    printf( "failed to create socket\n" );
    return false;
}

接下来,我们将UDP套接字绑定到一个端口号(例如30000)。每个套接字必须绑定到一个唯一的端口,因为当一个包到达时,端口号决定要传递到哪个套接字。不要使用低于1024的端口,因为它们是为系统保留的。还要尽量避免使用50000以上的端口,因为它们在动态分配端口时使用。

特殊情况:如果您不关心您的套接字绑定到什么端口,只需将“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\n" );
    return false;
}

现在,socket已经做好发送接收数据的准备了。

但是在上面的代码中,这个对“htons”的神秘调用是什么?这只是一个助手函数,它将16位整数值从主机字节顺序(小端或大端)转换为网络字节顺序(大端)。在socket结构中直接设置整数成员时,这是必需的。

在本文中,您将看到“htons”(host to network short)及其32位整数大小的堂兄“htonl”(host to network long),使用了多次,因此请注意你知道发生了什么。

Setting the socket as non-blocking

默认情况下,socket被设置为“阻塞模式”。

这意味着,如果你试图使用“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\n" );
        return false;
    }

#elif PLATFORM == PLATFORM_WINDOWS

    DWORD nonBlocking = 1;
    if ( ioctlsocket( handle, 
                      FIONBIO, 
                      &nonBlocking ) != 0 )
    {
        printf( "failed to set non-blocking\n" );
        return false;
    }

#endif

Windows 没有提供“fcntl”方法,因此我们用“ioctlsocket”代替。

Sending packets

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\n" );
    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 address = ( a << 24 ) | 
                       ( b << 16 ) | 
                       ( c << 8  ) | 
                         d;

sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl( address );
addr.sin_port = htons( port );

如你所见,我们首先将范围[0,255]内的a、b、c、d值组合为一个无符号整数,整数的每个字节现在对应于输入值。然后,我们用整型地址和端口初始化一个“sockaddr-in”结构,使用“htonl”和“htons”将整型地址和端口值从主机字节顺序转换为网络字节顺序。

特殊情况:如果您想给自己发送一个包,不需要查询您自己机器的IP地址,只需传入环回地址127.0.0.1,包就会被发送到本地机器。

Receiving packets

一旦您将一个UDP套接字绑定到一个端口,发送到套接字IP地址和端口的任何UDP数据包都将放置在一个队列中。要接收数据包,只需循环并调用“recvfrom”,直到它失败为止,EWOULDBLOCK指示没有更多的数据包要接收。

由于UDP是无连接的,数据包可能来自任意数量的不同计算机。每次你收到一个包时,“recvfrom”都会告诉你发送者的IP地址和端口,这样你就知道包是从哪里来的。

以下是如何循环和接收所有传入数据包:

while ( true )
{
    unsigned char packet_data[256];

    unsigned int max_packet_size = 
        sizeof( packet_data );

    #if PLATFORM == PLATFORM_WINDOWS
    typedef int socklen_t;
    #endif

    sockaddr_in from;
    socklen_t fromLength = sizeof( from );

    int bytes = recvfrom( socket, 
                          (char*)packet_data, 
                          max_packet_size,
                          0, 
                          (sockaddr*)&from, 
                          &fromLength );

    if ( 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

windows 万岁。

Socket class

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

但您会注意到,这些操作中的大多数都与平台稍有依赖性,每次执行套接字操作时,都必须记住ifdef并执行平台特定操作,这非常烦人。

我们将把所有的套接字功能包装成一个“socket”类来解决这个问题。在这里,我们将添加一个“地址”类,以便更容易地指定Internet地址。这就避免了每次发送或接收数据包时都必须手动编码或解码“sockaddr-in”结构。

让我们来添加socket类:

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;
};

和 address 类:

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;

private:

    unsigned int address;
    unsigned short port;
};

下面是如何用这些类发送和接收数据包:

// create socket

const int port = 30000;

Socket socket;

if ( !socket.Open( port ) )
{
    printf( "failed to create socket!\n" );
    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简单的多。

作为额外的好处,代码在所有平台上都是相同的,因为平台特定的所有内容都是在套接字和地址类中处理的。

Conclusion

现在,你有了一种独立于平台的方式来发送和接收数据包。Enjoy ?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1. Reliable sending algorithm: The reliable sending algorithm ensures that the data transmitted over the network is received by the receiver without any errors or loss. This is achieved by using mechanisms such as sequence numbers, acknowledgment messages, and retransmission of lost packets. The sender sends packets with a unique sequence number, and the receiver sends an acknowledgment message for every received packet. If the sender does not receive an acknowledgment message for a specific packet, it retransmits the packet. 2. Reliable receiving algorithm: The reliable receiving algorithm ensures that the data received by the receiver is correct and in the correct order. This is achieved by using mechanisms such as sequence numbers and acknowledgment messages. The receiver checks the sequence number of each received packet and sends an acknowledgment message for each packet. If a packet is received out of order, the receiver stores it until all the previous packets are received, and then it sends a cumulative acknowledgment message. 3. Flow control algorithm: The flow control algorithm ensures that the sender does not overwhelm the receiver with too much data. This is achieved by using mechanisms such as window size and acknowledgment messages. The receiver sends a window size to the sender, which indicates the maximum number of packets that the sender can send before receiving an acknowledgment message. The sender adjusts its transmission rate based on the window size received from the receiver. 4. Congestion control algorithm: The congestion control algorithm ensures that the network is not overloaded with too much traffic. This is achieved by using mechanisms such as slow start, congestion avoidance, and fast retransmit. In the slow start phase, the sender gradually increases its transmission rate until it reaches the maximum window size. In the congestion avoidance phase, the sender increases its transmission rate more slowly to avoid overloading the network. If the sender receives multiple duplicate acknowledgment messages, it assumes that a packet has been lost, and it performs fast retransmit to retransmit the lost packet immediately.

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值