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