网络IPC:套接字之寻址
在学习用套接字做一些有意义的事情之前,需要知道如何确定一个目标通信进程。
进程的标识有两个部分:计算机的网络地址可以帮助标识网络上想与之通信的计算机,而服务可以帮助标识计算机上特定的进程。
1、字节序
运行在同一台计算机上的进程相互通信时,一般不用考虑字节的顺序(字节序),字节序是一个处理器架构特性,用于指示像整数这样的大数据类型的内部字节顺序。图16-1显示一个32位整数内部的字节是如何排序的。
图16-1 32位整数内部的字节序
如果处理器架构支持大端(big-endian)字节序,那么最大字节地址对应于数字最低有效字节(LSB);小端(little-endian)字节序则相反:数字最低字节对应于最小字节地址。注意,不管字节如何排序,数字最高位总是在左边,最低位总是在右边。
网络协议指定了字节序,因此异构计算机系统能够交换协议信息而不会混淆字节序。TCP/IP协议栈采用大端字节序。应用程序交换格式化数据时,字节序问题就会出现。对于TCP/IP,地址用网络字节序表示,所以应用程序有时需要在处理器的字节序与网络字节序之间的转换。
对于TCP/IP应用程序,提供了四个通用函数以实施在处理器字节序和网络字节序之间的转换。
#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)”整数(即4个字节),
s表示“短(short)”整数(即2个字节)。
这四个函数定义在<arpa/inet.h>中,也有比较老的系统将其定义在<netinet/in.h>中。
2、地址格式
地址标识了特定通信域中的套接字端点,地址格式与特定的通信域相关。为使不同格式地址能够被传入到套接字函数,地址被强制转换成通用的地址结构sockaddr表示:
struct sockaddr { sa_family_t sa_family; /* address family */ char sa_data[]; /* variable-length address */ ...... };
套接字实现可以自由地添加额外的成员并且定义sa_data成员的大小。例如在Linux中,该结构定义如下:
struct sockaddr { sa_family_t sa_family; /* address family */ char sa_data[14]; /* variable-length address */ };
因特网地址定义在<netinet/in.h>中。在IPv4因特网域(AF_INET)中,套接字地址用如下结构sockaddr_in表示:
struct in_addr { int_addr_t s_addr; /* IPv4 address */ }; struct sockaddr_in { sa_family_t sin_family; /* address family */ in_port_t sin_port; /* port number */ struct in_addr sin_addr; /* IPv4 address */ };
数据类型in_port_t定义为uint16_t。数据类型in_addr_t定义成uint32_t。这些整数类型在<stdint.h>中定义并指定了相应的位数。与IPv4因特网域(AF_INET)相比较,IPv6因特网域(AF_INET6)套接字地址用如下结构sockaddr_in6表示:
struct in6_addr { uint8_t s6_addr[16]; /* IPv6 address */ }; struct sockaddr_in6 { sa_family_t sin6_family; /* address family */ in_port_t sin6_port; /* port number */ uint32_t sin6_flowinfo; /* traffic class and flow info */ struct in6_addr sin6_addr; /* IPv6 address */ uint32_t sin6_scope_id; /* set of interfaces for scope */ };
这些是Single UNIX Specification必须的定义,每个实现可以自由地添加额外的字段。例如,在Linux中,sockaddr_in定义如下:
struct sockaddr_in { sa_family_t sin_family; /* address family */ in_port_t sin_port; /* port number */ struct in_addr sin_addr; /* IPv4 address */ unsigned char sin_zero[8
]; /* filler */ };
其中成员sin_zero为填充字段,必须全部被置为0。
注意,尽管sockaddr_in与sockaddr_in6相差比较大,它们均被强制转换成sockaddr结构传入到套接字例程中。
有时,需要打印出能被人而不是计算机所理解的地址格式。BSD网络软件中包含了函数inet_addr和inet_ntoa,用于在二进制地址格式与点分十进制字符串表示(a.b.c.d)之间相互转换。这些函数仅用于IPv4地址,但功能相似的两个函数inet_ntop和inet_pton支持IPv4和IPv6地址。
#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。
对于inet_ntop,参数size指定了用以保存文本字符串的缓冲区(str)的大小。两个常数用于简化工作:INET_ADDRSTRLEN定义了足够大的空间来存放表示IPv4地址的文本字符串,INET6_ADDRSTRLEN定义了足够大的空间来存放表示IPv6地址的文本字符串。
对于inet_pton,如果domain是AF_INET,缓冲区addr需要有足够大的空间来存放32位地址,如果domain是AF_INET6则需要足够大的空间来存放128位地址。
3、地址查询
理想情况下,应用程序不需要了解套接字地址的内部结构。如果应用程序只是简单地传递类似于sockaddr结构的套接字地址,并且不依赖于任何协议相关的特性,那么可以与提供相同服务的许多不同协议协作。
历史上,BSD网络软件提供接口访问各种网络配置信息。http://www.cnblogs.com/nufangrensheng/p/3507496.html中,简要地讨论了网络数据文件和用来访问这种信息的函数。在本节,将更加详细地讨论一些细节,并且引入新的函数来查询寻址信息。
这些函数返回的网络配置信息可能存放在许多地方。它们可以保存在静态文件中(如/etc/hosts,/etc/services等),或者可以由命名服务管理,例如DNS(Domain Name System)或者NIS(Network Information Service)。无论这些信息放在何处,这些函数同样能够访问它们。
通过调用gethostent,可以找到给定计算机的主机信息。
#include <netdb.h> struct hostent *gethostent(void); 返回值:若成功则返回指针,若出错则返回NULL void sethostent(int stayopen); void endhostent(void);
如果主机数据文件没有打开,gethostent会打开它。函数gethostent返回文件的下一个条目。函数sethostent会打开文件,如果文件已经被打开,那么将其回绕。函数endhostent将关闭文件。
当gethostent返回时,得到一个指向hostent结构的指针,该结构可能包含一个静态的数据缓冲区。每次调用gethostent将会覆盖这个缓冲区。数据结构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 addresses */ ... };
返回的地址采用网络字节序。
两个附加的函数gethostbyname和gethostbyaddr,原来包含在hostent函数里面,现在被认为是过时的,马上将会看到其替代函数。
能够采用一套相似的接口来获得网络名字和网络号。
#include <netdb.h> struct netent *getnetbyaddr(uint32_t net, int type); struct netent *getnetbyname(const char *name); struct netent *getnetent(void); 以上三个函数的返回值:若成功则返回指针,若出错则返回NULL 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 */ ... };
网络号按照网络字节序返回。地址类型是一个地址族常量(例如AF_INET)。
可以将协议名字和协议号采用以下函数映射。
#include <netdb.h> struct protoent *getprotobyname(const char *name); struct protoent *getprotobynumber(int proto); struct protoent *getprotoent(void); 以上所有函数的返回值:若成功则返回指针,出错则返回NULL void setprotoent(int stayopen); void endprotoent(void);
POSIX.1定义的结构protoent至少包含如下成员:
struct protoent { char *p_name; /* protocol name */ char **p_aliases; /* pointer to alternate protocol name array */ int p_proto; /* protocol number */ ... };
服务是由地址的端口号部分表示的。每个服务由一个唯一的、熟知的端口号来提供。采用函数getservbyname可以将一个服务名字映射到一个端口号,函数getservbyport将一个端口号映射到一个服务名,或者采用函数getservent顺序扫描服务数据库。
#include <netdb.h> struct servent *getservbyname(const char *name, const char *proto); struct servent *getservbyport(int port, const char *proto); struct servent *getservent(void); 以上所有函数的返回值:若成功则返回指针,出错则返回NULL 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。
函数getaddrinfo允许将一个主机名字和服务名字映射到一个地址。
#include <sys/socket.h> #include <netdb.h> int getaddrinfo(const char *restrict host, const char *restrict service, const struct addrinfo *restrict hint, struct addrinfo **restrict res); 返回值:若成功则返回0,出错则返回非0错误码 void freeaddrinfo(struct addrinfo *ai);
需要提供主机名字、服务名字,或者两者都提供。如果仅仅提供一个名字,另外一个必须是个空指针。主机名字可以是一个节点名或点分十进制记法表示的主机地址。
函数getaddrinfo返回一个结构addrinfo的链表。可以用freeaddrinfo来释放一个或多个这种结构,这取决于用ai_next字段链接起来的结构有多少。
结构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(与aliases相对) name of host */ struct addrinfo *ai_next; /* next in list */ ... };
根据某些规则,可以提供一个可选的hint来选择地址。hint是一个用于过滤地址的模板,仅使用ai_family、ai_flags、ai_protocol和ai_socktype字段。剩余的整数字段必须设为0,并且指针字段为空。表15-6总结了在ai_flags中所用的标志,这写标志用来指定如何处理地址和名字。
表16-5 addrinfo结构标志
如果getaddrinfo失败,不能使用perror或strerror来生成错误消息。替代地,调用gai_strerror将返回的错误码转换成错误消息。
#include <netdb.h> const char *gai_strerror(int error); 返回值:指向描述错误的字符串的指针
函数getnameinfo将地址转换成主机名或者服务名。
#include <sys/socket.h> #include <netdb.h> int getnameinfo(const struct sockaddr *restrict addr, socklen_t alen, char *restrict host, socklen_t hostlen, char *restrict service, socklen_t servlen, unsigned int flags); 返回值:若成功则返回0,出错则返回非0值
套接字地址(addr)被转换成主机名或服务名。如果host非空,它指向一个长度为hostlen字节的缓冲区用于存储返回的主机名。同样,如果service非空,它指向一个长度为servlen字节的缓冲区用于存储返回的服务名。
参数flags指定一些转换的控制方式,表16-6总结了系统支持的标志。
表16-6 getnameinfo函数标志
实例
程序清单16-1说明了函数getaddrinfo的使用方法。
程序清单16-1 打印主机和服务信息
#include "apue.h" #include <netdb.h> #include <arpa/inet.h> #if defined(BSD) || defined(MACOS) #include <sys/socket.h> #include <netinet/in.h> #endif void print_family(struct addrinfo *aip) { printf(" family "); switch(aip->ai_family) { case AF_INET: printf("inet"); break; case AF_INET6: printf("inet6"); break; case AF_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_SEQPACKET: printf("seqpacket"); break; case SOCK_RAW: printf("raw"); break; default: printf("unknown (%d)", aip->ai_socktype); } } void print_protocol(struct addrinfo *aip) { printf(" protocol "); switch(aip->ai_protocol) { case 0: printf("default"); break; case IPPROTO_TCP: printf("TCP"); break; case IPPROTO_UDP: printf("UDP"); break; case IPPROTO_RAW: printf("raw"); 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"); #if defined(AI_NUMERICSERV) if(aip->ai_flags & AI_NUMERICSERV) printf(" numserv"); #endif #if defined(AI_V4MAPPED) if(aip->ai_flags & AI_V4MAPPED) printf(" v4mapped"); #endif #if defined(AI_ALL) if(aip->ai_flags & AI_ALL) printf(" all"); #endif } } int main(int argc, char *argv[]) { struct addrinfo *ailist, *aip; struct addrinfo hint; struct sockaddr_in *sinp; const char *addr; int err; char abuf[INET_ADDRSTRLEN]; if(argc != 3) err_quit("usage: %s nodename service", argv[0]); hint.ai_flags = AI_CANONNAME; hint.ai_family = 0; hint.ai_socktype = 0; hint.ai_protocol = 0; hint.ai_addrlen = 0; hint.ai_canonname = NULL; hint.ai_addr = NULL; hint.ai_next = NULL; if((err = getaddrinfo(argv[1], argv[2], &hint, &ailist)) != 0) err_quit("getaddrinfo error: %s", gai_strerror(err)); for(aip = ailist; aip != NULL; aip = aip->ai_next) { print_flags(aip); print_family(aip); print_type(aip); print_protocol(aip); printf("\n\thost %s", aip->ai_canonname?aip->ai_canonname:"-"); if(aip->ai_family == AF_INET) { 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); }
程序在Linux系统上运行输出如下:
4、将套接字与地址绑定
与客户端的套接字关联的地址没有太大的意义,可以让系统选一个默认的地址。然而,对于服务器,需要给一个接收客户端请求的套接字绑定一个众所周知的地址。客户端应有一种方法来发现用以连接服务器的地址,最简单的方法就是为服务器保留一个地址并且在/etc/services或者某个名字服务(name service)中注册。
可以用bind函数将地址绑定到一个套接字。
#include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t len); 返回值:若成功则返回0,出错则返回-1
对于所能使用的地址有一些限制:
- 在进程所运行的机器上,指定的地址必须有效,不能指定一个其他机器的地址。
- 地址必须和创建套接字时的地址族所支持的格式相匹配。
- 端口号必须不小于1024,除非该进程具有相应的特权(即为超级用户)。
- 一般只有套接字端点能够与地址绑定,尽管有些协议允许多重绑定。
对于因特网域,如果指定IP地址为INADDR_ANY,套接字端点可以被绑定到所有的系统网络接口。这意味着可以收到这个系统所安装的所有网卡的数据包。
可以调用函数getsockname来发现绑定到一个套接字的地址。
#include <sys/socket.h> int getsockname(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp); 返回值:若成功则返回0,出错则返回-1
调用getsockname之前,设置alenp为一个指向整数的指针,该整数指定缓冲区sockaddr的大小。返回时,该整数会被设置成返回地址的大小。如果该地址和提供的缓冲区长度不匹配,则将其截断而不报错。如果当前没有绑定到该套接字的地址,其结果没有定义。
如果套接字已经和对方连接,调用getpeername来找到对方的地址。
#include <sys/socket.h> int getpeername(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp); 返回值:若成功则返回0,若出错则返回-1
除了返回的是对方的地址之外,函数getpeername和getsockname一样。
本篇博文内容摘自《UNIX环境高级编程》(第2版),仅作个人学习记录所用。关于本书可参考:http://www.apuebook.com/。