[Python之路] 使用epoll實現高並發HTTP服務器


什么是epoll

我們在  Python多種方式實現並發的Web Server 的最后使用單進程+單線程+非阻塞+長連接實現了一個可並發處理客戶端連接的服務器。他的原理可以用以下的圖來描述:

 

 

解釋:

1.HTTP服務器是我們使用 單進程+單線程+非阻塞+長連接實現 的web服務器。

2.在實現的時候,我們創建了一個存放已接受Socket連接的列表,該列表是在應用程序的內存空間中的。如圖中深藍色部分

3.當有3個客戶端接入的時候,列表中一共存在3個對應的socket句柄,分別對應三個小黃框。

4.灰色小框代表服務器接收請求的socket。

5.我們在進行無限循環的時候,首先是檢查是否有新的客戶端接入,相當於檢查灰色小框是否有數據到達。然后輪詢3個小黃框對應socket是否有數據到達。輪詢的效率是很低的。

6.服務器在使用accept和recv時,實際上是委托操作系統幫他檢查是否有數據到達,由於這個列表的socket都處於用戶內存空間,所以需要將其復制到內核空間。操作系統檢查完畢后,如果有數據就返回數據給應用程序,如果沒有數據就以異常的方式通知應用程序。而且不光這樣,操作系統可能還同時在運行其他的應用程序,這樣效率會非常低。

 

我們再來看epoll的圖:

 

 

 

解釋:

1.我們可以看到,在結構上,最大的區別在於,存放socket的列表不處於應用程序內部。在epoll中,這個存放socket的列表處於一個特殊的內存空間,這個內存空間是應用程序與內核共享的空間。也就是說,當應用程序委托操作系統檢查是否有數據到達時,無需將復制數據給內核空間,操作系統可以直接進行檢查。

2.操作系統檢查到某個socket有數據到達,使用事件通知的形式,直接告訴應用程序,而不是以輪詢的方式。打個比方,一個廚師挨個問50個人餓了沒,如果餓了就給他東西吃,這是輪詢。而50個人中,誰餓了誰舉手,廚師就給吃的,這叫事件通知。很明顯,事件通知的效率會特別高。

 

實現代碼:

import socket

import re
import select


def handle_request(new_socket, recv_msg):
    # 從請求中解析出URI
    recv_lines = recv_msg.splitlines()

    # 使用正則表達式提取出URI
    ret = re.match(r"[^/]+(/[^ ]*)", recv_lines[0])

    if ret:
        # 獲取URI字符串
        file_name = ret.group(1)
        # 如果URI是/,則默認返回index.html的內容
        if file_name == "/":
            file_name = "/index.html"

    try:
        # 根據請求的URI,讀取相應的文件
        fp = open("." + file_name, "rb")
    except:
        # 找不到文件,響應404
        response_msg = "HTTP/1.1 404 NOT FOUND\r\n"
        response_msg += "\r\n"
        response_msg += "<h1>----file not found----</h1>"
        new_socket.send(response_msg.encode("utf-8"))
    else:
        html_content = fp.read()
        fp.close()

        response_body = html_content

        # 響應正確 200 OK
        response_header = "HTTP/1.1 200 OK\r\n"
        response_header += "Content-Length:%d\r\n" % len(response_body)
        response_header += "\r\n"

        response = response_header.encode("utf-8") + response_body

        # 返回響應數據
        new_socket.send(response)


def main():
    # 創建TCP SOCKET實例
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # # 設置重用地址
    # tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # 綁定地址(默認本機IP)和端口
    tcp_server_socket.bind(("", 7890))
    # 監聽
    tcp_server_socket.listen(128)

    # 將accept設置為非阻塞,這里設置一次,后面不管調多少次accept都是非阻塞的
    tcp_server_socket.setblocking(False)

    # 創建一個epoll對象
    epl = select.epoll()
    # 將監聽套接字對應的fd注冊到epoll中,並讓其監聽有沒有數據進來,所以使用EPOLLIN
    epl.register(tcp_server_socket.fileno(), select.EPOLLIN)

    # 定義一個字典,用於存放fd和套接字的對應關系,因為操作系統在事件通知的時候,使用的是fd,而不是套接字,我們需要使用fd來找到對應
    # 的套接字,從而可以調用accept和recv
    fd_event_dict = dict()

    # 循環接收客戶端連接
    while True:
        # 使用一個列表來接受操作系統的事件通知,poll()是阻塞的,當有數據到達時,poll才會解開阻塞
        fd_event_list = epl.poll()
        # 操作系統的事件通知返回一個列表(可能同時有多個套接字有數據進入),這個列表中的元素都是元組(fd,event)
        for fd, event in fd_event_list:
            # 首先判斷事件通知中的fd是否對應監聽套接字(監聽套接字調用accept)
            if fd == tcp_server_socket.fileno():
                new_socket, client_addr = tcp_server_socket.accept()
                # 監聽到一個新的客戶端連接,將new_socket也注冊到epoll中
                epl.register(new_socket.fileno(), select.EPOLLIN)
                # 並且將這個socket加入fd_event_dict字段,方便以后通過fd來獲取套接字
                fd_event_dict[new_socket.fileno()] = new_socket
            elif event == select.EPOLLIN:  # 如果不是監聽套接字,那么都是客戶端對應的套接字
                # 接收數據
                recv_data = fd_event_dict[fd].recv(1024).decode("utf-8")
                # 如果有數據
                if recv_data:
                    # 處理數據
                    handle_request(fd_event_dict[fd], recv_data)
                else:  # 如果沒有數據,則表示客戶端斷開連接
                    # 關閉fd對應的socket
                    fd_event_dict[fd].close()
                    # 從epoll中踢出已經斷開的fd
                    epl.unregister(fd)
                    # 從字典中刪除fd對應的記錄
                    del fd_event_dict[fd]

    # 關閉整個SOCKET
    tcp_server_socket.close()


if __name__ == "__main__":
    main()

解釋:

1.首先創建epoll對象

2.將監聽套接字對應fd注冊到epoll,並設置監聽數據的IN。

3.調用poll()函數,如果沒有數據到達,則處於阻塞狀態,如果有數據到達,則操作系統會返回一個事件通知列表。

4.遍歷列表,如果發現fd是監聽套接字對應fd,則使用監聽套接字調用accept,並將接收到的新的客戶端連接對應socket也注冊到epoll中,並將其存放到字典fd_event_dict中(方便后續使用fd獲取socket)。

5.如果不是監聽套接字,則直接從fd_event_dict中通過fd獲取對應的socket,然后調用recv來接收數據。

6.如果接收到的數據有內容,則調用請求處理邏輯。

7.如果接收到的數據為空,則表示客戶端主動調用了close,想要斷開連接。此時從fd_event_dict中通過fd獲取對應socket,然后調用socker.close()來關閉連接。

8.關閉連接后,將該socket從epoll中剔除,並且從fd_event_dict中刪除。

 

注意:該代碼無法在windows上運行,因為epoll是Linux2.6內核增加的新功能,windows並不支持。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM