Linux下Socket编程之UDP原理

来源:互联网 发布:2017淘宝生意好差 编辑:程序博客网 时间:2024/05/17 23:34

一。设计UDP Server类

人们通常用电话连线来说明TCP协议,而UDP协议,则常常用邮递来做比喻。与TCP有连接的信息传输方式不同,UDP协议被认为是对底层IP协议简单的扩展:协议并不保证每个数据包都会到达目的地,也不保证到达的顺序,而仅仅就是“尽力”的发送每一个数据包。我在这篇教程中有时候使用“数据包”有时候使用“数据报”,广义的说,这两个词意思类似,有代表一个有大小边缘的数据块。但是,用“数据包”的时候,我想强调的是这个数据块中所传送的数据部分;而“数据报”则更强调在数据块中对这段数据的信息和说明部分,比如IP首部,TCP和UDP首部,TCP和UDP报文段这些信息。TCP协议通过同步验证实现了TCP层面上的“数据流”传送,而下层的IP协议,依然是数据报形式的传送,这个我们在前面已经描述过,比如连接握手和断开握手,实际上都是发送的TCP数据报(TCP格式的IP数据报)。UDP格式的IP数据报为IP数据报指定了UDP端口,从而使这样的IP数据报的目的地能够精确到应用程序——没有端口指定的IP数据报目的地只能精确到具有IP地址的主机。另外,与TCP的无边缘保证相反,UDP数据包是有大小的,而其最大限制也即是IP数据包大小的最大限制:65,507字节(这里需要说明两点:1、IP数据包的理论最大值为2^16 - 1,即65,535字节,UDP数据报因为要包含UDP首部的信息,所以比这个值小一点;2、因为MTU的存在,实际传输中的IP数据包会被分封到1500字节以下。)
因为UDP是无连接的,就像一个邮筒,可以接受来自任何人的邮件;也可以发送给任何人的邮件。而每一次接受,都会得到来向的地址;每一次发送,也必须指明去向的地址。我们设计一个类,分别以lastfromSockAddr和destinationSockAddr表示最后一次来向的地址以及(下一次发送的)目的地地址。需要指出的是,因为防火墙的普遍存在,最后一次来向地址变得极其重要!这一点我们将在后面的讨论中看到。

class UDPServerSock: public BaseSock {
private:
    sockaddr_in serverSockAddr;
protected:
    mutable sockaddr_in lastfromSockAddr;
    sockaddr_in destinationSockAddr;
    
char* preBuffer;
    
int preBufferSize;
    mutable 
int preReceivedLength;
public:
    
explicit UDPServerSock(
            unsigned 
short server_port,
            
int pre_buffer_size = 32);
    
virtual ~UDPServerSock();
    
void UDPSetDest(const char* dest_IP,
            
const unsigned short& dest_port);
    
void UDPSetDest(const sockaddr_in& dest_sock_addr);
    
int UDPReceive() const;
    
int UDPSendtoDest(const char* send_data,
            
const int& data_length) const;
};
我们把最后一次来向地址以及预接收缓存中的收到的数据长度设置成mutable是因为我们希望接收UDPReceive()这个方法看起来是不改变对象的。每一次接收,实际上都会刷新lastfromSockAddr,而作为服务器,往往也是通过lastfromSockAddr去决定destinationSockAddr的。
UDPServerSock::UDPServerSock(unsigned short server_port,
                             
int pre_buffer_size):
preBufferSize(pre_buffer_size), preReceivedLength(
0)
{
    preBuffer 
= new char[preBufferSize];
    memset(
&serverSockAddr, 0sizeof(serverSockAddr));
    memset(
&lastfromSockAddr, 0sizeof(lastfromSockAddr));
    memset(
&destinationSockAddr, 0sizeof(destinationSockAddr));

    serverSockAddr.sin_family 
= AF_INET;
    serverSockAddr.sin_addr.s_addr 
= htonl(INADDR_ANY);
    serverSockAddr.sin_port 
= htons(server_port);
    

    sockFD 
= socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
    
if (sockFD < 0) {
        sockClass::error_info(
"sock() failed.");
    }

    
if (bind(    sockFD,
                (sockaddr
*)&serverSockAddr,
                
sizeof(serverSockAddr)) < 0) {
        sockClass::error_info(
"bind() failed.");
    }
}

