第十一章 網絡程式設計
第一節 用戶端-伺服器程式設計模型
- 每個網絡應用都是基于用戶端-伺服器模型的。采用這個模型,一個應用是由一個伺服器戶端提供某種服務。伺服器管理某種資源,并且通過操作這種資源來為它的用戶端提供某種服務。—個FTP伺服器就管理了一組磁盤檔案,它為用戶端進行它會為用戶端進行存儲和檢索。相似地一個電子郵件伺服器管理了一些檔案,它為用戶端進行讀和更新。
- 用戶端-伺服器模型中的基本操作是事務
- 事務由四步組成:
1)當一個用戶端需要服務時,它向伺服器發送一個請求,發起一個事務。例如,當Web覽器需要一個檔案時,它就發送一個請求給Web伺服器 2)伺服器收到請求後,解釋它,并以适當的方式操作它的資源。例如,當Web伺服器收到浏覽器發出的請求後,它就讀一個磁盤檔案 3)伺服器給用戶端發送一響應,并等待下一個請求。例如,Web伺服器将檔案發送回用戶端; 4)用戶端收到響應并處理它。例如,當Web浏覽器收到來自伺服器的一頁後,它就在螢幕上顯示此頁。
第二節 網絡
- 用戶端和伺服器通常運作在不同的主機上,并且通過計算機網絡的硬體和軟體資源來通信。網絡是複雜的系統,在這裡我們隻想了解一點皮毛。我們的目标是從程式員的角度給你一個可工作的思考模型。對于一個主機而言,網絡隻是又一種I/O裝置,作為資料源和資料接收方,如圖所示。一個插到I/O總線擴充槽的擴充卡提供了到網絡的實體接口。從網絡上接收到的資料從擴充卡經過I/O和存儲器總線拷貝到存儲器,典型地是通過DMA(譯者注:直接存儲器存取方式)傳送。相似地,資料也能從存儲器拷貝到網絡。
- 一個以太網段,包括電纜和集線器;每根電纜都有相同的最大位帶寬;集線器不加分辯地将一個端口上收到的每個位複制到其他所有的端口上。是以,每台主機都能看到每個位。
- 每個以太網擴充卡都有—個全球唯一的48位位址,它存儲在這個擴充卡的非易失性存儲器上。每個主機擴充卡都能看到這個幀,但是隻有目的主機實際讀取它。
- 橋接以太網 由 電纜和網橋 将多個以太網段連接配接起來,形成的較大的區域網路。連接配接網橋的電纜傳輸速率可以不同(例:網橋與網橋之間1GB/S, 網橋與集線器之間100MB/S)。
- 網橋作用:連接配接不同網段。同一網段内A向B傳輸資料時,幀到達網橋輸入端口,網橋将其丢棄,不予轉發。A向另一網段内C傳輸資料時,網橋才将幀拷貝到與相應網段連接配接的端口上。
- 區域網路由集線器和網橋及連接配接的電纜組成。
第三節 全球ip網際網路
- 全球IP網際網路是最著名和最成功的網際網路絡實作。從1969年起,它就以這樣或那樣的形式存在了。雖然網際網路的内部體系結構複雜而且不斷變化,但是自從20世紀80年代早期以來,用戶端-伺服器應用的組織就一直保持相當的穩定。下圖展示了一個網際網路用戶端-伺服器應用程式的基本硬體和軟體組織。每台網際網路主機都運作實作TCP/TP協定的軟體,幾乎每個現代計算機系統都支援這個協定。網際網路的用戶端和伺服器混合使用套接字接口函數和Unix I/O函數來進行通信。套接字函數典型地是作為會陷入核心的系統調用來實作的,并調用各種核心模式的TCP/IP函數。
ip位址
- 一個IP位址就是一個32位無符号整數。
- 網絡程式将IP位址存放在下圖所示的IP位址結構中。
- 因為網際網路主機可以有不同的主機位元組順序,TCP/IP為任意整數資料項定義了統一的網絡位元組順序(大端位元組順序)例如IP位址,它放在標頭中跨過網絡被攜帶。在IP位址結構中存放的位址總是以(大端法)網絡位元組順序存放的,即使主機位元組順序是小端法。
網際網路域名
- 網際網路用戶端和伺服器互相通信時使用的是IP位址。然而,對于人們而言,大整數是很難記住的,是以網際網路也定義了一組更加人性化的域名,以及一種将域名映射到IP位址的機制。域名是一串用句點分隔的單詞(字母、數字和破折号)。
- 域名集合形成了一個層次結構,每個域名編碼了它在這個層次中的位置。通過一個示例你将很容易了解這點。下展示了域名層次結構的一部分。層次結構可以表示為一棵樹。樹的節點表示城名,反向到根的路徑形成了域名。子樹稱為子域。層次結構中的第一層是個未命名的根節點。下一層是一組一級域名由非赢利組織(網際網路分酒名字數字協會)定義。常見的第一層域名包括com、edu、gov、org、net,這些域名是由ICANN的各個授權代理按照先到先服務的基礎配置設定的的。一旦一個組織得到了一個二級域名,那麼它就可以在這個子域中建立任何新的域名了。
網際網路連接配接
- 網際網路用戶端和伺服器通過在連接配接上發送和接收位元組流來通信。從連接配接一對程序的意義上而言,連接配接是點對點的。從資料可以同時雙向流動的角度來說,它是全雙工的。并且從(除了一些如粗心的耕鋤機操作員切斷了電纜引起災對性的失敗以外)由源程序發出的位元組流最終被目的程序以它發出的順序收到它的角度來說,它也是可靠的。
- 一個套接字是連接配接的一個端點。每個套接字都有相應的套接字位址,是由一個網際網路位址和一個16位的整數端口組成的,用“位址:端口”來表示。當用戶端發起一個連接配接請求時,用戶端套接字位址中的端口是由核心自動配置設定的,稱為臨時端口。然而,伺服器套接字位址中的端口通常是某個知名的端口,是和這個服務相對應的。例如,web伺服器通常使用端口80,電子郵件伺服器使用端口25。
第四節 套接字接口
套接字位址結構
- 從Unix核心的角度來看,一個套接字就是通信的一個端點。
socket函數
- Socket函數用戶端和伺服器使用函數來建立一個套接字描述符.
- 其中,AF_INET表明我們正在使用網際網路,而SCKET_STREAM表示這個套接字是網際網路連接配接一個端點。Socket傳回的clientfd描述符僅是部分打開的,還不能用于讀寫。如何完成打開套接字的工作,取決于我們是用戶端還是伺服器。
connect函數
- 用戶端通過connect函數來建立和伺服器的連接配接。
- connect函數試圖與套接字位址為serv_addr的伺服器建立一個網際網路連接配接,其中addrlen是size of ( sockaddr_in )。Connect函數會阻塞,一直到連接配接成功建立或是發生錯誤如果成功,sockfd描述符現在就準備好可以讀寫了,并且得到的連接配接是由套接字對刻畫的。
open_clientfd函數
bind函數
listen函數
- listen函數将sockfd從一個主動套接字轉化為一個監聽套接字。該套接字可以接受來自用戶端的連接配接請求。backlog參數暗示了核心在開始拒絕連接配接請求之前,該放入隊列中等待的未完成連接配接請求的數量
open_listenfd函數
accept函數
第五節 web伺服器
web基礎
- Web用戶端和伺服器之間的互動用的是一個基于文本的應用級協定,叫做HTTP。
- HTTP是一個簡單的協定。一個web用戶端(即浏覽器)打開一個到伺服器的網際網路連接配接。浏覽器讀取這些内容,并請求某些内容。伺服器響應所請求的内容,然後關閉連接配接。浏覽器讀取并把它顯示在螢幕内
- 主要的差別是Web内容可以用HTML來編寫。一個HTML程式(頁)包含指令(标記)它們告訴浏覽器如何顯示這頁中的各種文本和圖形對象。
web内容
Web伺服器以兩種不同的方式向用戶端提供内容:
- 取一個磁盤檔案,并将它的内容傳回給用戶端。
- 運作一個可執行檔案,并将它的輸出傳回給用戶端。
http事務
- http請求
- http響應
服務動态内容
- 用戶端如何将程式參數傳遞給伺服器
- 伺服器如何将參數傳遞給子程序
- 伺服器如何将其他資訊傳遞給子程序
- 子程序将它的輸出發送到哪裡
第十二章 并發程式設計
三種基本的構造并發程式的方法:
程序
I/O多路複用
線程
第一節 基于程序的并發程式設計
構造并發程式最簡單的方法——用程序
常用函數如下:
- fork
- exec
- waitpid
1.父程序需要關閉它的已連接配接描述符的拷貝(子程序也需要關閉)
2.必須要包括一個SIGCHLD處理程式來回收僵死子程序的資源
3.父子程序之間共享檔案表,但是不共享使用者位址空間,這個在以前的學習過程中提到過
第二節 基于I/O多路複用的并發程式設計
就是使用select函數要求核心挂起程序,隻有在一個或多個I/O事件發生後,才将控制傳回給應用程式。
select函數處理類型為fd_set的集合,即描述符集合,并在邏輯上描述為一個大小為n的位向量,每一位b[k]對應描述符k,但當且僅當b[k]=1,描述符k才表明是描述符集合的一個元素。
描述符能做的三件事:
- 配置設定他們
- 将一個此種類型的變量指派給另一個變量
- 用FD_ZERO、FD_SET、FD_CLR和FD_ISSET宏指令來修改和檢查它們
基于I/O多路複用的并發事件驅動伺服器
事件驅動程式:将邏輯流模型化為狀态機。
狀态機:
- 狀态
- 輸入事件
- 轉移
對于狀态機的了解,參考EDA課程中學習的狀态轉換圖的畫法和狀态機。
整體的流程是:
- select函數檢測到輸入事件
- add_client函數建立新狀态機
- check_clients函數執行狀态轉移(在課本的例題中是回送輸入行),并且完成時删除該狀态機。
幾個需要注意的函數:
- init_pool:初始化用戶端池
- add_client:添加一個新的用戶端到活動用戶端池中
- check_clients:回送來自每個準備好的已連接配接描述符的一個文本行
I/O多路複用技術的優劣
1.優點
- 相較基于程序的設計,給了程式員更多的對程式程式的控制
- 運作在單一程序上下文中,是以每個邏輯流都可以通路該程序的全部位址空間,共享資料容易實作
- 可以使用GDB調試
- 高效
2.缺點
- 編碼複雜
- 不能充分利用多核處理器
第三節 基于線程的并發程式設計
這種模式混合了以上兩種方法
線程:就是運作在程序上下文中的邏輯流。
每個線程都有它自己的線程上下文:
- 一個唯一的整數線程ID——TID
- 棧
- 棧指針
- 程式計數器
- 通用目的寄存器
- 條件碼
線程執行模型
1.主線程
在每個程序開始生命周期時都是單一線程——主線程,與其他程序的差別僅有:它總是程序中第一個運作的線程。
2.對等線程
某時刻主線程建立,之後兩個線程并發運作。
每個對等線程都能讀寫相同的共享資料。
3.主線程切換到對等線程的原因:
- 主線程執行一個慢速系統調用,如read或sleep
- 被系統的間隔計時器中斷
切換方式是上下文切換
對等線程執行一段時間後會控制傳遞回主線程,以此類推
4.線程和程序的差別
- 線程的上下文切換比程序快得多
- 組織形式:
- 程序:嚴格的父子層次
- 線程:一個程序相關線程組成對等(線程)池,和其他程序的線程獨立開來。一個線程可以殺死它的任意對等線程,或者等待他的任意對等線程終止。
Posix線程
Posix線程是C程式中處理線程的一個标準接口。基本用法是:
- 線程的代碼和本地資料被封裝在一個線程例程中
- 每個線程例程都以一個通用指針為輸入,并傳回一個通用指針。
建立線程
1.建立線程:pthread_create函數
建立一個新的線程,帶着一個輸入變量arg,在新線程的上下文運作線程例程f。
attr預設為NULL
參數tid中包含新建立線程的ID
2.檢視線程ID——pthread_self函數
傳回調用者的線程ID(TID)
終止線程
1.終止線程的幾個方式:
- 隐式終止:頂層的線程例程傳回
- 顯示終止:調用pthread_exit函數 *如果主線程調用,會先等待所有其他對等線程終止,再終止主線程和整個程序,傳回值為pthread_return
- 某個對等線程調用Unix的exit函數,會終止程序與其相關線程
- 另一個對等線程通過以目前線程ID作為參數調用pthread_cancle來終止目前線程
2.pthread_exit函數
3.pthread_cancle函數
回收已終止線程的資源
用pthread_join函數,這個函數會阻塞,知道線程tid終止,将線程例程傳回的(void*)指針指派為thread_return指向的位置,然後回收已終止線程占用的所有存儲器資源
分離線程
在任何一個時間點上,線程是可結合的,或是分離的。
1.可結合的線程
- 能夠被其他線程收回其資源和殺死
- 被收回錢,它的存儲器資源沒有被釋放
- 每個可結合線程要麼被其他線程顯式的收回,要麼通過調用pthread_detach函數被分離
2.分離的線程
- 不能被其他線程回收或殺死
- 存儲器資源在它終止時由系統自動釋放
3.pthread_detach函數
線程能夠通過以pthread_self()為參數的pthread_detach調用來分離他們自己。
第四節 多線程程式中的共享變量
一、線程存儲器模型
寄存器從不共享,虛拟存儲器總是共享的。
二、将變量映射到存儲器

