16.1 引言
在前面部分介绍的:管道、FIFO、消息队列、信号量和共享内存都是同一台计算机上的进程间通信,本节介绍的套接字是可以实现不同计算机之间的远程进程间通信。套接口是网络进程的 ID,在网络中每一个节点都有一个网络地址,也就是 IP 地址,两个进程间通信时,首先要确定各自所在网络节点的网络地址。但是,网络地址只要确定进程所在的计算机,由于一台计算机上同时可能有多个网络进程,所以仅凭网络地址还不能确定是网络中的哪一个进程,因此套接口中还需要其他信息,也就是端口。在一台计算机中,一个端口号只能分配给一个进程,所以,进程和端口之间是一一对应的关系。因此,使用端口号和网络地址的组合就能唯一地确定整个网络中的一个网络进程。
把网络应用程序中所用到的网络地址和端口号信息放在一个结构体中,也就是套接口地址结构。大多数的套接口函数都需要一个指向套接口地址结构的指针作为参数,并以此来传递地址信息。每个协议族都定义它自己的套接口地址结构,套接口地址结构都是以 sockaddr_ 开头,并以每个协议中名中的两个字母作为结尾。
16.2 套接字描述符
套接字是通信端点的抽象,实现端对端之间的通信。与应用程序要使用文件描述符访问文件一样,访问套接字需要套接字描述符。因此,很多能够操作文件描述符的函数也能够操作套接字描述符。要对套接字进行操作必须获取该套接字的描述符,可以调用函数 socket 实现:
/* 套接字 */
/*
* 函数功能:创建套接字描述符;
* 返回值:若成功则返回套接字描述符,若出错返回-1;
* 函数原型:
*/
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
/*
* 说明:
* socket类似于open对普通文件操作一样,都是返回描述符,后续的操作都是基于该描述符;
* domain表示套接字的通信域,不同的取值决定了socket的地址类型,其一般取值如下:
* (1)AF_INET IPv4因特网域
* (2)AF_INET6 IPv6因特网域
* (3)AF_UNIX Unix域
* (4)AF_UNSPEC 未指定
*
* type确定socket的类型,常用类型如下:
* (1)SOCK_STREAM 有序、可靠、双向的面向连接字节流
* (2)SOCK_DGRAM 长度固定的、无连接的不可靠报文传递
* (3)SOCK_RAW IP协议的数据报接口
* (4)SOCK_SEQPACKET 长度固定、有序、可靠的面向连接的报文传递
*
* protocol指定协议,常用取值如下:
* (1)0 选择type类型对应的默认协议
* (2)IPPROTO_TCP TCP传输协议
* (3)IPPROTO_UDP UDP传输协议
* (4)IPPROTO_SCTP SCTP传输协议
* (5)IPPROTO_TIPC TIPC传输协议
*
*/
当我们不再使用套接字描述符时,可以使用 close 函数关闭该套接字,并且释放该套接字描述符。但是close只有在最后一个活动引用被关闭时才释放网络端点。而shutdown允许使一个套接字处于不活动状态,无论引用它的文件描述符数目多少。套接字通信是双向的,如果我们不想完全关闭该套接字描述符,而只是想关闭读、写端其中一个端时,可以使用函数 shutdown 实现:
/*
* 函数功能:关闭套接字上的输入或输出;
* 返回值:若成功则返回0,若出错返回-1;
* 函数原型:
*/
#include <sys/socket.h>
int shutdown(int sockfd, int how);
/*
* 说明:
* sockfd表示待操作的套接字描述符;
* how表示具体操作,取值如下:
* (1)SHUT_RD 关闭读端,无法从套接字读取数据
* (2)SHUT_WR 关闭写端,即无法使用套接字发送数据
* (3)SHUT_RDWR 关闭读、写端,即不能发送和接收数据
*
*/
16.3 寻址
16.3.1字节序
计算机在内存中的数据存储有两种方式:一种是小端字节序,即内存低地址存储数据低字节,内存高地址存储数据高字节;另一种是大端字节序,即内存低地址存储数据高字节,内存高地址存储数据低字节;具体如下图所示:
网络字节序采用的是大端字节序。某个系统所采用的字节序称为主机字节序(也称处理器字节序),主机字节序可能是小端字节序,也有可能是大端字节序。在网络协议中处理多字节数据时采用的都是网络字节序,即大端字节序,而不是主机字节序。要把主机字节序和网络字节序相对应,则必须采用字节序转换函数,以下是主机字节序和网络字节序之间的转换函数:
/*
* 函数功能:主机字节序和网络字节序之间的转换;
* 返回值:返回对应类型表示的字节序;
* 函数原型:
*/
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostint32);//返回值:以网络字节序表示的32位整型数;
uint16_t htons(uint16_t hostint16);//返回值:以网络字节序表示的16位整型数;
uint32_t ntohl(uint32_t netint32);//返回值:以主机字节序表示的32位整型数;
uint16_t ntohs(uint16_t netint16);//返回值:以主机字节序表示的16位整型数;
/*
* 说明:
* 从以上函数我们可以发现:
* h表示“主机(host)”字节序,n表示“网络(network)”字节序;
* l表示“长(long)”整型,s表示“短(short)”整型;
*
*/
字节操作函数
在套接字编程中,经常会使用到字节操作函数,下面是一些经常用到的字节操作函数:
/*
* 函数功能:字节操作;
* 函数原型:
*/
#include <strings.h>
void bzero(void *dest, size_t nbytes);//将dest所存储的数据前nbytes字节初始化为0;
void bcopy(const void *str, void *dest, size_t nbytes);//将str所存储的数据前nbytes字节复制到dest中;
int bcmp(const void *ptr1, const void *ptr2, size_t nbytes);//比较两个字符的前nbytes字节的大小;
void *memset(void *dest, int c, size_t len);//将dest所存储的数据前len字节初始化为c;
void *memcopy(void *dest,const void *src, size_t nbytes);//将src所存储的数据前nbytes字节复制到dest中;
int memcmp(const void *ptr1, const void *ptr2, size_t nbytes);//比较两个字符的前nbytes字节的大小;
16.3.2 套接字的地址数据结构
套接字的地址标识了特定通信域中的套接字端点,套接字数据结构与使用它的网络(通信域)有关。在 Linux 中,每一种协议都有自己的网络地址数据结构,这些结构以 sockaddr_开头,不同的后缀表示不同的协议,例如 IPv4 对应的是 sockaddr_in 。为了使不同格式的地址数据结构能够传入到套接字函数中,则地址会被强制转换成通用的地址数据结构 sockaddr 表示,通用地址数据结构定义如下:
/* 地址数据结构 */
/* 通用的地址数据结构 */
struct sockaddr
{
uint8_t sa_len; /* total length */
sa_family_t sa_family; /* address family */
char sa_data[]; /* variable-length address */
};
/* Linux 下的地址数据结构 */
struct sockaddr
{
uint8_t sa_len; /* total length */
sa_family_t sa_family; /* address family */
char sa_data[14]; /* length address */
};
若要把网络字节序的地址打印成我们能够理解的格式,则需要转换函数把网络字节序转换成我们可读的地址格式,inet_ntop 和 inet_pton 这两个函数对 IP 地址格式进行转换,其定义如下:
#include<arpa/inet.h>
const char *inet_ntop(int domain, const void *restrict addr, char *restrict str, socklen_t size);
//若成功则返回地址字符串指针,出错则返回NULL;
int inet_pton(int domain, const char *restrict str, void *restrict addr);
//若成功则返回1,格式无效则返回0,出错则返回-1;
/*
* 说明:
* inet_ntop 是将网络字节序的二进制地址转换成文本字符串格式;
* inet_pton 是将文本字符串格式转换成网络字节序的二进制地址;
* 参数domain只能取值:AF_INET 或 AF_INET6;
*/
16.3.3 地址查询
为了获取主机的信息,我们可以调用以下函数进行实现,也可实现名字地址和数字地址之间的转换;
/* 地址查询 */
/*
* 函数功能:获取给定计算机的主机信息;
* 返回值:若成功则返回主机结构指针,若出错则返回NULL;
* 函数原型:
*/
#include <netdb.h>
struct hostent *gethostbyname(const char *hostname);//将主机名地址转换为数字地址;
struct hostent *gethostaddr(const char *addr, size_t len, int family);//将主机数字地址转换为名字地址;
struct hostent *gethostent(void);
void sethostent(int stayopen);
void endhostent(void);
/*
* 说明:
* 若主机数据文件没有打开,gethostent会打开它,该函数返回文件的下一条目;
* 函数sethostent会打开文件,若文件已打开,那么将其回绕;
* 函数endhostent将关闭文件;
* 其中hostent结构至少包含如下成员数据:
*/
struct hostent
{
char *h_name; /* name of host */
char **h_aliases; /* pointer to alternate host name array */
int h_addrtype; /* address type */
int h_length; /* length in bytes of address */
char **h_addr_list; /* pointer to array of network address */
};
上面的函数若成功调用,则会返回一个指向 hostent 结构的指针,若出错则返回 NULL,且设置全局变量 h_error 为相应值。一般的 socket 系统调用都将错误信息存储在全局变量 error 中,但是和主机 host 有关的系统调用,则将错误信息存储在 h_error 中,它的取值如下:
HOST_NOT_FOUND:找不到主机;
TRY_AGAIN:重试;
NO_RECOVERY:不可修复性错误;
NO_DATA:指定的名字有效,但是没有记录;
获取网络名字和网络号
/*
* 函数功能:获取网络名和网络号;
* 返回值:若成功则返回指针,出错返回NULL;
* 函数原型:
*/
#include <netdb.h>
struct netent *getnetbyaddr(uint32_t net, int type);
struct netent *getnetbyname(const char *name);
struct netent *getnetent(void);
void setnetent(int stayopen);
void endnetent(void);
/*
* netent 结构至少包含以下成员:
*/
struct netent
{
char *n_name; /* network name */
char *n_aliases; /* alternate network name array pointer */
int n_addrtype; /* address type */
uint32_t n_net; /* network number */
};
将 协议名字和协议号采用以下函数映射:
#include <netdb.h>
struct protoent *getprotobyname(const char *name);
struct protoent *getprotobynumber(int proto);
struct protoent *getprotoent(void);
//以上三个函数返回值:若成功则返回指针,出错返回NULL;
void setprotornt(int stayopen);
void endprotornt(void);
/*
* protoent 结构至少包含以下成员:
*/
struct protoent
{
char *p_name; /* protocol name */
char **p_aliases;/* pointer to alternate protocol name array */
int p_proto; /* protocol number */
};
端口号 与 服务名之间的映射:
服务是由地址的端口号部分表示的。每个服务由一个唯一的、熟知的端口号来提供。比如tcp的端口号是80.
/*
* 函数功能:服务名与端口号之间的映射;
* 返回值:若成功则返回指针,若出错则返回NULL;
* 函数原型:
*/
#include <netdb.h>
struct servent *getservbyname(const char *name, const char *proto); //将一个端口号映射到一个服务名
struct servent *getservbyport(int port, const char *proto);
struct servent *getservent(void);
void setservent(int stayopen);
void endservent(void);
/*
* 其中servent 结构至少包含以下成员:
*/
struct servent
{
char *s_name; /* service name */
char **s_aliases; /* pointer to alternate service name array */
int s_port; /* port number */
char *s_proto; /* name of protocol */
};
POSIX.1定义了若干新的函数,允许应用程序将一个主机名字和服务名字映射到一个地址,或者相反,这些函数代替老的函数gethostbyname和gethostbyaddr。
【地址 与 主机名】 和 【服务名 与 端口号】之间的转换:
/*
* 函数功能:将服务名和主机名映射到一个地址;
* 返回值:若成功则返回0,若出错则返回非0错误编码;
* 函数原型:
*/
#include <netdb.h>
#include <sys/socket.h>
int getaddrinfo(const char *host, const char *service, const struct addrinfo *hint, struct addrinfo **res);
void freeaddrinfo(struct addrinfo *ai); //释放ai
const char *gai_strerror(int error);//若getaddrinfo出错时,错误消息只能由该函数输出;
/*
* 说明:
* 该函数需要提供主机名或服务名,若只提供其中一个,则另一个必须指定为NULL;
* addrinfo是一个结构链表,其定义如下:
*/
struct addrinfo
{
int ai_flags; /* customize behavior */
int ai_family; /* address family */
int ai_socktype; /* socket type */
int ai_protocol; /* protocol */
socklen_t ai_addrlen; /* length in bytes of address */
struct sockaddr *ai_addr; /* address */
char *ai_canonname; /* canonical name of host */
struct addrinfo *ai_next; /* next in list */
};
/*
* 函数功能:将地址转换成服务名或主机名;
* 返回值:若成功则返回0,若出错则返回非0值;
* 函数原型:
*/
#include <netdb.h>
#include <sys/socket.h>
int getnameinfo(const struct sockadd *addr, socklen_t alen, char * host, socklen_t hostlen,
char * service, socklen_t servlen, unsigned int flags);
套接字地址(addr)被转换成主机名或服务名。如果host非空,它指向一个长度为hostlen字节的缓冲区用于存储返回的主机名。同样,如果service非空,它指向一个长度为servlen字节的缓冲区用于存储返回的服务名。
测试程序:打印主机和服务信息;
#include "apue.h"
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void print_family(struct addrinfo *aip);
void print_type(struct addrinfo *aip);
void print_flags(struct addrinfo *aip);
void print_protocol(struct addrinfo *aip);
int main(int argc, char **argv)
{
struct addrinfo *ailist, *aip;
struct addrinfo hint;
/* 定义IPv4的套接字地址结构 */
struct sockaddr_in *sinp;
const char *addr;
int err;
char abuf[INET_ADDRSTRLEN];
if(argc != 3)
err_quit("usage: %s nodename service", argv[0]);
/* 初始化addrinfo结构变量hint*/
hint.ai_family = 0;
hint.ai_socktype = 0;
hint.ai_protocol = 0;
hint.ai_addrlen = 0;
hint.ai_flags = AI_CANONNAME;//需要一个规范名,而不是别名;
hint.ai_addr = NULL;
hint.ai_next = NULL;//表示只有一个addrinfo链表结构;
hint.ai_canonname = NULL;
/* 将主机名argv[1]和服务名argv[2]映射到一个地址ailist */
if((err = getaddrinfo(argv[1], argv[2], &hint, &ailist)) != 0)
err_quit("getaddrinfo error: %s\n", gai_strerror(err));
/* 打印主机和服务信息 */
for(aip = ailist; aip != NULL; aip = aip->ai_next)
{
print_family(aip);
print_type(aip);
print_protocol(aip);
print_flags(aip);
printf("\n\thost %s", aip->ai_canonname ?aip->ai_canonname:"-");
if(aip->ai_family == AF_INET)
{
/* 获取IP地址,并把网络字节序的二进制地址转换为文本字符串地址 */
sinp = (struct sockaddr_in *)aip->ai_addr;
addr = inet_ntop(AF_INET, &sinp->sin_addr, abuf, INET_ADDRSTRLEN);
printf(" address %s", addr ? addr:"unknown");
printf(" port %d", ntohs(sinp->sin_port));
}
printf("\n");
}
exit(0);
}
void print_family(struct addrinfo *aip)
{
printf(" family-- ");
switch(aip->ai_family)
{
case AF_INET://IPv4
printf("inet");
break;
case AF_INET6://IPv6
printf("inet6");
break;
case AF_UNIX://UNIX域
printf("Unix");
break;
case AF_UNSPEC://未指定
printf("unspecified");
break;
default:
printf("unknown");
}
}
void print_type(struct addrinfo *aip)
{
printf(" type.. ");
switch(aip->ai_socktype)
{
case SOCK_STREAM:
printf("stream");
break;
case SOCK_DGRAM:
printf("datagram");
break;
case SOCK_RAW:
printf("raw");
break;
case SOCK_SEQPACKET:
printf("seqpacket");
break;
default:
printf("unknown (%d)", aip->ai_socktype);
}
}
void print_protocol(struct addrinfo *aip)
{
printf(" protocol++ ");
switch(aip->ai_protocol)
{
case IPPROTO_TCP:
printf("TCP");
break;
case IPPROTO_UDP:
printf("UDP");
break;
case IPPROTO_SCTP:
printf("SCTP");
break;
case 0:
printf("default");
break;
default:
printf("unknown (%d)", aip->ai_protocol);
}
}
void print_flags(struct addrinfo *aip)
{
printf(" flags ");
if(aip->ai_flags == 0)
printf("0");
else
{
if(aip->ai_flags & AI_PASSIVE)
printf(" passive ");
if(aip->ai_flags & AI_CANONNAME)
printf(" canon ");
if(aip->ai_flags & AI_NUMERICHOST)
printf(" numhost ");
}
}
输出结果:
[root@localhost 16]# ./a.out localhost nfs
family-- inet type.. stream protocol++ TCPflags canon
host localhost.localdomain address 127.0.0.1 port 2049
family-- inet type.. datagram protocol++ UDPflags canon
host - address 127.0.0.1 port 2049
16.3.4将套接字与地址绑定
在套接字进行通信时,首先要建立服务器进程和客户端进程之间的连接,连接创建成功之后才可以传输数据,以下是套接字通信的过程:
调用函数 socket 创建套接字描述符时,该套接字描述符是存储在它的协议族空间中,没有具体的地址,要使它有一个地址,可以调用函数 bind 使其与地址绑定。客户端的套接字关联的地址可有系统默认分配,因此不需要指定具体的地址。若要为服务器端套接字绑定地址,可以通过调用函数 bind 将套接字绑定到一个地址。
/* 套接字的基本操作 */
/*
* 函数功能:将地址绑定到一个套接字;
* 返回值:若成功则返回0,若出错则返回-1;
* 函数原型:
*/
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t len);
/*
* 说明:
* sockfd 为套接字描述符;
* addr是一个指向特定协议地址结构的指针;
* len是地址结构的长度;
*/
在地址使用方面有下面一些限制:
(1)在进程所运行的机器上,指定的地址必须有效,不能指定其他机器的地址;
(2)地址必须和创建套接字时的地址族所支持的格式相匹配;
(3)端口号必须不小于1024,除非该进程具有相应的特权(超级用户);
(4)一般只有套接字端点能够与地址绑定,尽管有些协议允许多重绑定;
bind 函数的调用不是必须的,可以在创建套接字描述符之后调用 connect 或 listen 函数,这时内核系统会自动给该套接字分配一个地址和端口。只有在希望进程使用某个特定的网络地址和端口时,才使用 bind 函数。
为了获取已绑定到套接字的地址,我们可以调用函数 getsockname 来实现:
/
*
* 函数功能:获取已绑定到一个套接字的地址;
* 返回值:若成功则返回0,若出错则返回-1;
* 函数原型:
*/
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *alenp);
/*
* 说明:
* 调用该函数之前,设置alenp为一个指向整数的指针,该整数指定缓冲区sockaddr的大小;
* 返回时,该整数会被设置成返回地址的大小,如果该地址和提供的缓冲区长度不匹配,则将其截断而不报错;
*/
/*
* 函数功能:获取套接字对方连接的地址;
* 返回值:若成功则返回0,若出错则返回-1;
* 函数原型:
*/
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *alenp);
/*
* 说明:
* 该函数除了返回对方的地址之外,其他功能和getsockname一样;
*/
16.4 建立连接
在处理面向连接的网络服务时,交换数据之前必须在请求的进程套接字和提供服务的进程套接字之间建立连接。可以调用函数 connect 建立一个连接:
/*
* 函数功能:建立连接;
* 返回值:若成功则返回0,出错则返回-1;
* 函数原型:
*/
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
/*
* 说明:
* sockfd是系统调用的套接字描述符;
* addr是目的套接字的地址,即想与之通信的服务器地址;
* len是目的套接字的大小;
*
* 如果sockfd没有绑定到一个地址,connect会给调用者绑定一个默认地址;
*/
在编写服务器程序时需要使用监听函数 listen 。服务器进程不知道要与谁连接,因此,它不会主动地要求与某个进程连接,只是一直监听是否有其他客户进程与之连接,然后响应该连接请求,并对它做出处理,一个服务进程可以同时处理多个客户进程的连接。一般函数 listen 与函数 accept 一起使用,先使用监听函数 listen,然后使用 accept 逐一处理各个连接,其定义如下:
/*
* 函数功能:接收连接请求;
* 函数原型:
*/
#include <sys/socket.h>
int listen(int sockfd, int backlog);//若成功则返回0,若出错则返回-1;
/*
* sockfd是套接字描述符;
* backlog是该进程所要入队请求的最大请求数量;
*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *len);//返回值:若成功返回套接字描述符,出错返回-1;
/*
* 说明:
* 该函数返回套接字描述符,该描述符连接到调用connect函数的客户端;
* 这个新的套接字描述符和原始的套接字描述符sockfd具有相同的套接字类型和地址族,而传给accept函数的套接字描述符sockfd没有关联到这个链接,
* 而是继续保持可用状态并接受其他连接请求;
* 若不关心客户端协议地址,可将addr和len参数设置为NULL,否则,在调用accept之前,应将参数addr设为足够大的缓冲区来存放地址,
* 并且将len设为指向代表这个缓冲区大小的整数指针;
* accept函数返回时,会在缓冲区填充客户端的地址并更新len所指向的整数为该地址的实际大小;
*
* 若没有连接请求等待处理,accept会阻塞直到一个请求到来;
*/
16.5 数据传输
套接字的连接建立完成之后,服务器进程和客户端进程之间就可以进行数据传输。数据传输过程中,我们可以使用 read 和 write 函数实现,但是想指定选项、从多个客户端接收数据包或发送带外数据时,可以采用以下函数,下面函数分别有三个数据发送函数和与之相对应的数据接收函数。
/* 数据传输 */
/*
* 函数功能:发送数据;
* 返回值:若成功则返回发送的字节数,若出错则返回-1;
* 函数原型:
*/
#include <sys/socket.h>
ssize_t send(int sockfd, void *buf, size_t nbytes, int flags);
/*
* 说明
* 该函数的功能类似与write函数,除了有标识符flags之外,其他的相同;
* flags标识符的取值如下:
* (1)MSG_DONTROUTE 勿将数据路由出本地网络
* (2)MSG_DONTWAIT 允许非阻塞操作
* (3)MSG_EOR 如果协议支持,此为记录结束
* (4)MSG_OOB 如果协议支持,发送带外数据
*
* 若send成功返回,并不必然表示连接另一端的进程接收数据,只能说数据已经无错误地发送到网络;
*
* 对于支持为报文设限的协议,若报文超过协议所支持的最大尺寸,send失败并将errno设为EMSGSIZE;
* 对于字节流协议,send会阻塞直到整个数据被传输;
*/
ssize_t sendto(int sockfd, void *buf, size_t nbytes, int flags,
const struct sockaddr *destaddr, socklen_t destlen);
/*
* 说明
* 该函数通过指定目标地址允许在无连接的套接字之间发送数据,无连接的套接字之间通信不能使用send函数,除非connect时预先设定目标地址;
*
*/
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
/*
* 说明
* 该函数可以使用不止一个的选择来通过套接字发送数据,可以指定多重缓冲区传输数据,类似与readv函数;
* msghdr结构至少包含以下成员:
*/
struct msghdr
{
void *msg_name; /* optional address */
socklen_t msg_namelen; /* address size in bytes */
struct iovec *msg_iov; /* array of IO buffers */
int msg_iovlen; /* number of elements in array */
void *msg_control; /* ancillary data */
socklen_t msg_controllen; /* number of ancillary bytes */
int msg_flags; /* flags for recevied message */
};
与发送数据函数相对应的也有三个接收数据的函数,其定义如下:
/* 数据传输 */
/*
* 函数功能:接收数据;
* 返回值:以字节计数的消息长度,若无可用消息或对方已经按序结束则返回0,若出错则返回-1;
* 函数原型:
*/
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
/*
* 说明
* 该函数的功能类似与read函数,除了有标识符flags之外,其他的相同;
* flags标识符的取值如下:
* (1)MSG_PEEK 返回报文内容而不真正取走报文
* (2)MSG_TRUNC 即使报文被截断,要求返回的是报文的实际长度
* (3)MSG_WAITALL 等待直到所有数据可用
* (4)MSG_OOB 如果协议支持,发送带外数据
*
*/
ssize_t recvfrom(int sockfd, void *buf, size_t nbytes, int flags,
struct sockaddr *addr, socklen_t *addrlen);
/*
* 说明
*
* 若addr非空,它将包含数据发送者的套接字端点地址,该函数常用于无连接套接字;
*/
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);