熟悉网络编程基础 API,理解与 TCP/IP 协议栈的关系。
socket 地址
主机字节序和网络字节序
根据数据存储顺序分为大端字节序( big endian)和小端字节序(little endian),下面代码可以判断字节序。
1 |
|
为了计算机通信数据正确,那必然要约定一致的字节序: - 主机字节序:目前大多 CPU 使用小端字节序,所以又被称为主机字节序 - 网络字节序:网络通信规定为大端字节序,也就是说发送和接收方都需要以大端字节序发送和接收
注意: 即使同一台机器上运行的由不同语言编写的进程,也有可能是不同字节序,所以需要有良好的编程习惯。 - JAVA 虚拟机统一采用大端字节序
当然,如果通信双方以字符串的方式来交互,那在传输数据的过程中,也不需要注意大小端问题。
Linux 提供了如下函数完成主机字节序和网络字节序的转换: 1
2
3
4
5
6
7
8
9
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
通用 socket 地址
sockaddr
表示 socket 地址: 1
2
3
4
5
6
7
8
typedef unsigned short int sa_family_t;
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}sa_family
表示地址族,这个与协议族有对应关系:
协议族 | 地址族 | 描述 | sa_data 含义 |
---|---|---|---|
PF_UNIX | AF_UNIX | UNIX 本地域协议族 | 文件路径名,最长 108 字节 |
PF_INET | AF_INET | TCP/IPv4 协议族 | 16 位端口号和 32 位地址 |
PF_INET6 | AF_INET6 | TCP/IPV6 协议族 | 16 位端口号,32 位流标识,128 位地址,32 位范围 ID |
1 | /* Protocol families. */ |
从上面定义可以看出它们的值是一样的,只是为了更好编码规范,需要根据当前对象使用对应的宏。
仅仅用 sockaddr
中的 sa_data
并不能完全容纳多种协议族的地址值,Linux 为此定义了
sockaddr_storage
: 1
2
3
4
5
6
7
8
9
10
11
12/* Structure large enough to hold any socket address (with the historical exception of AF_UNIX). */
//这里就等于 128 - sizeof(unsigned shrot int) - sizeof(unsigned long int)
struct sockaddr_storage
{
__SOCKADDR_COMMON (ss_); /* Address family, etc. */
char __ss_padding[_SS_PADSIZE];
__ss_aligntype __ss_align; /* Force desired alignment. */
};
专用 socket 地址
上面的通用地址结构体是以字节的方式格式化存储地址,这并不便于代码操作。
所以 Linux 为各个协议族提供了专门的 socket 地址结构体:
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
/* Structure describing the address of an AF_LOCAL (aka AF_UNIX) socket. */
struct sockaddr_un
{
__SOCKADDR_COMMON (sun_);
char sun_path[108]; /* Path name. */
};
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
/* IPv6 address */
struct in6_addr
{
union
{
uint8_t __u6_addr8[16];
} __in6_u;
};
/* 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)];
};
/* 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 */
};
这样在编程设置地址参数时就可以使用这些结构体,最后在调用对应函数时强制转换为
sockaddr
即可。
IP 地址转换函数
为了提高编程的可读性,Linux
提供了字符串方式地址到二进制地址的相互转换函数: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//将以字符串表示的 IPv4 地址转换为网络字节序的整数并存储于 inp 中
int inet_aton(const char *cp, struct in_addr *inp);
//将以字符串表示的 IPv4 地址转换为网络字节序的整数
in_addr_t inet_addr(const char *cp);
//将网络字节序的整数地址转换为字符串表示的 IPv4 地址
//此函数返回指向静态内存,所以其不可重入
char *inet_ntoa(struct in_addr in);
//将以字符串表示的 IPv4 或 IPv6 地址转换为网络字节序,并存储于对应的地址结构体中
int inet_pton(int af, const char *src, void *dst);
//将网络字节序的整数地址转换为字符串表示的 IPv4 或 IPv6 地址,size 指定转换的大小
//IPv4 大小至少为 INET_ADDRSTRLEN
//IPv6 大小至少为 INET6_ADDRSTRLEN
const char *inet_ntop(int af, const void *src,
char *dst, socklen_t size);
创建 socket
Linux 提供了 socket
函数来创建一个 socket 对象:
1
2
3
4
int socket(int domain, int type, int protocol);domain
指定底层协议族 +
AF_INET
表示 IPv4, AF_INET6
表示 IPv6,
AF_UNIX,AF_LOCAL
表示 UNIX 本地协议族 - type
指定服务类型,对于 TCP
协议则设置为
SOCK_STREAM
(流服务),对于 UDP
协议则设置为
SOCK_DGRAM
(数据报服务) + 以上参数可以与
SOCK_NONBLOCK
(非阻塞)和 SOCK_CLOEXEC
(用
fork 调用创建子进程时,子进程关闭该 socket)相与 - protocol
表示具体的协议,一般前两个值都已经决定了协议的唯一性,一般设 0
表示使用默认协议。
命名 socket
将一个 socket 与 socket 地址绑定称为给 socket 命名。
在服务器程序中,通常要命名 socket,只有命名后客户端才能知道如何连接它。 而在客户端中,通常不需要命名,采用系统自动分配的地址即可。
1 |
|
监听 socket
将 socket 命名之后,需要创建一个监听队列存放待处理的客户端连接:
1
2
3
4
//创建一个以 sockfd 对应的最大长度为 backlog 的监听队列
int listen(int sockfd, int backlog);
backlog
表示处于完全连接状态的 socket
的上限,半连接的上限由
/proc/sys/net/ipv4/tcp_max_syn_backlog
指定。
需要注意的是:backlog 代表可连接最大长度减一,比如 backlog 设置为 5,代表最多可以连接 6 个客户端。
使用 telnet
连接下面代码的服务端,并用
netstat -nt | grep <port>
的方式查看状态便可验证:
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
int main(int argc, char *argv[]) {
if (argc != 3) {
printf("usage: %s : <port> <backlog>\n", argv[0]);
return -1;
}
int port = atoi(argv[1]);
int backlog = atoi(argv[2]);
//设置地址
struct sockaddr_in sockaddr;
sockaddr.sin_family = AF_INET;
sockaddr.sin_port = htons(port);
sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
//新建 socket,类型是 IPv4 TCP
int socket_fd = socket(sockaddr.sin_family, SOCK_STREAM, 0);
if (socket_fd <= 0) {
perror("create socket failed:");
return -1;
}
//socket 命名
if (bind(socket_fd, (const struct sockaddr *)&sockaddr, sizeof(sockaddr)) < 0) {
perror("can't bind socket and addr:");
return -1;
}
//开始监听
if (listen(socket_fd, backlog) < 0) {
perror("listen failed!\n");
return -1;
}
while(1) {
sleep(1);
}
return 0;
}
接受连接
所谓的接受连接,是指从监听队列中取出一个 client 连接的节点,然后处理。 - accept 不会判断当前连接处于何种状态(比如客户端异常断开)
1 |
|
发起连接
客户端通过 connect
来主动发起连接: 1
2
3
4
5
//将 sockfd 与 addr 指向的地址进行连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
关闭连接
关闭连接可以使用 close
和 shutdown
:
1 |
|
数据读写
TCP 数据读写
1 |
|
需要明白的是:由于 TCP 是流数据通信,很可能 recv()
所返回的实际读取长度小于需求的长度,所以需要多次调用 recv()
才能得到完整的数据。
当
recv()
返回 0 时,代表对方已经关闭了连接
flags 常用的取值如下(这些逻辑可以通过逻辑或组合起来):
选项名 | 含义 | send | recv |
---|---|---|---|
MSG_CONFIRM | 仅用 SOCK_DGRAM,SOCK_RAW 类型,指示数据链路层协议持续监听对方回应,直到得到答复 | Y | N |
MSG_DONTROUTE | 不查看路由表,直接将数据发送给本地局域网内的主机 | Y | N |
MSG_DONTWAIT | 非阻塞操作 | Y | Y |
MSG_MORE | 内核超时等待更多数据写入发送缓存后一次性发送,提高传输效率 | Y | N |
MSG_WAITALL | 读操作仅在读取到指定数量的字节后才返回 | N | Y |
MSG_PEEK | 读取数据,但不清除读缓存 | N | Y |
MSG_OOB | 紧急数据的读写 | Y | Y |
MSG_NOSIGNAL | 往读端关闭的管道或 socket 连接中写入数据时,不会引发 SIGPIPE | Y | N |
UDP 数据读写
1 | ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, |
由于 UDP 没有连接的概念,所以在其发送和接收函数中需要包含与其通信的地址信息。
通用数据读写
1 | struct iovec { /* Scatter/gather array items */ |
通用数据读写函数既可以用于 TCP 也可以用于
UDP,这两个函数使用分散聚合模式来实现多段内存的读写: -
struct iovec
代表一块内存 - msg_iov
指向多段内存数组地址, msg_iovlen
指定数组长度 -
msg_name,msg_namelen
分别表示对端的 socket 地址和长度,对于
TCP 而言设置为 NULL - flags
设定与前面的数据读写标记一致
带外标记
当有带外标记(紧急)数据到达时,内核会产生异常事件或
SIGURG
信号,然后用户程序通过 sockatmark
判断下一个数据是否是带外数据,然后通过 MSG_OOB
标记接收数据。
1 |
|
为了理解带外标记,现在运行服务端,然后 PC 发送普通数据和带外数据来观察服务端的输出。
服务端代码: 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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("usage: %s <port>\n", argv[0]);
return -1;
}
int port = atoi(argv[1]);
//addr
struct sockaddr_in socket_addr;
memset(&socket_addr, 0, sizeof(socket_addr));
socket_addr.sin_family = AF_INET;
socket_addr.sin_port = htons(port);
socket_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//socket
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (socket_fd < 0) {
perror("can't create socket:");
return -1;
}
//bind
if (bind(socket_fd, (const struct sockaddr *)&socket_addr, sizeof(socket_addr)) < 0) {
perror("bind socket and address failed:");
return -1;
}
//listen
if (listen(socket_fd, 5) < 0) {
perror("listen failed:");
return -1;
}
printf("I'm waiting for client...\n");
//accept
int client_fd = 0;
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
if ((client_fd = accept(socket_fd, (struct sockaddr *)&client_addr, &addr_len)) < 0) {
perror("accept failed:");
return -1;
}
printf("connected to client ip: %s, port: %d\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
ssize_t recv_len;
char recv_buf[RECV_BUF_SIZE];
memset(recv_buf, 0, RECV_BUF_SIZE);
recv_len = recv(client_fd, recv_buf, RECV_BUF_SIZE - 1, 0);
printf("1.received %ld bytes : %s\n", recv_len, recv_buf);
memset(recv_buf, 0, RECV_BUF_SIZE);
recv_len = recv(client_fd, recv_buf, RECV_BUF_SIZE - 1, MSG_OOB);
if(recv_len < 0){
perror("recv failed:");
}
printf("2.received %ld bytes : %s\n", recv_len, recv_buf);
memset(recv_buf, 0, RECV_BUF_SIZE);
recv_len = recv(client_fd, recv_buf, RECV_BUF_SIZE - 1, 0);
printf("3.received %ld bytes : %s\n", recv_len, recv_buf);
close(client_fd);
close(socket_fd);
return 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
int main(int argc, char *argv[]) {
if (argc != 3) {
printf("usage: %s <hostname> <port>\n", argv[0]);
return -1;
}
int port = atoi(argv[2]);
//addr
struct sockaddr_in socket_addr;
memset(&socket_addr, 0, sizeof(socket_addr));
socket_addr.sin_family = AF_INET;
socket_addr.sin_port = htons(port);
struct hostent *host_info = gethostbyname(argv[1]);
assert(host_info);
printf("I have found the ip address of host %s is:\n", host_info->h_name);
int i = 0;
do {
printf("%s: %s\n", host_info->h_addrtype == AF_INET ? "ipv4" : "ipv6",inet_ntoa(*(struct in_addr *)host_info->h_addr_list[i]));
i++;
} while (host_info->h_addr_list[i]);
socket_addr.sin_addr.s_addr = *(uint32_t *)host_info->h_addr_list[0];
//socket
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (socket_fd < 0) {
perror("can't create socket:");
return -1;
}
//connect
if (connect(socket_fd, (const struct sockaddr *)&socket_addr, sizeof(socket_addr)) < 0) {
perror("connect to server failed:");
return -1;
}
const char *oob_data = "abc";
const char *normal_data = "123";
send(socket_fd, normal_data, strlen(normal_data), 0);
send(socket_fd, oob_data, strlen(oob_data), MSG_OOB);
send(socket_fd, normal_data, strlen(normal_data), 0);
close(socket_fd);
return 0;
}
1 | 服务端启动 |
可以看到:虽然客户端发送的带外数据是 "abc" ,但是只有最后一个字符 "c"
被当做带外数据。且服务器对正常数据的接收将被带外数据截断,也就是无法通过一个
recv
全部读出。
同时 Wireshark 抓取的信息如下:
其过程如下: 1. 握手:客户端首先与服务器进行 3 次握手 2.
发送普通数据“123” 3.
发送紧急数据“abc123”,此时URG
标志位置位,且紧急指针的值为
3,也就是说c
为紧急数据 4. 客户端发送断开数据报 5.
服务器应答普通数据 6. 服务器应答紧急数据 7. 服务器应答结束报文 8.
客户端返回应答,最终便断开了连接
地址信息
1 |
|
socket 选项
以下函数用于获取和设置 socket
文件描述符属性的函数:
1 |
|
需要注意的是:对服务端而言,需要在 listen
之前设置 socket。对于客户端而言,需要在 connect
之前设置
socket。
下面根据协议栈的 Level 来说明常用的设置。
SOL_SOCKET(通用 socket 选项,与协议无关)
- SO_DEBUG: 打开调试信息
- SO_REUSEADDR: 重用本地地址(而不是让 TCP 连接处于
TIME_WAIT
状态,等待很久后才能重用此地址)- 也可以通过修改
/proc/sys/net/ipv4/tcp_tw_reuse
达到同样的需求1
2
3
4int reuse = 1;
if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
perror("Can't set socket:");
}
- 也可以通过修改
- SO_TYPE:获取 socket 类型
- SO_ERROR: 获取并清除 socket 错误状态
- SO_DONTROUTE: 不查看路由表,直接将数据发送给本地局域网内的主机,与
send 函数的
MSG_DONTROUTE
效果一样 - SO_RCVBUF:TCP 接收缓冲区大小
- 也可以设置
/proc/sys/net/ipv4/tcp_rmem
- 也可以设置
- SO_SNDBUF: TCP 发送缓冲区大小
- 也可以设置
/proc/sys/net/ipv4/tcp_wmem
- 也可以设置
- SO_KEEPALIVE:发送周期性保活报文以维持连接
- 关于 keepalive 的理解参考此链接
- SO_OOBINLINE:将带外数据存放于普通数据缓存中,用户使用普通读取方式获取
- SO_LINGER:若有数据待发送,则延迟关闭,通过
linger
结构体配置是立即关闭,还是发送残留数据后关闭 - SO_RCVLOWAT: TCP 接收缓存区低水位标记,当缓存数据大于低水位时,应用程序便可以读取
- SO_SNDLOWAT:TCP 发送缓存区低水位标记,当空闲数据大于低水位时,应用程序便可以发送
- SO_RCVTIMEO: 接收数据超时
- SO_SNDTIMEO:发送数据超时
下面通过实例理解 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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
int main(int argc, char *argv[]) {
if (argc != 3) {
printf("usage: %s <port> <recv buffer size>\n", argv[0]);
return -1;
}
int port = atoi(argv[1]);
//addr
struct sockaddr_in socket_addr;
memset(&socket_addr, 0, sizeof(socket_addr));
socket_addr.sin_family = AF_INET;
socket_addr.sin_port = htons(port);
socket_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//socket
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if(socket_fd < 0) {
perror("can't create socket:");
return -1;
}
int recvbuf = atoi(argv[2]);
int len = sizeof(recvbuf);
setsockopt(socket_fd, SOL_SOCKET, SO_RCVBUF, &recvbuf, sizeof(recvbuf));
getsockopt(socket_fd, SOL_SOCKET, SO_RCVBUF, &recvbuf, (socklen_t *)&len);
printf("I want to set recv buf is %d, actually recv buf is %d\n",
atoi(argv[2]), recvbuf);
//bind
if (bind(socket_fd, (const struct sockaddr *)&socket_addr, sizeof(socket_addr)) < 0) {
perror("bind socket and address failed:");
return -1;
}
//listen
if (listen(socket_fd, 5) < 0) {
perror("listen failed!\n");
return -1;
}
printf("I'm waiting for client...\n");
//accept
int client_fd = 0;
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
if ((client_fd = accept(socket_fd, (struct sockaddr *)&client_addr, &addr_len)) < 0) {
perror("accept failed!\n");
return -1;
}
printf("connected to client ip: %s, port: %d\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
char buffer[BUFFER_SIZE];
memset(buffer, 0, BUFFER_SIZE);
while (recv(client_fd, buffer, BUFFER_SIZE - 1, 0) > 0) {
}
close(client_fd);
close(socket_fd);
return 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
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
int main(int argc, char *argv[]){
if (argc != 4) {
printf("usage: %s <hostname> <port> <send buffer size>\n", argv[0]);
return -1;
}
int port = atoi(argv[2]);
//addr
struct sockaddr_in socket_addr;
memset(&socket_addr, 0, sizeof(socket_addr));
socket_addr.sin_family = AF_INET;
socket_addr.sin_port = htons(port);
struct hostent *host_info = gethostbyname(argv[1]);
assert(host_info);
printf("I have found the ip address of host %s is:\n", host_info->h_name);
int i = 0;
do {
printf("%s: %s\n", host_info->h_addrtype == AF_INET ? "ipv4" : "ipv6",
inet_ntoa(*(struct in_addr *)host_info->h_addr_list[i]));
i++;
} while (host_info->h_addr_list[i]);
socket_addr.sin_addr.s_addr = *(uint32_t *)host_info->h_addr_list[0];
//socket
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (socket_fd < 0) {
perror("can't create socket:");
return -1;
}
int sendbuf = atoi(argv[3]);
int len = sizeof(sendbuf);
setsockopt(socket_fd, SOL_SOCKET, SO_SNDBUF, &sendbuf, sizeof(sendbuf));
getsockopt(socket_fd, SOL_SOCKET, SO_SNDBUF, &sendbuf, &len);
printf("I want to set send buf is %d, actually it is %d\n",
atoi(argv[3]), sendbuf);
//connect
if (connect(socket_fd, (const struct sockaddr *)&socket_addr, sizeof(socket_addr)) < 0) {
perror("connect to server failed:");
return -1;
}
char buffer[BUFFER_SIZE];
memset(buffer, 'a', BUFFER_SIZE);
send(socket_fd, buffer, BUFFER_SIZE, 0);
close(socket_fd);
return 0;
}
1 | 服务端启动 |
可以看到接收缓冲区大小会被限制,系统会主动增加这些值。
通过 Wireshark 抓取:
分析其流程如下: 1. 握手 + 客户端发送的窗口大小是 64240,扩大因子是 128
+ 服务端返回的窗口大小是 1152,扩大因子是 1 2.
发送:在这个过程中,客户端会分段多次发送数据,等待服务端读取数据后再次发送
3. 断开:这次断开是 4
次挥手,服务端先返回应答,然后再返回对于FIN
数据报的应答 -
客户端发送FIN
数据报的时候也顺带发送了最后的数据
IPPROTO_IP(IPv4 选项)
- IP_TOS:服务类型,用于设置最大延迟、最大吞吐等
- IP_TTL:存活时间,最多可以中转多少个路由器
IPPROTO_IPV6(IPv6 选项)
- IPV6_NEXTHOP: 下一跳 IP 地址
- IPV6_RECVPKTINFO:接收分组信息
- IPV6_DONTFRAG:禁止分片
- IPV6_RECVTCLASS:接收通信类型
IPPROTO_TCP(TCP 选项)
- TCP_MAXSEG: TCP 最大报文段大小
- TCP_NODELAY: 禁止 Nagle 算法
网络信息
socket 地址指的是 IP 地址和端口号的集合,但这两个信息都是数值。
如果能够通过字符串的形式转换一次,客户端的访问将比较方便。 - 其中 IP 地址对应主机名 ,端口号对应服务名 - 服务端修改 IP 地址后并不会影响客户端。
在局域网中,如果没有架设 DNS,则可以通过在服务端和客户端安装
avahi-daemon
通过 hostname.local
的方式访问。
gethostbyname, gethostbyaddr
1 |
|
gethostbyname
函数首先在本地的 /etc/hosts
文件中查找主机,如果没有找到再去访问 DNS 服务器。
需要注意的是: h_addr_list
是以网络字节序存放的字节数组,而不是字符串!要以字符串显示需要使用
inet_ntoa
函数。
getservbyname, getservbyport
1 |
|
以上的转换关系都是通过读取 /etc/services
文件来获取服务信息的。
getaddrinfo
1 |
|
getnameinfo
1 |
|