0%

Linux网络编程基础API

Linux网络编程基础API

学习《Linux高性能服务器编程》第五章Linux网络编程基础API,为了印象深刻一些,多动手多实践,所以记下这个笔记。

socket地址API

主机字节序和网络字节序

计算机硬件有两种储存数据的方式:大端字节序(big endian)小端字节序(little endian)

  • 大端字节序:高位字节在前,低位字节在后,符合人类读写数值的方法。
  • 小端字节序:低位字节在前,高位字节在后

想要判别机器的字节序可以使用如下的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>

void byteorder()
{
union
{
short value;
char union_bytes[sizeof(short)];
} test;
test.value = 0x0102;
if ((test.union_bytes[0] == 1) && (test.union_bytes[1] == 2))
{
printf("big endian\n");
}
else if ((test.union_bytes[0] == 2) && (test.union_bytes[1] == 1))
{
printf("little endian\n");
}
else
{
printf("unknown...\n");
}
}

int main()
{

byteorder();
return 0;
}

运行结果:

image-20220809181538872

这段代码使用的原理是union变量所占用的内存长度等于最长的成员的内存长度。

所以testvalueunion_bytes是共用一段内存的。因为在c中short是16位也就是2字节,char是8位也就是1字节,所以union_bytes数组的大小是2。

我们给value赋值为0x0102。如果是机器是高位存储,那么union_bytes数组第一个元素存储0x01,第二个元素存储0x02,如果是机器是高位存储,那么union_bytes数组第一个元素存储0x02,第二个元素存储0x01

image-20220809185922374

扩展到32位,四字节来说以0x12345678为例,那么

大端字节序:0x12345678

小端字节序:0x78563412

总结来说就是大端字节序和小端字节序的区别就是以字节为单位的存储方式不同。

在网络中两台使用不同字节序的主机之间直接传递时,接收端必然会造成错误。书中说解决的方法是发送端总是把要发送的数据转化成大端字节序数据再发送,接受端知道传送过来的数据总是采用大端字节序,所以接收端根据自身采用的字节序再对数据进行一定的处理(小端进行转换,大端就不转换)。

Linux提供了4个函数来完成主机字节序和网络字节序之间的转换:

image-20220810153657086

1
2
3
4
5
#include <netinet/in.h>
uint32_t ntohl (uint32_t __netlong);
uint16_t ntohs (uint16_t __netshort);
uint32_t htonl (uint32_t __hostlong);
uint16_t htons (uint16_t __hostshort);

它们的含义是就是首字母缩写(这谁看的出来),比如”htonl”表示“host to network long”,即将长整型(32bit)的主机字节序数据转化为网络字节序数据。这四个函数中,长整型uint32_t函数通常用来转换IP地址,短整型uint16_t函数用来转化端口号。

简单示例展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <netinet/in.h>
#include <cstdio>

int main(int argc, char const *argv[])
{

uint16_t port = 258;
uint16_t p = htons(port);
port = ntohs(p);
printf("htons :%u \n", p);
printf("ntohs :%u \n", port);

return 0;
}

运行结果:

image-20220810181643405

513二进制:0000 0010 0000 0001

258二进制:0000 0001 0000 0010

可以看出两者字节序是不同的

socket地址

通用socket地址

sockaddr

socket网络接口中表示socket地址的是结构体sockaddr,他的定义在头<bits/socket.h>中,我看在我的电脑上看到的是如下的定义(各个版本不同,可能实现不同,我这里和书上就不大相同):

image-20220810185029006

1
2
3
4
5
6
7
#include <bits/socket.h>
/* Structure describing a generic socket address. */
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};

其中__SOCKADDR_COMMON定义在<bits/sockaddr.h>

image-20220810185204703

1
2
3
4
5
6
7
8
9
#include <bits/sockaddr.h>
/* POSIX.1g specifies this type name for the `sa_family' member. */
typedef unsigned short int sa_family_t;

/* This macro is used to declare the initial common members
of the data types used for socket addresses, `struct sockaddr',
`struct sockaddr_in', `struct sockaddr_un', etc. */

#define __SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##family

__SOCKADDR_COMMON是定义的一个函数,它返回一个sa_family_t类型的数据,数据的名字是sa_prefixfamily,其中sa_prefix是你传进去的值。比如:__SOCKADDR_COMMON (sa_)其实就是返回sa_family_t sa_family

所以sockaddr其实就是两个成员,一个是sa_family_t(地址族)类型的变量sa_family,一个char数组类型的变量sa_data

sa_family_t常见的协议族(protocol family,也称domain)和对应的地址族如下表所示

协议族 地址族 描述
PF_UNIX AF_UNIX UNIX本地协议族
PF_INET AF_INET IPv4协议族
PF_INET6 AF_INET6 IPv6协议族

宏PF_*和AF_*都定义在<bits/socket.h>当中,两者的值相同,所以两者可以混用

image-20220811094342093

image-20220811094510529

sa_data成员用于存放socket地址值。不同的协议族的地址值有不同的含义和长度。

协议族 地址值含义和长度
PF_UNIX 文件的路径名,长度可达108字节
PF_INET 16bit端口号和32bit IPv4地址,共6字节
PF_INET6 16bit端口号,32bit流标识,128bit IPv6地址,32bit范围ID,共26字节
sockaddr_storage

可以看出14字节的sa_data根本无法容纳多数协议族的地址值。所以,Linux中定义了新的通用socket地址结构体(其实就是把存放地址的数组加大了):

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <bits/socket.h>
/* Structure large enough to hold any socket address (with the historical
exception of AF_UNIX). */
#define __ss_aligntype unsigned long int
#define _SS_PADSIZE \
(_SS_SIZE - __SOCKADDR_COMMON_SIZE - sizeof (__ss_aligntype))

struct sockaddr_storage
{
__SOCKADDR_COMMON (ss_); /* Address family, etc. */
char __ss_padding[_SS_PADSIZE];
__ss_aligntype __ss_align; /* Force desired alignment. */
};

其中_SS_SIZE__SOCKADDR_COMMON_SIZE<bits/sockaddr.h>当中

1
2
3
4
5
#include <bits/sockaddr.h>
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))

/* Size of struct sockaddr_storage. */
#define _SS_SIZE 128

image-20220811135111456

image-20220811135134566

这个结构体提供了足够大的空间用于存放地址值,并且是内存对齐的。

ss_(其实是ss_family)是sa_family_t类型(介绍sockaddr有提到),即unsigned short int类型,2字节。

__ss_align__ss_aligntype类型,即unsigned long int类型,4字节

__ss_paddingchar类型数组,大小为_SS_PADSIZE,而_SS_PADSIZE=_SS_SIZE - __SOCKADDR_COMMON_SIZE - sizeof (__ss_aligntype)=128-2-4=122字节,完全足够保存地址值。

综上sockaddr_storage是128字节大小,保证了内存对齐。

专用socket地址

上面两种通用的socket地址使用起来显然不够方便,因为将IP地址和端口等信息直接放在同一个char数组中,那要得到IP地址和端口信息都得费好大劲进行操作。因此,Linux为各个协议族提供了专门的socket地址结构体。

UNIX本地协议族使用sockaddr_un,数据结构很简单,只有一个保存地址族类型的sun_(其实是sun_family)和保存文件位置的sun_path

1
2
3
4
5
6
7
#include <sys/un.h>
/* Structure describing the address of an AF_LOCAL (aka AF_UNIX) socket. */
struct sockaddr_un
{
__SOCKADDR_COMMON (sun_);
char sun_path[108]; /* Path name. */
};

IPv4协议族使用sockaddr_in

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <netinet/in.h>
/* Structure describing an Internet socket address. */
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */

/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr)
- __SOCKADDR_COMMON_SIZE
- sizeof (in_port_t)
- sizeof (struct in_addr)];
};

其中in_port_t定义、in_addr_t结构如下

1
2
3
4
5
6
7
8
9
10
#include <netinet/in.h>
/* Type to represent a port. */
typedef uint16_t in_port_t;

/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};

image-20220811143542024

image-20220811143600470

image-20220811143622907

可以看的出来sin_(其实是sin_family)存放地址族类型,sin_port存放端口,sin_addr存放地址。sin_zero为了让sockaddr_in大小和sockaddr相同,为什么有这个成员,个人感觉这是因为所以专用socket在实际使用中都需要转化为通用socket地址类型socketaddr,因为socket编程接口使用的是参数类型是socketaddr

IPv6协议族使用sockaddr_in6

1
2
3
4
5
6
7
8
9
10
#include <netinet/in.h>
/* Ditto, for IPv6. */
struct sockaddr_in6
{
__SOCKADDR_COMMON (sin6_);
in_port_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};

其中in6_addr如下,因为IPv6不是学习重点,这里就不过多展开介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* IPv6 address */
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
#ifdef __USE_MISC
# define s6_addr16 __in6_u.__u6_addr16
# define s6_addr32 __in6_u.__u6_addr32
#endif
};

除此之外需要注意:所有专用socket地址(以及sockaddr_storage)类型的变量在实际使用时都需要转化为通用socket地址类型sockaddr(强制转换即可),因为所有socket编程接口使用的地址参数的类型都是sockaddr。

IP地址转化函数

通常来说,人们更喜欢用点分十进制的字符串来表示IPv4地址,但是在编程的过程中,我们需要把这个字符串转化为整数才能使用,但是输出的时候我们又需要把整数转化成点分十进制的字符串,这样方便观察。所以系统提供了3个函数用于点分十进制的字符串IPv4地址和整数的IPv4地址之间的转化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <arpa/inet.h>

/* Convert Internet host address from numbers-and-dots notation in CP
into binary data in network byte order. */
extern in_addr_t inet_addr (const char *__cp) __THROW;

/* Convert Internet host address from numbers-and-dots notation in CP
into binary data and store the result in the structure INP. */
extern int inet_aton (const char *__cp, struct in_addr *__inp) __THROW;

/* Convert Internet number in IN to ASCII representation. The return value
is a pointer to an internal array containing the string. */
extern char *inet_ntoa (struct in_addr __in) __THROW;

inet_addr函数将用点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址。它失败时返回 INADDR_NONE

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <arpa/inet.h>
#include <cstdio>

int main(int argc, char const *argv[])
{

in_addr_t ip = inet_addr("192.168.167.14");
if (ip == INADDR_NONE)
printf("ip error\n");
else
printf("ip convert by inet_addr %u \n", ip);
return 0;
}

image-20220811170759741

inet_aton功能和inet_addr相同,但是将结果存在在in_addr_t指向的地址结构当中,函数成功返回1,失败返回0。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <arpa/inet.h>
#include <cstdio>

int main(int argc, char const *argv[])
{
struct in_addr ip;
int ret = inet_aton("192.168.167.14", &ip);
if (0 == ret)
printf("ip error\n");
else
printf("ip convert by inet_aton %u \n", ip.s_addr);
return 0;
}

image-20220811171444397

inet_ntoa函数将整数的IPv4地址转化为点分十进制字符串的IPv4。成功时返回转换的字符串地址值,失败时返回-1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <arpa/inet.h>
#include <cstdio>

int main(int argc, char const *argv[])
{
struct in_addr ip;

int ret = inet_aton("192.168.167.14", &ip);
if (0 == ret)
printf("ip error\n");
else
printf("ip convert by inet_aton %u \n", ip.s_addr);

char *ip_str = inet_ntoa(ip);
printf("address :%s \n", ip_str);
return 0;
}

image-20220811173503409

需要注意的是inet_ntoa函数内部使用一个静态变量存储转化的结果,函数的返回值指向该静态内存,因此inet_ntoa是不可重入的,这一点需要多注意。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <arpa/inet.h>
#include <cstdio>