UDPServerSock::
~UDPServerSock()
{
    delete [] preBuffer;
    close(sockFD);
}
构造函数依然使用socket()建立sockFD,然后通过bind()将本机的SockAddr(主要是指定了端口)绑定到这个sockFD上。
我们重载了UDPSetDest()这个方法,可以有两种方式去指定目标地址destinationSockAddr——既可以指定IP地址和端口,也可以直接赋值以sockaddr_in结构。
void UDPServerSock::UDPSetDest(const char* dest_IP,
                               
const unsigned short& dest_port)
{
    destinationSockAddr.sin_family 
= AF_INET;
    destinationSockAddr.sin_addr.s_addr 
= inet_addr(dest_IP);
    destinationSockAddr.sin_port 
= htons(dest_port);
}

void UDPServerSock::UDPSetDest(const sockaddr_in& dest_sock_addr)
{
    destinationSockAddr.sin_family 
= dest_sock_addr.sin_family;
    destinationSockAddr.sin_addr.s_addr 
= dest_sock_addr.sin_addr.s_addr;
    destinationSockAddr.sin_port 
= dest_sock_addr.sin_port;
}
最后是接收和发送。我们知道TCP里面recv()返回0表示连接正常断开,而UDP里面,则仅仅就是表示收到0字节的数据包。可见,数据大小为0,并不代表数据包为空,因为这个数据包实际也是一个数据报,包含着TCP数据报的各种必要信息。
int UDPServerSock::UDPReceive() const
{
    socklen_t
 from_add_len = sizeof(lastfromSockAddr); //use int in win32
    preReceivedLength 
= recvfrom(    sockFD,
                                    preBuffer,
                                    preBufferSize,
                                    
0,
                                    (sockaddr
*)&lastfromSockAddr,
                                    
&from_add_len);
    
if ( preReceivedLength < 0) {
        sockClass::error_info(
"recv() failed.");
    }

    
return preReceivedLength;
}

int UDPServerSock::UDPSendtoDest(const char* send_data,
                                 
const int& data_length) const
{
    
int send_message_size = sendto(    sockFD,
                                    send_data,
                                    data_length,
                                    
0,
                                    (sockaddr
*)&destinationSockAddr,
                                    
sizeof(destinationSockAddr));
    
if (send_message_size < 0) {
        sockClass::error_info(
"send() failed.");
    }
    
if (send_message_size != data_length) {
        sockClass::error_info(
            
"send() sent a different number of bytes than expected.");
    }
    
return send_message_size;
}
二。设计UDP Client类
UDP的客户端看起来几乎就是服务器端的翻版,甚至比服务器端更简单——因为不需要bind()本机地址:
class UDPClientSock: public BaseSock {
protected:
    mutable sockaddr_in lastfromSockAddr;
    sockaddr_in destinationSockAddr;
    
char* preBuffer;
    
int preBufferSize;
    mutable 
int preReceivedLength;
public:
    
explicit UDPClientSock(int pre_buffer_size = 32);
    
virtual ~UDPClientSock();
    
void UDPSetDest(const char* dest_IP,
            
const unsigned short& dest_port);
    
void UDPSetDest(const sockaddr_in& dest_sock_addr);
    
int UDPReceive() const;
    
int UDPSendtoDest(const char* send_data,
            
const int& data_length) const;
};
在最初设计这个类的时候,我曾经考虑过安排一个服务器地址的私有数据成员,并且在构造函数里面指定服务器的地址。但是,后来我觉得使用“目的地”比“服务器”更加能体现出UDP无连接的本质特点。TCP之所以有个服务器,是因为TCP的客户端只能和自己的服务器端通讯。而UDP的客户端可以与任何一个UDP端口通讯——只要知道对方的地址(IP地址和UDP端口)就可以发送数据包。况且,在网络情况越来越复杂的今天,很多服务器都不仅仅使用一个IP地址或者域名,比如网站和游戏服务器,而对于客户端来说,只是在意连接到了指定的网站,比如google,而并不清楚是连接到google的哪个服务器。程序内部可能会根据网络条件对具体连接的服务器地址进行调整,所以,可以随时根据具体情况指定“目的地”,而不是一开始就指定一个“服务器”地址,这种策略显得更加灵活。
通常情况下,客户端也并不在意lastfromSockAddr,因为最后一次来向的地址,往往就是目的地服务器的地址。我们说过,服务器的端口是指定的,这是为了让客户端明确的知道,可以去连接。而客户端的端口的端口则是系统指定的——我们并没有在客户端调用bind(),所以socket机制会自动帮我们绑定一个端口。通常客户端自己也不需要知道这个端口号是多少,只有接收到这次UDP数据报的服务器端知道,并且按照这个端口号将服务器的信息传送过来——没有收到这个端口发出的数据报的UDP端口很难知道这个系统指定的端口号是多少。但是,因为这个UDP端口实际上是可以接受来自其他任何UDP端口的数据的,所以,如果你需要验证发送某次数据的地址是不是你所期望的,比如是不是来自服务器,可能就会用到lastfromSockAddr。
UDPClientSock::UDPClientSock(int pre_buffer_size):
preBufferSize(pre_buffer_size), preReceivedLength(
0)
{
    preBuffer 
= new char[preBufferSize];
    memset(
&lastfromSockAddr, 0sizeof(lastfromSockAddr));
    memset(
&destinationSockAddr, 0sizeof(destinationSockAddr));

    sockFD 
= socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
    
if (sockFD < 0) {
        sockClass::error_info(
"sock() failed.");
    }
}

