在 linux 中的网络编程会涉及到很多的细节,这里简单整理常见的问题,例如套接子信息、域名解析等等,有些也可能不怎么常见,以备不时之需。
结构体
在 linux c 的网络编程中包含了 4 个比较核心的数据结构,包括了 struct sockaddr_in
struct sockaddr_in6
struct sockaddr
struct sockaddr_storage
,其大小分别为 16bytes、28bytes、16bytes、128bytes 。
对于 ipv4 来说,struct sockaddr
是通用的 socket 地址,其定义如下:
#include <netinet/in.h>
// ipv4 af_inet
struct sockaddr {
unsigned short sa_family; // 2 bytes address family, af_xxx
char sa_data[14]; // 14 bytes of protocol address
};
struct sockaddr_in {
short sin_family; // 2 bytes e.g. af_inet, af_inet6
unsigned short sin_port; // 2 bytes e.g. htons(3490)
struct in_addr sin_addr; // 4 bytes see struct in_addr, below
char sin_zero[8]; // 8 bytes zero this if you want to
};
struct in_addr {
unsigned long s_addr; // 4 bytes load with inet_pton()
};
// ipv6 af_inet6
struct sockaddr_in6 {
short sin6_family; // 2 bytes af_inet6
unsigned short sin6_port; // 2 bytes transport layer port
uint32_t sin6_flowinfo; // 4 bytes ipv6 flow information
struct in6_addr sin6_addr; // 16 bytes ipv6 address
uint32_t sin6_scope_id; // 4 bytes ipv6 scope-id
};
struct in6_addr {
union {
uint8_t u6_addr8[16];
uint16_t u6_addr16[8];
uint32_t u6_addr32[4];
} in6_u;
#define s6_addr in6_u.u6_addr8
#define s6_addr16 in6_u.u6_addr16
#define s6_addr32 in6_u.u6_addr32
};
其中 struct in_addr
就是 32 位 ip 地址。
struct in_addr {
unsigned long s_addr;
};
对于 ipv4 在使用的时候,一般使用 struct sockaddr_in
作为函数 (如 bind()
) 的参数传入,使用时再转换为 struct sockaddr
即可,毕竟都是 16 个字符长。
使用示例如下:
int sockfd;
struct sockaddr_in addr;
addr.sin_family = af_inet;
addr.sin_port = htons(myport);
addr.sin_addr.s_addr = inet_addr("192.168.0.1");
bzero(&(addr.sin_zero), 8);
sockfd = socket(af_inet, sock_stream, 0);
bind(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr));
实际上,最开始只有 ipv4 ,也就是为什么看到 struct sockaddr
和 struct sockaddr_in
的大小是相同的,而 struct sockaddr_in6
却大于两者。
出现了 ipv6 之后,为了兼容所有的协议,才出现了 struct sockaddr_storage
这一结构体。
总结
可以使用 struct sockaddr_storage
保存当前所有协议的地址信息,但是如果只支持 ipv4 和 ipv6 功能,在空间上会有些浪费,所以实际上建议使用 union
扩展一个。
域名解析
getaddrinfo()
函数的前身是做 dns 解析的 gethostbyname()
函数,现在通过 getaddrinfo()
可以做 dns 解析以及 service name 查询,不过是同步查询,如下是相关的声明:
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
int getaddrinfo(const char *node, const char *service,
const struct addrinfo *hints,
struct addrinfo **res);
void freeaddrinfo(struct addrinfo *res);
const char *gai_strerror(int errcode);
其中 node 可以是域名、ip,如 "www.example.com"
;而 service 可以是 "http"
或者端口号,其定义在 /etc/services
文件中。
使用场景
通常有两种使用方式:a) 建立 server 监听本机所有的 ip 地址;b) 作为客户端链接到服务器,需要解析服务器的地址信息。
int status;
struct addrinfo hints; /* 指定入参配置 */
struct addrinfo *res; /* 返回结果 */
memset(&hints, 0, sizeof(hints));
hints.ai_family = af_unspec; /* 返回ipv4以及ipv6,也可以指定 af_inet 或 af_inet6 */
hints.ai_socktype = sock_stream; /* 使用tcp协议 */
hints.ai_flags = ai_passive; /* 返回结果中会填写本地的ip地址 */
if ((status = getaddrinfo(null, "3490", &hints, &servinfo)) != 0) {
fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
exit(1);
}
其中 flags
参数设置为 ai_passive
表示获取本地 ip 地址,这样在调用函数时,第一个参数可以指定为 null
。
关于客户端的使用可以直接参考如下的示例。
#define _posix_source
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main(int argc, char **argv)
{
int status;
struct addrinfo hints, *res, *this;
char ipaddr[inet6_addrstrlen];
if (argc != 2) {
fprintf(stderr, "usage: showip hostname\n");
return 1;
}
memset(&hints, 0, sizeof hints);
hints.ai_family = af_unspec; /* af_inet(ipv4) af_inet6(ipv6) */
hints.ai_socktype = sock_stream; /* tcp stream sockets */
if ((status = getaddrinfo(argv[1], null, &hints, &res))) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status));
return 2;
}
printf("ip addresses for %s:\n\n", argv[1]);
for(this = res; this != null; this = this->ai_next) {
void *addr;
char *ipver;
if (this->ai_family == af_inet) { /* ipv4 */
struct sockaddr_in *ipv4;
ipv4 = (struct sockaddr_in *)this->ai_addr;
addr = &(ipv4->sin_addr);
ipver = "ipv4";
} else { /* ipv6 */
struct sockaddr_in6 *ipv6;
ipv6 = (struct sockaddr_in6 *)this->ai_addr;
addr = &(ipv6->sin6_addr);
ipver = "ipv6";
}
/* convert the ip to a string and print it */
inet_ntop(this->ai_family, addr, ipaddr, sizeof(ipaddr));
printf("%s:\t%s\n", ipver, ipaddr);
}
freeaddrinfo(res);
return 0;
}
另外,在设置 flag
时有两个常用的选项:
ai_addrconfig
只返回可用地址,例如服务端支持ipv4
和ipv6
,而本地只支持ipv4
,那么就只返回ipv4
地址。ai_canonname
返回真实地址,上述代码可以通过this->ai_canonname
获取。
ip 地址
ip 有两种表达方式,分别为点分十进制(numbers and dots notation) 以及整形 (binary data),对于 ipv4 实际上是一个 32bit 的整形,假设地址为 a.b.c.d
,那么对应的整形是 a<<24 + b<<16 + c<<8 + d
。
在 mysql 中,可以通过 select inet_aton('192.168.1.38');
函数将点分格式转化为整形,然后再通过 select inet_ntoa(3232235814);
执行反向转换。
而 ipv6 采用的是 128 位,通常以 16 位为一组,每组之间以冒号 :
分割,总共分为 8 组,例如 2001:0db8:85a3:08d3:1319:8a2e:0370:7344
。
地址转换
对于 c 语言中的地址转换函数,也就是 bsd 网络软件包,可通过 inet_addr()
、inet_aton()
和 inet_ntoa()
三个函数用于二进制地址格式与点分十进制之间的相互转换,但是仅仅适用于 ipv4。
另外,两个新函数 inet_ntop()
和 inet_pton()
具有相似的功能,字母 p 代表 presentation,字母 n 代表 numeric,并且同时支持 ipv4 和 ipv6 。
#include <sys/socket.h>
//----- 将点分地址转换成网络字节序的ip地址
/* inaddr_none: error */
in_addr_t inet_addr(const char *strptr);
/* 1:ok, 0:error */
int inet_aton(const char *strptr, struct in_addr *addrptr);
/* 1:ok, 0:invalid, -1:failed */
int inet_pton(int family, const char *strptr, void *addrptr);
//----- 将点分地址转换成主机字节序的ip地址
in_addr_t inet_network(const char *cp);
//----- 网络字节序ip转化点分十进制
char* inet_ntoa(struct in_addr inaddr);
/* null: error */
const char* inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
inet_addr()
与 inet_aton()
不同在于其返回值为转换后的 32 位网络字节序二进制值,不过这样会存在一个问题,返回的有效 ip 地址应该为 0.0.0.0
到 255.255.255.255
,如果函数出错,返回常量值 inaddr_none
(一般为 0xffffffff
),也就是 255.255.255.255
(ipv4 的有限广播地址) 。
对于 inet_ntop()
和 inet_pton()
两个函数,family 参数可以是 af_inet
或者 af_inet6
,如果不是这两个会返回错误,且将 errno
置为 eafnosupport
。
缓冲区的大小在 <netinet/in.h>
中定义:
#define inet_addrstrlen 16 /* for ipv4 dotted-decimal */
#define inet6_addrstrlen 46 /* for ipv6 hex string */
如果缓冲区无法容纳表达格式结果 (包括空字符),则返回一个空指针,并置 errno 为 enospc
。
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(void)
{
char ip[] = "255.0.0.1";
in_addr_t rc; /* 一般通过 typedef uint32_t in_addr_t; 定义 */
int status;
struct in_addr addr;
rc = inet_addr(ip); /* 返回网络字节序 */
if (rc == 0xffffffff) {
fprintf(stderr, "format error '%s'\n", ip);
return -1;
}
fprintf(stdout, "inet_addr() ==> 0x%x\n", rc);
rc = inet_network(ip); /* 返回主机字节序 */
if (rc == 0xffffffff) {
fprintf(stderr, "format error '%s'\n", ip);
return -1;
}
fprintf(stdout, "inet_network() ==> 0x%x\n", rc);
status = inet_aton(ip, &addr);
if (status == 0) {
fprintf(stderr, "format error '%s'\n", ip);
return -1;
}
fprintf(stdout, "inet_aton() rc(%d) ==> 0x%x\n", status, addr.s_addr);
/* support ipv4(af_inet) and ipv6(af_inet6) */
status = inet_pton(af_inet, ip, &addr.s_addr);
if (status == 0) {
fprintf(stderr, "format error '%s'\n", ip);
return -1;
}
fprintf(stdout, "inet_aton() rc(%d) ==> 0x%x\n", status, addr.s_addr);
char str[inet_addrstrlen];
if(inet_ntop(af_inet, &addr.s_addr, str, sizeof(str)) == null) {
fprintf(stderr, "format error 0x%x\n", addr.s_addr);
return -1;
}
fprintf(stdout, "inet_network() ==> %s\n", str);
return 0;
}
ipv6 格式
这里不介绍报文的格式,只介绍 ipv6 地址的格式。
ipv6 地址的 128 位分成了由冒号分割的 8 段,每段 2 个字节 16 位,这 16 位由 16 进制表示,这里是一些例子,左边是完整的格式,右边是缩写格式。
0000:0000:0000:0000:0000:0000:0000:0000 ::
0000:0000:0000:0000:0000:0000:0000:0001 ::1
ff02:0000:0000:0000:0000:0000:0000:0001 ff02::1
fc00:0001:a000:0b00:0000:0527:0127:00ab fc00:1:a000:b00::527:127:ab
2001:0000:1111:000a:00b0:0000:9000:0200 2001:0:1111:a:b0::9000:200
2001:0db8:0000:0000:abcd:0000:0000:1234 2002:db8::abcd:0:0:1234 or 2001:db8:0:0:abcd::1234
2001:0db8:aaaa:0001:0000:0000:0000:0100 2001:db8:aaaa:1::100
其中有两条缩写规则:
- 每段里前面的 0 可以省略,例如
:0001:
可缩写成:1:
、:0000:
可以缩写成:0:
; - 冒号里全是 0 可以忽略,相邻多个 0 可以一起忽略掉,例如
:0000:0000:
可以被缩写成::
。
注意,如果地址中有多个连续为 0 的段,只能将其中的一个缩写成 ::
,如果两个都缩写,就不知道每个缩写具体缩写了多少个 0 ,如上 2001:0db8:0000:0000:abcd:0000:0000:1234
的缩写,不能被缩写成 2001:db8::abcd::1234
,一般是哪种方法省略的 0 越多就用哪种。