- 之前我們搭建了一個(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ù)接受新的連接。