int main(int argc, char const *argv[])
{
struct in_addr ip;

inet_aton("192.168.167.14", &ip);
char *ip_str1 = inet_ntoa(ip);

inet_aton("192.168.167.15", &ip);
char *ip_str2 = inet_ntoa(ip);

printf("address :%s \n", ip_str1);
printf("address :%s \n", ip_str2);
return 0;
}

image-20220811180430697

除此之外,下面两个函数也能完成前三个函数的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <arpa/inet.h>
/* Convert from presentation format of an Internet number in buffer
starting at CP to the binary network format and store result for
interface type AF in buffer starting at BUF. */
extern int inet_pton (int __af, const char *__restrict __cp,
void *__restrict __buf) __THROW;

/* Convert a Internet address in binary network format for interface
type AF in buffer starting at CP to presentation form and place
result in buffer of length LEN astarting at BUF. */
extern const char *inet_ntop (int __af, const void *__restrict __cp,
char *__restrict __buf, socklen_t __len)
__THROW;

inet_pton函数将用字符串表示的P地址__cp(用点分十进制字符串表示的IPv4地址或用十六进制字符串表示的IPv6地址)转换成用网络字节序整数表示的IP地址,并把转换结果存储于__buf指向的内存中。其中,__af参数指定地址族,可以是AF_INET或者AF_INET6inet_pton成功时返回1,失败则返回0并设置errno

__restrictemmm目前找不到定义,但是看了下restrict关键字,是指告诉编译器传入的两个指针不指向同一数据,方便进行优化用来提升性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <arpa/inet.h>
#include <cstdio>
#include <errno.h>

int main(int argc, char const *argv[])
{
char ip_str[] = "192.168.167.42";
in_addr_t ip;
int ret = inet_pton(AF_INET, ip_str, &ip);
if (0 == ret)
perror("ip error\n");
else
printf("ip convert by inet_pton %u \n", ip);

struct in_addr in_ip;
ret = inet_pton(AF_INET, ip_str, &in_ip);
if (0 == ret)
perror("ip error\n");
else
printf("ip convert by inet_pton %u \n", in_ip.s_addr);

return 0;
}

image-20220811183002035

inet_ntop函数进行相反的转换,前三个参数的含义与inet_pton的参数相同,最后一个参数 __len指定目标存储单元的大小。下面的两个宏能帮助我们指定这个大小(分别用于IPv4和IPv6):

1
2
3
#include <netinet/in.h>	
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46

inet_ntop成功时返回目标存储单元的地址,失败则返回NULL并设置errno。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <arpa/inet.h>
#include <cstdio>
#include <errno.h>

int main(int argc, char const *argv[])
{
char ip_str[] = "192.168.167.42";

struct in_addr in_ip;
int ret = inet_pton(AF_INET, ip_str, &in_ip);
if (0 == ret)
perror("ip error\n");
else
printf("ip convert by inet_pton %u \n", in_ip.s_addr);

char ip_str2[1024];

const char *ip_str3 = inet_ntop(AF_INET, &in_ip, ip_str2, sizeof(ip_str2));

if (ip_str3 == NULL)
perror("ip error \n");
else
{
printf("address :%s \n", ip_str2);
printf("address :%s \n", ip_str3);
}

return 0;
}

image-20220811220718920

值得注意的是ip_str2ip_str3的地址相同,也就是说传入参数和返回值相同,虽然不知道为啥这样设计。

image-20220811221357471

创建socket

socket使用系统调用可以创建一个socket

1
2
3
4
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

domain参数是告诉系统使用的是那个底层协议族,一般都是使用IPv4,所以使用AF_INET即可。关于socket系统调用支持的所有协议族,可以查看man手册(虽然参数名不一样,但是并无大碍)。

image-20220812095854483

type参数指定服务类型。服务类型主要有SOCK_STREAM服务(流服务)和SOCK_UGRAM(数据报)服务。对TCP/IP协议族而言,其值取SOCK_STREAM表示传输层使用TCP协议,取SOCK_DGRAM表示传输层使用UDP协议。

image-20220812101617247

并且从Linux内核2.6.17起,增加了SOCK_NONBLOCKSOCK_CLOEXEC这两个标志值,表示将新创建的socket设为非阻塞,以及fork调用创建子进程时在子进程中关闭该socket。在Linux内核2.6.17前,需要调用fcntl进行设置。

protocol参数设置具体的协议。但是在前两个参数确定的情况下,这个参数的值基本上唯一的,所有几乎在所有情况下,我们都把这个值设置为0,表示使用默认协议。

socket系统调用成功时返回一个socket文件描述符,失败则返回-1并设置errno。

1
2
3
4
5
6
7
8
9
#include <sys/socket.h>

int main(int argc, char const *argv[])
{
int lfd = 0;
lfd = socket(AF_INET, SOCK_STREAM, 0); //创建一个 socket
close(lfd);
return 0;
}

命名socket

创建socket时,我们指定了地址族,但是并没有给定具体的地址,这样作为服务器别人是访问不到我们的。将一个socket 与socket地址绑定称为给socket命名。命名socket的系统调用是bind。

1
2
3
4
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

bindaddr所指的socket地址分配给未命名的sockfd文件描述符,addrlen参数指出该socket地址的长度。

image-20220812104757237

bind成功时返回0,失败则返回-1并设置errno。其中两种常见的errnoEACCESEADDRINUSE,它们的含义分别是:

  • EACCES,被绑定的地址是受保护的地址,仅超级用户能够访问。比如普通用户将socket绑定到知名服务端口(端口号为0~1023)上时,bind将返回EACCES错误。
  • EADDRINUSE,被绑定的地址正在使用中。比如将socket绑定到一个处于TIME_WAIT状态的socket地址。

image-20220812105430276

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>


#define SERV_PORT 8080

