Linux C 网络编程

2017-08-21 language c/cpp

在 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 sockaddrstruct 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 只返回可用地址,例如服务端支持 ipv4ipv6,而本地只支持 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.0255.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 越多就用哪种。