UDPClientSock::
~UDPClientSock()
{
    delete [] preBuffer;
    close(sockFD);
}
其它4个类方法,跟server端的简直一模一样。
void UDPClientSock::UDPSetDest(const char* dest_IP,
                               
const unsigned short& dest_port)
{
    destinationSockAddr.sin_family 
= AF_INET;
    destinationSockAddr.sin_addr.s_addr 
= inet_addr(dest_IP);
    destinationSockAddr.sin_port 
= htons(dest_port);
}

void UDPClientSock::UDPSetDest(const sockaddr_in& dest_sock_addr)
{
    destinationSockAddr.sin_family 
= dest_sock_addr.sin_family;
    destinationSockAddr.sin_addr.s_addr 
= dest_sock_addr.sin_addr.s_addr;
    destinationSockAddr.sin_port 
= dest_sock_addr.sin_port;
}

int UDPClientSock::UDPReceive() const
{
    socklen_t
 from_add_len = sizeof(lastfromSockAddr); //use int in win32
    preReceivedLength 
= recvfrom(    sockFD,
                                    preBuffer,
                                    preBufferSize,
                                    
0,
                                    (sockaddr
*)&lastfromSockAddr,
                                    
&from_add_len);
    
if ( preReceivedLength < 0) {
        sockClass::error_info(
"recv() failed.");
    }

    
return preReceivedLength;
}

int UDPClientSock::UDPSendtoDest(const char* send_data,
                                 
const int& data_length) const
{
    
int send_message_size = sendto(    sockFD,
                                    send_data,
                                    data_length,
                                    
0,
                                    (sockaddr
*)&destinationSockAddr,
                                    
sizeof(destinationSockAddr));
    
if (send_message_size < 0) {
        sockClass::error_info(
"send() failed.");
    }
    
if (send_message_size != data_length) {
        sockClass::error_info(
            
"send() sent a different number of bytes than expected.");
    }
    
return send_message_size;
}
三。UDP的系统缓存队列
UDP的系统缓存队列与TCP的相比,有两点显著的不同:
1、UDP没有SendQ。UDP的数据包不会被处理,通过调用sendto()(或者在connect()之后也可以调用send())将数据直接发送。
2、UDP的数据在缓存队列中是有边缘保证的,也就是说,数据包是有大小的。每次调用recvfrom()(或者在connect()之后调用recv())都会试图接收一个完整的数据包——因此,UDP程序所指定的接收缓存大小应该足够存放每一个UDP数据包,否则,多余的部分就会被抛弃,并且recvfrom()(或recv())返回一个异常(-1,并且抛出异常代码)。

