Linux 高性能服务器--第五章

5.1.1 判断主机是小端字节序还是大端字节序

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
#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("大端字节序\n");
}
else if(test.union_bytes[0] == 2 && test.union_bytes[1] == 1){
printf("小端字节序\n");
}
else{
printf("不清楚\n");
}
}

int main(){
byteOrder();
return 0;
}

5.1.3专用socket地址

TCP/IP协议族有sockaddr_insockaddr_in6两个专用socket地址结构体,他们分别用于IPv4IPv6,这里我只介绍sockaddr_in

1
2
3
4
5
6
7
8
9
struct sockaddr_in{
sa_family_t sin_family; // 地址族:AF_INET
u_int16_t sin_port; // 端口号:要用网络字节序表示
struct in_addr sin_addr; // IPv4地址结构体
}

struct in_addr{
u_int32_t s_addr; // 要用网络字节序表示
}

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

5.1.4IP地址转换函数

1
2
3
4
5
6
7
8
9
10
11
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst); // 用于将字符串表示的IP地址转换为用网络字节序整数表示的IP地址
const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt); // 最后一个参数指定目标存储单元的大小,这两个宏可以帮我们指定
/*
inet_pton成功时返回1,失败时返回0并设置errno
inet_ntop成功时返回目标存储单元的地址,失败返回NULL并设置errno
*/

#include <netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 64

5.2创建socket

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

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

/*
domain一般为PF_INET(IPv4),或者PF_INET6(IPv6)
type参数指定服务类型,主要有SOCK_STREAM和SOCK_UGRAM
*/

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

5.3命名socket

创建socket时,我们给它指定了地址族,但是并未指定使用该地址族中的哪个具体socket地址。将一个socket与socket地址绑定称为给socket命名。在服务器程序中,我们通常需要命名socket,因为只有命名后客户端才知道该如何连接它。客户端通常不需要命名socket,而是采用匿名方式,也就是使用操作系统自动分配的socket地址。

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

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

/*
bind将my_addr所指的socket地址分配给未命名的socket文件描述符,addrlen参数指出该socket地址的长度
bind成功时返回0,失败时返回-1并设置errno,其中两种常见的errno是:EACCES和EADDRINUSE。
*/

5.4监听socket

socket被命名之后,还不能立即接收客户端连接,我们需要使用如下系统调用来创建一个监听队列以存放待处理的客户连接

1
2
3
4
5
6
7
#include <sys/socket.h>
int listen(int sockfd, int backlog);

/*
sockfd参数指定被监听的socket,backlog参数提示内核监听队列的最大长度。监听队列的长度如果超过backlog,服务器将不受理新的客户端连接,客户端也将收到ECONNREFUSED错误信息。(实际上最多可以接收backlog + 1个客户端连接)
listen成功时返回0,失败时返回-1并设置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
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
88
89
90
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>

/*
basename使用示例:
`basename()` 是一个常用于处理文件路径和程序参数的库函数。
它的主要目的是从一个给定的路径中提取基础名(base name),也就是最后一部分的名称,不包含任何前导的目录。

来自 `<libgen.h>`(或在某些系统中是 `<string.h>` 或 `<strings.h>`),`basename()` 函数的原型如下:

char *basename(char *path);

让我们看一些使用 `basename()` 的例子:

1. 输入 `/home/user/documents/file.txt` 返回 `file.txt`
2. 输入 `/home/user/documents/folder/` 返回 `folder`
3. 输入 `/home/user/documents/` 返回 `documents`
4. 输入 `file.txt` 返回 `file.txt`

在给出的代码示例中:

printf("usage: %s ip_address port_num backlog\n", basename(argv[0]));

`argv[0]` 通常是程序的名称,包括它被执行时的完整路径。
使用 `basename()` 函数,你可以仅提取程序的实际名称,而不包括其路径,这在显示帮助或错误消息时特别有用。

举个例子,假设程序的完整路径是 `/home/user/my_program`,那么 `basename(argv[0])` 就会返回 `my_program`。这样,上述的 `printf` 语句将输出:

usage: my_program ip_address port_num backlog

需要注意的是,`basename()` 函数可能会修改其参数,也可能返回一个指向静态存储区的指针。
因此,如果原始路径字符串不应被修改,那么在调用 `basename()` 之前,最好先复制这个字符串。
*/

