自己寫(xiě)一個(gè)web服務(wù)器(2)

  • 之前我們搭建了一個(gè)極簡(jiǎn)的WSGI服務(wù)器灶似, 可以處理基本的HTTP請(qǐng)求逸月,但是由于我們建立的服務(wù)一次只能處理一個(gè)客戶(hù)端請(qǐng)求疲扎,在當(dāng)前的請(qǐng)求處理完成之前絮蒿,它不能接受新的連接。

服務(wù)器的代碼

  • 我們將處理請(qǐng)求的這塊邏輯抽出來(lái)绩卤,代碼如下:
import socket

SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5

def handle_request(client_connection):
    request = client_connection.recv(1024)
    print(request.decode())
    http_response = b"""\
HTTP/1.1 200 OK

Hello, World!
"""
    client_connection.sendall(http_response)

def serve_forever():
    listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    listen_socket.bind(SERVER_ADDRESS)
    listen_socket.listen(REQUEST_QUEUE_SIZE)
    print('Serving HTTP on port {port} ...'.format(port=PORT))

    while True:
        client_connection, client_address = listen_socket.accept()
        handle_request(client_connection)
        client_connection.close()

if __name__ == '__main__':
    serve_forever()

仔細(xì)看途样,當(dāng)handle_request方法還沒(méi)有結(jié)束的時(shí)候,循環(huán)會(huì)阻塞在這里濒憋,無(wú)法監(jiān)聽(tīng)后續(xù)的請(qǐng)求何暇。

客戶(hù)端與服務(wù)端之間的通信

  • 為了讓兩個(gè)程序通過(guò)網(wǎng)絡(luò)彼此通訊,我們用到了socket凛驮,那么socket是什么呢裆站?

socket

  • socket是一個(gè)通信終端的抽象概念,它允許程序通過(guò)文件描述符與另一個(gè)程序通信
  • TCP連接的socket對(duì)是一個(gè)擁有4個(gè)值的tuple,用來(lái)標(biāo)識(shí)TCP連接的兩個(gè)端點(diǎn): 本地IP地址宏胯、本地端口羽嫡、外部IP地址、外部端口肩袍。
  • socket對(duì)是網(wǎng)絡(luò)上每個(gè)tcp連接的唯一標(biāo)識(shí)厂僧。這兩個(gè)成對(duì)的值標(biāo)識(shí)各自端點(diǎn),一個(gè)IP地址和一個(gè)端口號(hào)了牛,通常被稱(chēng)為一個(gè)socket。
  • 例子
    socket pair
  • tuple {10.10.10.2:49152, 12.12.12.3:8888} 是客戶(hù)端上一個(gè)唯一標(biāo)識(shí)兩個(gè)TCP連接終端的socket辰妙, {12.12.12.3:8888, 10.10.10.2:49152} 是服務(wù)端上一個(gè)唯一標(biāo)識(shí)相同的兩個(gè)TCP連接終端的socket鹰祸。IP地址12.12.12.3和端口8888在TCP連接中用來(lái)識(shí)別服務(wù)器端點(diǎn)(同樣適用于客戶(hù)端)。

Python建立socket鏈接

  • 服務(wù)器創(chuàng)建一個(gè)TCP/IP socket鏈接

    • 新建socket連接
    # AF_INET:服務(wù)器之間網(wǎng)絡(luò)通信密浑。  SOCK_STREAM: 流式socket , for TCP
    listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    • 設(shè)置一些socket選項(xiàng)(這是可選的)
    # SOL_SOCKET: 想要在套接字級(jí)別上設(shè)置選項(xiàng)蛙婴,就必須把level設(shè)置為 SOL_SOCKET
    # SO_REUSEADDR: 打開(kāi)或關(guān)閉地址復(fù)用功能。當(dāng)值不等于0時(shí)尔破,打開(kāi)街图,否則,關(guān)閉懒构。
    listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    • 服務(wù)器綁定地址
    # 在TCP中餐济,調(diào)用bind允許你指定端口號(hào),IP地址胆剧,要么兩個(gè)都有絮姆,要么就都沒(méi)有。
    listen_socket.bind(SERVER_ADDRESS)
    
    • 開(kāi)始監(jiān)聽(tīng)
    # listen方法只供服務(wù)器調(diào)用秩霍。它告訴內(nèi)核應(yīng)該接受給這個(gè)socket傳入的連接請(qǐng)求
    # REQUEST_QUEUE_SIZE代表連接請(qǐng)求隊(duì)列的長(zhǎng)度
    listen_socket.listen(REQUEST_QUEUE_SIZE)
    
    • 上述步驟完成后篙悯,服務(wù)器開(kāi)始逐個(gè)接受客戶(hù)端連接。當(dāng)一個(gè)連接可用accept返回要連接的客戶(hù)端socket铃绒。然后服務(wù)器讀從客戶(hù)端socket取請(qǐng)求數(shù)據(jù)鸽照,打印出響應(yīng)標(biāo)準(zhǔn)輸出然后給客戶(hù)端socket傳回消息。然后服務(wù)器關(guān)閉客戶(hù)端連接颠悬,準(zhǔn)備接受一個(gè)新的客戶(hù)端連接矮燎。
  • 客戶(hù)端連接服務(wù)器

    • 和服務(wù)端0建立連接類(lèi)似
     import socket
    
     # create a socket and connect to a server
     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
     sock.connect(('localhost', 8888))
    
     # send and receive some data
     sock.sendall(b'test')
     data = sock.recv(1024)
     print(data.decode())
    
    • 客戶(hù)端只需提供服務(wù)器的遠(yuǎn)程地址或是主機(jī)名和遠(yuǎn)程端口號(hào)來(lái)連接。
    • 客戶(hù)端沒(méi)有調(diào)用bind和accept赔癌。其原因是客戶(hù)端不關(guān)心本地IP地址和端口號(hào)漏峰。客戶(hù)端調(diào)用connect時(shí)內(nèi)核中的TCP/IP socket會(huì)自動(dòng)分配本地IP地址和端口號(hào)届榄。本地端口被稱(chēng)為臨時(shí)端口浅乔。