int main(int argc, char const *argv[])
{
int lfd = 0, cfd = 0;
int ret, i;

struct sockaddr_in serv_addr, clit_addr; // 定义服务器地址结构 和 客户端地址结构
socklen_t clit_addr_len; // 客户端地址结构大小

serv_addr.sin_family = AF_INET; // IPv4
serv_addr.sin_port = htons(SERV_PORT); // 转为网络字节序的 端口号
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 获取本机任意有效IP

lfd = socket(AF_INET, SOCK_STREAM, 0); //创建一个 socket
if (lfd == -1)
{
perror("socket error");
}

bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); //给服务器socket绑定地址结构(IP+port)
close(lfd);
return 0;
}

监听socket

socket被命名后,还需要调用listen创建一个监听队列来存放处理的客户连接。

1
2
3
4
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);

image-20220812112118847

sockfd参数指定被监听的socket。backlog参数提示内核监听队列的最大长度。监听队列的长度如果超过backlog,服务器将不受理新的客户连接,客户端也将收到ECONNREFUSED错误信息。

在内核版本2.2之前的Linux中,backlog参数是指所有处于半连接状态(SYN_RCVD)和完全连接状态(ESTABLISHED)的socket 的上限。但自内核版本2.2之后,它只表示处于完全连接状态的socket的上限,处于半连接状态的socket的上限则由/proc/sys/net/ipv4/tcp_max_syn_backlog内核参数定义。backlog参数的典型值是5。

image-20220812113608890

listen成功时返回0,失败则返回-1并设置erron

本来想测试backlog这个参数的效果,但是怎么也成功不了,不知道原因,以后有机会再进行尝试吧。

接受连接

接受连接通过accept进行

1
2
3
4
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

image-20220812143816774

sockfd指执行过listen的监听套接字的文件描述符。

addr是传出参数,用来获取接受连接的远端socket地址,地址的长度由addrlen参数指出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <arpa/inet.h>
#include <cstdio>
#include <string.h>
#include <cstdlib>
#include <assert.h>
#include <errno.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
if (argc <= 2)
{
printf("usage: %s ip_address port_number\n", basename(argv[0]));
return 1;
}
const char *ip = argv[1];
int port = atoi(argv[2]);

sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);

int sock = socket(AF_INET, SOCK_STREAM, 0);
assert(sock >= 0);

int ret = bind(sock, (sockaddr *)&address, sizeof(address));
assert(ret != -1);

ret = listen(sock, 5);
assert(ret != -1);

struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
int connfd = accept(sock, (struct sockaddr *)&client, &client_addrlength);

if (connfd < 0)
{
printf("errno is: %d\n", errno);
}
else
{
char remote[INET_ADDRSTRLEN];
printf("connected with ip: %s and port: %d\n",
inet_ntop(AF_INET, &client.sin_addr, remote, INET_ADDRSTRLEN), ntohs(client.sin_port));
close(connfd);
}

close(sock);
return 0;
}

image-20220812151534158

并且书上面的实验说明了accept直接从监听队列中取出连接,而不论连接处于何种状态,更不关心任何网络状况的变化。比如:客户端在服务器accept之前就断网了,accept还是可以正常进行,它并不会返回错误。

发起连接

发动连接一般是客户端进行的,通过系统调用connect与服务器进行连接。

1
2
3
4
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

image-20220812154447716

sockfd参数由socket系统调用返回一个socketaddr参数是服务器监听的socket地址。addrlen参数指这个地址长度。

connect成功时返回0。一旦成功建立连接,sockfd就唯一地标识了这个连接,客户端就可以通过读写sockfd来与服务器通信。connect失败则返回-1并设置errno。其中两种常见的errnoECONNREFUSEDETIMEDOUT,它们的含义如下:

  • ECONNREFUSED表示目标端口不存在,连接被拒绝。
  • ETIMEDOUT表示连接超时。

关闭连接

关闭连接一般来说使用

1
2
#include <unistd.h>
int close(int fd);

fd参数是待关闭的socket。不过,close并不会立即关闭这个连接,而是将fd的引用数量减1,直到fd引用数量为0,才真正关闭连接。在多进程程序中,一次fork系统调用默认将父进程中socket的引用计算加1,因此必须在子进程和父进程都对该socket进行close调用才能将连接关闭。

如果想立刻终止连接,直接调用shutdown

1
2
#include <sys/socket.h>
int shutdown(int sockfd, int how);

image-20220812160846669

sockfd参数是待关闭的socket,howto参数决定了shutdown的行为。

可选值 含义
SHUT_RD 关闭sockfd上读的这一半。应用程序不能再针对socket文件描述符执行读操作,并且该sockct接收缓冲区中的数据都被丢弃。
SHUT_WR 关闭sockfd上写的这一半。sockfd 的发送缓冲区中的数据会在真正关闭连接之前全部发送出去,应用程序不可再对该socket文件描述符执行写操作。这种情况下,连接处于半关闭状态。
SHUT_RDWR 同时关闭sockfd上的读和写。

可以看出shutdown可以灵活的关闭socket上的读或写。而close在关闭连接时只能将socket上的读和写同时关闭。

shutdown成功时返回0,失败则返回-1并设置errno

数据读写

TCP数据读写

对文件的读写操作readwrite同样适用于socket。但是socket编程接口提供了几个专门用于socket数据读写的系统调用,它们增加了对数据的读写的控制。在TCP中流数据读写的系统调用是:

1
2
3
4
5
#include <sys/types.h>
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

recv读取sockfd上的数据,buflen参数分别指定读缓冲区的位置和大小。

recv成功读取时返回实际读取到的数据长度,它可能小于我们期望的长度len。因此需要多次调用recv才能读取到完整的数据。recv返回0,意味着对方已经关闭连接。recv出错时返回-1并设置errno

send发送sockfd上的数据。buflen参数分别指定写缓冲区的位置和大小。

send成功读取时返回实际读取到的数据长度,出错时返回-1并设置errno

