从简单到高并发服务器(一)
一個單線程的回聲服務器 (Echo Server)
我們從一個簡單的服務器開始說起。
它可以接受一個客戶的連接,接收消息,然后把這個消息發送回去,關閉連接——完工。我們用 Linux 和 iOS / OSX 上都通用的 BSD Socket 來編寫這個服務器的代碼。主體部分大概是這樣的:(C++ 語法)
這段代碼當然是很粗糙(誤:粗口),可能會有內存泄漏,如果客戶發送的消息過長會接收不完全……各種各樣的問題,但是它基本上呈現出了一個服務器程序到底是怎樣運作的。
以下是代碼中提到的,要實現一個TCP服務器幾個重要的工作:
綁定監聽地址,并開始監聽(注1和注2)
等待客戶端連接(注4)
接收客戶端發送的數據(注5)
發送回復(注6)
實際上以上這四點也是任何服務器都要完成的事情。
如果是使用 Udp 的話,則不需要等待客戶端連接這個步驟,這是因為 Udp 是面向數據包而不是面向連接的傳輸協議;而使用 Tcp 則需要等待客戶端連接,實際上還會涉及到“三路握手” (3-way handshake) 這個建立 Tcp 連接的過程。
但是這個握手過程,由于是屬于 TCP 協議的標準部分,因此實際上是由操作系統來幫助我們完成的(所有支持 TCP/IP 協議棧的操作系統都會替程序員完成這個過程)。我們只需要通過調用 accept 這個API,就相當于告訴系統“現在開始幫我處理握手這個事情,有人找你握手了再來告訴我吧”。
線程與阻塞
握手過程調用 accept 會阻塞整個程序的執行,阻塞是什么意思呢?
如果我們寫代碼的時候,寫一個死循環,就如代碼中 注3 那樣:
即使不運行這個程序,你也應該可以預料到,在屏幕上會不斷打出一行行的內容。這說明,程序沒有被阻塞的情況下,就會一直執行下去。嚴格來說,printf 也會阻塞,只不過阻塞的時間非常短,并且可以自動解除阻塞狀態,具體的解釋以后再說。
而調用 accept 就不可以自動解除阻塞狀態了——如果你成功運行剛才的代碼,你會看到,屏幕輸出了 before accept. 之后,并沒有馬上接著輸出 handle client sock: ——程序一直停留在 accept 被調用的地方,也可以認為是 accept 一直沒有返回結果。
阻塞的本質是,操作系統把執行你的代碼的線程暫停了,而線程則是操作系統安排CPU調度的基本單位,這通常意味著操作系統把 CPU 拿去干其他事情了,而你的程序不能使用 CPU進行計算,只能暫停。直到有一個客戶成功連接到你的服務器為止。
為了模擬這個事情,我們可以使用 python + gevent 來模擬很多(300)個客戶端并發地不停發起TCP連接:
from __future__ import print_function from gevent.socket import socket as gsocket import gevent import socketdef do_connect(addr, index):if 0: client_sock = socket.socket()while True:client_sock = gsocket(socket.AF_INET,socket.SOCK_STREAM,socket.IPPROTO_TCP)print(addr)client_sock.connect(addr)print('client {0} connected.'.format(index))gevent.sleep(10)client_sock.send('Hello World')data = client_sock.recv(1024)print('recv data: {0}'.format(data))if __name__ == '__main__':server_addr = ('127.0.0.1', 5432)greenlets = list()for i in xrange(300):g = gevent.spawn(do_connect, server_addr, i)greenlets.append(g)gevent.joinall(greenlets)然后,如無意外,你就可以看到程序繼續得到執行,非常有規律地重復,并且總是按順序地一個連接一個連接地處理。如果注意到客戶端的輸出話你可能會看到,在后面的發起的連接都超時了,會看到很多 Traceback。
這肯定不是我們日常訪問網站所能得到的體驗:很快就可以連接上并且看到網頁的內容(當然,在天朝,例外有很多)。所以這不是理想的高并發服務器。
為什么比較早發起連接的客戶端不會超時,而后面發起的會超時呢?原因就是服務器端在阻塞等待 IO 的時候,單線程無法響應其他請求。
為了驗證這個結論,你可以把客戶端代碼中,發送數據前 gevent.sleep 的時間加長,例如改為20 秒,你會發現更多的連接會超時—— 因為服務器花費在等待客戶端發送數據的時間更多了,那么在相同超時時間前服務窗口內能夠 accept 的連接數量就更少了。
(未完待續)
總結
以上是生活随笔為你收集整理的从简单到高并发服务器(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JVM 指令集
- 下一篇: Solr在Weblogic中部署遇到的问