文件描述符

  • 當(dāng)一個(gè)進(jìn)程打開(kāi)現(xiàn)有的文件,創(chuàng)建一個(gè)新的文件,或者創(chuàng)建一個(gè)新的socket連接的時(shí)候靖苇,內(nèi)核返回給它的一個(gè)非負(fù)整數(shù)席噩。
  • 在UNIX中,一切都是文件贤壁,內(nèi)核通過(guò)文件描述符指向一個(gè)打開(kāi)的文件悼枢。當(dāng)你需要讀寫(xiě)文件的時(shí)候,就是用文件描述符來(lái)識(shí)別的脾拆。
  • UNINS shell默認(rèn)分配文件描述符0給標(biāo)準(zhǔn)輸入進(jìn)程馒索,1是標(biāo)準(zhǔn)輸出,2是標(biāo)準(zhǔn)錯(cuò)誤

怎么保證你的服務(wù)器能同時(shí)處理多個(gè)請(qǐng)求名船?或者換個(gè)說(shuō)法绰上,如何編寫(xiě)并發(fā)服務(wù)器?

  • 在UNIX下渠驼,最簡(jiǎn)單的方式是用一個(gè)fork()系統(tǒng)調(diào)用
    import os
    import socket
    import time
    
    SERVER_ADDRESS = (HOST, PORT) = '', 8888
    REQUEST_QUEUE_SIZE = 5
    
    
    def handle_request(client_connection):
        request = client_connection.recv(1024)
        print(
            'Child PID: {pid}. Parent PID {ppid}'.format(
                pid=os.getpid(),
                ppid=os.getppid(),
            )
        )
        print(request.decode())
        http_response = b"""\
    HTTP/1.1 200 OK
    Hello, World!
    """
        client_connection.sendall(http_response)
        time.sleep(60)
    
    
    def serve_forever():
        listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        listen_socket.bind(SERVER_ADDRESS)
        listen_socket.listen(REQUEST_QUEUE_SIZE)
        print('Serving HTTP on port {port} ...'.format(port=PORT))
        print('Parent PID (PPID): {pid}\n'.format(pid=os.getpid()))
    
        while True:
            client_connection, client_address = listen_socket.accept()
            pid = os.fork()
            if pid == 0:  # child
                listen_socket.close()  # close child copy
                handle_request(client_connection)
                client_connection.close()
                os._exit(0)  # child exits here
            else:  # parent
                client_connection.close()  # close parent copy and loop over
    
    if __name__ == '__main__':
        serve_forever()
    
  • 運(yùn)行多個(gè)curl命令之后蜈块,即使子進(jìn)程處理一個(gè)進(jìn)程之后會(huì)休眠60秒,但是這并不會(huì)影響處理其它的客戶(hù)端請(qǐng)求迷扇,因?yàn)樗鼈兪峭耆?dú)立的進(jìn)程了百揭。
  • 在調(diào)用一次fork()之后,新進(jìn)程會(huì)是原進(jìn)程的子進(jìn)程蜓席,原進(jìn)程稱(chēng)為父進(jìn)程器一。 子進(jìn)程會(huì)復(fù)制父進(jìn)程的數(shù)據(jù)信息。而后程序就分兩個(gè)進(jìn)程繼續(xù)運(yùn)行了厨内。在子進(jìn)程內(nèi)盹舞,這個(gè)方法會(huì)返回0;在父進(jìn)程內(nèi)隘庄,這個(gè)方法會(huì)返回子進(jìn)程的編號(hào)PID踢步。可以使用PID來(lái)區(qū)分兩個(gè)進(jìn)程丑掺。
  • 在上面的代碼中获印,父進(jìn)程關(guān)閉了客戶(hù)端的連接,那么子進(jìn)程是怎么繼續(xù)讀取客戶(hù)端的socket連接的呢街州?
    • 父進(jìn)程fork出一個(gè)子進(jìn)程之后兼丰,這個(gè)子進(jìn)程得到了一個(gè)父進(jìn)程文件描述符。
    • 內(nèi)核根據(jù)文件描述符的值來(lái)決定是否關(guān)閉連接socket唆缴,只有其值為0才會(huì)關(guān)閉鳍征。
    • 服務(wù)器產(chǎn)生一個(gè)子進(jìn)程,子進(jìn)程拷貝父進(jìn)程文件描述符面徽,內(nèi)核增加引用描述符的值艳丛。在一個(gè)父進(jìn)程一個(gè)子進(jìn)程的例子中匣掸,描述符引用值就是2,當(dāng)父進(jìn)程關(guān)閉連接socket氮双,它只會(huì)把引用值減為1碰酝,不會(huì)小到讓內(nèi)核關(guān)閉socket。
    • 子進(jìn)程也關(guān)閉了父進(jìn)程監(jiān)聽(tīng)socket的重復(fù)拷貝戴差,是因?yàn)樗魂P(guān)心接受新的客戶(hù)端連接送爸,而只在乎處理已連接客戶(hù)端的響應(yīng):
      listen_socket.close() # close child copy