flags用于控制数据的接收和发送,一般来说设置为0,也可以进行设置,从而进行控制。

控制参数可以通过man手册进行查看,这里直接截取书上的表格

image-20220812171929139

UDP数据读写

1
2
3
4
5
#include <sys/types.h>
#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

针对UDP系统提供的是读写函数是recvfromsendto,其中函数recvfromsendto前4个参数和recvsend意义相同,最后两个是发送端/接收端的地址。因为UDP是没有连接的概念,所以调用这两个函数的时候都要指定地址。recvfromsendto的返回值和recvsend也相同,所以不用过多介绍。

除此之外,recvfromsendto也可以用于TCP使用,只需要把最后两个参数设置为NULL即可。

通用数据读写函数

1
2
3
4
5
#include <sys/types.h>
#include <sys/socket.h>

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

recvmsgsendmsg的参数中sockfdflags比较简单,复杂一些的参数就是msgmsg的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <sys/socket.h>:

struct iovec { /* Scatter/gather array items */
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};

struct msghdr {
void *msg_name; /* Optional address */
socklen_t msg_namelen; /* Size of address */
struct iovec *msg_iov; /* Scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* Ancillary data, see below */
size_t msg_controllen; /* Ancillary data buffer len */
int msg_flags; /* Flags on received message */
};

msg_name指向socket地址,对于TCP协议无意义,所以在TCP协议中设置为NULL,而对于UDP等其他协议就说明了发送或者接收的地址。msg_namelen指定socket地址的长度。

msg_ioviovec类型的指针,根据注释来判断应该是个数组。iovec结构体封装了一块内存的起始位置和长度。msg_iovlen指定这样的iovec结构对象有多少个。

对于recvmsg而言,数据将被读取并存放在msg_iovlen块分散的内存中,这些内存的位置和长度则由msg_iov指向的数组指定,这称为分散读( scatter read);对于sendmsg而言,msg_iovlen块分散内存中的数据将被一并发送,这称为集中写( gather write)。

为什么要有分散读和集中写呢,这其实是一个非常方便的使用,方便传输结构不同的数据。比如:发送http应答时,我们可以把前面的请求头请求的文件分为两个buffer,但是最终一起进行写入,减少了拼接带来的麻烦。同理我接收的时候也是想请求头请求的文件分开,所以使用分散读。

msg_flags成员无须设定,它会复制recvmsg/sendmsgflags参数的内容以影响数据读写过程。recvmsg还会在调用结束前,将某些更新后的标志设置到msg_flags中。

recvmsg/sendmsgflags参数以及返回值的含义均与sendrecvflags参数及返回值相同。

msg_controlmsg_controllen成员用于辅助数据的传送。目前书中并未进行讲解,后续再补充。

recvmsgsendmsg的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <arpa/inet.h>
#include <cstdio>
#include <string.h>
#include <cstdlib>
#include <assert.h>
#include <errno.h>
#include <unistd.h>

#define BUFFER_SIZE 256
int main(int argc, char const *argv[])
{
if (argc <= 2)
{
printf("usage: %s ip_address port_number\n", basename(argv[0]));
return 1;
}
const char *ip = argv[1];
int port = atoi(argv[2]);

sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);

int sock = socket(AF_INET, SOCK_STREAM, 0);
assert(sock >= 0);

int ret = bind(sock, (sockaddr *)&address, sizeof(address));
assert(ret != -1);

ret = listen(sock, 5);
assert(ret != -1);

struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
int connfd = accept(sock, (struct sockaddr *)&client, &client_addrlength);

if (connfd < 0)
{
printf("errno is: %d\n", errno);
}
else
{
char remote[INET_ADDRSTRLEN];
printf("connected with ip: %s and port: %d\n",
inet_ntop(AF_INET, &client.sin_addr, remote, INET_ADDRSTRLEN), ntohs(client.sin_port));

char buffer1[6];
char buffer2[BUFFER_SIZE];
struct msghdr msg;
bzero(&msg, sizeof(msg));
//设置集中写
struct iovec iovec_arry[2];
iovec_arry[0].iov_base = (void *)buffer1;
iovec_arry[0].iov_len = sizeof(buffer1);
iovec_arry[1].iov_base = (void *)buffer2;
iovec_arry[1].iov_len = sizeof(buffer2);

msg.msg_iov = iovec_arry;
msg.msg_iovlen = 2;

int n = recvmsg(connfd, &msg, 0);
assert(n != -1);
printf(" have recv %d byte msg1 %s and msg2 %s \n", n, buffer1, buffer2);
close(connfd);
}
close(sock);
return 0;
}

sendmsg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <arpa/inet.h>
#include <cstdio>
#include <string.h>
#include <cstdlib>
#include <assert.h>
#include <errno.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
if (argc <= 2)
{
printf("usage: %s ip_address port_number\n", basename(argv[0]));
return 1;
}
const char *ip = argv[1];
int port = atoi(argv[2]);

sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);

int sock = socket(AF_INET, SOCK_STREAM, 0);
assert(sock >= 0);

int ret = connect(sock, (struct sockaddr *)&address, sizeof(address));
assert(ret == 0);

char buffer1[] = "hello";
char buffer2[] = "world";

struct msghdr msg;
bzero(&msg, sizeof(msg));
// 因为是针对TCP,所以msg_name无意义
msg.msg_name = NULL;
msg.msg_namelen = 0;

//设置集中写
struct iovec iovec_arry[2];
iovec_arry[0].iov_base = (void *)buffer1;
iovec_arry[0].iov_len = sizeof(buffer1);
iovec_arry[1].iov_base = (void *)buffer2;
iovec_arry[1].iov_len = sizeof(buffer2);

msg.msg_iov = iovec_arry;
msg.msg_iovlen = 2;

int n = sendmsg(sock, &msg, 0);
assert(n != -1);
printf(" have send %d byte msg1 %s and msg2 %s \n", n, buffer1, buffer2);
close(sock);
return 0;
}

