在前面部分介绍的:管道、FIFO、消息队列、信号量和共享内存都是同一台计算机上的进程间通信,本节介绍的套接字是可以实现不同计算机之间的远程进程间通信。套接口是网络进程的 ID,在网络中每一个节点都有一个网络地址,也就是 IP 地址,两个进程间通信时,首先要确定各自所在网络节点的网络地址。但是,网络地址只要确定进程所在的计算机,由于一台计算机上同时可能有多个网络进程,所以仅凭网络地址还不能确定是网络中的哪一个进程,因此套接口中还需要其他信息,也就是端口。在一台计算机中,一个端口号只能分配给一个进程,所以,进程和端口之间是一一对应的关系。因此,使用端口号和网络地址的组合就能唯一地确定整个网络中的一个网络进程。
把网络应用程序中所用到的网络地址和端口号信息放在一个结构体中,也就是套接口地址结构。大多数的套接口函数都需要一个指向套接口地址结构的指针作为参数,并以此来传递地址信息。每个协议族都定义它自己的套接口地址结构,套接口地址结构都是以 sockaddr_ 开头,并以每个协议中名中的两个字母作为结尾。
套接字描述符
套接字是通信端点的抽象,实现端对端之间的通信。与应用程序要使用文件描述符访问文件一样,访问套接字需要套接字描述符。因此,很多能够操作文件描述符的函数也能够操作套接字描述符。要对套接字进行操作必须获取该套接字的描述符,可以调用函数 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 函数关闭该套接字,并且释放该套接字描述符。如果我们不想完全关闭该套接字描述符,而只是想关闭读、写端其中一个端时,可以使用函数 shutdown 实现:
/*
* 函数功能:关闭套接字上的输入或输出;
* 返回值:若成功则返回0,若出错返回-1;
* 函数原型:
*/
#include <sys/socket.h>
int shutdown(int sockfd, int how);
/*
* 说明:
* sockfd表示待操作的套接字描述符;
* how表示具体操作,取值如下:
* (1)SHUT_RD 关闭读端,即不能接收数据
* (2)SHUT_WR 关闭写端,即不能发送数据
* (3)SHUT_RDWR 关闭读、写端,即不能发送和接收数据
*
*/
寻址
字节序
计算机在内存中的数据存储有两种方式:一种是小端字节序,即内存低地址存储数据低字节,内存高地址存储数据高字节;另一种是大端字节序,即内存低地址存储数据高字节,内存高地址存储数据低字节;具体如下图所示:
网络字节序采用的是大端字节序。某个系统所采用的字节序称为主机字节序(也称处理器字节序),主机字节序可能是小端字节序,也有可能是大端字节序。在网络协议中处理多字节数据时采用的都是网络字节序,即大端字节序,而不是主机字节序。要把主机字节序和网络字节序相对应,则必须采用字节序转换函数,以下是主机字节序和网络字节序之间的转换函数:
/*
* 函数功能:主机字节序和网络字节序之间的转换;
* 返回值:返回对应类型表示的字节序;
* 函数原型:
*/
#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字节的大小;
套接字的地址数据结构
套接字的地址标识了特定通信域中的套接字端点,套接字数据结构与使用它的网络(通信域)有关。在 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;
*/
地址查询
为了获取主机的信息,我们可以调用以下函数进行实现,也可实现名字地址和数字地址之间的转换;
/* 地址查询 */
/*
* 函数功能:获取给定计算机的主机信息;
* 返回值:若成功则返回主机结构指针,若出错则返回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 */
};
端口号 与 服务名之间的映射:
/*
* 函数功能:服务名与端口号之间的映射;
* 返回值:若成功则返回指针,若出错则返回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 */
};
【地址 与 主机名】 和 【服务名 与 端口号】之间的转换:
/*
* 函数功能:将服务名和主机名映射到一个地址;
* 返回值:若成功则返回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);
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);
测试程序:打印主机和服务信息;
#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;
/* 将主机名和服务名映射到一个地址 */
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 ");
}
}
输出结果:
./socket localhost nfs
family-- inet type.. stream protocol++ TCP flags canon
host localhost address 127.0.0.1 port 2049
family-- inet type.. datagram protocol++ UDP flags canon
host - address 127.0.0.1 port 2049
参考资料:
《UNIX高级环境编程》