前言 - 思考還是 socket 寫過一點點, 總感覺很彆扭. 例如 read, recv, recvfrom 這些為啥這麼奇葩. 這是 linux 的設計嗎. 這種強糅合的 read 代碼, '帶壞'了多少人. 想起很久以前看過的 <<UNIX痛恨者手冊>>, 外加上常寫點跨平臺 庫. 不得不思考 ...
--------------------------------------------------------------------------------------------------------------------------------------------------
前言 - 思考還是
--------------------------------------------------------------------------------------------------------------------------------------------------
socket 寫過一點點, 總感覺很彆扭. 例如 read, recv, recvfrom 這些為啥這麼奇葩. 這是 linux 的設計嗎.
這種強糅合的 read 代碼, '帶壞'了多少人. 想起很久以前看過的 <<UNIX痛恨者手冊>>, 外加上常寫點跨平臺
庫. 不得不思考設計, 發現
1) winds 對於 socket 設計比 linux POSIX 設計理解更加友好一丟丟
2) linux 性能比 winds 好. (開源哲學 對沖 精英文化)
3) 應用層是個不完備的域, 不要一條衚衕走不到頭
(備註 : 有一段日子特別討厭 winds, 及其喜歡羡慕 unix, 但是隨著成長認識有了很大變化, 痛恨沒錢沒時間)
--------------------------------------------------------------------------------------------------------------------------------------------------
正文 - 來點證明
--------------------------------------------------------------------------------------------------------------------------------------------------
1. 如果可以不妨多寫點跨平臺, 線程安全的代碼
不妨舉個爛大街的例子, 我們經常在處理時間的時候直接用 gettimeofday
#include <sys/time.h> int gettimeofday(struct timeval * tv, struct timezone * tz); The functions gettimeofday() can get and set the time as well as a timezone. The tv argument is a struct timeval (as specified in <sys/time.h>): struct timeval { time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds */ }; and gives the number of seconds and microseconds since the Epoch (see time(2)). The tz argument is a struct timezone: struct timezone { int tz_minuteswest; /* minutes west of Greenwich */ int tz_dsttime; /* type of DST correction */ }; If either tv or tz is NULL, the corresponding structure is not set or returned. (However, compilation warnings will result if tv is NULL.) The use of the timezone structure is obsolete; the tz argument should normally be specified as NULL.
只是簡單的得到當前時間秒數和微秒, 附贈一個時區消息. 這個函數一眼看過去, 設計的不優美.
如果希望你的代碼能夠在 winds 上面也奔跑, 可能需要一個移植版本
#ifdef _MSC_VER
#include <winsock2.h> // // gettimeofday - Linux sys/time.h 中得到微秒的一種實現 // tv : 返回結果包含秒數和微秒數 // tz : 包含的時區,在winds上這個變數沒有用不返回 // return : 預設返回0 // inline int gettimeofday(struct timeval * tv, void * tz) { struct tm st; SYSTEMTIME wtm; GetLocalTime(&wtm); st.tm_year = wtm.wYear - 1900; st.tm_mon = wtm.wMonth - 1; // winds的計數更好些 st.tm_mday = wtm.wDay; st.tm_hour = wtm.wHour; st.tm_min = wtm.wMinute; st.tm_sec = wtm.wSecond; st.tm_isdst = -1; // 不考慮夏令時 tv->tv_sec = (long)mktime(&st); // 32位使用數據強轉 tv->tv_usec = wtm.wMilliseconds * 1000; // 毫秒轉成微秒 return 0; } #endif
同樣你的工作量已經起來了. 不管高不高效. 總是個下策. 這裡有個更好的主意, 利用 timespec_get
#include <time.h> /* Set TS to calendar time based in time base BASE. */ int timespec_get (struct timespec *ts, int base) { switch (base) { case TIME_UTC: if (__clock_gettime (CLOCK_REALTIME, ts) < 0) return 0; break; default: return 0; } return base; }
C11 標準提供的獲取秒和納秒的時間函數, CL 和 GCC clang 都提供了支持. 上面是glibc中一個實現, 是不是很 low.
扯一點
1.1 寫代碼應該有很強的目的, 非特殊領域應該弱化針對性
1.2 上層應用, 應該首要向著標準靠攏, 其次是操作系統, 再到編譯器
對於CL 實現了 timespec_get, 應該最主要目的是為了 C++11基礎特性支持, 還有 clang 的實現.
--------------------------------------------------------------------------------------------------------------------------------------------------
2. 你是否和我一樣曾經因為 WSAStartup 大罵微軟SB
寫 socket winds 一定會有下麵三部曲, 或者兩部曲.
// 1. CL 編譯器 設置 引入庫 ws2_32.lib 引入巨集 _WINSOCK_DEPRECATED_NO_WARNINGS // 2. 載入 socket dll WSADATA wsad; WSAStartup(WINSOCK_VERSION, &wsad); // 3. 卸載 WSACleanup
當時想, linux 為啥木有上面這麼無意義的操作. 其實其中有個故事, 當初微軟不得了時期, 無法和unix socket互連.
後面來回扯, 其它無數巨擎給其 Winsock 升級, dll 版本變化厲害. 所以有了上面拋給用戶層載入綁定dll版本的操作.
那麼再linux 上面真的不需要嗎. 其實也需要, 只是在運行 _start 時候幫助我們做了. 所以這點上面完全可以這麼
封裝
// // socket_init - 單例啟動socket庫的初始化方法 // inline void socket_init(void) { #ifdef _MSC_VER WSADATA wsad; WSAStartup(WINSOCK_VERSION, &wsad); #elif __GUNC__ signal(SIGPIPE, SIG_IGN) #endif }
--------------------------------------------------------------------------------------------------------------------------------------------------
3. 還記得 read, recv, recvfrom 嗎 ?
還處在一切皆文件支配的恐懼中嗎. 實現這種思路無外乎註冊和switch工廠分支. 那就意味著 read 是個雜糅
體. 在我們只是使用 socket fd 讀取的時候 最終 read -> recv 這個函數調用, 即 recv(fd, buf, sz, 0). 對於後者
ssize_t __libc_recv (int fd, void *buf, size_t len, int flags) { #ifdef __ASSUME_RECV_SYSCALL return SYSCALL_CANCEL (recv, fd, buf, len, flags); #elif defined __ASSUME_RECVFROM_SYSCALL return SYSCALL_CANCEL (recvfrom, fd, buf, len, flags, NULL, NULL); #else return SOCKETCALL_CANCEL (recv, fd, buf, len, flags); #endif }
可以表明 recv 和 recvfrom 實現層面有過糾纏. 但是和 read 上層沒有耦合. 所以對於單純 TCP socket 最好的
做法還是 recv 走起.
#include <sys/types.h> #include <sys/socket.h> ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
其中對於 recv flags 有下麵幾個多平臺都支持的巨集
#define MSG_OOB 0x1 /* process out-of-band data */ #define MSG_PEEK 0x2 /* peek at incoming message */ #define MSG_DONTROUTE 0x4 /* send without using routing tables */ #if(_WIN32_WINNT >= 0x0502) #define MSG_WAITALL 0x8 /* do not complete until packet is completely filled */ #endif //(_WIN32_WINNT >= 0x0502)
其實開發中, MSG_OOB 帶外數據, 除非學習. 否則無意義. MSG_PEEK 在以前的 \r\n 切分流協議的時候還用.
現在基本都沒有場景. MSG_WAITALL 可以嘗試一下替代很久以前的 for read. 可以有輕微提升性能.
recv(fd, buf, len, 0) or recv(fd, buf, len, MSG_WAITALL) 用在你的常說的'高性能'伺服器中而不是大雜燴 read.
--------------------------------------------------------------------------------------------------------------------------------------------------
4. 是否為 listen, accept 好奇過 !
首先從 listen 和 accept 一對好cp說起. 其實大體過程無外乎 listen -> connect -> accept . 這裡只是從用法
而言首先看 listen 部分
/* * Perform a listen. Basically, we allow the protocol to do anything * necessary for a listen, and if that works, we mark the socket as * ready for listening. */ SYSCALL_DEFINE2(listen, int, fd, int, backlog) { struct socket *sock; int err, fput_needed; int somaxconn; sock = sockfd_lookup_light(fd, &err, &fput_needed); if (sock) { somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn; if ((unsigned int)backlog > somaxconn) backlog = somaxconn; err = security_socket_listen(sock, backlog); if (!err) err = sock->ops->listen(sock, backlog); fput_light(sock->file, fput_needed); } return err; }
這段 listen 代碼寫得真好看. 我從中看出來, 內核的思路還是註冊. 對於 backlog 存在一個最大值.
所以對於高性能伺服器 listen 正確的寫法推薦
listen(fd, SOMAXCONN)
把 listen創建的監聽和鏈接成功隊列大小交給操作系統的內核配置.
對於 accept 原本想講一講 accept4 + SOCK_NONBLOCK 降低 socket 開發流程. 但是一想起 unix or winds
應該不支持算了. 還是老實 accept + O_NONBLOCK.
SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *, upeer_sockaddr, int __user *, upeer_addrlen) { return sys_accept4(fd, upeer_sockaddr, upeer_addrlen, 0); }
突然意識到優化就是生命枯竭, 打擊痛點才是王道.
--------------------------------------------------------------------------------------------------------------------------------------------------
5. 你為 select 苦惱過嗎, 去它的 poll
其實想想 select 這種函數設計的真的很奇葩. select -> poll -> epoll 從床上到床下經歷過多少夜晚.
主要是 winds 和 linux 對於 select 完全是兩個函數, 恰巧名字一樣. 通過下麵一個不好的材料瞭解
select 開發中的用法. 為什麼講 select, 因為方便 winds 移植調試 !! iocp很弔但是真的很難把它和epoll
揉在一起. 因為二者都很意外. epoll 是 61 + 10 分 一個iocp是 90 - 20 分. 如果強揉就要對 socket 行為
讀寫鏈接都需要抽出一層. 但是用 select 只需要抽出 poll 監聽觸發抽出來就可以了. 後期有時間我們
詳細分析 iocp. 當前帶大家感受下 epoll 那些操作.
#include <sys/epoll.h> int epoll_create(int size); int epoll_create1(int flags); epoll_create() creates a new epoll(7) instance. Since Linux 2.6.8, the size argument is
ignored, but must be greater than zero; see NOTES below. epoll_create() returns a file descriptor referring to the new epoll instance. This file
descriptor is used for all the subse‐quent calls to the epoll interface. When no longer
required, the file descriptor returned by epoll_create() should be closed by using close(2).
When all file descriptors referring to an epoll instance have been closed, the kernel
destroys the instance and releases the associated resources for reuse. epoll_create1() If flags is 0, then, other than the fact that the obsolete size argument is dropped,
epoll_create1() is the same as epoll_create(). The following value can be included in
flags to obtain different behavior: EPOLL_CLOEXEC Set the close-on-exec (FD_CLOEXEC) flag on the new file descriptor. See the description of
the O_CLOEXEC flag in open(2) for reasons why this may be useful.
更加具體是
SYSCALL_DEFINE1(epoll_create, int, size) { if (size <= 0) return -EINVAL; return sys_epoll_create1(0); }
從上面可以看出來目前推薦的 epoll_create 用法是
epoll_create1(EPOLL_CLOEXEC)
不再需要 size這個歷史包袱, 並且 exec 重新開進程的時候能夠 close 返回的 efd 防止句柄泄漏.
還有一個就是關於 epoll 的 EPOLLIN 預設LT水平觸髮狀態, 另外一個是 EPOLLET 邊緣觸發.
/* Flags for epoll_create1. */ #define EPOLL_CLOEXEC O_CLOEXEC /* Valid opcodes to issue to sys_epoll_ctl() */ #define EPOLL_CTL_ADD 1 #define EPOLL_CTL_DEL 2 #define EPOLL_CTL_MOD 3 /* Epoll event masks */ #define EPOLLIN 0x00000001 #define EPOLLPRI 0x00000002 #define EPOLLOUT 0x00000004 #define EPOLLERR 0x00000008 #define EPOLLHUP 0x00000010 /* Set the Edge Triggered behaviour for the target file descriptor */ #define EPOLLET (1U << 31)
對於普通伺服器例如游戲伺服器, 大型Web系統伺服器 LT 這種高級 select 操作就足夠了. 剛好把驗證
代碼拋給上層. ET 模式的話就需要在框架的網路層處理包異常. 但是安全的高速度的通道通信可以嘗試
一套ET流程交互. epoll 功能特別好理解, 註冊, 監聽, 返回結果. 最噁心就是返回結果的操作.
不妨展示個局部代碼
// // sp_wait - poll 的 wait函數, 等待別人自投羅網 // sp : poll 模型 // e : 返回的操作事件集 // max : e 的最大長度 // return : 返回待操作事件長度, <= 0 表示失敗 // int sp_wait(poll_t sp, struct event e[], int max) { struct epoll_event ev[max]; int i, n = epoll_wait(sp, ev, max, -1); for (i = 0; i < n; ++i) { uint32_t flag = ev[i].events; e[i].s = ev[i].data.ptr; e[i].write = flag & EPOLLOUT; e[i].read = flag & (EPOLLIN | EPOLLHUP); e[i].error = flag & EPOLLERR; } return n; }
一個最簡單的展示結果, 這裡就處理了 EPOLLOUT 和 EPOLLHUP 還有 EPOLLERR 枚舉.
EPOLLHUP 解決 listen -> connect -> accept 占用資源不釋放, 空轉問題. 其實想想最簡單的TCP網路也不好搞.
要求很多 (網路細節, 是個大工程)
--------------------------------------------------------------------------------------------------------------------------------------------------
6. 講的有點泛泛, 文末不妨展示個 不忘初心
#include <stdio.h> #include <limits.h> #include <stdint.h> // // 強迫症 × 根治 // file : len.c // make : gcc -g -Wall -O2 -o love.out love.c // test : objdump -S love.out // int main(int argc, char * argv[]) { const char heoo[] = "Hello World"; for (size_t i = sizeof heoo - 1; i < SIZE_MAX; --i) printf(" %c", heoo[i]); putchar('\n'); return 0; }
--------------------------------------------------------------------------------------------------------------------------------------------------
後記 - 力求走過
--------------------------------------------------------------------------------------------------------------------------------------------------
錯誤是難免的歡迎指正.
昨日重現 : http://music.163.com/m/song?id=3986241&userid=16529894
The Carpenters - Yesterday Once[SD,854x480].mp4 : https://pan.baidu.com/s/1slA0yU5