运行结果:

image-20220813123219330

需要注意的是recvmsg只有在前面的buffer使用完之后,才会使用后面的buffer。这也是为啥把buffer1的大小设置为6。

地址信息函数

如果我们要查询一个连接socket的本端socket地址,以及远端的socket地址,可以使用下面两个函数。

1
2
3
4
#include <sys/socket.h>

int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

getsockname获得sockfd对应的本端地址(本地自己的地址),getpeername获得sockfd对应的远端地址(远端连接的地址)。两个函数都把地址存储在addr参数指定的内存中,将该地址的长度存放在addrlen当中。

如果实际socket地址的长度大于addr所指内存区的大小,那么该socket地址将被截断。两个函数成功时返回0,失败返回-1并设置errno

我写了代码测试了一下,使用telnet进行连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <arpa/inet.h>
#include <cstdio>
#include <string.h>
#include <cstdlib>
#include <assert.h>
#include <errno.h>
#include <unistd.h>

#define BUFFER_SIZE 256
int main(int argc, char const *argv[])
{
if (argc <= 2)
{
printf("usage: %s ip_address port_number\n", basename(argv[0]));
return 1;
}
const char *ip = argv[1];
int port = atoi(argv[2]);

sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);

int sock = socket(AF_INET, SOCK_STREAM, 0);
assert(sock >= 0);

int ret = bind(sock, (sockaddr *)&address, sizeof(address));
assert(ret != -1);

ret = listen(sock, 5);
assert(ret != -1);

struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
int connfd = accept(sock, (struct sockaddr *)&client, &client_addrlength);

if (connfd < 0)
{
printf("errno is: %d\n", errno);
}
else
{
char remote[INET_ADDRSTRLEN];
printf("connected with ip: %s and port: %d\n",
inet_ntop(AF_INET, &client.sin_addr, remote, INET_ADDRSTRLEN), ntohs(client.sin_port));

// 获得本端地址
struct sockaddr_in addr;
socklen_t addrlen = sizeof(addr);
ret = getsockname(connfd, (sockaddr *)&addr, &addrlen);
assert(ret == 0);
printf("getsockname info ip: %s and port: %d , addrlen is %d \n",
inet_ntop(AF_INET, &addr.sin_addr, remote, INET_ADDRSTRLEN), ntohs(addr.sin_port), addrlen);

// 获得远端地址
struct sockaddr_in addr2;
socklen_t addrlen2 = sizeof(addr2);
ret = getpeername(connfd, (sockaddr *)&addr2, &addrlen2);
assert(ret == 0);
printf("getpeername info ip: %s and port: %d , addrlen is %d \n",
inet_ntop(AF_INET, &addr2.sin_addr, remote, INET_ADDRSTRLEN), ntohs(addr2.sin_port), addrlen2);

close(connfd);
}

close(sock);
return 0;
}

image-20220813163338463

socket选项

读取和设置socket文件描述的方法如下

1
2
3
4
5
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

sockfd参数指定被操纵的目标socket,level参数指定要操作的协议选项,optname参数则指定选项的名字,optvaloptlen参数分别是操作选项的值和长度。截图了一下书中的表格。

socket选项

getsockoptsetsockopt这两个函数成功时返回0,失败时返回-1并设置errno

需要注意的是,在服务器端setsockopt最好在listen之前进行调用(因为连接socket只能由accept调用返回,而accept 从 listen 监听队列中接受的连接至少已经完成了TCP三次握手的前两个步骤)。同理,对客户端而言,这些socket选项则应该在调用connect 函数之前设置,因为connect调用成功返回之后,TC三次握手已完成。

SO_REUSEADDR

设置服务器可以立即重启,不需要等待TIME_WAIT状态过去,可以使用SO_REUSEADDR

1
2
3
4
int sock = socket( PF_INET, SOCK_STREAM, 0 );
assert( sock >= 0 );
int reuse = 1;
setsockopt( sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof( reuse ) );

经过setsockopt的设置之后,即使sock处于TIME_WAIT状态,与之绑定的socket地址也可以立即被重用。

SO_RCVBUF和SO_SNDBUF

SO_RCVBUFSO_SNDBUF分别设置TCP接收缓冲区和发送缓冲区的大小。但是,当我们使用setsockopt设置TCP缓冲区大小时,系统都会将其值进行加倍,并且不会小于某个值。TCP接收缓冲区最小值是256字节,发送缓冲区最小是2048字节。小值是2048字节(不过,不同的系统可能有不同的默认最小值)。系统这样做的目的,主要是确保一个TCP连接拥有足够的空闲缓冲区来处理拥塞(比如快速重传算法就期望TCP接收缓冲区能至少容纳4个大小为SMSS的TCP报文段)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

#define BUFFER_SIZE 1024

int main(int argc, char *argv[])
{
if (argc <= 4)
{
printf("usage: %s ip_address port_number receive_buffer_size\n", basename(argv[0]));
return 1;
}
const char *ip = argv[1];
int port = atoi(argv[2]);

struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);

int sock = socket(PF_INET, SOCK_STREAM, 0);
assert(sock >= 0);
int recvbuf = atoi(argv[3]);
int len = sizeof(recvbuf);
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &recvbuf, sizeof(recvbuf));
getsockopt(sock, SOL_SOCKET, SO_RCVBUF, &recvbuf, (socklen_t *)&len);
printf("the receive buffer size after settting is %d\n", recvbuf);

int sendbuf = atoi(argv[4]);
len = sizeof(sendbuf);
setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &sendbuf, sizeof(sendbuf));
getsockopt(sock, SOL_SOCKET, SO_SNDBUF, &sendbuf, (socklen_t *)&len);
printf("the tcp send buffer size after setting is %d\n", sendbuf);

close(sock);
return 0;
}

image-20220813180440008

emmm不知道为啥大小是这样,后续再看看。

网络信息API

