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