# Linux REUSEPORT 端口复用原理
看如下代码,分别编写了两个 server 服务端代码,均指定了 SO_REUSEPORT 标志位,两个 server 绑定了同一个端口,当我们使用 本地地址 和 其他地址 访问时,内核将会根据 访问地址来选择 server 来服务客户端(生成 client socket 放入 accept 队列),那么如果是两个相同的 外部地址呢?负载均衡 ------- 内核级别的。
// server 1
int serverListen(char *serverIP, int serverPort){
int serverFD;
int opt = 1;
struct sockaddr_in addr;
serverFD = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
setsockopt(serverFD, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)); // 这里设置了复用端口标志位
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(serverIP);
addr.sin_port = htons(serverPort);
int err = bind(serverFD, (struct sockaddr *)&addr, sizeof(addr));
if (err) {
printf("error code: %d \n",errno);
perror(NULL);
exit(-1);
}
listen(serverFD, 0);
return serverFD;
};
int main(int argc, char *argv[]) {
int serverFD = serverListen("127.0.0.1", 8080); // 监听 本地回环 地址
int clientFD = accept(serverFD,NULL,NULL);
printf("%s\n","127.0.0.1");
close(clientFD);
close(serverFD);
return 1;
}
// server 1
int serverListen(char *serverIP, int serverPort){
int serverFD;
int opt = 1;
struct sockaddr_in addr;
serverFD = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
setsockopt(serverFD, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)); // 这里设置了复用端口标志位
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(serverIP);
addr.sin_port = htons(serverPort);
int err = bind(serverFD, (struct sockaddr *)&addr, sizeof(addr));
if (err) {
printf("error code: %d \n",errno);
perror(NULL);
exit(-1);
}
listen(serverFD, 0);
return serverFD;
};
int main(int argc, char *argv[]) {
int serverFD = serverListen("0.0.0.0", 8080); // 监听 所有 地址
int clientFD = accept(serverFD,NULL,NULL);
printf("%s\n","0.0.0.0");
close(clientFD);
close(serverFD);
return 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
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
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
# SO_REUSEPORT 设置原理
源码如下,很明显,直接设置 sk->sk_reuseport 为1。
SYSCALL_DEFINE5(setsockopt, int, fd, int, level, int, optname, char __user *, optval, int, optlen)
{
int err, fput_needed;
struct socket *sock;
if (optlen < 0)
return -EINVAL;
sock = sockfd_lookup_light(fd, &err, &fput_needed); // 根据 fd 找到 socket 结构
if (sock != NULL) {
if (level == SOL_SOCKET) // 在socket级别设置socket属性
err =
sock_setsockopt(sock, level, optname, optval,
optlen);
else // 否则在 linux sock 层面设置
err =
sock->ops->setsockopt(sock, level, optname, optval,
optlen);
out_put:
fput_light(sock->file, fput_needed);
}
return err;
}
int sock_setsockopt(struct socket *sock, int level, int optname,char __user *optval, unsigned int optlen){
struct sock *sk = sock->sk;
int val;
int valbool;
if (get_user(val, (int __user *)optval)) // 将用户态数据复制到内核中
return -EFAULT;
valbool = val ? 1 : 0;
...
switch (optname) {
...
case SO_REUSEPORT:
sk->sk_reuseport = valbool; // 设置 socket 的 sk_reuseport 为 1
break;
...
}
...
}
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
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
# TCP 层处理端口复用
我们从源码中看到,当 skb 传递到 TCP 层时,将会根据 四元组中 目的IP 和 目的端口 找到服务于 当前 skb 的server sock,至于流程,注释打的很详细了。我们这里通过源码来解释为何在例子中:localhost 访问 和 外部IP 访问 所唤醒的进程不一样?从得分情况看:当精确匹配 IP 时 得分加 4 ,所以 localhost 域名解析后 为 127.0.0.1 与绑定 ip相同,所以 将有监听该地址的 server sock 来服务。
int tcp_v4_rcv(struct sk_buff *skb){
struct sock *sk;
...
sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest); // 找到 当前 skb 所表示的 sock (监听状态的 server sock 或者已经建立链接的 sock,这里我们看 监听状态的 server sock 即可) 我们需要研究的REUSEPORT 的负载均衡 将会在该方法实现
if (!sock_owned_by_user(sk)) { // sk 未被上锁,那么进入该分支(看这里即可)
if (!tcp_prequeue(sk, skb)) // 我们不开启延迟放入预备队列,所以返回0,取反为 1 进入分支
ret = tcp_v4_do_rcv(sk, skb);
} else // sk 被上锁,先放入backlog 队列,等待释放后处理
sk_add_backlog(sk, skb);
...
}
static inline struct sock *__inet_lookup_skb(struct inet_hashinfo *hashinfo,
struct sk_buff *skb,
const __be16 sport,
const __be16 dport)
{
struct sock *sk = skb_steal_sock(skb); // 复用当前 skb 未释放的 sock,若获取成功,那么直接返回(当然 我们不看它)
const struct iphdr *iph = ip_hdr(skb);
if (sk)
return sk;
else
return __inet_lookup(dev_net(skb_dst(skb)->dev), hashinfo,
iph->saddr, sport,
iph->daddr, dport, inet_iif(skb));
}
static inline struct sock *__inet_lookup(struct net *net,
struct inet_hashinfo *hashinfo,
const __be32 saddr, const __be16 sport,
const __be32 daddr, const __be16 dport,
const int dif)
{
u16 hnum = ntohs(dport); // 当前 skb tcp 头部的目标端口,也即当前我们需要连接的 server fd 的端口(咳咳,连接的四元组,在混沌学堂中,一定要注意听哈)
struct sock *sk = __inet_lookup_established(net, hashinfo,
saddr, sport, daddr, hnum, dif); // 已经建立连接
return sk ? : __inet_lookup_listener(net, hashinfo, saddr, sport,
daddr, hnum, dif); // 看这里,注意:我们有多个server fd监听同一个端口,所以将会在这里面负载均衡
}
struct sock *__inet_lookup_listener(struct net *net,
struct inet_hashinfo *hashinfo,
const __be32 saddr, __be16 sport,
const __be32 daddr, const unsigned short hnum,
const int dif){
struct sock *sk, *result;
struct hlist_nulls_node *node;
// 根据设备和目的端口号计算hash值,找到hash entry 所在下标,也即server fd 所在链表(采用链地址法构建的hash表)
unsigned int hash = inet_lhashfn(net, hnum);
struct inet_listen_hashbucket *ilb = &hashinfo->listening_hash[hash];
int score, hiscore, matches = 0, reuseport = 0;
u32 phash = 0;
rcu_read_lock();
begin:
result = NULL;
hiscore = 0; // 初始化最高分
sk_nulls_for_each_rcu(sk, node, &ilb->head) { // 遍历链表
score = compute_score(sk, net, hnum, daddr, dif); // 计算当前 server sock 对 skb IP 和 TCP 头部的目标地址和端口的得分
if (score > hiscore) { // 当前得分大于之前的 计算得分
result = sk; // 保存当前sk 作为服务 当前 server sock
hiscore = score;
reuseport = sk->sk_reuseport;
if (reuseport) { // 指定复用端口,那么需要保存hash值,以免下次TCP SYN 连接时为同一个 server sock
phash = inet_ehashfn(net, daddr, hnum,
saddr, sport);
matches = 1; // 匹配成功
}
} else if (score == hiscore && reuseport) { // 相同得分,同时指定复用端口,那么根据 hash值与 matches 匹配值 的高 32为0,来决定是否使用当前 相同得分的 server sock
matches++;
if (((u64)phash * matches) >> 32 == 0)
result = sk;
phash = next_pseudo_random32(phash);
}
}
...
}
// 计算 目标IP 和 目标端口 计算当前 监听的 server sk 的得分
static inline int compute_score(struct sock *sk, struct net *net,
const unsigned short hnum, const __be32 daddr,
const int dif)
{
int score = -1;
struct inet_sock *inet = inet_sk(sk);
if (net_eq(sock_net(sk), net) && // 开启namespace 那么判断是否处于 统一个 ns 的net设备,若没有开启 为 1
inet->inet_num == hnum && // 目的端口与当前 server sock 监听的端口 必须相同
!ipv6_only_sock(sk)) {
__be32 rcv_saddr = inet->inet_rcv_saddr;
score = sk->sk_family == PF_INET ? 2 : 1; // PF_INET协议簇 得分为 2 其他为 1
if (rcv_saddr) { // 指定了监听IP地址,那么精确匹配 IP 地址,若匹配成功 得分加 4
if (rcv_saddr != daddr) // 监听IP不匹配直接返回 -1
return -1;
score += 4;
}
if (sk->sk_bound_dev_if) { // 若绑定了指定设备索引,那么比较当前 skb 进入的 设备索引号 dif, 匹配失败返回 -1 ,否则加分 4
if (sk->sk_bound_dev_if != dif)
return -1;
score += 4;
}
}
return score;
}
// 随便看下就行,以下代码不重要
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb){
if (sk->sk_state == TCP_ESTABLISHED) { // sock 已经处于链接状态(server fd 处于监听状态,我们不走该分支)
...
}
...
if (sk->sk_state == TCP_LISTEN) { // server fd ,此时进入该分支,建立连接对端的socket
struct sock *nsk = tcp_v4_hnd_req(sk, skb); // 调用 syn_recv_sock 回调函数 创建 对等 sock
if (!nsk)
goto discard;
...
} else
sock_rps_save_rxhash(sk, skb);
...
}
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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# 绑定时 对于 REUSEPORT 的处理?
请读者根据上一篇对于 REUSEADDR 的解释和本文的解释,自行研究吧,真的很简单,培养下自我思考的能力吧。我写出来那终究是我的,不懂得可以在混沌学堂群里,给 StartSky 提问,他会给你画图!