Socket能够实现网络上的不同主机之间或同一主机的不同对象之间的数据通信。所以,Socket已经是一类通用通信接口的集合。
- 地址表示数据结构
IP协议使用的地址描述数据结构,使用需要包括头文件netinet/in.h。
Linux下该结构的典型原型声明如下:
236 /* Structure describing an Internet socket address. */
237 struct sockaddr_in
238 {
239 __SOCKADDR_COMMON (sin_);
240 in_port_t sin_port; /* Port number. */
241 struct in_addr sin_addr; /* Internet address. */
242
243 /* Pad to size of `struct sockaddr'. */
244 unsigned char sin_zero[sizeof (struct sockaddr) -
245 __SOCKADDR_COMMON_SIZE -
246 sizeof (in_port_t)
247 sizeof (struct in_addr)];
248 };
243 行以后的填充字段我们这里不用深入讨论,以下我们讨论我们需要关心并填充的字段。
其中240行的in_port_t sin_port为端口号,应该是一个16位二进制整数,通常1024号以下端口需要root权限才可以使用。另外有很多已经约定对应了特定服务的端口号,具体可以查看/etc/services,在选用自定义协议端口号时,需要尽量不要和已知服务重合。
241 行的struct in_addr sin_addr Socket在通信时使用的IP地址结构,Linux下原型如下:
29 /* Internet address. */
30 typedef uint32_t in_addr_t;
31 struct in_addr
32 {
33 in_addr_t s_addr;
34 };
填充这个结构的s_addr域即可,这是一个32位二进制整数代表的IP地址。对应一个本机有效网络接口的地址,也可以填充为INADDR_ANY,来代表本机所有可用的网络地址。大部分时候都会用INADDR_ANY来填充此处就可以了。
239行是一个宏:_SOCKADDR_COMMON (sin);,Linux上,该宏的定义在bits/sockaddr.h文件中,原型如下:
34 #define __SOCKADDR_COMMON(sa_prefix) \
35 sa_family_t sa_prefix##family
这个宏在编译时会被展开为如下形式:
sa_family_t sin_family
该字段赋值为AF_INET,表示为IPv4协议族。
一段典型的填充IP地址数据结构的代码类似这样:
……
struct sockaddr_in addr;
……
addr.sin_family = AF_INET; /* 使用IPv4协议 */
addr.sin_port = htons(80) /* 设置端口号为80 */
addr.sin_addr.s_addr = inet_addr(“192.168.0.1”) /* 设置IP地址为192.168.0.1 */
注意:sin_port和sin_addr.s_addr两个值都是多字节的整数,socket规定这里必须使用网络字节序。
- 网络字节序和本地字节序之间的转换
手工进行字节序的转换往往是不方便的,对于可移植的程序来说更是如此。总是需要知道自己的本地主机字节序也是很麻烦的。所以,系统提供了四个固定的函数,用来在本地字节序和网络字节序之间转换。这四个函数包含在头文件arpa/inet.h中,分别是:
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong):
uint16_t ntohs(uint16_t netshort);
这四个函数功能依次列举如下:
- 32位整数从主机字节序转换为网络字节序;
- 16位整数从主机字节序转换为网络字节序;
- 32位整数从网络字节序转换为主机字节序;
- 16位整数从网络字节序转换为主机字节序。
- 主机名和地址转换函数
在实际网络编程过程中,往往需要在IP地址的点分十进制表示和二进制表示之间相互转化,也需要进行主机名和地址的转换,系统提供了一系列函数,一般需要包含一下头文件netinet/in.h和arpa/inet.h。
- in_addr_t inet_addr(const char *cp)
这个函数将一个点分十进制的IP地址字符串转换成in_addr_t类型,该类型实际上是一个32位无符号整数,事实上就是前文提到的 struct in_addr结构中的s_addr域的数据类型。注意这个二进制表示的IP地址规定是网络字节序。
这个函数其实在前文举例填充struct sockaddr_in的时候用过了。192.168.0.1在PC上会被转换成0x0100A8C0。
- char *inet_ntoa(struct in_addr in)
此函数可以将结构struct in_addr中的二进制IP地址转换为一个点分十进制表示的字符串,返回这个字符串的首指针。使用起来很方便。但是要注意,它返回的缓冲区是静态分配的,在并发或者异步使用时要小心,可能缓冲区随时可能被其它调用改写。
如下调用,会将一个网络字节序二进制无符号32位整数表示的IP地址0x0100A8C0转换为点分十进制表示“192.168.0.1”。
……
char *str;
struct in_addr addr = {
s_addr = 0x0100A8C0,
}
……
str = inet_ntoa(addr);
……
- 通过主机名获取IP地址
实际应用中,很多时候得到的通信另一方是主机名,所以需要将主机名转换为IP地址。传统上,有两个函数声明在netdb.h中来进行这个操作。
其中一个是gethostbyname()函数,原型如下:
struct hostent *gethostbyname(const char *name);
直接根据主机名字符串返回一个struct hostent结构。此返回的数据结构有可能是静态分配的。
还有一个函数gethostbyname2(),它在Linux/glibc中是一个GNU扩展,原型如下:
struct hostent *gethostbyname2(consts char *name, in af);
相对gethostbyname(),多一个af参数,可以指明需要解析的地址协议类型,对于IPv4就是AF_INET。其他参数和行为类似。
其中struct hostent在Linux下原型是这样的:
99 /* Description of data base entry for a single host. */
100 struct hostent
101 {
102 char *h_name; /* Official name of host. */
103 char **h_aliases; /* Alias list. */
104 int h_addrtype; /* Host address type. */
105 int h_length; /* Length of address. */
106 char **h_addr_list; /* List of addresses from name server. */
107 #if defined __USE_MISC || defined __USE_GNU
108 # define h_addr h_addr_list[0] /* Address, for backward compatibility.*/
109 #endif
110 };
其中h_name是主机名。h_addr_list是一个变长指针表,除最后一个指针为NULL表示结束外,每个非NULL成员均分别指向一个网络字节序表示的二进制IP地址
通常的使用流程如下:
……
22 /* hent = gethostbyname(hname); */
23 hent = gethostbyname2(hname, AF_INET);
24
25 if (NULL == hent) {
26 perror("gethostbyname failed");
27 fprintf(stderr, "host: %s\n", hname);
28 goto failure;
29 }
30
31 printf("hostname: %s\naddress list: ", hent->h_name);
32 for(i = 0; hent->h_addr_list[i]; i++) {
33 printf("%s\t", inet_ntoa(*(struct in_addr*)(hent->h_addr_list[i])));
34 }
……
23行使用gethostbyname2()和22行注释中的gethostbyname()在这样的用法下是相同的。25行检察返回值,如果是NULL说明获取失败。31行可以打印出数据结构中存储的主机名。32到34行的循环中打印出全部的IP地址,一个主机名可能对应多个IP地址。列表中出现NULL指针表示IP地址列表结束。