(在上图中,我们用虚线的数据包边缘表示TCP中的无边缘保证;而UDP中字节之间用虚线隔开表示UDP的数据不会以字节为单位进行传输)
此外,UDP的RecvQ还可能存在于TCP的第三个不同:我们说,UCP是无连接的,当然,我们也可以调用connect()将UDP连接起来,但是在默认无连接的情况下,UDP的RecvQ中可以缓存来自所有远程地址的数据包——这不仅仅在很多时候很不方便,如果我们只希望接收一个特定地址的数据,比如作为客户端只希望接收来自服务器的数据;而且,因为这个缓存可以被任何信息进入,从而也是一个安全隐患,很可能这个缓存在短时间内就会被垃圾信息所填满。
因此,很多时候我们也会用到“有连接”的UDP。
四。“有连接"的UDP

虽然UDP是无连接的,但是也可以通过调用connect()将本地的UDP socket FD与一个远程的UDP socket FD连接起来——只需要指定这个远程sockFD的地址,假设这个地址是sockaddr_in remoteSockAddr,代码如下:

    if (connect(sockFD,
                (sockaddr
*)&remoteSockAddr,
                
sizeof(remoteSockAddr)) < 0) {
        sockClass::error_info(
"connect() failed.");
    }
建立连接后的UDP RecvQ就不会将非来自remoteSockAddr的数据包收入。
请注意UDP的connect()与TCP的connect()很不相同,TCP是连接服务器的监听socket,并且会阻塞直到服务器调用accept()。一般的说法,UDP的连接并不会改变UDP的各种特点,比如,即使连接,UDP也不知道远程主机是否在线连接或者是否断开——但是,我个人认为,改变了本机的RecvQ接收数据包的过滤机制,也就改变了UDP原本可以接收来自任何地址信息的属性。
如果希望断开UDP的连接,需要使用一个特定的“断开”地址,代码如下:
    sockaddr descon_sock_addr;
    memset(
&descon_sock_addr, 0sizeof(descon_sock_addr));
    descon_sock_addr.sa_family 
= AF_UNSPEC;
    
if (connect(sockFD,
                
&descon_sock_addr,
                
sizeof(descon_sock_addr)) < 0) {
        sockClass::error_info(
"des connect() failed.");
    }
请注意这里的地址族AF_UNSPEC直接赋值给了一个sockaddr结构。我试过,使用sockaddr_in也是可以的,但是无论是哪个结构,首先都得将整个结构对象清零,否则可能报错。
五。预读MSG_PEEK
recv()和recvfrom()的第4个参数可以调整函数行为。
#include <sys/types.h>
#include 
<sys/socket.h>
ssize_t recv(
int s, void *buf, size_t len, int flags);
ssize_t recvfrom(
int s, void *buf, size_t len, int flags,
                              struct sockaddr *from, socklen_t *fromlen);
因为UDP是按数据包接收的,我们在接收之前并不知道这个数据包有多大。一个策略是,我们准备足够大的应用程序缓存以免出错,但是这个“足够大”的概念是建立在我们对传送的数据事先有了解的情况下,比如是我们自己设计服务器端和客户端并且制定应用层协议;另外一种策略是,将一个数据包的相关信息记录在数据包的前面的一些字节中,比如说大小,这样,我们可以通过预读数据包的前面一段,得到这个数据包的相关信息,比如说大小,然后再安排缓存。
这个预读的flag就是MSG_PEEK。使用预读后,RecvQ的下一条UDP数据包信息被读出来,但是并不从RecvQ中弹出。
UDP也可以通过recvfrom()预读获得来向的远程地址,从而可以提供给比如connect()等函数使用。
需要说明的是,在Linux下(我是Debian系统)从一个n字节的UDP数据包中预读取小于n个字节的数据是完全没有问题的;但是在WinSock下会引起一个异常10040(WSAEMSGSIZE),即是说win32下recv()或者recvfrom()在这种情况下会返回-1。其异常信息大概是读取的数据长度小于数据包的长度——而这个正是我们计划中的事情。

原创粉丝点击