絡(luò)輪詢與 IOCP 適配分析:從 WSAPoll/select 到 memory BIO 的實(shí)踐指南)
OpenSSL 在 Windows 上的網(wǎng)絡(luò)輪詢與 IOCP 適配分析從 WSAPoll/select 到 memory BIO 的實(shí)踐指南【免費(fèi)下載鏈接】opensslGeneral purpose TLS and crypto library項目地址: https://gitcode.com/GitHub_Trending/ope/openssl導(dǎo)讀Windows 平臺的 socket API 與 POSIX 存在一系列有趣的差異沒有原生poll(2)WSAPoll(2)長期存在缺陷而高性能 I/O 必須依賴 IOCPI/O Completion Ports這一與輪詢范式完全不同的完成通知模型。本文基于 OpenSSL 倉庫 doc/designs/ddd/WINDOWS.md 的官方設(shè)計文檔系統(tǒng)分析 Windows 網(wǎng)絡(luò)輪詢的歷史與現(xiàn)實(shí)、IOCP 與輪詢模型的本質(zhì)差異并逐一評估 DDDDemo-Driven Design演示驅(qū)動設(shè)計系列示例在 Windows 下的適配性最后結(jié)合 ddd-05-mem-nonblocking.c 與 ddd-06-mem-uv.c 的源碼給出memory BIO 打通 IOCP這一經(jīng)過驗(yàn)證的實(shí)戰(zhàn)方案。讀完本文你將理解為什么 libssl 不需要也無法直接支持 IOCP以及如何在自己基于 OpenSSL 的應(yīng)用中接入 Windows 異步 I/O 模型。Windows 網(wǎng)絡(luò)輪詢的現(xiàn)實(shí)poll(2) 缺失與 select() 的另類語義WSAPoll(2)一個遲到的、曾經(jīng)有缺陷的替代品在 POSIX 平臺上應(yīng)用可以調(diào)用poll(2)系統(tǒng)調(diào)用以位掩碼bitmask的方式批量監(jiān)聽文件描述符FD的可讀、可寫與錯誤事件。但Windows 并不提供poll(2)系統(tǒng)調(diào)用。微軟在 Vista 中引入了WSAPoll(2)本意是補(bǔ)齊這一能力然而它攜帶一個微軟拒絕修復(fù)的 bug使其在很長一段時間內(nèi)形同虛設(shè)。直到 Windows 10 的某個構(gòu)建版本該 bug 才終于被修復(fù)。由此得到的結(jié)論是WSAPoll(2)在今天是一種可行的方案但僅適用于較新版本的 Windows。對于需要兼容老系統(tǒng)的應(yīng)用它并不是一個安全的默認(rèn)選擇。select()Windows 版其實(shí)更接近 POSIX poll()在傳統(tǒng)上Windows 上的輪詢工作一直由select()承擔(dān)。但與 POSIX 平臺相比select()的調(diào)用方式存在一個重要差異POSIXselect()接收一個 FD 位掩碼bitmaskWindowsselect()接收一個內(nèi)嵌固定長度 socket 句柄數(shù)組的結(jié)構(gòu)體。這種差異并非設(shè)計者的任性而是由 Windows 的對象模型決定的——Windows 上的 socket 是 NT 內(nèi)核句柄NT kernel handles并非像 FD 那樣連續(xù)分配因此無法用簡單的位掩碼表達(dá)。諷刺的是正因?yàn)槿绱薟indows 的select()在語義上非常接近 POSIX 的poll()——它們都是在給定的一組 socket 句柄上等待事件。所以對 Windows 開發(fā)者而言select()一直是輪詢polling場景下可行的選擇。IOCPWindows 的高性能異步模型與輪詢范式不相容為什么 select()/poll() 都不是高性能選項無論是select()還是poll()都屬于就緒通知readiness notification模型內(nèi)核告訴應(yīng)用某個 socket 現(xiàn)在可以讀/寫了應(yīng)用隨后自行發(fā)起實(shí)際的數(shù)據(jù)讀寫。這種模型在多連接、高吞吐場景下存在可擴(kuò)展性瓶頸這也是 Linux 上 epoll、BSD/macOS 上 kqueue 應(yīng)運(yùn)而生的原因。而在 Windows 上系統(tǒng)不提供任何類似 epoll 或 kqueue 的機(jī)制。官方給出的高性能網(wǎng)絡(luò) I/O 路徑是I/O Completion PortsIOCP。輪詢報告就緒IOCP 報告完成文檔 WINDOWS.md 點(diǎn)明了兩種模型最本質(zhì)的區(qū)別輪詢polling報告的是就緒readiness而 IOCP 報告的是操作完成completion of an operation。在 IOCP 模型下你對 socket 發(fā)起一次讀或?qū)懏?dāng)該讀/寫真正完成時一個完成事件才會被投遞到關(guān)聯(lián)的 IOCP 隊列。這是一種與輪詢根本不同的模型從概念上講它更像 libuv 這類高層異步 I/O 庫的工作方式libuv 內(nèi)部在 Windows 上正是基于 IOCP 實(shí)現(xiàn)的。為什么在 IOCP 之上做輪詢幾乎不可能文檔給出了一個非常精辟的工程觀察IOCP 是一種比輪詢更高層的接口——在輪詢之上構(gòu)建一個 IOCP 風(fēng)格的接口是容易的你可以在可讀/可寫事件觸發(fā)后立即發(fā)起 I/O并在完成時投遞完成事件但在 IOCP 之上構(gòu)建一個輪詢風(fēng)格的接口卻幾乎不可能因?yàn)檩喸円髴?yīng)用在事件循環(huán)中主動詢問現(xiàn)在能不能讀而 IOCP 只能在異步操作結(jié)束后通知你二者無法互相模擬。正是這種阻抗失配impedance discontinuity導(dǎo)致現(xiàn)實(shí)中絕大多數(shù)異步 I/O 庫內(nèi)部都維護(hù)著兩套實(shí)現(xiàn)一套基于輪詢用于 Linux/macOS 等一套基于 IOCP用于 Windows。文檔明確列舉了libuv 與 nanomsg作為例子——與其費(fèi)盡心思彌合兩種模型的差異不如分別為輪詢式 I/O 反應(yīng)器和IOCP 式實(shí)現(xiàn)各寫一份代碼這反而更簡單、更可靠。逐一評估DDD 系列示例在 Windows/IOCP 下的適用性DDDDemo-Driven Design是 OpenSSL 項目為支撐 QUIC 等 API 演進(jìn)而維護(hù)的一套代表性 API 用法示例目錄說明見 doc/designs/ddd/README.md。WINDOWS.md 對其中 6 個示例在 Windows IOCP 場景下的適用性逐一給出了結(jié)論示例類型IOCP 適用性評估ddd-01-conn-blocking.cS-BIOc阻塞不適用阻塞式示例IOCP 無從談起ddd-02-conn-nonblocking.cA-BIOc非阻塞不支持socket 由 OpenSSL 管理IOCP 不受支持ddd-03-fd-blocking.cS-AOSF阻塞不適用阻塞式示例IOCP 無從談起ddd-04-fd-nonblocking.cA-AOSF非阻塞不支持libssl 經(jīng)BIO_set_fd拿到 FDBIO_s_sock不支持 overlapped I/Oddd-05-mem-nonblocking.cA-BIOmmemory BIO可行應(yīng)用完全掌控 memory BIO 與網(wǎng)絡(luò)間的數(shù)據(jù)搬運(yùn)可自由使用 IOCPddd-06-mem-uv.cA-BIOmmemory BIO libuv已證明libuv 在 Windows 上內(nèi)部使用 IOCP直接驗(yàn)證了該路徑阻塞示例ddd-01 / ddd-03與 IOCP 無關(guān)ddd-01-conn-blocking是最簡單的阻塞式用法應(yīng)用通過BIO_new_ssl_connect(ctx)創(chuàng)建連接 BIO把名稱解析、連接建立乃至 TLS 握手全部交給 OpenSSL 內(nèi)部處理對應(yīng)BIO_s_connect家族即 README 中的 BIOc 模式。ddd-03-fd-blocking則是應(yīng)用自己創(chuàng)建 socket 并通過SSL_set_fd交給 libssl 的阻塞版本AOSF 模式。阻塞模型本身與異步完成通知的 IOCP 屬于不同世界因此文檔的結(jié)論直截了當(dāng)IOCP 不適用not applicable。ddd-02-conn-nonblockingsocket 歸 OpenSSL 管IOCP 無門該示例的非阻塞版本中連接建立同樣由BIO_s_connect驅(qū)動socket 的生命周期由 OpenSSL 內(nèi)部管理應(yīng)用拿不到可自行支配的 socket 句柄來發(fā)起異步 I/O因此文檔判定IOCP 不受支持。如果應(yīng)用需要 IOCP這條路從入口處就堵死了。ddd-04-fd-nonblockingBIO_s_sock 與 overlapped I/O 的鴻溝ddd-04-fd-nonblocking中應(yīng)用自行創(chuàng)建 socket再通過BIO_set_fd把 FD 交給 libsslAOSF 模式。文檔給出的關(guān)鍵判斷是BIO_s_sock似乎不支持 overlapped即基于 IOCP 的I/O因?yàn)檫@會要求使用專門的WSASend()和WSARecv()函數(shù)而不是標(biāo)準(zhǔn)的send()/recv()。也就是說Windows 上要接入 IOCP必須以 overlapped 句柄 WSASend/WSARecv發(fā)起 I/O而BIO_s_sock實(shí)現(xiàn)在 crypto/bio/bio_sock.c 等文件中走的是標(biāo)準(zhǔn)send()/recv()路徑兩者無法兼容。文檔在此補(bǔ)充了一個很務(wù)實(shí)的推論既然 libssl 的BIO_s_sock本來就不支持 IOCP那么任何正在使用BIO_s_sock的應(yīng)用顯然本身也沒有在嘗試使用 IOCP——因此我們完全不必為如何把 ddd-04 適配到 IOCP而擔(dān)憂。這類應(yīng)用的 I/O 模型與 IOCP 天然互斥保持現(xiàn)狀即可。ddd-05-mem-nonblocking應(yīng)用掌控一切IOCP 大門敞開ddd-05-mem-nonblocking是文檔給出的 IOCP 可行路徑由于應(yīng)用完全掌控數(shù)據(jù)在 memory BIO 與網(wǎng)絡(luò)之間的搬運(yùn)無論是從網(wǎng)絡(luò)讀入再喂給 memory BIO還是從 memory BIO 取出再寫出到網(wǎng)絡(luò)應(yīng)用完全可以按自己的意愿使用 IOCP。這為后續(xù)兩個示例奠定了理論基礎(chǔ)。memory BIO 方案源碼剖析把 libssl 當(dāng)作純狀態(tài)機(jī)ddd-05-mem-nonblocking.c 展示了這一模式的核心結(jié)構(gòu)。其設(shè)計哲學(xué)在文件頭注釋中寫得很清楚應(yīng)用向 OpenSSL 傳入 memory BIO意味著它既控制解密側(cè)數(shù)據(jù)何時從 SSL 對象讀寫也控制網(wǎng)絡(luò)加密數(shù)據(jù)何時送入/取出 OpenSSL。這樣 OpenSSL 就被當(dāng)作一個不做任何自身網(wǎng)絡(luò) I/O 調(diào)用的純狀態(tài)機(jī)它永遠(yuǎn)不會看到或創(chuàng)建任何網(wǎng)絡(luò) socket 的文件描述符。連接結(jié)構(gòu)雙向內(nèi)存管道typedef struct app_conn_st { SSL *ssl; BIO *ssl_bio, *net_bio; int rx_need_tx, tx_need_rx; } APP_CONN;在new_conn()中應(yīng)用通過 BIO 對BIO pair把 libssl 與網(wǎng)絡(luò)徹底解耦BIO *internal_bio, *net_bio; if (BIO_new_bio_pair(internal_bio, 0, net_bio, 0) 0) { ... } SSL_set_bio(ssl, internal_bio, internal_bio);BIO_new_bio_pair創(chuàng)建一對雙向內(nèi)存緩沖區(qū) BIO實(shí)現(xiàn)在 crypto/bio/bss_bio.cinternal_bio掛在 SSL 對象內(nèi)部供 libssl 讀寫加解密數(shù)據(jù)net_bio留在應(yīng)用手中用于與真實(shí)網(wǎng)絡(luò) socket 交互。libssl 從始至終只跟內(nèi)存打交道網(wǎng)絡(luò)字節(jié)流的進(jìn)出完全由應(yīng)用的read_net_tx/write_net_rx兩個函數(shù)驅(qū)動分別對應(yīng)BIO_read(net_bio, ...)與BIO_write(net_bio, ...)。值得一提的 QUIC 差異可見 REPORT.md編譯時定義USE_QUIC時demo 改用BIO_new_bio_dgram_pair以獲得帶數(shù)據(jù)報datagram語義的雙向內(nèi)存 BIO保證 QUIC 報文的邊界不被破壞同時文件注釋明確警告QUIC 下應(yīng)用用于緩沖數(shù)據(jù)報的緩沖區(qū)若小于 1472 字節(jié)必須調(diào)整否則報文會被截斷行為類似read(2)。非阻塞收發(fā)WANT_READ / WANT_WRITE 驅(qū)動的狀態(tài)記錄tx()與rx()是非阻塞收發(fā)的核心它們通過SSL_get_error識別SSL_ERROR_WANT_READ/SSL_ERROR_WANT_WRITE并記錄反向需求int tx(APP_CONN *conn, const void *buf, int buf_len) { l BIO_write(conn-ssl_bio, buf, buf_len); if (l 0) { rc SSL_get_error(conn-ssl, l); switch (rc) { case SSL_ERROR_WANT_READ: conn-tx_need_rx 1; /* 想寫卻需要先讀 → 等 POLLIN */ case SSL_ERROR_WANT_CONNECT: case SSL_ERROR_WANT_WRITE: return -2; /* 等價 EWOULDBLOCK */ default: return -1; /* 錯誤 */ } } else { conn-tx_need_rx 0; } return l; }rx()對稱地維護(hù)rx_need_tx。隨后get_conn_pending_tx()/get_conn_pending_rx()把這兩個狀態(tài)翻譯成應(yīng)用事件循環(huán)需要的 poll 事件POLLIN/POLLOUT/POLLERR。對于 QUIC 構(gòu)建事件判定改由SSL_net_read_desired/SSL_net_write_desired兩個 API 驅(qū)動詳見 REPORT.md 對 API 演進(jìn)過程的記錄。把 libssl 接上任意事件循環(huán)最后demo 中的pump()展示了應(yīng)用側(cè)的事件循環(huán)膠水層用net_rx_space()BIO_ctrl_get_write_guarantee與net_tx_avail()BIO_ctrl_pending決定要不要監(jiān)聽可讀/可寫事件然后網(wǎng)絡(luò)可讀時read(fd, ...)讀入字節(jié) →write_net_rx(conn, ...)喂給 libssl網(wǎng)絡(luò)可寫時read_net_tx(conn, ...)取出 libssl 產(chǎn)出的密文 →write(fd, ...)送出。這個pump內(nèi)部的poll()調(diào)用在 Windows 上完全可以替換為select()/WSAPoll()甚至替換為IOCP 的異步讀寫——因?yàn)?demo 已經(jīng)不再讓 libssl 觸碰任何 socket網(wǎng)絡(luò)側(cè)用什么 I/O 模型完全由應(yīng)用決定。這正是 ddd-05 能夠兼容 IOCP 的根本原因。實(shí)戰(zhàn)驗(yàn)證memory BIO libuv 組合ddd-06libuv 與 IOCP 的關(guān)系ddd-06-mem-uv.c 將上面的模式搬到了libuv之上。libuv 是 Node.js 使用的異步 I/O 庫在 Windows 上其內(nèi)部正是基于 IOCP 實(shí)現(xiàn)而在 Unix 系平臺基于 epoll/kqueue。因此libssl memory BIO libuv這個組合在 Windows 上運(yùn)行時libssl 之上疊著的正是一條 IOCP 鏈路——它直接證明了 memory BIO 方案可以支撐 IOCP 場景。關(guān)鍵實(shí)現(xiàn)要點(diǎn)demo 的結(jié)構(gòu)比 ddd-05 更工程化引入了上層寫請求UPPER_WRITE_OP與網(wǎng)絡(luò)層寫請求LOWER_WRITE_OP兩個隊列上層寫app_write()→write_deferred()把應(yīng)用數(shù)據(jù)封裝成UPPER_WRITE_OP入隊隨后try_write()循環(huán)調(diào)用SSL_write若返回SSL_ERROR_WANT_READ例如 TLS 重協(xié)商或 QUIC 流控該 op 留在隊中待后續(xù)事件到達(dá)時由handle_pending_writes()續(xù)寫。網(wǎng)絡(luò)層寫flush_write_buf()從net_bio讀走 libssl 產(chǎn)出的密文封裝為LOWER_WRITE_OP通過uv_write()TLS 場景或uv_udp_send()QUIC 場景交給 libuv完成回調(diào)net_write_done()釋放緩沖并繼續(xù)flush_write_buf()。網(wǎng)絡(luò)層讀net_read_done()在 libuv 讀事件到達(dá)時把收到的字節(jié)BIO_write進(jìn)net_bio再驅(qū)動handshake_ssl()或on_rx_push()內(nèi)部SSL_read取出明文并回調(diào)應(yīng)用。QUIC 定時器#ifdef USE_QUIC分支用SSL_get_event_timeout()獲取事件處理截止時間通過uv_timer_t周期性調(diào)用SSL_handle_events()實(shí)現(xiàn) QUIC 時鐘驅(qū)動同樣見 REPORT.md。這套雙層隊列 BIO 對 事件回調(diào)的骨架就是文檔所說的用 memory BIO 支持 IOCP 用法的完整可運(yùn)行示范。文檔的外部佐證WINDOWS.md 還提到一個來自社區(qū)實(shí)踐的觀察粗略瀏覽 GitHub 上的代碼可以發(fā)現(xiàn)人們確實(shí)在使用 IOCP 搭配 libssl 時幾乎都是通過傳入 memory BIO 的方式實(shí)現(xiàn)的。因此 ddd-05 與 ddd-06 本質(zhì)上就是在復(fù)現(xiàn)這一真實(shí)用例尤其 ddd-06 在 Windows 上內(nèi)部就直接走 IOCP是這一結(jié)論的最強(qiáng)證明。構(gòu)建與運(yùn)行把 demo 跑起來驗(yàn)證Makefile 提供了完整的構(gòu)建規(guī)則每個 demo 都會生成-tls與-quic-DUSE_QUIC兩個變體ddd-%-tls: ddd-%.c $(CC_CMD) ddd-%-quic: ddd-%.c $(CC_CMD) -DUSE_QUIC ddd-%-uv-tls: ddd-%-uv.c $(CC_CMD) -luv ddd-%-uv-quic: ddd-%-uv.c $(CC_CMD) -luv -DUSE_QUIC要點(diǎn)編譯鏈接-lcrypto -lssl頭文件路徑為-I../../../include相對于 doc/designs/ddd 目錄構(gòu)建ddd-06-mem-uv需要 libuv 庫與頭文件Ubuntu 上安裝libuv1-dev即可鏈接共享庫運(yùn)行時需把 libcrypto/libssl 加入庫路徑例如LD_LIBRARY_PATH../../.. ./ddd-01-conn-blocking-tls運(yùn)行需要默認(rèn)證書庫可用必要時可設(shè)置SSL_CERT_DIR。結(jié)論與工程建議WINDOWS.md 給出的最終結(jié)論清晰而克制既然 libssl 本身就不支持 IOCP我們不必對此特別擔(dān)憂但在最壞情況下總有可行的解決方案——正如 demo 5 和 demo 6 所示。整理成工程建議即阻塞式應(yīng)用ddd-01/ddd-03 模式與 IOCP 無關(guān)維持現(xiàn)狀即可無需改造socket 由 OpenSSL 管理的非阻塞應(yīng)用ddd-02 模式本就不在 IOCP 路徑上無需糾結(jié)應(yīng)用自己持有 socket 的應(yīng)用ddd-04 模式BIO_s_sock只走send()/recv()無法 overlapped如果確實(shí)需要 IOCP應(yīng)轉(zhuǎn)向 memory BIO 方案需要 IOCP 的高性能應(yīng)用采用 ddd-05/ddd-06 的 memory BIO 模式讓 libssl 做純狀態(tài)機(jī)網(wǎng)絡(luò)側(cè)自由選擇select()、WSAPoll()或完整的 IOCP 異步 I/O。ddd-06 已用 libuvWindows 上即 IOCP證明了這條路的可行性。這套結(jié)論并非局限于 Windows——memory BIO 模式同樣是把 OpenSSL 接入任意第三方異步框架libuv、libevent、自研事件循環(huán)的通用鑰匙也是 OpenSSL 為 QUIC 時代準(zhǔn)備的 API 用法基線參見 REPORT.md 中對各 demo 的 QUIC 適配記錄。【免費(fèi)下載鏈接】opensslGeneral purpose TLS and crypto library項目地址: https://gitcode.com/GitHub_Trending/ope/openssl創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考