static bool stop = false;

static void term_handler(int sig){
stop = true;
}

int main(int argc, char *argv[]){
signal(SIGTERM, term_handler);

if(argc <= 3){
printf("usage: %s ip_address port_num backlog\n", basename(argv[0]));
return 1;
}

char *ip = argv[1];
int port = atoi(argv[2]);

int sockfd = socket(PF_INET, SOCK_STREAM, 0);
assert(sockfd != -1);

int sign = 0;

struct sockaddr_in server_addr;
server_addr.sin_family = PF_INET;
server_addr.sin_port = htons(port);
inet_pton(PF_INET, ip, &server_addr.sin_addr);

sign = bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
assert(sign != -1);

sign = listen(sockfd, 5);
assert(sign != -1);

while(!stop){
sleep(1);
}

close(sockfd);
return 0;
}

/*
结果忘记截图了,大家可以自己试一下,具体步骤
./test 172.30.78.145 8000 5
另开终端多次 telnet 172.30.78.145 8000
netstat -nt | grep 8000
*/

这个命令组合使用了两个命令:netstatgrep,并通过管道(|)将第一个命令的输出作为第二个命令的输入。我会为你逐步解释它:

  1. netstat -nt:

    • netstat: 这是一个命令行工具,用于显示网络状态,包括网络连接、路由表、接口统计等。
    • -n: 表示以数字形式显示地址和端口号,而不是尝试解析它们的名称。
  • -t: 仅显示TCP连接。

因此,netstat -nt 的输出会列出系统上所有活动的TCP连接,同时显示它们的源和目标IP地址以及端口号,并直接显示数字而不进行名称解析。

  1. |:
  • 这是一个管道操作符,用于将前一个命令的输出作为后一个命令的输入。
  1. grep 8000:
    • grep: 是一个强大的文本搜索工具,用于搜索匹配的字符串。
    • 8000: 是你想在 netstat 的输出中搜索的字符串。
    这个命令会从 netstat 的输出中筛选出所有包含 “8000” 的行,这通常意味着你正在查找与端口 8000 相关的所有活动连接。

综上所述,netstat -nt | grep 8000 会显示所有在端口 8000 上的活动TCP连接。

5.5接受连接

代码:接受一个异常的连接

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
#include <iostream>
#include <stdio.h>
#include <libgen.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>

int main(int argc, char* argv[]){
if(argc <= 2){
printf("运行程序,需输入这三个参数:%s, ip_address, port_num\n", basename(argv[0]));
return 1;
}

const char* ip = argv[1];
int port = atoi(argv[2]);

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

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

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

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

sleep(20);

struct sockaddr_in client;
socklen_t client_len = sizeof(client);
int connfd = accept(sock, (struct sockaddr*)&client, &client_len);
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, client_len),
ntohs(client.sin_port));
close(connfd);
}
close(sock);
return 0;
}

1、第一次运行报错,undefined reference to main,这种情况一般有三种可能:

  • 没有定义main函数
  • main函数的main拼写错误
  • 刚写的代码忘记保存了

2、accept函数是阻塞的,上述代码即服务器端运行的时候,会阻塞在accept处,一旦客户端请求建立连接,服务器立马终止程序。注意accept只是从listen监听队列中取出连接,它不会理会客户端处于什么状态。

3、一直在思考select/poll/epoll这些有什么用。首先因为listen是有监听队列的,劣势就在于只能一个个处理,并且同时接入的连接数有限。比如队列长度为5,处理完一个,再建立下一个连接,这样如果某一个连接处理很长时间一直阻塞在那里,就导致后面的新请求连接建立超时。很直观的想法是fork新进程或者创建新线程来处理新连接,每来一个连接我就创建一个来跟他对接。这样资源消耗太大。因此就有了select/poll/epoll,先把连接建立起来并放进文件描述符,最后从这里面寻找哪些发生了可读可写事件,也避免了因为读写事件造成的阻塞(没有数据到来就阻塞了)。