- 之前我們搭建了一個極簡的WSGI服務器, 可以處理基本的HTTP請求,但是由于我們建立的服務一次只能處理一個客戶端請求,在當前的請求處理完成之前,它不能接受新的連接。
服務器的代碼
- 我們將處理請求的這塊邏輯抽出來,代碼如下:
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()
仔細看,當handle_request方法還沒有結束的時候,循環會阻塞在這里,無法監聽后續的請求。
客戶端與服務端之間的通信
- 為了讓兩個程序通過網絡彼此通訊,我們用到了socket,那么socket是什么呢?
socket
- socket是一個通信終端的抽象概念,它允許程序通過文件描述符與另一個程序通信
- TCP連接的socket對是一個擁有4個值的tuple,用來標識TCP連接的兩個端點: 本地IP地址、本地端口、外部IP地址、外部端口。
- socket對是網絡上每個tcp連接的唯一標識。這兩個成對的值標識各自端點,一個IP地址和一個端口號,通常被稱為一個socket。
- 例子
socket pair
- tuple {10.10.10.2:49152, 12.12.12.3:8888} 是客戶端上一個唯一標識兩個TCP連接終端的socket, {12.12.12.3:8888, 10.10.10.2:49152} 是服務端上一個唯一標識相同的兩個TCP連接終端的socket。IP地址12.12.12.3和端口8888在TCP連接中用來識別服務器端點(同樣適用于客戶端)。
Python建立socket鏈接
-
服務器創建一個TCP/IP socket鏈接
- 新建socket連接
# AF_INET:服務器之間網絡通信。 SOCK_STREAM: 流式socket , for TCP listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- 設置一些socket選項(這是可選的)
# SOL_SOCKET: 想要在套接字級別上設置選項,就必須把level設置為 SOL_SOCKET # SO_REUSEADDR: 打開或關閉地址復用功能。當值不等于0時,打開,否則,關閉。 listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- 服務器綁定地址
# 在TCP中,調用bind允許你指定端口號,IP地址,要么兩個都有,要么就都沒有。 listen_socket.bind(SERVER_ADDRESS)
- 開始監聽
# listen方法只供服務器調用。它告訴內核應該接受給這個socket傳入的連接請求 # REQUEST_QUEUE_SIZE代表連接請求隊列的長度 listen_socket.listen(REQUEST_QUEUE_SIZE)
- 上述步驟完成后,服務器開始逐個接受客戶端連接。當一個連接可用accept返回要連接的客戶端socket。然后服務器讀從客戶端socket取請求數據,打印出響應標準輸出然后給客戶端socket傳回消息。然后服務器關閉客戶端連接,準備接受一個新的客戶端連接。
-
客戶端連接服務器
- 和服務端0建立連接類似
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())
- 客戶端只需提供服務器的遠程地址或是主機名和遠程端口號來連接。
- 客戶端沒有調用bind和accept。其原因是客戶端不關心本地IP地址和端口號。客戶端調用connect時內核中的TCP/IP socket會自動分配本地IP地址和端口號。本地端口被稱為臨時端口。
文件描述符
- 當一個進程打開現有的文件,創建一個新的文件,或者創建一個新的socket連接的時候,內核返回給它的一個非負整數。
- 在UNIX中,一切都是文件,內核通過文件描述符指向一個打開的文件。當你需要讀寫文件的時候,就是用文件描述符來識別的。
- UNINS shell默認分配文件描述符0給標準輸入進程,1是標準輸出,2是標準錯誤
怎么保證你的服務器能同時處理多個請求?或者換個說法,如何編寫并發服務器?
- 在UNIX下,最簡單的方式是用一個fork()系統調用
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()
- 運行多個curl命令之后,即使子進程處理一個進程之后會休眠60秒,但是這并不會影響處理其它的客戶端請求,因為它們是完全獨立的進程了。
- 在調用一次fork()之后,新進程會是原進程的子進程,原進程稱為父進程。 子進程會復制父進程的數據信息。而后程序就分兩個進程繼續運行了。在子進程內,這個方法會返回0;在父進程內,這個方法會返回子進程的編號PID??梢允褂肞ID來區分兩個進程。
- 在上面的代碼中,父進程關閉了客戶端的連接,那么子進程是怎么繼續讀取客戶端的socket連接的呢?
- 父進程fork出一個子進程之后,這個子進程得到了一個父進程文件描述符。
- 內核根據文件描述符的值來決定是否關閉連接socket,只有其值為0才會關閉。
- 服務器產生一個子進程,子進程拷貝父進程文件描述符,內核增加引用描述符的值。在一個父進程一個子進程的例子中,描述符引用值就是2,當父進程關閉連接socket,它只會把引用值減為1,不會小到讓內核關閉socket。
- 子進程也關閉了父進程監聽socket的重復拷貝,是因為它不關心接受新的客戶端連接,而只在乎處理已連接客戶端的響應:
listen_socket.close() # close child copy
小結
- 服務器socket創建過程(socket,bind,listen,accept)
- 客戶端socket創建過程(socket,connect)
- fork()函數的意義:創建子進程,復制父進程的數據信息。而后程序就分兩個進程繼續運行
- 在UNIX下寫并發服務器最簡單的方法是用fork()系統調用。一個進程fork出一個新進程,它就變成新進程的父進程。
- 調用fork后,父進程和子進程公用同樣的文件描述符。內核用文件描述符應用值來決定關閉或打開文件/socket。
- 服務器父進程的角色:從客戶端接受新的連接,fork一個子進程去處理請求,繼續接受新的連接。