小結(jié)

  • 服務(wù)器socket創(chuàng)建過(guò)程(socket,bind暖释,listen袭厂,accept)
  • 客戶(hù)端socket創(chuàng)建過(guò)程(socket,connect)
  • fork()函數(shù)的意義:創(chuàng)建子進(jìn)程球匕,復(fù)制父進(jìn)程的數(shù)據(jù)信息纹磺。而后程序就分兩個(gè)進(jìn)程繼續(xù)運(yùn)行
  • 在UNIX下寫(xiě)并發(fā)服務(wù)器最簡(jiǎn)單的方法是用fork()系統(tǒng)調(diào)用。一個(gè)進(jìn)程fork出一個(gè)新進(jìn)程谐丢,它就變成新進(jìn)程的父進(jìn)程。
  • 調(diào)用fork后蚓让,父進(jìn)程和子進(jìn)程公用同樣的文件描述符乾忱。內(nèi)核用文件描述符應(yīng)用值來(lái)決定關(guān)閉或打開(kāi)文件/socket。
  • 服務(wù)器父進(jìn)程的角色:從客戶(hù)端接受新的連接历极,fork一個(gè)子進(jìn)程去處理請(qǐng)求窄瘟,繼續(xù)接受新的連接。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末趟卸,一起剝皮案震驚了整個(gè)濱河市蹄葱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌锄列,老刑警劉巖图云,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異邻邮,居然都是意外死亡竣况,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)筒严,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)丹泉,“玉大人,你說(shuō)我怎么就攤上這事鸭蛙∧『蓿” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵娶视,是天一觀(guān)的道長(zhǎng)晒哄。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么揩晴? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任勋陪,我火速辦了婚禮,結(jié)果婚禮上硫兰,老公的妹妹穿的比我還像新娘诅愚。我一直安慰自己,他們只是感情好劫映,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布违孝。 她就那樣靜靜地躺著,像睡著了一般泳赋。 火紅的嫁衣襯著肌膚如雪雌桑。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天祖今,我揣著相機(jī)與錄音校坑,去河邊找鬼。 笑死千诬,一個(gè)胖子當(dāng)著我的面吹牛耍目,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播徐绑,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼邪驮,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了傲茄?” 一聲冷哼從身側(cè)響起毅访,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎盘榨,沒(méi)想到半個(gè)月后喻粹,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡草巡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年磷斧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片捷犹。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡弛饭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出萍歉,到底是詐尸還是另有隱情侣颂,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布枪孩,位于F島的核電站憔晒,受9級(jí)特大地震影響藻肄,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜拒担,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一嘹屯、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧从撼,春花似錦州弟、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至掏婶,卻和暖如春啃奴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背雄妥。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工最蕾, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人老厌。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓瘟则,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親梅桩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子壹粟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 補(bǔ)一份記錄拜隧。 從成都的4宿百、5階上課回來(lái), 就有發(fā)心洪添,把重慶的讀書(shū)會(huì)也做起來(lái)垦页。 不論人數(shù)多少, 關(guān)鍵在于堅(jiān)持干奢。 我受...
    黃雪_辣媽們的細(xì)碎時(shí)光閱讀 339評(píng)論 0 0
  • 報(bào)名參加了日更痊焊,堅(jiān)持更了十幾天,其中有一天因?yàn)橛刑厥馇闆r沒(méi)有堅(jiān)持忿峻,結(jié)果簡(jiǎn)書(shū)給了復(fù)活卡薄啥,讓我有機(jī)會(huì)復(fù)活一次,我暗下決...
    子皿悠悠閱讀 522評(píng)論 3 4