關于我們
書單推薦
新書推薦
|
Linux多線程服務端編程
《Linux多線程服務端編程:使用muduo C++網(wǎng)絡庫》主要講述采用現(xiàn)代C++在x86-64 Linux上編寫多線程TCP網(wǎng)絡服務程序的主流常規(guī)技術,重點講解一種適應性較強的多線程服務器的編程模型,即one loop per thread。這是在Linux下以native語言編寫用戶態(tài)高性能網(wǎng)絡程序最成熟的模式,掌握之后可順利地開發(fā)各類常見的服務端網(wǎng)絡應用程序。本書以muduo網(wǎng)絡庫為例,講解這種編程模型的使用方法及注意事項。
《Linux多線程服務端編程:使用muduo C++網(wǎng)絡庫》的宗旨是貴精不貴多。掌握兩種基本的同步原語就可以滿足各種多線程同步的功能需求,還能寫出更易用的同步設施。掌握一種進程間通信方式和一種多線程網(wǎng)絡編程模型就足以應對日常開發(fā)任務,編寫運行于公司內(nèi)網(wǎng)環(huán)境的分布式服務系統(tǒng)。
示范在多核時代采用現(xiàn)代C++ 編寫 多線程TCP 網(wǎng)絡服務器的正規(guī)做法
本書主要講述采用現(xiàn)代C++ 在x86-64 Linux 上編寫多線程TCP 網(wǎng)絡服務程序的主流常規(guī)技術,這也是我對過去5 年編寫生產(chǎn)環(huán)境下的多線程服務端程序的經(jīng)驗總結(jié)。本書重點講解多線程網(wǎng)絡服務器的一種IO 模型,即one loop per thread。這是一種適應性較強的模型,也是Linux 下以native 語言編寫用戶態(tài)高性能網(wǎng)絡程序最成熟的模式,掌握之后可順利地開發(fā)各類常見的服務端網(wǎng)絡應用程序。本書以muduo網(wǎng)絡庫為例,講解這種編程模型的使用方法及注意事項。
muduo 是一個基于非阻塞IO 和事件驅(qū)動的現(xiàn)代C++ 網(wǎng)絡庫,原生支持oneloop per thread 這種IO 模型。muduo 適合開發(fā)Linux 下的面向業(yè)務的多線程服務端網(wǎng)絡應用程序,其中“面向業(yè)務的網(wǎng)絡編程”的定義見附錄A!艾F(xiàn)代C++”指的不是C++11 新標準,而是2005 年TR1 發(fā)布之后的C++ 語言和庫。與傳統(tǒng)C++ 相比,現(xiàn)代C++ 的變化主要有兩方面:資源管理(見第1 章)與事件回調(diào)(見第449 頁)。 本書不是多線程編程教程,也不是網(wǎng)絡編程教程,更不是C++ 教程。讀者應該已經(jīng)大致讀過《UNIX 環(huán)境高級編程》、《UNIX 網(wǎng)絡編程》、《C++ Primer》或與之內(nèi)容相近的書籍。本書不談C++11,因為目前(2012 年)主流的Linux 服務端發(fā)行版的g++ 版本都還停留在4.4,C++11 進入實用尚需一段時日。 本書適用的硬件環(huán)境是主流x86-64 服務器,多路多核CPU、幾十GB 內(nèi)存、千兆以太網(wǎng)互聯(lián)。除了第5 章講診斷日志之外,本書不涉及文件IO。 本書分為四大部分,第1 部分“C++ 多線程系統(tǒng)編程”考察多線程下的對象生命期管理、線程同步方法、多線程與C++ 的結(jié)合、高效的多線程日志等。第2 部分“muduo 網(wǎng)絡庫”介紹使用現(xiàn)成的非阻塞網(wǎng)絡庫編寫網(wǎng)絡應用程序的方法,以及muduo 的設計與實現(xiàn)。第3 部分“工程實踐經(jīng)驗談”介紹分布式系統(tǒng)的工程化開發(fā)方法和C++ 在工程實踐中的功能特性取舍。第4 部分“附錄”分享網(wǎng)絡編程和C++語言的學習經(jīng)驗。 本書的宗旨是貴精不貴多。掌握兩種基本的同步原語就可以滿足各種多線程同步的功能需求,還能寫出更易用的同步設施。掌握一種進程間通信方式和一種多線程網(wǎng)絡編程模型就足以應對日常開發(fā)任務,編寫運行于公司內(nèi)網(wǎng)環(huán)境的分布式服務系統(tǒng)。(本書不涉及分布式存儲系統(tǒng),也不涉及UDP。) 術語與排版范例 本書大量使用英文術語,甚至有少量英文引文。設計模式的名字一律用英文,例如Observer、Reactor、Singleton。在中文術語不夠突出時,也會使用英文,例如class、heap、event loop、STL algorithm 等。注意幾個中文C++ 術語: 對象實體(instance)、函數(shù)重載決議(resolution)、模板具現(xiàn)化(instantiation)、覆寫(override)虛函數(shù)、提領(dereference)指針。本書中的英語可數(shù)名詞一般不用復數(shù)形式,例如兩個class,6 個syscall;但有時會用(s) 強調(diào)中文名詞是復數(shù)。fd 是文件描述符(file descriptor)的縮寫!癈PU 數(shù)目”一般指的是核(core)的數(shù)目。容量單位kB、MB、GB 表示的字節(jié)數(shù)分別為103、106、109,在特別強調(diào)準確數(shù)值時,會分別用KiB、MiB、GiB 表示210、220、230 字節(jié)。用諸如§11.5 表示本書第11.5 節(jié),L42 表示上下文中出現(xiàn)的第42 行代碼。[JCP]、[CC2e] 等是參考文獻,見書末清單。 一般術語用普通羅馬字體,如mutex、socket;C++ 關鍵字用無襯線字體,如class、this、mutable;函數(shù)名和class 名用等寬字體,如fork(2)、muduo::EventLoop,其中fork(2) 表示系統(tǒng)函數(shù)fork() 的文檔位于manpage 第2 節(jié),可以通過man 2 fork命令查看。如果函數(shù)名或類名過長,可能會折行,行末有連字號“-”,如EventLoop-ThreadPool。文件路徑和URL 采用窄字體,例如muduo/base/Date.h、http://chenshuo.com。用中文楷體表示引述別人的話。 代碼 本書的示例代碼以開源項目的形式發(fā)布在GitHub 上,地址是 本書配套頁面提供全部源代碼打包下載,正文中出現(xiàn)的類似recipes/thread 的路徑是壓縮包內(nèi)的相對路徑,讀者不難找到其對應的GitHub URL。本書引用代碼的形式如下,左側(cè)數(shù)字是文件的行號,右側(cè)的“muduo/base/Types.h”是文件路徑1。例如下面這幾行代碼是muduo::string 的typedef。 muduo/base/Types.h 15 namespace muduo 16 { 17 18 #ifdef MUDUO_STD_STRING 19 using std::string; 20 #else // !MUDUO_STD_STRING 21 typedef __gnu_cxx::__sso_string string; 22 #endif muduo/base/Types.h 在第6、7 兩章的muduo 示例代碼中,路徑muduo/examples/XXX 會簡寫為examples/XXX。此外,第8 章會把recipes/reactor/XXX 簡寫為reactor/XXX。 本書假定讀者熟悉diff -u 命令的輸出格式,用于表示代碼的改動。 本書正文中出現(xiàn)的代碼有時為了照顧排版而略有改寫,例如改變縮進規(guī)則,去掉單行條件語句前后的花括號等。就編程風格而論,應以電子版代碼為準。 陳碩 中國·香港
陳碩,北京師范大學碩士,擅長C++ 多線程網(wǎng)絡編程和實時分布式系統(tǒng)架構(gòu)。曾在摩根士丹利IT 部門工作5 年,從事實時外匯交易系統(tǒng)開發(fā)。現(xiàn)在在美國加州硅谷某互聯(lián)網(wǎng)大公司工作,從事大規(guī)模分布式系統(tǒng)的可靠性工程。編寫了開源C++ 網(wǎng)絡庫muduo,參與翻譯了《代碼大全(第2 版)》和《C++ 編程規(guī)范(繁體版)》,整理了《C++ Primer (第4 版)(評注版)》,并曾多次在各地技術大會演講。
第1 部分 C++ 多線程系統(tǒng)編程
第1 章 線程安全的對象生命期管理 1.1 當析構(gòu)函數(shù)遇到多線程 1.1.1 線程安全的定義 1.1.2 MutexLock 與MutexLockGuard 1.1.3 一個線程安全的Counter 示例 1.2 對象的創(chuàng)建很簡單. 1.3 銷毀太難 1.3.1 mutex 不是辦法 1.3.2 作為數(shù)據(jù)成員的mutex 不能保護析構(gòu). 1.4 線程安全的Observer 有多難. 1.5 原始指針有何不妥. 1.6 神器shared_ptr/weak_ptr 1.7 插曲:系統(tǒng)地避免各種指針錯誤. 1.8 應用到Observer 上 1.9 再論shared_ptr 的線程安全. 1.10 shared_ptr 技術與陷阱 1.11 對象池. 1.11.1 enable_shared_from_this 1.11.2 弱回調(diào). 1.12 替代方案 1.13 心得與小結(jié). 1.14 Observer 之謬. 第2 章 線程同步精要 2.1 互斥器(mutex) . 2.1.1 只使用非遞歸的mutex 2.1.2 死鎖 2.2 條件變量(condition variable) . 2.3 不要用讀寫鎖和信號量 2.4 封裝MutexLock、MutexLockGuard、Condition 2.5 線程安全的Singleton 實現(xiàn) 2.6 sleep(3) 不是同步原語 2.7 歸納與總結(jié). 2.8 借shared_ptr 實現(xiàn)copy-on-write 第3 章 多線程服務器的適用場合與常用編程模型 3.1 進程與線程. 3.2 單線程服務器的常用編程模型 3.3 多線程服務器的常用編程模型 3.3.1 one loop per thread 3.3.2 線程池. 3.3.3 推薦模式 3.4 進程間通信只用TCP . 3.5 多線程服務器的適用場合. 3.5.1 必須用單線程的場合. 3.5.2 單線程程序的優(yōu)缺點. 3.5.3 適用多線程程序的場景 3.6 “多線程服務器的適用場合”例釋與答疑 第4 章 C++ 多線程系統(tǒng)編程精要 4.1 基本線程原語的選用. 4.2 C/C++ 系統(tǒng)庫的線程安全性. 4.3 Linux 上的線程標識 4.4 線程的創(chuàng)建與銷毀的守則. 4.4.1 pthread_cancel 與C++ . 4.4.2 exit(3) 在C++ 中不是線程安全的. 4.5 善用__thread 關鍵字. 4.6 多線程與IO 4.7 用RAII 包裝文件描述符. 4.8 RAII 與fork() . 4.9 多線程與fork() . 4.10 多線程與signal 4.11 Linux 新增系統(tǒng)調(diào)用的啟示 第5 章 高效的多線程日志 5.1 功能需求 5.2 性能需求 5.3 多線程異步日志 5.4 其他方案 第2 部分 muduo 網(wǎng)絡庫 第6 章 muduo 網(wǎng)絡庫簡介 6.1 由來. 6.2 安裝. 6.3 目錄結(jié)構(gòu) 6.3.1 代碼結(jié)構(gòu) 6.3.2 例子 6.3.3 線程模型 6.4 使用教程 6.4.1 TCP 網(wǎng)絡編程本質(zhì)論. 6.4.2 echo 服務的實現(xiàn). 6.4.3 七步實現(xiàn)finger 服務. 6.5 性能評測 6.5.1 muduo 與Boost.Asio、libevent2 的吞吐量對比 6.5.2 擊鼓傳花:對比muduo 與libevent2 的事件處理效率 6.5.3 muduo 與Nginx 的吞吐量對比. 6.5.4 muduo 與ZeroMQ 的延遲對比. 6.6 詳解muduo 多線程模型. 6.6.1 數(shù)獨求解服務器 6.6.2 常見的并發(fā)網(wǎng)絡服務程序設計方案. 第7 章 muduo 編程示例 7.1 五個簡單TCP 示例 7.2 文件傳輸 7.3 Boost.Asio 的聊天服務器. 7.3.1 TCP 分包 7.3.2 消息格式 7.3.3 編解碼器LengthHeaderCodec 7.3.4 服務端的實現(xiàn). 7.3.5 客戶端的實現(xiàn). 7.4 muduo Buffer 類的設計與使用. 7.4.1 muduo 的IO 模型 7.4.2 為什么non-blocking 網(wǎng)絡編程中應用層buffer 是必需的 7.4.3 Buffer 的功能需求 7.4.4 Buffer 的數(shù)據(jù)結(jié)構(gòu) 7.4.5 Buffer 的操作. 7.4.6 其他設計方案. 7.4.7 性能是不是問題 7.5 一種自動反射消息類型的Google Protobuf 網(wǎng)絡傳輸方案 7.5.1 網(wǎng)絡編程中使用Protobuf 的兩個先決條件. 7.5.2 根據(jù)type name 反射自動創(chuàng)建Message 對象 7.5.3 Protobuf 傳輸格式 7.6 在muduo 中實現(xiàn)Protobuf 編解碼器與消息分發(fā)器 7.6.1 什么是編解碼器(codec) 7.6.2 實現(xiàn)ProtobufCodec . 7.6.3 消息分發(fā)器(dispatcher)有什么用 7.6.4 ProtobufCodec 與ProtobufDispatcher 的綜合運用. 7.6.5 ProtobufDispatcher 的兩種實現(xiàn) 7.6.6 ProtobufCodec 和ProtobufDispatcher 有何意義. 7.7 限制服務器的最大并發(fā)連接數(shù) 7.7.1 為什么要限制并發(fā)連接數(shù) 7.7.2 在muduo 中限制并發(fā)連接數(shù) 7.8 定時器. 7.8.1 程序中的時間. 7.8.2 Linux 時間函數(shù) 7.8.3 muduo 的定時器接口. 7.8.4 Boost.Asio Timer 示例 7.8.5 Java Netty 示例 7.9 測量兩臺機器的網(wǎng)絡延遲和時間差. 7.10 用timing wheel 踢掉空閑連接 7.10.1 timing wheel 原理 7.10.2 代碼實現(xiàn)與改進 7.11 簡單的消息廣播服務. 7.12 “串并轉(zhuǎn)換”連接服務器及其自動化測試 7.13 socks4a 代理服務器 7.13.1 TCP 中繼器 7.13.2 socks4a 代理服務器 7.13.3 N : 1 與1 : N 連接轉(zhuǎn)發(fā) 7.14 短址服務 7.15 與其他庫集成. 7.15.1 UDNS . 7.15.2 c-ares DNS . 7.15.3 curl . 7.15.4 更多 第8 章 muduo 網(wǎng)絡庫設計與實現(xiàn) 8.0 什么都不做的EventLoop . 8.1 Reactor 的關鍵結(jié)構(gòu) 8.1.1 Channel class . 8.1.2 Poller class 8.1.3 EventLoop 的改動. 8.2 TimerQueue 定時器 8.2.1 TimerQueue class . 8.2.2 EventLoop 的改動. 8.3 EventLoop::runInLoop() 函數(shù) 8.3.1 提高TimerQueue 的線程安全性. 8.3.2 EventLoopThread class 8.4 實現(xiàn)TCP 網(wǎng)絡庫 8.5 TcpServer 接受新連接 8.5.1 TcpServer class 8.5.2 TcpConnection class . 8.6 TcpConnection 斷開連接. 8.7 Buffer 讀取數(shù)據(jù) 8.7.1 TcpConnection 使用Buffer 作為輸入緩沖. 8.7.2 Buffer::readFd() 8.8 TcpConnection 發(fā)送數(shù)據(jù). 8.9 完善TcpConnection 8.9.1 SIGPIPE 8.9.2 TCP No Delay 和TCP keepalive 8.9.3 WriteCompleteCallback 和HighWaterMarkCallback . 8.10 多線程TcpServer . 8.11 Connector . 8.12 TcpClient . 8.13 epoll 8.14 測試程序一覽. 第3 部分 工程實踐經(jīng)驗談 第9 章 分布式系統(tǒng)工程實踐 9.1 我們在技術浪潮中的位置. 9.1.1 分布式系統(tǒng)的本質(zhì)困難 9.1.2 分布式系統(tǒng)是個險惡的問題. 9.2 分布式系統(tǒng)的可靠性淺說. 9.2.1 分布式系統(tǒng)的軟件不要求7 24 可靠 9.2.2 “能隨時重啟進程”作為程序設計目標. 9.3 分布式系統(tǒng)中心跳協(xié)議的設計 9.4 分布式系統(tǒng)中的進程標識. 9.4.1 錯誤做法 9.4.2 正確做法 9.4.3 TCP 協(xié)議的啟示 9.5 構(gòu)建易于維護的分布式程序. 9.6 為系統(tǒng)演化做準備. 9.6.1 可擴展的消息格式 9.6.2 反面教材:ICE 的消息打包格式. 9.7 分布式程序的自動化回歸測試 9.7.1 單元測試的能與不能. 9.7.2 分布式系統(tǒng)測試的要點 9.7.3 分布式系統(tǒng)的抽象觀點 9.7.4 一種自動化的回歸測試方案. 9.7.5 其他用處 9.8 分布式系統(tǒng)部署、監(jiān)控與進程管理的幾重境界. 9.8.1 境界1:全手工操作. 9.8.2 境界2:使用零散的自動化腳本和第三方組件. 9.8.3 境界3:自制機群管理系統(tǒng),集中化配置. 9.8.4 境界4:機群管理與naming service 結(jié)合. 第10 章 C++ 編譯鏈接模型精要 10.1 C 語言的編譯模型及其成因. 10.1.1 為什么C 語言需要預處理 10.1.2 C 語言的編譯模型. 10.2 C++ 的編譯模型 10.2.1 單遍編譯 10.2.2 前向聲明 10.3 C++ 鏈接(linking) . 10.3.1 函數(shù)重載 10.3.2 inline 函數(shù). 10.3.3 模板 10.3.4 虛函數(shù). 10.4 工程項目中頭文件的使用規(guī)則 10.4.1 頭文件的害處. 10.4.2 頭文件的使用規(guī)則 10.5 工程項目中庫文件的組織原則 10.5.1 動態(tài)庫是有害的 10.5.2 靜態(tài)庫也好不到哪兒去 10.5.3 源碼編譯是王道 第11 章 反思C++ 面向?qū)ο笈c虛函數(shù) 11.1 樸實的C++ 設計 11.2 程序庫的二進制兼容性 11.2.1 什么是二進制兼容性. 11.2.2 有哪些情況會破壞庫的ABI . 11.2.3 哪些做法多半是安全的 11.2.4 反面教材:COM . 11.2.5 解決辦法 11.3 避免使用虛函數(shù)作為庫的接口 11.3.1 C++ 程序庫的作者的生存環(huán)境 11.3.2 虛函數(shù)作為庫的接口的兩大用途 11.3.3 虛函數(shù)作為接口的弊端 11.3.4 假如Linux 系統(tǒng)調(diào)用以COM 接口方式實現(xiàn) 11.3.5 Java 是如何應對的 11.4 動態(tài)庫接口的推薦做法 11.5 以boost::function 和boost::bind 取代虛函數(shù). 11.5.1 基本用途 11.5.2 對程序庫的影響 11.5.3 對面向?qū)ο蟪绦蛟O計的影響. 11.6 iostream 的用途與局限 11.6.1 stdio 格式化輸入輸出的缺點. 11.6.2 iostream 的設計初衷. 11.6.3 iostream 與標準庫其他組件的交互. 11.6.4 iostream 在使用方面的缺點. 11.6.5 iostream 在設計方面的缺點. 11.6.6 一個300 行的memory buffer output stream . 11.6.7 現(xiàn)實的C++ 程序如何做文件IO . 11.7 值語義與數(shù)據(jù)抽象. 11.7.1 什么是值語義. 11.7.2 值語義與生命期 11.7.3 值語義與標準庫 11.7.4 值語義與C++ 語言 11.7.5 什么是數(shù)據(jù)抽象 11.7.6 數(shù)據(jù)抽象所需的語言設施 11.7.7 數(shù)據(jù)抽象的例子 第12 章 C++ 經(jīng)驗談 12.1 用異或來交換變量是錯誤的. 12.1.1 編譯器會分別生成什么代碼. 12.1.2 為什么短的代碼不一定快 12.2 不要重載全局::operator new() 12.2.1 內(nèi)存管理的基本要求. 12.2.2 重載::operator new() 的理由. 12.2.3 ::operator new() 的兩種重載方式. 12.2.4 現(xiàn)實的開發(fā)環(huán)境 12.2.5 重載::operator new() 的困境. 12.2.6 解決辦法:替換malloc() 12.2.7 為單獨的class 重載::operator new() 有問題嗎. 12.2.8 有必要自行定制內(nèi)存分配器嗎 12.3 帶符號整數(shù)的除法與余數(shù). 12.3.1 語言標準怎么說 12.3.2 C/C++ 編譯器的表現(xiàn). 12.3.3 其他語言的規(guī)定 12.3.4 腳本語言解釋器代碼. 12.3.5 硬件實現(xiàn) 12.4 在單元測試中mock 系統(tǒng)調(diào)用 12.4.1 系統(tǒng)函數(shù)的依賴注入. 12.4.2 鏈接期墊片(link seam) 12.5 慎用匿名namespace . 12.5.1 C 語言的static 關鍵字的兩種用法. 12.5.2 C++ 語言的static 關鍵字的四種用法 12.5.3 匿名namespace 的不利之處. 12.5.4 替代辦法 12.6 采用有利于版本管理的代碼格式. 12.6.1 對diff 友好的代碼格式 12.6.2 對grep 友好的代碼風格. 12.6.3 一切為了效率. 12.7 再探std::string . 12.7.1 直接拷貝(eager copy) . 12.7.2 寫時復制(copy-on-write) . 12.7.3 短字符串優(yōu)化(SSO) 12.8 用STL algorithm 輕松解決幾道算法面試題 12.8.1 用next_permutation() 生成排列與組合 12.8.2 用unique() 去除連續(xù)重復空白. 12.8.3 用{make,push,pop}_heap() 實現(xiàn)多路歸并 12.8.4 用partition() 實現(xiàn)“重排數(shù)組,讓奇數(shù)位于偶數(shù)前面” 12.8.5 用lower_bound() 查找IP 地址所屬的城市. 第4 部分 附錄 附錄A 談一談網(wǎng)絡編程學習經(jīng)驗 附錄B 從《C++ Primer(第4 版)》入手學習C++ 附錄C 關于Boost 的看法 附錄D 關于TCP 并發(fā)連接的幾個思考題與試驗
附錄A
談一談網(wǎng)絡編程學習經(jīng)驗 本文談一談我在學習網(wǎng)絡編程方面的一些個人經(jīng)驗!熬W(wǎng)絡編程”這個術語的范圍很廣,本文指用Sockets API 開發(fā)基于TCP/IP 的網(wǎng)絡應用程序,具體定義見§A.1.5 “網(wǎng)絡編程的各種任務角色”。 受限于本人的經(jīng)歷和經(jīng)驗,本附錄的適應范圍是: o x86-64 Linux 服務端網(wǎng)絡編程,直接或間接使用Sockets API。 o 公司內(nèi)網(wǎng)。不一定是局域網(wǎng),但總體位于公司防火墻之內(nèi),環(huán)境可控。 本文可能不適合: o PC 客戶端網(wǎng)絡編程,程序運行在客戶的PC 上,環(huán)境多變且不可控。 o Windows 網(wǎng)絡編程。 o 面向公網(wǎng)的服務程序。 o 高性能網(wǎng)絡服務器。 本文分兩個部分: 1. 網(wǎng)絡編程的一些“胡思亂想”,以自問自答的形式談談我對這一領域的認識。 2. 幾本必看的書,基本上還是W. Richard Stevents 的那幾本。 另外,本文沒有特別說明時均暗指TCP 協(xié)議,“連接”是“TCP 連接”,“服務 端”是“TCP 服務端”。 A.1 網(wǎng)絡編程的一些“胡思亂想” 以下大致列出我對網(wǎng)絡編程的一些想法,前后無關聯(lián)。 A.1.1 網(wǎng)絡編程是什么 網(wǎng)絡編程是什么?是熟練使用Sockets API 嗎?說實話,在實際項目里我只用過 兩次Sockets API,其他時候都是使用封裝好的網(wǎng)絡庫。 第一次是2005 年在學校做一個羽毛球賽場計分系統(tǒng):我用C# 編寫運行在PC上的軟件,負責比分的顯示;再用C# 寫了運行在PDA 上的計分界面,記分員拿著PDA 記錄比分;這兩部分程序通過TCP 協(xié)議相互通信。這其實是個簡單的分布式系統(tǒng),體育館有幾片場地,每個場地都有一名拿PDA 的記分員,每個場地都有兩臺顯示比分的PC (顯示器是42 寸平板電視,放在場地的對角,這樣兩邊看臺的觀眾都能看到比分)。這兩臺PC 的功能不完全一樣,一臺只負責顯示當前比分,另一臺還要負責與PDA 通信,并更新數(shù)據(jù)庫里的比分信息。此外,還有一臺PC 負責周期性地從數(shù)據(jù)庫讀出全部7 片場地的比分,顯示在體育館墻上的大屏幕上。這臺PC 上還運行著一個程序,負責生成比分數(shù)據(jù)的靜態(tài)頁面,通過FTP 上傳發(fā)布到某門戶網(wǎng)站的體育頻道。系統(tǒng)中還有一個錄入賽程(參賽隊、運動員、出場順序等)數(shù)據(jù)庫的程序,運行在數(shù)據(jù)庫服務器上。算下來整個系統(tǒng)有十來個程序,運行在二十多臺設備(PC 和PDA)上,還要考慮可靠性,避免single point of failure。 這是我第一次寫實際項目中的網(wǎng)絡程序,當時寫下來的感覺是像寫命令行與用戶交互的程序:程序在命令行輸出一句提示語,等待客戶輸入一句話,然后處理客戶輸入,再輸出下一句提示語,如此循環(huán)。只不過這里的“客戶”不是人,而是另一個程序。在建立好TCP 連接之后,雙方的程序都是read/write 循環(huán)(為求簡單,我用的是blocking 讀寫),直到有一方斷開連接。 第二次是2010 年編寫muduo 網(wǎng)絡庫,我再次拿起了Sockets API,寫了一個基于Reactor 模式的C++ 網(wǎng)絡庫。寫這個庫的目的之一就是想讓日常的網(wǎng)絡編程從Sockets API 的瑣碎細節(jié)中解脫出來,讓程序員專注于業(yè)務邏輯,把時間用在刀刃上。muduo 網(wǎng)絡庫的示例代碼包含了幾十個網(wǎng)絡程序,這些示例程序都沒有直接使用Sockets API。 在此之外,無論是實習還是工作,雖然我寫的程序都會通過TCP 協(xié)議與其他程序打交道,但我沒有直接使用過Sockets API。對于TCP 網(wǎng)絡編程,我認為核心是處理“三個半事件”,見§6.4.1 “TCP 網(wǎng)絡編程本質(zhì)論”。程序員的主要工作是在事件處理函數(shù)中實現(xiàn)業(yè)務邏輯,而不是和Sockets API“較勁”。 這里還是沒有說清楚“網(wǎng)絡編程”是什么,請繼續(xù)閱讀后文§A.1.5“網(wǎng)絡編程的各種任務角色”。 A.1.2 學習網(wǎng)絡編程有用嗎 以上說的是比較底層的網(wǎng)絡編程,程序代碼直接面對從TCP 或UDP 收到的數(shù)據(jù)以及構(gòu)造數(shù)據(jù)包發(fā)出去。在實際工作中,另一種常見的情況是通過各種client library來與服務端打交道,或者在現(xiàn)成的框架中填空來實現(xiàn)server,或者采用更上層的通信方式。比如用libmemcached 與memcached 打交道,使用libpq 來與PostgreSQL 打交道,編寫Servlet 來響應HTTP 請求,使用某種RPC 與其他進程通信,等等。這些情況都會發(fā)生網(wǎng)絡通信,但不一定算作“網(wǎng)絡編程”。如果你的工作是前面列舉的這些,學習TCP/IP 網(wǎng)絡編程還有用嗎? 我認為還是有必要學一學,至少在troubleshooting 的時候有用。無論如何,這些library 或framework 都會調(diào)用底層的Sockets API 來實現(xiàn)網(wǎng)絡功能。當你的程序遇到一個線上問題時,如果你熟悉Sockets API,那么從strace 不難發(fā)現(xiàn)程序卡在哪里,盡管可能你沒有直接調(diào)用這些Sockets API。另外,熟悉TCP/IP 協(xié)議、會用tcpdump 也非常有助于分析解決線上網(wǎng)絡服務問題。 A.1.3 在什么平臺上學習網(wǎng)絡編程 對于服務端網(wǎng)絡編程,我建議在Linux 上學習。 如果在10 年前,這個問題的答案或許是FreeBSD,因為FreeBSD“根正苗紅”,在2000 年那一次互聯(lián)網(wǎng)浪潮中扮演了重要角色,是很多公司首選的免費服務器操作系統(tǒng)。2000 年那會兒Linux 還遠未成熟,連epoll 都還沒有實現(xiàn)。(FreeBSD 在2001年發(fā)布4.1 版,加入了kqueue,從此C10k 不是問題。) 10 年后的今天,事情起了一些變化,Linux 成為市場份額最大的服務器操作系統(tǒng)。在Linux 這種大眾系統(tǒng)上學網(wǎng)絡編程,遇到什么問題會比較容易解決。因為用的人多,你遇到的問題別人多半也遇到過;同樣因為用的人多,如果真的有什么內(nèi)核bug,很快就會得到修復,至少有work around 的辦法。如果用別的系統(tǒng),可能一個問題發(fā)到論壇上半個月都不會有人理。從內(nèi)核源碼的風格看,F(xiàn)reeBSD 更干凈整潔,注釋到位,但是無奈它的市場份額遠不如Linux,學習Linux 是更好的技術投資。 A.1.4 可移植性重要嗎 寫網(wǎng)絡程序要不要考慮移植性?要不要跨平臺?這取決于項目需要,如果貴公司做的程序要賣給其他公司,而對方可能使用Windows、Linux、FreeBSD、Solaris、AIX、HP-UX 等等操作系統(tǒng),這時候當然要考慮移植性。如果編寫公司內(nèi)部的服務器上用的網(wǎng)絡程序,那么大可只關注一個平臺,比如Linux。因為編寫和維護可移植的網(wǎng)絡程序的代價相當高,平臺間的差異可能遠比想象中大,即便是POSIX 系統(tǒng)之間也有不小的差異(比如Linux 沒有SO_NOSIGPIPE 選項,Linux 的pipe(2) 是單向的,而FreeBSD 是雙向的),錯誤的返回碼也大不一樣。 我就不打算把muduo 往Windows 或其他操作系統(tǒng)移植。如果需要編寫可移植的網(wǎng)絡程序,我寧愿用libevent、libuv、Java Netty 這樣現(xiàn)成的庫,把“臟活、累活”留給別人。 A.1.5 網(wǎng)絡編程的各種任務角色 計算機網(wǎng)絡是個big topic,涉及很多人物和角色,既有開發(fā)人員,也有運維人員。比方說:公司內(nèi)部兩臺機器之間ping 不通,通常由網(wǎng)絡運維人員解決,看看是布線有問題還是路由器設置不對;兩臺機器能ping 通,但是程序連不上,經(jīng)檢查是本機防火墻設置有問題,通常由系統(tǒng)管理員解決;兩臺機器能連上,但是丟包很嚴重,發(fā)現(xiàn)是網(wǎng)卡或者交換機的網(wǎng)口故障,由硬件維修人員解決;兩臺機器的程序能連上,但是偶爾發(fā)過去的請求得不到響應,通常是程序bug,應該由開發(fā)人員解決。 本文主要關心開發(fā)人員這一角色。下面簡單列出一些我能想到的跟網(wǎng)絡打交道的編程任務,其中前三項是面向網(wǎng)絡本身,后面幾項是在計算機網(wǎng)絡之上構(gòu)建信息系統(tǒng)。 1. 開發(fā)網(wǎng)絡設備,編寫防火墻、交換機、路由器的固件(firmware)。 2. 開發(fā)或移植網(wǎng)卡的驅(qū)動。 3. 移植或維護TCP/IP 協(xié)議棧(特別是在嵌入式系統(tǒng)上)。 4. 開發(fā)或維護標準的網(wǎng)絡協(xié)議程序,HTTP、FTP、DNS、SMTP、POP3、NFS。 5. 開發(fā)標準網(wǎng)絡協(xié)議的“附加品”,比如HAProxy、squid、varnish 等Web loadbalancer。 6. 開發(fā)標準或非標準網(wǎng)絡服務的客戶端庫,比如ZooKeeper 客戶端庫、memcached客戶端庫。 7. 開發(fā)與公司業(yè)務直接相關的網(wǎng)絡服務程序,比如即時聊天軟件的后臺服務器、網(wǎng)游服務器、金融交易系統(tǒng)、互聯(lián)網(wǎng)企業(yè)用的分布式海量存儲、微博發(fā)帖的內(nèi)部廣播通知等等。 8. 客戶端程序中涉及網(wǎng)絡的部分,比如郵件客戶端中與POP3、SMTP 通信的部分,以及網(wǎng)游的客戶端程序中與服務器通信的部分。 本文所指的“網(wǎng)絡編程”專指第7 項,即在TCP/IP 協(xié)議之上開發(fā)業(yè)務軟件。換句話說,不是用Sockets API 開發(fā)muduo 這樣的網(wǎng)絡庫,而是用libevent、muduo、Netty、gevent 這樣現(xiàn)成的庫開發(fā)業(yè)務軟件,muduo 自帶的十幾個示例程序是業(yè)務軟件的代表。 A.1.6 面向業(yè)務的網(wǎng)絡編程的特點 與通用的網(wǎng)絡服務器不同,面向公司業(yè)務的專用網(wǎng)絡程序有其自身的特點。 業(yè)務邏輯比較復雜,而且時常變化 如果寫一個HTTP 服務器,在大致實現(xiàn)HTTP 1.1 標準之后,程序的主體功能一般不會有太大的變化,程序員會把時間放在性能調(diào)優(yōu)和bug 修復上。而開發(fā)針對公司業(yè)務的專用程序時,功能說明書(spec)很可能不如HTTP 1.1 標準那么細致明確。更重要的是,程序是快速演化的。以即時聊天工具的后臺服務器為例,可能第一版只支持在線聊天;幾個月之后發(fā)布第二版,支持離線消息;又過了幾個月,第三版支持隱身聊天;隨后,第四版支持上傳頭像;如此等等。這要求程序員能快速響應新的業(yè)務需求,公司才能保持競爭力。由于業(yè)務時常變化(假設每月一次版本升級),也會降低服務程序連續(xù)運行時間的要求。相反,我們要設計一套流程,通過輪流重啟服務器來完成平滑升級(§9.2.2)。 不一定需要遵循公認的通信協(xié)議標準 比方說網(wǎng)游服務器就沒什么協(xié)議標準,反正客戶端和服務端都是本公司開發(fā)的,如果發(fā)現(xiàn)目前的協(xié)議設計有問題,兩邊一起改就行了。由于可以自己設計協(xié)議,因此我們可以繞開一些性能難點,簡化程序結(jié)構(gòu)。比方說,對于多線程的服務程序,如果用短連接TCP 協(xié)議,為了優(yōu)化性能通常要精心設計accept 新連接的機制2,避免驚群并減少上下文切換。但是如果改用長連接,用最簡單的單線程accept 就行了。 程序結(jié)構(gòu)沒有定論 對于高并發(fā)大吞吐的標準網(wǎng)絡服務,一般采用單線程事件驅(qū)動的方式開發(fā),比如HAProxy、lighttpd 等都是這個模式。但是對于專用的業(yè)務系統(tǒng),其業(yè)務邏輯比較復雜,占用較多的CPU 資源,這種單線程事件驅(qū)動方式不見得能發(fā)揮現(xiàn)在多核處理器的優(yōu)勢。這留給程序員比較大的自由發(fā)揮空間,做好了“橫掃千軍”,做爛了一敗涂地。我認為目前one loop per thread 是通用性較高的一種程序結(jié)構(gòu),能發(fā)揮多核的優(yōu)勢,見§3.3 和§6.6。 性能評判的標準不同 如果開發(fā)httpd 這樣的通用服務,必然會和開源的Nginx、lighttpd 等高性能服務器比較,程序員要投入相當?shù)木θ?yōu)化程序,才能在市場上占有一席之地。而面向業(yè)務的專用網(wǎng)絡程序不一定是IO bound,也不一定有開源的實現(xiàn)以供對比性能,優(yōu)化方向也可能不同。程序員通常更加注重功能的穩(wěn)定性與開發(fā)的便捷性。性能只要一代比一代強即可。 網(wǎng)絡編程起到支撐作用,但不處于主導地位 程序員的主要工作是實現(xiàn)業(yè)務邏輯,而不只是實現(xiàn)網(wǎng)絡通信協(xié)議。這要求程序員深入理解業(yè)務。程序的性能瓶頸不一定在網(wǎng)絡上,瓶頸有可能是CPU、Disk IO、數(shù)據(jù)庫等,這時優(yōu)化網(wǎng)絡方面的代碼并不能提高整體性能。只有對所在的領域有深入的了解,明白各種因素的權衡(trade-off),才能做出一些有針對性的優(yōu)化。現(xiàn)在的機器上,簡單的并發(fā)長連接echo服務程序不用特別優(yōu)化就做到十多萬qps,但是如果每個業(yè)務請求需要1ms 密集計算,在8 核機器上充其量能達到8 000 qps,優(yōu)化IO 不如去優(yōu)化業(yè)務計算(如果投入產(chǎn)出合算的話)。 A.1.7 幾個術語 互聯(lián)網(wǎng)上的很多“口水戰(zhàn)”是由對同一術語的不同理解引起的,比如我寫的《多線程服務器的適用場合》3,就曾經(jīng)被人說是“掛羊頭賣狗肉”,因為這篇文章中舉的master 例子“根本就算不上是個網(wǎng)絡服務器。因為它的瓶頸根本就跟網(wǎng)絡無關! 網(wǎng)絡服務器 “網(wǎng)絡服務器”這個術語確實含義模糊,到底指硬件還是軟件?到底是服務于網(wǎng)絡本身的機器(交換機、路由器、防火墻、NAT),還是利用網(wǎng)絡為其他人或程序提供服務的機器(打印服務器、文件服務器、郵件服務器)?每個人根據(jù)自己熟悉的領域,可能會有不同的解讀。比方說,或許有人認為只有支持高并發(fā)、高吞吐量的才算是網(wǎng)絡服務器。 為了避免無謂的爭執(zhí),我只用“網(wǎng)絡服務程序”或者“網(wǎng)絡應用程序”這種含義明確的術語!伴_發(fā)網(wǎng)絡服務程序”通常不會造成誤解。 客戶端?服務端? 在TCP 網(wǎng)絡編程中,客戶端和服務端很容易區(qū)分,主動發(fā)起連接的是客戶端,被動接受連接的是服務端。當然,這個“客戶端”本身也可能是個后臺服務程序,HTTP proxy 對HTTP server 來說就是個客戶端。 客戶端編程?服務端編程? 但是“服務端編程”和“客戶端編程”就不那么好區(qū)分了。比如Web crawler,它會主動發(fā)起大量連接,扮演的是HTTP 客戶端的角色,但似乎應該歸入“服務端編程”。又比如寫一個HTTP proxy,它既會扮演服務端--被動接受Web browser 發(fā)起的連接,也會扮演客戶端--主動向HTTP server 發(fā)起連接,它究竟算服務端還是客戶端?我猜大多數(shù)人會把它歸入服務端編程。 那么究竟如何定義“服務端編程”? 服務端編程需要處理大量并發(fā)連接?也許是,也許不是。比如云風在一篇介紹網(wǎng)游服務器的博客4 中就談到,網(wǎng)游中用到的“連接服務器”需要處理大量連接,而“邏輯服務器”只有一個外部連接。那么開發(fā)這種網(wǎng)游“邏輯服務器”算服務端編程還是客戶端編程呢?又比如機房的服務進程監(jiān)控軟件,并發(fā)數(shù)跟機器數(shù)成正比,至多也就是兩三千的并發(fā)連接。(再大規(guī)模就超出本書的范圍了。) 我認為,“服務端網(wǎng)絡編程”指的是編寫沒有用戶界面的長期運行的網(wǎng)絡程序,程序默默地運行在一臺服務器上,通過網(wǎng)絡與其他程序打交道,而不必和人打交道。與之對應的是客戶端網(wǎng)絡程序,要么是短時間運行,比如wget;要么是有用戶界面(無論是字符界面還是圖形界面)。本文主要談服務端網(wǎng)絡編程。 ……
你還可能感興趣
我要評論
|