socket当中两要素:IP和端口号,都是用数值表示的。但是有时候我们可以使用主机名代替IP,使用服务名代替端口号。

1
2
telnet 127.0.0.1 80
telnet localhost www

这个功能就是使用网络信息API实现的。

gethostbyname和gethostbyaddr

gethostbyname函数根据主机名称获取主机的完整信息,gethostbyaddr函数根据IP地址获取主机的完整信息。gethostbyname函数通常先在本地的/etc/hosts配置文件中查找主机,如果没有找到,再去访问DNS服务器。这两个函数的定义如下:

1
2
3
4
5
6
7
#include <netdb.h>
extern int h_errno;

struct hostent *gethostbyname(const char *name);

#include <sys/socket.h> /* for AF_INET */
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);

name参数表示目标主机的主机名。

addr参数指定目标主机的IP地址,len参数指定addr的所指定IP的长度

type参数指定IP地址的类型,比如AF_INET

其中hostent定义如下:

1
2
3
4
5
6
7
8
9
#include <netdb.h>

struct hostent {
char *h_name; /* official name of host */
char **h_aliases; /* alias list */
int h_addrtype; /* host address type */
int h_length; /* length of address */
char **h_addr_list; /* list of addresses */
}

参数介绍

h_name:主机名
h_aliases:主机别名列表,可能有多个
h_addrtype:地址类型(地址族)
h_length:地址长度
h_addr_list:按网络字节序列出的主机IP地址列表

从网上找了个图显示了一下

img

gethostbyname举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>

int main(int argc, char **argv)
{
if (argc != 2)
{
printf("Use example: %s www.baidu.com\n", *argv);
return -1;
}

char *name = argv[1];
struct hostent *hptr;

hptr = gethostbyname(name);
if (hptr == NULL)
{
printf("gethostbyname error for host: %s: %s\n", name, hstrerror(h_errno));
return -1;
}
//输出主机名
printf("\tofficial: %s\n", hptr->h_name);

//输出主机的别名
char **pptr;
char str[INET_ADDRSTRLEN];
for (pptr = hptr->h_aliases; *pptr != NULL; pptr++)
{
printf("\talias: %s\n", *pptr);
}

//输出ip地址
switch (hptr->h_addrtype)
{
case AF_INET:
pptr = hptr->h_addr_list;
for (; *pptr != NULL; pptr++)
{
printf("\taddress: %s\n",
inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)));
}
break;
default:
printf("unknown address type\n");
break;
}

return 0;
}

image-20220814105118617

gethostbyaddr举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>

int main(int argc, char **argv)
{
if (argc != 2)
{
printf("Use example: %s 127.0.0.1\n", *argv);
return -1;
}

char *ip = argv[1];
struct in_addr addr;

inet_pton(AF_INET, ip, &addr);
struct hostent *hptr;

hptr = gethostbyaddr(&addr, sizeof(addr), AF_INET);
if (hptr == NULL)
{
printf("gethostbyaddr error for host: %s: %s\n", ip, hstrerror(h_errno));
return -1;
}
//输出主机名
printf("\tofficial: %s\n", hptr->h_name);

//输出主机的别名
char **pptr;
char str[INET_ADDRSTRLEN];
for (pptr = hptr->h_aliases; *pptr != NULL; pptr++)
{
printf("\talias: %s\n", *pptr);
}

//输出ip地址
switch (hptr->h_addrtype)
{
case AF_INET:
pptr = hptr->h_addr_list;
for (; *pptr != NULL; pptr++)
{
printf("\taddress: %s\n",
inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)));
}
break;
default:
printf("unknown address type\n");
break;
}

return 0;
}

image-20220814105222471

getservbyname和getservbyport

getservbyname函数根据名称获取某个服务的完整信息,getservbyport函数根据端口号获取某个服务的完整信息。它们实际上都是通过读取/etc/services文件来获取服务的信息的。这两个函数的定义如下:

1
2
3
4
#include <netdb.h>

struct servent *getservbyname(const char *name, const char *proto);
struct servent *getservbyport(int port, const char *proto);

name参数指定目标服务的名字。

port参数指定目标服务对应的端口号。

proto参数指定服务类型,给它传递“tcp”表示获取流服务,给它传递“udp”表示获取数据报服务,给它传递NULL则表示获取所有类型的服务。

函数返回的servent的定义如下:

1
2
3
4
5
6
7
#include <netdb.h>
struct servent {
char *s_name; /* official service name */
char **s_aliases; /* alias list */
int s_port; /* port number */
char *s_proto; /* protocol to use */
}

s_name:服务名称

s_aliases:服务别名列表,可能有多个

s_port:端口号

s_proto:服务类型,通常是tcp或者udp

getservbyname举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <unistd.h>
#include <assert.h>

int main(int argc, char const *argv[])
{

struct servent *servinfo = getservbyname("ssh", "tcp");
assert(servinfo);
printf("name is %s\n", servinfo->s_name);

char **pptr;
for (pptr = servinfo->s_aliases; *pptr != NULL; pptr++)
{
printf("alias: %s\n", *pptr);
}
printf("port is %d\n", ntohs(servinfo->s_port));
printf("protocol is %s\n", servinfo->s_proto);
return 0;
}

image-20220814141312451

getservbyport举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <unistd.h>
#include <assert.h>

int main(int argc, char const *argv[])
{

int port = 80;
struct servent *servinfo = getservbyport(htons(port), "tcp");
assert(servinfo);
printf("name is %s\n", servinfo->s_name);

char **pptr;
for (pptr = servinfo->s_aliases; *pptr != NULL; pptr++)
{
printf("alias: %s\n", *pptr);
}
printf("port is %d\n", ntohs(servinfo->s_port));
printf("protocol is %s\n", servinfo->s_proto);
return 0;
}

image-20220814142922859

