多年編寫生産環境下多綫程服務端程序經驗之精華,示範在多核時代采用現代C++編寫多綫程TCP網絡服務器的正規做法
《Linux多綫程服務端編程 使用muduoC++網絡庫》主要講述采用現代C++在x86-64Linux上編寫多綫程TCP網絡服務程序的主流常規技術,重點講解一種適應性較強的多綫程服務器的編程模型,即oneloopperthread。這是在Linux下以native語言編寫用戶態高性能網絡程序成熟的模式,掌握之後可順利地開發各類常見的服務端網絡應用程序。本書以muduo網絡庫為例,講解這種編程模型的使用方法及注意事項。
《Linux多綫程服務端編程 使用muduoC++網絡庫》的宗旨是貴精不貴多。掌握兩種基本的同步原語就可以滿足各種多綫程同步的功能需求,還能寫齣更易用的同步設施。掌握一種進程間通信方式和一種多綫程網絡編程模型就足以應對日常開發任務,編寫運行於公司內網環境的分布式服務係統。
陳碩,北京師範大學碩士,擅長C++ 多綫程網絡編程和實時分布式係統架構。曾在摩根士丹利IT 部門工作5 年,從事實時外匯交易係統開發。現在在美國加州矽榖某互聯網大公司工作,從事大規模分布式係統的可靠性工程。編寫瞭開源C++ 網絡庫muduo,參與翻譯瞭《代碼大全(第2 版)》和《C++ 編程規範(繁體版)》,整理瞭《C++ Primer (第4 版)(評注版)》,並曾多次在各地技術大會演講。
第1 部分 C++ 多綫程係統編程
第1 章 綫程安全的對象生命期管理
1.1 當析構函數遇到多綫程
1.1.1 綫程安全的定義
1.1.2 MutexLock 與MutexLockGuard
1.1.3 一個綫程安全的Counter 示例
1.2 對象的創建很簡單.
1.3 銷毀太難
1.3.1 mutex 不是辦法
1.3.2 作為數據成員的mutex 不能保護析構.
1.4 綫程安全的Observer 有多難.
1.5 原始指針有何不妥.
1.6 神器shared_ptr/weak_ptr
1.7 插麯:係統地避免各種指針錯誤.
1.8 應用到Observer 上
1.9 再論shared_ptr 的綫程安全.
1.10 shared_ptr 技術與陷阱
1.11 對象池.
1.11.1 enable_shared_from_this
1.11.2 弱迴調.
1.12 替代方案
1.13 心得與小結.
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 實現
2.6 sleep(3) 不是同步原語
2.7 歸納與總結.
2.8 藉shared_ptr 實現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 單綫程程序的優缺點.
3.5.3 適用多綫程程序的場景
3.6 “多綫程服務器的適用場閤”例釋與答疑
第4 章 C++ 多綫程係統編程精要
4.1 基本綫程原語的選用.
4.2 C/C++ 係統庫的綫程安全性.
4.3 Linux 上的綫程標識
4.4 綫程的創建與銷毀的守則.
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 新增係統調用的啓示
第5 章 高效的多綫程日誌
5.1 功能需求
5.2 性能需求
5.3 多綫程異步日誌
5.4 其他方案
第2 部分 muduo 網絡庫
第6 章 muduo 網絡庫簡介
6.1 由來.
6.2 安裝.
6.3 目錄結構
6.3.1 代碼結構
6.3.2 例子
6.3.3 綫程模型
6.4 使用教程
6.4.1 TCP 網絡編程本質論.
6.4.2 echo 服務的實現.
6.4.3 七步實現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 數獨求解服務器
6.6.2 常見的並發網絡服務程序設計方案.
第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 服務端的實現.
7.3.5 客戶端的實現.
7.4 muduo Buffer 類的設計與使用.
7.4.1 muduo 的IO 模型
7.4.2 為什麼non-blocking 網絡編程中應用層buffer 是必需的
7.4.3 Buffer 的功能需求
7.4.4 Buffer 的數據結構
7.4.5 Buffer 的操作.
7.4.6 其他設計方案.
7.4.7 性能是不是問題
7.5 一種自動反射消息類型的Google Protobuf 網絡傳輸方案
7.5.1 網絡編程中使用Protobuf 的兩個先決條件.
7.5.2 根據type name 反射自動創建Message 對象
7.5.3 Protobuf 傳輸格式
7.6 在muduo 中實現Protobuf 編解碼器與消息分發器
7.6.1 什麼是編解碼器(codec)
7.6.2 實現ProtobufCodec .
7.6.3 消息分發器(dispatcher)有什麼用
7.6.4 ProtobufCodec 與ProtobufDispatcher 的綜閤運用.
7.6.5 ProtobufDispatcher 的兩種實現
7.6.6 ProtobufCodec 和ProtobufDispatcher 有何意義.
7.7 限製服務器的最大並發連接數
7.7.1 為什麼要限製並發連接數
7.7.2 在muduo 中限製並發連接數
7.8 定時器.
7.8.1 程序中的時間.
7.8.2 Linux 時間函數
7.8.3 muduo 的定時器接口.
7.8.4 Boost.Asio Timer 示例
7.8.5 Java Netty 示例
7.9 測量兩颱機器的網絡延遲和時間差.
7.10 用timing wheel 踢掉空閑連接
7.10.1 timing wheel 原理
7.10.2 代碼實現與改進
7.11 簡單的消息廣播服務.
7.12 “串並轉換”連接服務器及其自動化測試
7.13 socks4a 代理服務器
7.13.1 TCP 中繼器
7.13.2 socks4a 代理服務器
7.13.3 N : 1 與1 : N 連接轉發
7.14 短址服務
7.15 與其他庫集成.
7.15.1 UDNS .
7.15.2 c-ares DNS .
7.15.3 curl .
7.15.4 更多
第8 章 muduo 網絡庫設計與實現
8.0 什麼都不做的EventLoop .
8.1 Reactor 的關鍵結構
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() 函數
8.3.1 提高TimerQueue 的綫程安全性.
8.3.2 EventLoopThread class
8.4 實現TCP 網絡庫
8.5 TcpServer 接受新連接
8.5.1 TcpServer class
8.5.2 TcpConnection class .
8.6 TcpConnection 斷開連接.
8.7 Buffer 讀取數據
8.7.1 TcpConnection 使用Buffer 作為輸入緩衝.
8.7.2 Buffer::readFd()
8.8 TcpConnection 發送數據.
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 部分 工程實踐經驗談
第9 章 分布式係統工程實踐
9.1 我們在技術浪潮中的位置.
9.1.1 分布式係統的本質睏難
9.1.2 分布式係統是個險惡的問題.
9.2 分布式係統的可靠性淺說.
9.2.1 分布式係統的軟件不要求7 24 可靠
9.2.2 “能隨時重啓進程”作為程序設計目標.
9.3 分布式係統中心跳協議的設計
9.4 分布式係統中的進程標識.
9.4.1 錯誤做法
9.4.2 正確做法
9.4.3 TCP 協議的啓示
9.5 構建易於維護的分布式程序.
9.6 為係統演化做準備.
9.6.1 可擴展的消息格式
9.6.2 反麵教材:ICE 的消息打包格式.
9.7 分布式程序的自動化迴歸測試
9.7.1 單元測試的能與不能.
9.7.2 分布式係統測試的要點
9.7.3 分布式係統的抽象觀點
9.7.4 一種自動化的迴歸測試方案.
9.7.5 其他用處
9.8 分布式係統部署、監控與進程管理的幾重境界.
9.8.1 境界1:全手工操作.
9.8.2 境界2:使用零散的自動化腳本和第三方組件.
9.8.3 境界3:自製機群管理係統,集中化配置.
9.8.4 境界4:機群管理與naming service 結閤.
第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 函數重載
10.3.2 inline 函數.
10.3.3 模闆
10.3.4 虛函數.
10.4 工程項目中頭文件的使用規則
10.4.1 頭文件的害處.
10.4.2 頭文件的使用規則
10.5 工程項目中庫文件的組織原則
10.5.1 動態庫是有害的
10.5.2 靜態庫也好不到哪兒去
10.5.3 源碼編譯是王道
第11 章 反思C++ 麵嚮對象與虛函數
11.1 樸實的C++ 設計
11.2 程序庫的二進製兼容性
11.2.1 什麼是二進製兼容性.
11.2.2 有哪些情況會破壞庫的ABI .
11.2.3 哪些做法多半是安全的
11.2.4 反麵教材:COM .
11.2.5 解決辦法
11.3 避免使用虛函數作為庫的接口
11.3.1 C++ 程序庫的作者的生存環境
11.3.2 虛函數作為庫的接口的兩大用途
11.3.3 虛函數作為接口的弊端
11.3.4 假如Linux 係統調用以COM 接口方式實現
11.3.5 Java 是如何應對的
11.4 動態庫接口的推薦做法
11.5 以boost::function 和boost::bind 取代虛函數.
11.5.1 基本用途
11.5.2 對程序庫的影響
11.5.3 對麵嚮對象程序設計的影響.
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 現實的C++ 程序如何做文件IO .
11.7 值語義與數據抽象.
11.7.1 什麼是值語義.
11.7.2 值語義與生命期
11.7.3 值語義與標準庫
11.7.4 值語義與C++ 語言
11.7.5 什麼是數據抽象
11.7.6 數據抽象所需的語言設施
11.7.7 數據抽象的例子
第12 章 C++ 經驗談
12.1 用異或來交換變量是錯誤的.
12.1.1 編譯器會分彆生成什麼代碼.
12.1.2 為什麼短的代碼不一定快
12.2 不要重載全局::operator new()
12.2.1 內存管理的基本要求.
12.2.2 重載::operator new() 的理由.
12.2.3 ::operator new() 的兩種重載方式.
12.2.4 現實的開發環境
12.2.5 重載::operator new() 的睏境.
12.2.6 解決辦法:替換malloc()
12.2.7 為單獨的class 重載::operator new() 有問題嗎.
12.2.8 有必要自行定製內存分配器嗎
12.3 帶符號整數的除法與餘數.
12.3.1 語言標準怎麼說
12.3.2 C/C++ 編譯器的錶現.
12.3.3 其他語言的規定
12.3.4 腳本語言解釋器代碼.
12.3.5 硬件實現
12.4 在單元測試中mock 係統調用
12.4.1 係統函數的依賴注入.
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 短字符串優化(SSO)
12.8 用STL algorithm 輕鬆解決幾道算法麵試題
12.8.1 用next_permutation() 生成排列與組閤
12.8.2 用unique() 去除連續重復空白.
12.8.3 用{make,push,pop}_heap() 實現多路歸並
12.8.4 用partition() 實現“重排數組,讓奇數位於偶數前麵”
12.8.5 用lower_bound() 查找IP 地址所屬的城市.
第4 部分 附錄
附錄A 談一談網絡編程學習經驗
附錄B 從《C++ Primer(第4 版)》入手學習C++
附錄C 關於Boost 的看法
附錄D 關於TCP 並發連接的幾個思考題與試驗
附錄A
談一談網絡編程學習經驗
本文談一談我在學習網絡編程方麵的一些個人經驗。“網絡編程”這個術語的範圍很廣,本文指用Sockets API 開發基於TCP/IP 的網絡應用程序,具體定義見§A.1.5 “網絡編程的各種任務角色”。
受限於本人的經曆和經驗,本附錄的適應範圍是:
x86-64 Linux 服務端網絡編程,直接或間接使用Sockets API。
公司內網。不一定是局域網,但總體位於公司防火牆之內,環境可控。
本文可能不適閤:
PC 客戶端網絡編程,程序運行在客戶的PC 上,環境多變且不可控。
Windows 網絡編程。
麵嚮公網的服務程序。
高性能網絡服務器。
本文分兩個部分:
1. 網絡編程的一些“鬍思亂想”,以自問自答的形式談談我對這一領域的認識。
2. 幾本必看的書,基本上還是W. Richard Stevents 的那幾本。
另外,本文沒有特彆說明時均暗指TCP 協議,“連接”是“TCP 連接”,“服務
端”是“TCP 服務端”。
A.1 網絡編程的一些“鬍思亂想”
以下大緻列齣我對網絡編程的一些想法,前後無關聯。
A.1.1 網絡編程是什麼
網絡編程是什麼?是熟練使用Sockets API 嗎?說實話,在實際項目裏我隻用過
兩次Sockets API,其他時候都是使用封裝好的網絡庫。
第一次是2005 年在學校做一個羽毛球賽場計分係統:我用C# 編寫運行在PC上的軟件,負責比分的顯示;再用C# 寫瞭運行在PDA 上的計分界麵,記分員拿著PDA 記錄比分;這兩部分程序通過TCP 協議相互通信。這其實是個簡單的分布式係統,體育館有幾片場地,每個場地都有一名拿PDA 的記分員,每個場地都有兩颱顯示比分的PC (顯示器是42 寸平闆電視,放在場地的對角,這樣兩邊看颱的觀眾都能看到比分)。這兩颱PC 的功能不完全一樣,一颱隻負責顯示當前比分,另一颱還要負責與PDA 通信,並更新數據庫裏的比分信息。此外,還有一颱PC 負責周期性地從數據庫讀齣全部7 片場地的比分,顯示在體育館牆上的大屏幕上。這颱PC 上還運行著一個程序,負責生成比分數據的靜態頁麵,通過FTP 上傳發布到某門戶網站的體育頻道。係統中還有一個錄入賽程(參賽隊、運動員、齣場順序等)數據庫的程序,運行在數據庫服務器上。算下來整個係統有十來個程序,運行在二十多颱設備(PC 和PDA)上,還要考慮可靠性,避免single point of failure。
這是我第一次寫實際項目中的網絡程序,當時寫下來的感覺是像寫命令行與用戶交互的程序:程序在命令行輸齣一句提示語,等待客戶輸入一句話,然後處理客戶輸入,再輸齣下一句提示語,如此循環。隻不過這裏的“客戶”不是人,而是另一個程序。在建立好TCP 連接之後,雙方的程序都是read/write 循環(為求簡單,我用的是blocking 讀寫),直到有一方斷開連接。
第二次是2010 年編寫mudu網絡庫,我再次拿起瞭Sockets API,寫瞭一個基於Reactor 模式的C++ 網絡庫。寫這個庫的目的之一就是想讓日常的網絡編程從Sockets API 的瑣碎細節中解脫齣來,讓程序員專注於業務邏輯,把時間用在刀刃上。mudu網絡庫的示例代碼包含瞭幾十個網絡程序,這些示例程序都沒有直接使用Sockets API。
在此之外,無論是實習還是工作,雖然我寫的程序都會通過TCP 協議與其他程序打交道,但我沒有直接使用過Sockets API。對於TCP 網絡編程,我認為核心是處理“三個半事件”,見§6.4.1 “TCP 網絡編程本質論”。程序員的主要工作是在事件處理函數中實現業務邏輯,而不是和Sockets API“較勁”。
這裏還是沒有說清楚“網絡編程”是什麼,請繼續閱讀後文§A.1.5“網絡編程的各種任務角色”。
A.1.2 學習網絡編程有用嗎
以上說的是比較底層的網絡編程,程序代碼直接麵對從TCP 或UDP 收到的數據以及構造數據包發齣去。在實際工作中,另一種常見的情況是通過各種client library來與服務端打交道,或者在現成的框架中填空來實現server,或者采用更上層的通信方式。比如用libmemcached 與memcached 打交道,使用libpq 來與PostgreSQL 打交道,編寫Servlet 來響應HTTP 請求,使用某種RPC 與其他進程通信,等等。這些情況都會發生網絡通信,但不一定算作“網絡編程”。如果你的工作是前麵列舉的這些,學習TCP/IP 網絡編程還有用嗎?
我認為還是有必要學一學,至少在troubleshooting 的時候有用。無論如何,這些library 或framework 都會調用底層的Sockets API 來實現網絡功能。當你的程序遇到一個綫上問題時,如果你熟悉Sockets API,那麼從strace 不難發現程序卡在哪裏,盡管可能你沒有直接調用這些Sockets API。另外,熟悉TCP/IP 協議、會用tcpdump 也非常有助於分析解決綫上網絡服務問題。
A.1.3 在什麼平颱上學習網絡編程
對於服務端網絡編程,我建議在Linux 上學習。
如果在10 年前,這個問題的答案或許是FreeBSD,因為FreeBSD“根正苗紅”,在2000 年那一次互聯網浪潮中扮演瞭重要角色,是很多公司首選的免費服務器操作係統。2000 年那會兒Linux 還遠未成熟,連epoll 都還沒有實現。(FreeBSD 在2001年發布4.1 版,加入瞭kqueue,從此C10k 不是問題。)
10 年後的今天,事情起瞭一些變化,Linux 成為市場份額最大的服務器操作係統。在Linux 這種大眾係統上學網絡編程,遇到什麼問題會比較容易解決。因為用的人多,你遇到的問題彆人多半也遇到過;同樣因為用的人多,如果真的有什麼內核bug,很快就會得到修復,至少有work around 的辦法。如果用彆的係統,可能一個問題發到論壇上半個月都不會有人理。從內核源碼的風格看,FreeBSD 更乾淨整潔,注釋到位,但是無奈它的市場份額遠不如Linux,學習Linux 是更好的技術投資。
A.1.4 可移植性重要嗎
寫網絡程序要不要考慮移植性?要不要跨平颱?這取決於項目需要,如果貴公司做的程序要賣給其他公司,而對方可能使用Windows、Linux、FreeBSD、Solaris、AIX、HP-UX 等等操作係統,這時候當然要考慮移植性。如果編寫公司內部的服務器上用的網絡程序,那麼大可隻關注一個平颱,比如Linux。因為編寫和維護可移植的網絡程序的代價相當高,平颱間的差異可能遠比想象中大,即便是POSIX 係統之間也有不小的差異(比如Linux 沒有SO_NOSIGPIPE 選項,Linux 的pipe(2) 是單嚮的,而FreeBSD 是雙嚮的),錯誤的返迴碼也大不一樣。
我就不打算把mudu往Windows 或其他操作係統移植。如果需要編寫可移植的網絡程序,我寜願用libevent、libuv、Java Netty 這樣現成的庫,把“髒活、纍活”留給彆人。
A.1.5 網絡編程的各種任務角色
計算機網絡是個big topic,涉及很多人物和角色,既有開發人員,也有運維人員。比方說:公司內部兩颱機器之間ping 不通,通常由網絡運維人員解決,看看是布綫有問題還是路由器設置不對;兩颱機器能ping 通,但是程序連不上,經檢查是本機防火牆設置有問題,通常由係統管理員解決;兩颱機器能連上,但是丟包很嚴重,發現是網卡或者交換機的網口故障,由硬件維修人員解決;兩颱機器的程序能連上,但是偶爾發過去的請求得不到響應,通常是程序bug,應該由開發人員解決。
本文主要關心開發人員這一角色。下麵簡單列齣一些我能想到的跟網絡打交道的編程任務,其中前三項是麵嚮網絡本身,後麵幾項是在計算機網絡之上構建信息係統。
1. 開發網絡設備,編寫防火牆、交換機、路由器的固件(firmware)。
2. 開發或移植網卡的驅動。
3. 移植或維護TCP/IP 協議棧(特彆是在嵌入式係統上)。
4. 開發或維護標準的網絡協議程序,HTTP、FTP、DNS、SMTP、POP3、NFS。
5. 開發標準網絡協議的“附加品”,比如HAProxy、squid、varnish 等Web loadbalancer。
6. 開發標準或非標準網絡服務的客戶端庫,比如ZooKeeper 客戶端庫、memcached客戶端庫。
7. 開發與公司業務直接相關的網絡服務程序,比如即時聊天軟件的後颱服務器、網遊服務器、金融交易係統、互聯網企業用的分布式海量存儲、微博發帖的內部廣播通知等等。
8. 客戶端程序中涉及網絡的部分,比如郵件客戶端中與POP3、SMTP 通信的部分,以及網遊的客戶端程序中與服務器通信的部分。
本文所指的“網絡編程”專指第7 項,即在TCP/IP 協議之上開發業務軟件。換句話說,不是用Sockets API 開發mudu這樣的網絡庫,而是用libevent、muduo、Netty、gevent 這樣現成的庫開發業務軟件,mudu自帶的十幾個示例程序是業務軟件的代錶。
A.1.6 麵嚮業務的網絡編程的特點
與通用的網絡服務器不同,麵嚮公司業務的專用網絡程序有其自身的特點。
業務邏輯比較復雜,而且時常變化 如果寫一個HTTP 服務器,在大緻實現HTTP 1.1 標準之後,程序的主體功能一般不會有太大的變化,程序員會把時間放在性能調優和bug 修復上。而開發針對公司業務的專用程序時,功能說明書(spec)很可能不如HTTP 1.1 標準那麼細緻明確。更重要的是,程序是快速演化的。以即時聊天工具的後颱服務器為例,可能第一版隻支持在綫聊天;幾個月之後發布第二版,支持離綫消息;又過瞭幾個月,第三版支持隱身聊天;隨後,第四版支持上傳頭像;如此等等。這要求程序員能快速響應新的業務需求,公司纔能保持競爭力。由於業務時常變化(假設每月一次版本升級),也會降低服務程序連續運行時間的要求。相反,我們要設計一套流程,通過輪流重啓服務器來完成平滑升級(§9.2.2)。
不一定需要遵循公認的通信協議標準 比方說網遊服務器就沒什麼協議標準,反正客戶端和服務端都是本公司開發的,如果發現目前的協議設計有問題,兩邊一起改就行瞭。由於可以自己設計協議,因此我們可以繞開一些性能難點,簡化程序結構。比方說,對於多綫程的服務程序,如果用短連接TCP 協議,為瞭優化性能通常要精心設計accept 新連接的機製2,避免驚群並減少上下文切換。但是如果改用長連接,用最簡單的單綫程accept 就行瞭。
程序結構沒有定論 對於高並發大吞吐的標準網絡服務,一般采用單綫程事件驅動的方式開發,比如HAProxy、lighttpd 等都是這個模式。但是對於專用的業務係統,其業務邏輯比較復雜,占用較多的CPU 資源,這種單綫程事件驅動方式不見得能發揮現在多核處理器的優勢。這留給程序員比較大的自由發揮空間,做好瞭“橫掃韆軍”,做爛瞭一敗塗地。我認為目前one loop per thread 是通用性較高的一種程序結構,能發揮多核的優勢,見§3.3 和§6.6。
性能評判的標準不同 如果開發httpd 這樣的通用服務,必然會和開源的Nginx、lighttpd 等高性能服務器比較,程序員要投入相當的精力去優化程序,纔能在市場上占有一席之地。而麵嚮業務的專用網絡程序不一定是Ibound,也不一定有開源的實現以供對比性能,優化方嚮也可能不同。程序員通常更加注重功能的穩定性與開發的便捷性。性能隻要一代比一代強即可。
網絡編程起到支撐作用,但不處於主導地位 程序員的主要工作是實現業務邏輯,而不隻是實現網絡通信協議。這要求程序員深入理解業務。程序的性能瓶頸不一定在網絡上,瓶頸有可能是CPU、Disk IO、數據庫等,這時優化網絡方麵的代碼並不能提高整體性能。隻有對所在的領域有深入的瞭解,明白各種因素的權衡(trade-off),纔能做齣一些有針對性的優化。現在的機器上,簡單的並發長連接echo服務程序不用特彆優化就做到十多萬qps,但是如果每個業務請求需要1ms 密集計算,在8 核機器上充其量能達到8 000 qps,優化I不如去優化業務計算(如果投入産齣閤算的話)。
A.1.7 幾個術語
互聯網上的很多“口水戰”是由對同一術語的不同理解引起的,比如我寫的《多綫程服務器的適用場閤》3,就曾經被人說是“掛羊頭賣狗肉”,因為這篇文章中舉的master 例子“根本就算不上是個網絡服務器。因為它的瓶頸根本就跟網絡無關。”
網絡服務器 “網絡服務器”這個術語確實含義模糊,到底指硬件還是軟件?到底是服務於網絡本身的機器(交換機、路由器、防火牆、NAT),還是利用網絡為其他人或程序提供服務的機器(打印服務器、文件服務器、郵件服務器)?每個人根據自己熟悉的領域,可能會有不同的解讀。比方說,或許有人認為隻有支持高並發、高吞吐量的纔算是網絡服務器。
為瞭避免無謂的爭執,我隻用“網絡服務程序”或者“網絡應用程序”這種含義明確的術語。“開發網絡服務程序”通常不會造成誤解。
客戶端?服務端? 在TCP 網絡編程中,客戶端和服務端很容易區分,主動發起連接的是客戶端,被動接受連接的是服務端。當然,這個“客戶端”本身也可能是個後颱服務程序,HTTP proxy 對HTTP server 來說就是個客戶端。
客戶端編程?服務端編程? 但是“服務端編程”和“客戶端編程”就不那麼好區分瞭。比如Web crawler,它會主動發起大量連接,扮演的是HTTP 客戶端的角色,但似乎應該歸入“服務端編程”。又比如寫一個HTTP proxy,它既會扮演服務端--被動接受Web browser 發起的連接,也會扮演客戶端--主動嚮HTTP server 發起連接,它究竟算服務端還是客戶端?我猜大多數人會把它歸入服務端編程。
那麼究竟如何定義“服務端編程”?
服務端編程需要處理大量並發連接?也許是,也許不是。比如雲風在一篇介紹網遊服務器的博客4 中就談到,網遊中用到的“連接服務器”需要處理大量連接,而“邏輯服務器”隻有一個外部連接。那麼開發這種網遊“邏輯服務器”算服務端編程還是客戶端編程呢?又比如機房的服務進程監控軟件,並發數跟機器數成正比,至多也就是兩三韆的並發連接。(再大規模就超齣本書的範圍瞭。)
我認為,“服務端網絡編程”指的是編寫沒有用戶界麵的長期運行的網絡程序,程序默默地運行在一颱服務器上,通過網絡與其他程序打交道,而不必和人打交道。與之對應的是客戶端網絡程序,要麼是短時間運行,比如wget;要麼是有用戶界麵(無論是字符界麵還是圖形界麵)。本文主要談服務端網絡編程。
……
本書主要講述采用現代C++ 在x86-64 Linux 上編寫多綫程TCP 網絡服務程序的主流常規技術,這也是我對過去5 年編寫生産環境下的多綫程服務端程序的經驗總結。本書重點講解多綫程網絡服務器的一種IO 模型,即one loop per thread。這是一種適應性較強的模型,也是Linux 下以native 語言編寫用戶態高性能網絡程序最成熟的模式,掌握之後可順利地開發各類常見的服務端網絡應用程序。本書以muduo網絡庫為例,講解這種編程模型的使用方法及注意事項。
muduo 是一個基於非阻塞IO 和事件驅動的現代C++ 網絡庫,原生支持oneloop per thread 這種IO 模型。muduo 適閤開發Linux 下的麵嚮業務的多綫程服務端網絡應用程序,其中“麵嚮業務的網絡編程”的定義見附錄A。“現代C++”指的不是C++11 新標準,而是2005 年TR1 發布之後的C++ 語言和庫。與傳統C++ 相比,現代C++ 的變化主要有兩方麵:資源管理(見第1 章)與事件迴調(見第449 頁)。
本書不是多綫程編程教程,也不是網絡編程教程,更不是C++ 教程。讀者應該已經大緻讀過《UNIX 環境高級編程》、《UNIX 網絡編程》、《C++ Primer》或與之內容相近的書籍。本書不談C++11,因為目前(2012 年)主流的Linux 服務端發行版的g++ 版本都還停留在4.4,C++11 進入實用尚需一段時日。
本書適用的硬件環境是主流x86-64 服務器,多路多核CPU、幾十GB 內存、韆兆以太網互聯。除瞭第5 章講診斷日誌之外,本書不涉及文件IO。
本書分為四大部分,第1 部分“C++ 多綫程係統編程”考察多綫程下的對象生命期管理、綫程同步方法、多綫程與C++ 的結閤、高效的多綫程日誌等。第2 部分“muduo 網絡庫”介紹使用現成的非阻塞網絡庫編寫網絡應用程序的方法,以及muduo 的設計與實現。第3 部分“工程實踐經驗談”介紹分布式係統的工程化開發方法和C++ 在工程實踐中的功能特性取捨。第4 部分“附錄”分享網絡編程和C++語言的學習經驗。
本書的宗旨是貴精不貴多。掌握兩種基本的同步原語就可以滿足各種多綫程同步的功能需求,還能寫齣更易用的同步設施。掌握一種進程間通信方式和一種多綫程網絡編程模型就足以應對日常開發任務,編寫運行於公司內網環境的分布式服務係統。(本書不涉及分布式存儲係統,也不涉及UDP。)
術語與排版範例
本書大量使用英文術語,甚至有少量英文引文。設計模式的名字一律用英文,例如Observer、Reactor、Singleton。在中文術語不夠突齣時,也會使用英文,例如class、heap、event loop、STL algorithm 等。注意幾個中文C++ 術語: 對象實體(instance)、函數重載決議(resolution)、模闆具現化(instantiation)、覆寫(override)虛函數、提領(dereference)指針。本書中的英語可數名詞一般不用復數形式,例如兩個class,6 個syscall;但有時會用(s) 強調中文名詞是復數。fd 是文件描述符(file descriptor)的縮寫。“CPU 數目”一般指的是核(core)的數目。容量單位kB、MB、GB 錶示的字節數分彆為103、106、109,在特彆強調準確數值時,會分彆用KiB、MiB、GiB 錶示210、220、230 字節。用諸如§11.5 錶示本書第11.5 節,L42 錶示上下文中齣現的第42 行代碼。[JCP]、[CC2e] 等是參考文獻,見書末清單。
一般術語用普通羅馬字體,如mutex、socket;C++ 關鍵字用無襯綫字體,如class、this、mutable;函數名和class 名用等寬字體,如fork(2)、muduo::EventLoop,其中fork(2) 錶示係統函數fork() 的文檔位於manpage 第2 節,可以通過man 2 fork命令查看。如果函數名或類名過長,可能會摺行,行末有連字號“-”,如EventLoop-ThreadPool。文件路徑和URL 采用窄字體,例如muduo/base/Date.h、http://chenshuo.com。用中文楷體錶示引述彆人的話。
代碼
本書的示例代碼以開源項目的形式發布在GitHub 上,地址是
本書配套頁麵提供全部源代碼打包下載,正文中齣現的類似recipes/thread 的路徑是壓縮包內的相對路徑,讀者不難找到其對應的GitHub URL。本書引用代碼的形式如下,左側數字是文件的行號,右側的“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 命令的輸齣格式,用於錶示代碼的改動。
本書正文中齣現的代碼有時為瞭照顧排版而略有改寫,例如改變縮進規則,去掉單行條件語句前後的花括號等。就編程風格而論,應以電子版代碼為準。
陳碩
中國·香港
說好的C++四大件呢!
評分書不錯,挺專業的,包裝也精美
評分好書,非常經典,值得珍藏。
評分可以
評分已經開啓閱讀,東西很不多。非常棒
評分對於初學者其中的一些思考有解惑作用
評分書很不錯,寫得很好,贊一個!好評!
評分東西不錯
評分給老公買的工具書
本站所有內容均為互聯網搜尋引擎提供的公開搜索信息,本站不存儲任何數據與內容,任何內容與數據均與本站無關,如有需要請聯繫相關搜索引擎包括但不限於百度,google,bing,sogou 等
© 2025 book.cndgn.com All Rights Reserved. 新城书站 版權所有