三、共享變量
變量v是共享的——當且僅當它的一個執行個體被一個以上的線程引用。
第五節 用信号量同步線程
一、進度圖
進度圖是将n個并發線程的執行模型化為一條n維笛卡爾空間中的軌迹線,原點對應于沒有任何線程完成一條指令的初始狀态。
當n=2時,狀态比較簡單,是比較熟悉的二維坐标圖,橫縱坐标各代表一個線程,而轉換被表示為有向邊
轉換規則:
- 合法的轉換是向右或者向上,即某一個線程中的一條指令完成
- 兩條指令不能在同一時刻完成,即不允許出現對角線
- 程式不能反向運作,即不能出現向下或向左
而一個程式的執行曆史被模型化為狀态空間中的一條軌迹線。
線程循環代碼的分解:
- H:在循環頭部的指令塊
- L:加載共享變量cnt到線程i中寄存器%eax的指令。
- U:更新(增加)%eax的指令
- S:将%eax的更新值存回到共享變量cnt的指令
- T:循環尾部的指令塊
幾個概念
- 臨界區:對于線程i,操作共享變量cnt内容的指令L,U,S構成了一個關于共享變量cnt的臨界區。
- 不安全區:兩個臨界區的交集形成的狀态
- 安全軌迹線:繞開不安全區的軌迹線
二、信号量
信号量實作互斥的基本原理;
定義對信号量的兩個原子操作——P和V
P(wait) 程序阻塞; 程序進入s.queue隊列; end;
V(signal)喚醒隊首程序; 将程序從s.queue阻塞隊列中移出; end;
三、使用信号量來實作互斥
wait(s)/signal(s)的應用
- 程序進入臨界區之前,首先執行wait(s)原語,若s.count<0,則程序調用阻塞原語,将自己阻塞,并插入到s.queue隊列排隊;
- 注意,阻塞程序不會占用處理機時間,不是“忙等”。直到某個從臨界區退出的程序執行signal(s)原語,喚醒它;
- 一旦其它某個程序執行了signal(s)原語中的s.count+1操作後,發現s.count ≤0,即阻塞隊列中還有被阻塞程序,則調用喚醒原語,把s.queue中第一個程序修改為就緒狀态,送就緒隊列,準備執行臨界區代碼。
- wait操作用于申請資源(或使用權),程序執行wait原語時,可能會阻塞自己;
- signal操作用于釋放資源(或歸還資源使用權),程序執行signal原語時,有責任喚醒一個阻塞程序。
三、利用信号量來排程共享資源
信号量有兩個作用:
- 實作互斥
- 排程共享資源
信号量的實體意義
- s.count >0表示還可執行wait(s)而不會阻塞的程序數(可用資源數)。每執行一次wait(s)操作,就意味着請求配置設定一個機關的資源。
- 當s.count ≤0時,表示已無資源可用,是以請求該資源的程序被阻塞。此時,s.count的絕對值等于該信号量阻塞隊列中的等待程序數。執行一次signal操作,就意味着釋放一個機關的資源。若s.count<0,表示s.queue隊列中還有被阻塞的程序,需要喚醒該隊列中的第一個程序,将它轉移到就緒隊列中。
第七節 其他并發問題
一、線程安全性
一個線程是安全的,當且僅當被多個并發線程反複的調用時,它會一直産生正确的結果。
四個不相交的線程不安全函數類以及應對措施:
- 不保護共享變量的函數——用P和V這樣的同步操作保護共享變量
- 保持跨越多個調用的狀态的函數——重寫,不用任何static資料。
- 傳回指向靜态變量的指針的函數——①重寫;②使用加鎖-拷貝技術。
- 調用線程不安全函數的函數——參考之前三種
二、可重入性
當它們被多個線程調用時,不會引用任何共享資料。
1.顯式可重入的:
所有函數參數都是傳值傳遞,沒有指針,并且所有的資料引用都是本地的自動棧變量,沒有引用靜态或全劇變量。
2.隐式可重入的:
調用線程小心的傳遞指向非共享資料的指針。
三、競争
1.競争發生的原因:
一個程式的正确性依賴于一個線程要在另一個線程到達y點之前到達它的控制流中的x點。也就是說,程式員假定線程會按照某種特殊的軌迹穿過執行狀态空間,忘了一條準則規定:線程化的程式必須對任何可行的軌迹線都正确工作。
2.消除方法:
動态的為每個整數ID配置設定一個獨立的塊,并且傳遞給線程例程一個指向這個塊的指針
四、死鎖
解決死鎖的方法
a.不讓死鎖發生:
- 靜态政策:設計合适的資源配置設定算法,不讓死鎖發生---死鎖預防;
- 動态政策:程序在申請資源時,系統審查是否會産生死鎖,若會産生死鎖則不配置設定---死鎖避免。
b.讓死鎖發生:
程序申請資源時不進行限制,系統定期或者不定期檢測是否有死鎖發生,當檢測到時解決死鎖----死鎖檢測與解除。