C 中级 - SO_REUSEPORT 和 SO_REUSEADDR
引言 - 问题由来
刚开始学习网络编程时候, 常听到一个词, 先开启 "端口复用 SO_REUSEADDR". 那时很一知半解,
就知道该那么写了. 心里一直有些奇怪, 语义不通呀为啥这么翻译. 后面随着相声听多了, 就明白了些
道理.
倒排索引为啥叫倒排索引? https://www.zhihu.com/question/23202010
(这个梗告诉 wo, 索引和反向索引要比正排索引和倒排索引容易理解好多, 信达雅 : )
随后逛网络帖子恰好看见布道师陈硕介绍 SO_REUSEPORT 时候, 他说的实用起来应该很爽.
Linux 4.5/4.6 中对 SO_REUSEPORT 的改进 https://zhuanlan.zhihu.com/p/25533528
文章简单的通过数据结构来表明 SO_REUSEPORT 开启后, 会将 sock 结构放入 port 为 key 的
hash 结构中. 一联想, 就发现到 epoll 多线程解决方案, 通过 epoll + thread + listen fd epoll 搞.
是不是很有意思.
后面开始搜集 SO_REUSEPORT 资料, 看到这个
浅析套接字中SO_REUSEPORT和SO_REUSEADDR的区别 https://blog.csdn.net/Yaokai_AssultMaster/article/details/68951150
从中提炼几个简单信息. 我们以 linux 行为为基准, 顺带引述 winds 行为.
linux -:
1) . 端口复用 SO_REUSEPORT, 可以顶地址复用 SO_REUSEADDR.
2) . 都有 userID 安全检查
winds -:
1). 只有 SO_REUSEADDR, 轻微像 SO_REUSEPORT 支持多端口绑定.
但只有最后一个绑定的 socket 能够接收数据. 最后一个 closesocket 后, 最后"第二个"顶.
(猜测采用的是 list 结构, 每次 add 到 head, del 到 head. )
2). 没有 userID 安全检查, 依赖它独有安全的选项 SO_EXCLUSIVEADDRUSE
通过上面信息, 不妨写个通用的复用代码 socket_set_reuse 用起来会很舒服.
// socket_set_reuse - 开启端口和地址复用 inline int socket_set_enable(socket_t s, int optname) { int ov = 1; return setsockopt(s, SOL_SOCKET, optname, (void *)&ov, sizeof ov); } inline int socket_set_reuse(socket_t s) { return socket_set_enable(s, SO_REUSEPORT); }
其中 winds 平台构造了如下定义
#ifdef _MSC_VER #define SO_REUSEPORT SO_REUSEADDR typedef SOCKET socket_t;#endif
上面翻译文章中提供链接挺好
stackoverflow SO_REUSEADDR 和 SO_REUSEPORT differ 回答很有水准
https://stackoverflow.com/questions/14388706/socket-options-so-reuseaddr-and-so-reuseport-how-do-they-differ-do-they-mean-t/14388707#14388707
扯了这么多, 后面会构造代码来验证和表现结果.
正文 - 实验验证
首先从 linux 入手, 写一段 SO_REUSEPORT 验证代码 port.c
#include <time.h> #include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <signal.h> #include <pthread.h> #include <sys/types.h> #include <netdb.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #include <netinet/tcp.h> // // CERR - 打印错误信息 // IF - 条件判断异常退出的辅助宏 // #define CERR(fmt, ...) \ fprintf(stderr, "[%s:%s:%d][%d:%s]" fmt "\n", \ __FILE__, __func__, __LINE__, errno, strerror(errno), ##__VA_ARGS__) #define IF(cond) \ if ((cond)) do { \ CERR(#cond); \ exit(EXIT_FAILURE); \ } while(0) // accept_example - SO_REUSEPORT accept example void accept_example(void); // times_buf - 时间串缓存 char * times_buf(char buf[BUFSIZ]); // // SO_REUSEPORT :) // int main(int argc, char * argv[]) { // start 10 pthread run accept_example pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); for (int i = 0; i < 10; ++i) { pthread_t tid; IF(pthread_create(&tid, &attr, (void * (*)(void *))accept_example, NULL)); } pthread_attr_destroy(&attr); // main accept block accept_example(); return 0; } // UINT_PORT - 监听端口 #define UINT_PORT (8088) // socket_set_enable - 开始 socket 开关 inline static int socket_set_enable(int s, int optname) { int ov = 1; return setsockopt(s, SOL_SOCKET, optname, (void *)&ov, sizeof ov); } // accept_example - SO_REUSEPORT accept example void accept_example(void) { // 构造 TCP socket int s = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); IF(s == ~0); // 开启地址复用 IF(socket_set_enable(s, SO_REUSEADDR)); IF(socket_set_enable(s, SO_REUSEPORT)); // 构造地址 struct sockaddr_in addr = { AF_INET, htons(UINT_PORT), { INADDR_ANY }, }; socklen_t sen = sizeof addr; const struct sockaddr * pddr = (const struct sockaddr *)&addr; // 绑定地址 IF(bind(s, pddr, sen)); // 开始监听 IF(listen(s, SOMAXCONN)); // 等待链接过来, 最多 3 次 for (int i = 0; i < 3; ++i) { char v[BUFSIZ]; struct sockaddr_in cddr; socklen_t cen = sizeof cddr; printf("[%ld] accept start ... %s\n", pthread_self(), times_buf(v)); int c = accept(s, (struct sockaddr *)&cddr, &cen); if (s == ~0) { CERR("accept s = %d is error", s); break; } // 连接成功打印链接消息 printf("[%ld] [%s:%d] accept success ... %s\n", pthread_self(), inet_ntoa(cddr.sin_addr), ntohs(cddr.sin_port), times_buf(v)); close(c); } close(s); } // times_buf - 时间串缓存 char * times_buf(char buf[BUFSIZ]) { struct tm m; struct timespec s; timespec_get(&s, TIME_UTC); localtime_r(&s.tv_sec, &m); snprintf(buf, BUFSIZ, "%04d-%02d-%02d %02d:%02d:%02d %03ld", m.tm_year + 1900, m.tm_mon + 1, m.tm_mday, m.tm_hour, m.tm_min, m.tm_sec, s.tv_nsec / 1000000); return buf; }
立即尝试跑一跑
gcc -g -Wall -o port.out port.c -lpthread ./port.out
看看结果
顺带写个 winds 实验版本 reuseport_test.c (跨平台)
#include <times.h> #include <socket.h> #include <thread.h> // UINT_PORT - 监听端口 #define UINT_PORT (8088) // accept_example - SO_REUSEPORT accept example void accept_example(int id); // reuseport_test - 端口复用测试 void reuseport_test(void) { // start 10 pthread run accept_example for (int i = 1; i <= 10; ++i) { IF(pthread_async(accept_example, i)); } msleep(609); // main accept block accept_example(0); } // accept_example - SO_REUSEPORT accept example void accept_example(int id) { // 构造 TCP socket socket_t s = socket_stream(); IF(s == INVALID_SOCKET); // 开启地址复用 IF(socket_set_reuse(s)); // 构造地址 sockaddr_t addr = {{ AF_INET, htons(UINT_PORT) }}; // 绑定地址 IF(socket_bind(s, addr)); // 开始监听 IF(socket_listen(s)); // 等待链接过来, 最多 3 次 for (int i = 0; i < 3; ++i) { times_t v; sockaddr_t cddr; printf("[%2d] accept start ... %s\n", id, times_str(v)); socket_t c = socket_accept(s, cddr); if (s == INVALID_SOCKET) { CERR("accept s = %lld is error", (long long)s); break; } // 连接成功打印链接消息 char ip[INET_ADDRSTRLEN]; printf("[%2d] [%s:%d] accept success ... %s\n", id, socket_pton(cddr, ip), ntohs(cddr->sin_port), times_str(v)); socket_close(c); } socket_close(s); }
是不是很清爽, 舒服还是 structc 框架给予的 ~ &(左旋转 90 度) 力量
structc https://github.com/wangzhione/structc
玩别人玩剩下的也挺好玩的 哈哈 ~ : )
毕竟写过 -|
后记 - 搬运打杂
错误是难免的, 欢迎指正, 共同提升, 感受纯粹的力量
浪子回头 - http://music.163.com/#/song?id=516728102