需要指出的是,上面讨论的4个函数都是不可重入的,即非线程安全的。不过netdb.h头文件给出了它们的可重入版本。正如Linux下所有其他函数的可重入版本的命名规则那样,这些函数的函数名是在原函数名尾部加上_r (re-entrant)

getaddrinfo

getaddrinfo函数既能通过主机名获得IP地址(内部使用的是gethostbyname函数),也能通过服务名获得端口号(内部使用的是getservbyname函数)。它是否可重人取决于其内部调用的gethostbynamegetservbyname函数是否是它们的可重入版本。该函数的定义如下:

1
2
3
4
5
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res);

node参数可以接收主机名,也可以接收字符串表示的IP地址,用点分十分制。

service参数可以接收服务名,也可以接收字符串表示的十进制端口。

hints参数是给getaddrinfo的一个提示,以对getaddrinfo的输出进行更精确的控制。hints参数可以设置为NULL,表示允许getaddrinfo反馈任何可用的结果。

res参数返回一个链表,这个链表用于存储getaddrinfo反馈的结果。

除此之外,在我们调用完getaddrinfo之后,需要使用freeaddrinfo对res进行内存释放。

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
void freeaddrinfo(struct addrinfo *res);

addrinfo的定义如下

1
2
3
4
5
6
7
8
9
10
struct addrinfo {
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
socklen_t ai_addrlen;
struct sockaddr *ai_addr;
char *ai_canonname;
struct addrinfo *ai_next;
};

ai_family:地址族,比如:AF_INET

ai_socktype:服务类型,比如:SOCK_STREAM

ai_protocol:指具体的网络协议

ai_addrlen:地址ai_addr的长度

ai_addr:指向socket的地址

ai_canonname:主机的别名

ai_next:链表的下一个对象

ai_flags可以取下表中标志

image-20220814160248251

当我们使用hints参数的时候,可以设置其ai_flagsai_familyai_socktypeai_protocol四个字段,其他字段则必须被设置为NULL。

根据主机名获取IP地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <assert.h>

int main(int argc, char **argv)
{
if (argc != 2)
{
printf("Use example: %s www.baidu.com\n", *argv);
return -1;
}

char *name = argv[1];
struct addrinfo hints;
struct addrinfo *res, *cur;
int ret;
struct sockaddr_in *addr;
char ipbuf[16];

memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_family = AF_INET; /* Allow IPv4 */
hints.ai_flags = AI_PASSIVE; /* For wildcard IP address */
hints.ai_protocol = 0; /* Any protocol */
hints.ai_socktype = SOCK_STREAM;

ret = getaddrinfo(name, NULL, &hints, &res);
assert(ret >= 0);

for (cur = res; cur != NULL; cur = cur->ai_next)
{
addr = (struct sockaddr_in *)cur->ai_addr;
printf("ip: %s\n", inet_ntop(AF_INET, &addr->sin_addr, ipbuf, cur->ai_addrlen));
printf("alias: %s\n", cur->ai_canonname);
}
freeaddrinfo(res);
return 0;
}

image-20220814165923059

不过不知道为啥别名为null

根据主机名和端口号获取地址信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <assert.h>

int main(int argc, char **argv)
{
if (argc != 2)
{
printf("Use example: %s 80\n", *argv);
return -1;
}

char *port = argv[1];
char *hostname = "localhost";
struct addrinfo hints;
struct addrinfo *res, *cur;
int ret;
struct sockaddr_in *addr;
char ipbuf[16];

memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_family = AF_UNSPEC; /* Allow IPv4 */
hints.ai_flags = AI_PASSIVE; /* For wildcard IP address */
hints.ai_protocol = 0; /* Any protocol */
hints.ai_socktype = 0;

ret = getaddrinfo(hostname, port, &hints, &res);

assert(ret >= 0);

for (cur = res; cur != NULL; cur = cur->ai_next)
{
addr = (struct sockaddr_in *)cur->ai_addr;
printf("ip: %s\n", inet_ntop(AF_INET, &addr->sin_addr, ipbuf, cur->ai_addrlen));
printf("port: %d\n", ntohs(addr->sin_port));
printf("alias: %s\n", cur->ai_canonname);
}
freeaddrinfo(res);
return 0;
}

image-20220814170106763

不过不知道为啥别名为null

getnameinfo

getnameinfo函数能通过socket地址同时获得以字符串表示的主机名(内部使用的是gethostbyaddr函数)和服务名(内部使用的是getservbyport函数)。它是否可重入取决于其内部调用的gethostbyaddr和 getservbyport函数是否是它们的可重入版本。该函数的定义如下:

1
2
3
4
5
#include <sys/socket.h>
#include <netdb.h>

int getnameinfo(const struct sockaddr *addr, socklen_t addrlen,char *host, socklen_t hostlen,
char *serv, socklen_t servlen, int flags);

getnameinfo将返回的主机名存储在host参数指向的缓存中,将服务名存储在serv参数指向的缓存中,hostlenservlen参数分别指定这两块缓存的长度。flags参数控制getnameinfo的行为,它可以接收下表中的选项。

image-20220814171842523

getaddrinfogetnameinfo函数成功时返回0,失败则返回错误码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <assert.h>

int main(int argc, char **argv)
{
if (argc != 3)
{
printf("Use example: %s 127.0.0.1 80\n", *argv);
return -1;
}

char *ip = argv[1];
int port = atoi(argv[2]);
char hostname[128] = {0};
char servername[128] = {0};
struct sockaddr_in addr_dst;
memset(&addr_dst, 0, sizeof(addr_dst));
addr_dst.sin_family = AF_INET;
addr_dst.sin_addr.s_addr = inet_addr(ip);
addr_dst.sin_port = htons(port);

int ret = getnameinfo((struct sockaddr *)&addr_dst, sizeof(addr_dst), hostname, sizeof(hostname), servername, sizeof(servername), 0);
assert(ret == 0);
printf("hostname IP: %s \n", hostname);
printf("servername : %s \n", servername);
return 0;
}

image-20220814181037933