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\n" );
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\n" );
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\n" );
return false;
}
#elif PLATFORM == PLATFORM_WINDOWS
DWORD nonBlocking = 1;
if ( ioctlsocket( handle, FIONBIO, &nonBlocking ) != 0 )
{
printf( "failed to set non-blocking socket\n" );
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\n", 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!\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和 address 类中。
Conclusion
现在我们有平台独立的方式来收发UDP数据包。
UDP是无连接传模式,我想创建一个事例程序来证明这点。所以,我写了个简单的例子,它从文本文件中读取IP地址,然后每秒发送一个数据包到这些地址。每次这个程序收到一个数据包,就会打印出来这个包来自哪里,并打印出包的大小。
你可以很容易的设置它,这样在本地机器上你就有大量的节点发送数据包到对方,传递不同的端口号码到应用程序的多个实例,像这样:
> Node 30000
> Node 30001
> Node 30002
etc...
然后每个节点将尝试发送数据包到对方节点,它像一个迷你点对点的设置
我在MacOSX开发了这个程序,但是你应该能够在任何类unix系统或Windows很容易编译,所以让我知道你是否有任何修改来兼容不同主机。一旦你尝试稍稍修改事例程序,那么将会有更有趣的事发生。在下一章中,我将展示给你怎么基于UDP协议建立一个虚拟连接,加入和超时退出。
RUDP之二 —— Sending and Receiving Packets的更多相关文章
- Monitoring and Tuning the Linux Networking Stack: Receiving Data
http://blog.packagecloud.io/eng/2016/06/22/monitoring-tuning-linux-networking-stack-receiving-data/ ...
- Tinyos学习笔记(二)
1.TinyOS communication tools java serialApp -comm serial@/dev/ttyUSB0:telosb java net.tinyos.tools.L ...
- Socket programming in C on Linux | tutorial
TCP/IP socket programming This is a quick guide/tutorial to learning socket programming in C languag ...
- the Linux Kernel: Traffic Control, Shaping and QoS
−Table of Contents Journey to the Center of the Linux Kernel: Traffic Control, Shaping and QoS 1 Int ...
- Python socket – network programming tutorial
原文:https://www.binarytides.com/python-socket-programming-tutorial/ --------------------------------- ...
- PatentTips - Highly-available OSPF routing protocol
BACKGROUND OF THE INVENTION FIG. 1A is a simplified block diagram schematically representing a typic ...
- python scapy的使用总结
基本命令 ls() List all available protocols and protocol options lsc() List all available scapy command f ...
- WCF学习系列二---【WCF Interview Questions – Part 2 翻译系列】
http://www.topwcftutorials.net/2012/09/wcf-faqs-part2.html WCF Interview Questions – Part 2 This WCF ...
- 树莓派与Arduino Leonardo使用NRF24L01无线模块通信之基于RF24库 (二) 发送自定义数据
在我的项目里,树莓派主要作为中心节点,用于接收数据,Arduino作为子节点,用于发送数据,考虑到以后会有很多子节点,但又不至于使得代码过于繁琐,因此所有的传输数据添加一个头部编号用于区分不同节点. ...
随机推荐
- hdu2929 Bigger Is Better
题意 给出n根木棍,要你拼一个最大的数,并且这个数是m的倍数. 题解 显然越长的数越大.设\(dp[i][j]\)表示用i根木棍并且\(mod m = j\)的最大长度. 我们很容易想出dp方程,再用 ...
- elastichq 离线安装
plugin install file:///home/hadoop/xxxx.zip 奇怪的是,这样安装成功后访问host:port:9200/_plugin/hq/ 仍然会报错,找不到一些js函数 ...
- java支持跨平台获取cpuid、主板id、硬盘id、mac地址 (兼容windows、Linux)
windows: package cn.net.comsys.helper.system.info; import java.io.BufferedReader; import java.io.F ...
- Win7旗舰版-X86-X64-快速装机版
装机版作品简介 Win7 32/64位旗舰版 6.5z 专注于Win7,致力于做更好用的系统!一如既往的品质,不流氓,不欺骗,不夸大!一直在改进,只为做得更好!万千用户的信赖,作者的品质保证! 作品摘 ...
- JVM内存模型、指令重排、内存屏障概念解析
在高并发模型中,无是面对物理机SMP系统模型,还是面对像JVM的虚拟机多线程并发内存模型,指令重排(编译器.运行时)和内存屏障都是非常重要的概念,因此,搞清楚这些概念和原理很重要.否则,你很难搞清楚哪 ...
- 忍不住记录下小型的CMDB系统
- shell的一些应用场景
列出每个IP的连接数 netstat -n | awk '/^tcp/{print $5}' | awk -F: '!/^::/{print $1}' | sort | uniq -c | sort ...
- 作为一名前端er,从武汉来到深圳三个月有感
来到深圳已经三个月了,从最开始的担心自己的能力不够怕不能够在深圳这个互联网产品及其发达的城市立足下来,到现在已经慢慢地拾起了一丁点的信心了 (虽然还有很多知识是不够的.但是相当于之前我的,我是觉得我已 ...
- javascript数据结构与算法--链表
链表与数组的区别? 1. 定义: 数组又叫做顺序表,顺序表是在内存中开辟一段连续的空间来存储数据,数组可以处理一组数据类型相同的数据,但不允许动态定义数组的大小,即在使用数组之前必须确定数组的大小. ...
- openjdk 完全编译指南
从openjdk.java.net下载openjdk的软件包,你就获得了所有相关的源码. 强烈建议首先仔细看懂 README-builds.html 指南. 在执行 make all 之前,首先要 执 ...