14_Web伺服器-併發伺服器
1.伺服器概述
1.硬體伺服器(IBM,HP): 主機 叢集
2.軟體伺服器(HTTPserver Django flask): 網路伺服器,在後端提供網路功能邏輯處理資料處理的程式或者架構等
3.伺服器架構
c/s(客戶端client伺服器server): b/s架構充分發揮了PC機的效能
b/s(瀏覽器browser伺服器server): 隸屬於c/s架構,b/s架構統一了應用的介面
4.伺服器追求: 處理速度快,資料更安全,併發量大
硬體: 更高配置,更多主機,整合,分佈
軟體: 程式佔有更少的資源,更流暢的執行,處理更多的併發
2.伺服器模型
1.基本的伺服器模型
1.迴圈伺服器模型
模式: 單執行緒程式,迴圈接收連線或者請求然後處理,處理後繼續迴圈
缺點: 不能同時處理多個客戶端的並行,不允許某個客戶端長期佔有伺服器
優點: 結構比較簡單,適用於UDP程式,要求處理請求可以很快完成
2.IO多路複用伺服器模型
模式: 通過同時監控多個IO來達到IO併發的目的
缺點: 也是單執行緒,不能長期阻塞,不適用處理大量CPU佔有高的程式
3.併發伺服器模型
模式:
每有一個客戶端連結請求就建立一個新的程序或執行緒處理客戶端的請求
而主程序或主執行緒可以繼續接收其它客戶端的連線
缺點: 資源消耗比較大,客戶端需要長期佔有伺服器的情況
優點: 多工,高併發
2.併發伺服器-多程序(fork, multiprocessing)
1.建立套接字,繫結,監聽
3.建立子程序處理客戶端請求,父程序繼續準備接受新的客戶端連線
4.客戶端退出,銷燬相應的子程序
3.併發伺服器-多執行緒(threading)
1.相比多程序伺服器的優缺點
缺點: 需要用到同步互斥,可能受到GIL的影響(網路IO執行緒併發可以)
優點: 資源消耗比較少
2.使用模組: threading socket
3.步驟
1.建立套接字,繫結,監聽
2.接收客戶端連線請求,建立新的執行緒
3.主執行緒繼續接收下一個客戶端連線請求,分支執行緒處理客戶端事件
4.處理事件結束退出執行緒關閉套接字
4.併發伺服器-多協程(gevent + monkey)
5.使用整合模組完成網路併發-socketserver模組
實現程序tcp併發的類
'ForkingMixIn','TCPServer', 'StreamRequestHandler'
實現程序udp併發的類
'ForkingMixIn', 'UDPServer', 'DatagramRequestHandler',
實現執行緒tcp併發的類
'ThreadingMixIn', 'TCPServer', 'StreamRequestHandler'
實現執行緒udp併發的類
'ThreadingMixIn', 'UDPServer','DatagramRequestHandler',
其他類
'ForkingTCPServer' = 'ForkingMixIn' + 'TCPServer'
'ForkingUDPServer' = 'ForkingMixIn' + 'UDPServer'
'ThreadingTCPServer' = 'ThreadingMixIn' + 'TCPServer'
'ThreadingUDPServer' = 'ThreadingMixIn' + 'UDPServer'
3.HTTP超文字傳輸協議概述
1.在Web應用中,伺服器把網頁傳給瀏覽器,實際上就是把網頁的HTML程式碼傳送給瀏覽器讓瀏覽器顯示出
2.瀏覽器和伺服器之間的傳輸協議是HTTP,HTTP是在網路上傳輸HTML的協議,用於瀏覽器和伺服器的通訊
3.HTML是一種用來定義網頁的文字,會HTML,就可以編寫網頁
4.用途:
1.網站中網頁的傳輸和資料傳輸
2.基於HTTP協議的程式設計傳輸資料
5.特點:
1.應用層協議,傳輸層使用TCP連線
2.簡單靈活介面使用方便
3.幾乎支援所有的資料型別
4.是無狀態的
5.長連線(HTTP 1.1)
4.Web網站概述
1.瀏覽器訪問網站過程
1.域名解析
2.向伺服器傳送三次握手
3.客戶端(瀏覽器)發起HTTP請求
4.傳輸層使用TCP協議建立連線,層層打包將請求內容傳送給伺服器
5.web伺服器解包後解析HTTP請求,交給後端應用程式處理
6.後端應用得到結果,通過web伺服器回發給前端
7.傳送tcp四次回送
8.瀏覽器訪問網站流程圖: https://www.processon.com/view/link/5efcac3507912929cb6b33df
2.請求(request)的結構格式
1.請求行(確定具體的請求型別):
GET(請求方法) /index.thml(請求資源) HTTP/1.1(協議版本)
# 請求方法
GET: 獲取網路資源
POST: 提交一定的附加資料,得到返回結果
HEAD: 獲取響應的頭資訊
PUT: 更新伺服器資源
DELETE: 刪除伺服器資源
TRACE: 用於測試
CONNECT: 保留方法
OPTIONS: 請求獲取伺服器效能和資訊
3.請求頭(對請求內容的資訊描述)
# 選項: 值
Accept: text/html,application/xhtml+xml # 瀏覽器可以接收的文字格式
Accept-Encoding: gzip, deflate # 瀏覽器可以接收的壓縮格式
Accept-Language: zh-CN,zh;q=0.9 # 瀏覽器可以接收的語言
Connection: keep-alive # 長連線
Cookie: BAIDUID=8A4DA4339C1B8A74DD251F7D9F834C76:FG=1
Host: news.baidu.com # 請求的伺服器地址
Referer: https://www.baidu.com/ # 防盜鏈,防止惡意請求
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) # 傳送請求的瀏覽器的版本
3.空行
4.請求體(具體請求引數):
get請求: get引數 &a=1&b=2
post請求: post提交的內容
3.響應(response)的結構格式
1.響應行(反饋響應的情況): HTTP/1.1(協議版本) 200(響應碼) OK(資訊)
# 響應碼
1xx: 提示資訊,表示請求已經接收,正在處理
2xx: 請求響應成功
3xx: 重定向,完成任務需要其它操作
4xx: 客戶端錯誤
5xx: 服務端錯誤
# HTTP響應狀態碼劃分
100-199: 表示成功接收請求,要求客戶端繼續提交下一次請求才能完成整個處理過程
200-299: 表示成功接收請求並已完成整個處理過程,常用200
300-399: 未完成請求,客戶需進一步細化請求,常用302,307,304
400-499: 客戶端的請求有錯誤,常用404
500-599: 伺服器端出現錯誤,常用500
# 響應碼示例
200 成功
401 沒有訪問許可權
404 資源不存在
500 伺服器發生未知錯誤
503 伺服器暫時無法執行
2.響應頭(對響應的具體描述)
# 選項: 值
Accept: text/html,application/xhtml+xml,application/xml
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
Cookie: BAIDUID=8A4DA4339C1B8A74DD251F7D9F834C76:FG=1
Host: news.baidu.com
Referer: https://www.baidu.com/
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64)
3.空行
4.響應體(具體返回給客戶的內容): 檔案,圖片等
5.訪問百度請求資訊和應答資訊
1.描述資訊
General # 簡單的描述 Request URL: https://www.baidu.com/ # 請求網址 Request Method: GET # 請求方法 Status Code: 200 OK # 狀態程式碼 有資料 成功 Remote Address: 14.215.177.39:443 # 遠端地址:伺服器地址 Referrer Policy: no-referrer-when-downgrade # 表示從https協議降為http協議時不傳送referrer給跳轉網站的伺服器
2.應答資訊
Response Headers view source # 響應頭 源始頭 Bdpagetype: 1 # 百度伺服器定義的特定值 Bdqid: 0xedfaa2040000c90c # 百度伺服器定義的特定值 Cache-Control: private # 快取控制:私有 Connection: Keep-Alive # 連線:tpc socket的可靠連線 Content-Encoding: gzip # 伺服器通過這個頭告訴瀏覽器資料的壓縮格式 Content-Type: text/html # 伺服器通過這個頭告訴瀏覽器回送資料的型別 Cxy_all: baidu+d83b58345e15ab078f816c529e1cef79 Date: Sun, 14 Apr 2019 02:47:43 GMT # 時間 Expires: Sun, 14 Apr 2019 02:47:31 GMT # 到期時間 Server: BWS/1.1 # 伺服器通過這個頭告訴瀏覽器伺服器的型號 Set-Cookie: delPer=0; path=/; domain=.baidu.com Set-Cookie: BDSVRTM=0; path=/ Set-Cookie: BD_HOME=0; path=/ Set-Cookie: H_PS_PSSID=1427_28827_21091_28768_28722_28558_28832_28585_28604_28606; path=/; domain=.baidu.com """Set-Cookie 伺服器與客戶端驗證身份的資訊 其中path=/; domain=.baidu.com為域名判斷條件,如果滿足該條件,才可以向伺服器傳送cookie """ Strict-Transport-Security: max-age=172800 # 通知瀏覽器,這個網站禁止使用HTTP方式載入,瀏覽器應該自動把所有嘗試使用HTTP的請求自動替換為HTTPS請求 Transfer-Encoding: chunked # Transfer-Encoding,是一個 HTTP 頭部欄位(響應頭域),字面意思是傳輸編碼,最新的 HTTP規範裡,只定義了一種編碼傳輸:分塊編碼(chunked),資料分解成一系列資料塊,並以一個或多個塊傳送,這樣伺服器可以傳送資料而不需要預先知道傳送內容的總大小 Vary: Accept-Encoding # 告訴代理伺服器快取兩種版本的資源:壓縮和非壓縮 X-Ua-Compatible: IE=Edge,chrome=1 # 這是一個文件相容模式的定義,主要用於加強程式碼對IE的相容性,強制IE使用當前本地最新版標準模式渲染或者用chrome核心渲染 Response Headers view parsed # 響應頭 已解析源始頭 HTTP/1.1 200 OK # 使用HTTP協議進行傳輸的中協議版本號為1.1請求狀態碼為200 OK Bdpagetype: 1 # 百度伺服器定義的特定值 Bdqid: 0xfa9b2b6a00029507 # 百度伺服器定義的特定值 Cache-Control: private # 對資料進行快取 Connection: keep-alive # # 連線:tpc socket的可靠連線 Content-Encoding: gzip # 傳輸資源的檔案壓縮格式 Content-Type: text/html;charset=utf-8 # 傳輸資源的檔案型別和編碼型別 Date: Thu, 04 Jun 2020 08:56:33 GMT # 資源的傳送時間 Expires: Thu, 04 Jun 2020 08:56:29 GMT # 資源過期時間 Server: BWS/1.1 # 伺服器的系統版本,其中BWS為百度自有伺服器(Baidu Web Server) Set-Cookie: BDSVRTM=0; path=/ Set-Cookie: BD_HOME=1; path=/ Set-Cookie: H_PS_PSSID=31728_1460_31326_21078_31069_31762_31605_31322_30824; path=/; domain=.baidu.com """Set-Cookie 伺服器與客戶端驗證身份的資訊 其中path=/; domain=.baidu.com為域名判斷條件,如果滿足該條件,才可以向伺服器傳送cookie """ Strict-Transport-Security: max-age=172800 """Strict-Transport-Security: max-age=172800 使用https安全傳輸,防止在瀏覽器預設http請求和伺服器https資源之間進行轉換的時候,遭受中間人攻擊 瀏覽器收到該條響應欄位,會在max-age秒之內自動將http轉換成https的安全訪問形式,過期後會重複之前的步驟 """ Traceid: 1591260993276042138618058074865138832647 # 標記了瀏覽器發起的某個請求,這個id可在服務端從接收請求到 響應請求中流轉,甚至接力傳遞給下游應用中流轉,用於唯一標記和定外這次請求,一般用於定位請求日誌 X-Ua-Compatible: IE=Edge,chrome=1 # # 這是一個文件相容模式的定義,主要用於加強程式碼對IE的相容性,強制IE使用當前本地最新版標準模式渲染或者用chrome核心渲染 Transfer-Encoding: chunked # Transfer-Encoding,是一個 HTTP 頭部欄位(響應頭域),字面意思是傳輸編碼,最新的 HTTP規範裡,只定義了一種編碼傳輸:分塊編碼(chunked),資料分解成一系列資料塊,並以一個或多個塊傳送,這樣伺服器可以傳送資料而不需要預先知道傳送內容的總大小
3.請求資訊
Request Headers view source # 請求頭 源始頭 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3 """Accept 客戶端希望接受的檔案型別,後面的q=0.9(0<q<1)代表權重係數,越大表示越期待 """ Accept-Encoding: gzip, deflate, br # 設定接受的編碼格式 Accept-Language: zh-CN,zh;q=0.9 # 瀏覽器支援的語言為簡體中文和中文,優先支援簡體中文 Cache-Control: max-age=0 # 快取控制 Connection: keep-alive """Connection: keep-alive 該欄位可以承載三種不同型別的標籤: HTTP首部欄位名,列出了只與此連線有關的首部任意標籤值,用於描述此連線的非標準選項值(close)表示的是TCP連線是否關閉,HTTP資料傳輸採用的TCP三次握手協議 如果值為close,則該次傳輸資料結束關閉TCP連線,下次傳輸資料,要重新進行三次握手 如果值為預設的keep-alive,則保持資料通道,下次資料傳輸可以直接傳遞,省去三次握手的過程 一般我們只用值close來控制TCP連線狀態 """ Cookie: BIDUPSID=D97A5AC4617B3E6A2486D73D9F89F5AF; PSTM=1590403180; BAIDUID=D97A5AC4617B3E6AA36FE3B792FD9646:FG=1; BD_UPN=12314353; cflag=13%3A3; BD_HOME=1; H_PS_PSSID=31728_1460_31326_21078_31069_31762_31605_31322_30824; delPer=0; BD_CK_SAM=1; BDORZ=B490B5EBF6F3CD402E515D22BCDA1598; yjs_js_security_passport=64f53ded3515c88a93fa8fb6ab0e4dafa11418ec_1591253192_js; PSINO=5; COOKIE_SESSION=1708_0_9_9_24_19_0_3_9_7_4_6_1708_0_6_0_1591254838_0_1591254844%7C9%230_0_1590910191%7C1 Host: www.baidu.com # # 瀏覽器發給伺服器,宣告瀏覽器訪問哪臺主機 Upgrade-Insecure-Requests: 1 """Upgrade-Insecure-Requests: 1 該欄位用於http和https格式的過渡,一般如果請求http時,伺服器資源為https,或是相反,瀏覽器會提示或報錯 而Google瀏覽器使用該欄位向伺服器表明,瀏覽器本身可以將http轉換成https,伺服器可以隨意發,瀏覽器會自動將http://該成https:// """ User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36 """User-Agent 使用者Agent代理,即使用者發出HTTP請求的客戶端程式(例:瀏覽器Mozilla/5.0版本,Chrome/73.0.3683.86版本,spider爬蟲,Web robot等) """ Request Headers view parsed # 請求頭 已解析源始頭 GET / HTTP/1.1 # 請求方式 請求目錄 協議版本 Host: www.baidu.com # 瀏覽器發給伺服器,宣告瀏覽器訪問哪臺主機 Connection: keep-alive # 瀏覽器發給伺服器,宣告請求完後是斷開連結還是維持連結 Cache-Control: max-age=0 # 快取控制 Upgrade-Insecure-Requests: 1 # 瀏覽器發給伺服器,宣告自己支援這種操作,也就是我能讀懂你伺服器發過來的上面這條資訊,並且在以後發請求的時候不用http而用https User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36 # 瀏覽器發給伺服器,宣告支援的瀏覽器及版本 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3 # 瀏覽器發給伺服器,宣告瀏覽器所支援的資料型別 Accept-Encoding: gzip, deflate, br # 瀏覽器發給伺服器,宣告瀏覽器支援的編碼型別 Accept-Language: zh-CN,zh;q=0.9 # 瀏覽器發給伺服器,宣告瀏覽器支援的語言 Cookie: BAIDUID=BF236BEC6E4EF4DCF1AD838C2D217739:FG=1; PSTM=1555138860; BIDUPSID=88B1B87B29B6B36C8E48F551D3574BFE; BD_UPN=12314353; BDORZ=B490B5EBF6F3CD402E515D22BCDA1598; H_PS_PSSID=1427_28827_21091_28768_28722_28558_28832_28585_28604_28606; BDUSS=HU1UVhKRnNDMEhiYjlOVjNPQUhYfkxlZ2xTejV-c0Y0c2RzUVFSQUxtTVBOZHBjSUFBQUFBJCQAAAAAAAAAAAEAAABZiE1gQW5nZWxzxbe6wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA-oslwPqLJcW; delPer=0; BD_CK_SAM=1; PSINO=6; H_PS_645EC=c374KRUZaisqZePbVCOq52p9mRyTUyW7oNYJp%2Bbf2HRkfC7pCZ%2F5tPvURnM; ZD_ENTRY=baidu; BD_HOME=1
6.Web靜態伺服器-單任務
# html資料夾網盤連結: https://pan.baidu.com/s/1wbZ92KLstff3GurLV_C0lg 密碼: hp9o # 下載完命令列解壓後放到程式的同級路徑下,解壓命令: tar -zxvf 06_html.tar.gz -C ./ import socket import re def handle_request(client_socket): """為一個客戶端進行服務""" # 1.接收瀏覽器傳送過來的HTTP請求 recv_data = client_socket.recv(1024).decode('utf-8', errors="ignore") request_header_lines = recv_data.splitlines() for line in request_header_lines: print(line) # GET /index.html HTTP/1.1 http_request_line = request_header_lines[0] get_file_name = re.match("[^/]+(/[^ ]*)", http_request_line).group(1) print("file name is ===>%s" % get_file_name) # for test # 如果沒有指定訪問哪個頁面,則預設訪問index.html # GET / HTTP/1.1 if get_file_name == "/": get_file_name = DOCUMENTS_ROOT + "/index.html" else: get_file_name = DOCUMENTS_ROOT + get_file_name print("file name is ===2>%s" % get_file_name) # for test # 2.返回HTTP格式的資料給瀏覽器 try: f = open(get_file_name, "rb") except IOError: # 404表示沒有這個頁面 response_headers = "HTTP/1.1 404 not found\r\n" response_headers += "\r\n" response_body = "====sorry, file not found====".encode("utf-8") else: # 2.1 準備傳送給瀏覽器的資料-->header response_headers = "HTTP/1.1 200 OK\r\n" # 2.2 用一個空的行與body進行隔開 response_headers += "\r\n" # 2.3 準備傳送給瀏覽器的資料-->body response_body = f.read() f.close() finally: # 因為頭資訊在組織的時候,是按照字串組織的,不能與以二進位制開啟檔案讀取的資料合併,因此分開發送 # 先發送response的頭資訊 client_socket.send(response_headers.encode("utf-8")) # 再發送body client_socket.send(response_body) # 關閉套接字 client_socket.close() def main(): """作為程式的主控制入口""" # 1.建立資料流套接字 tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 2.設定當伺服器先close 即伺服器端4次揮手之後資源能夠立即釋放,保證下次執行程式時可以立即繫結7890埠 tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 3.繫結伺服器地址 tcp_server_socket.bind(("", 7890)) # 4.套接字由主動改為監聽,並設定佇列長度 tcp_server_socket.listen(128) while True: # 5.等待客戶端連線 client_socket, client_addr = tcp_server_socket.accept() # 6.為客戶端服務 handle_request(client_socket) # 7.關閉監聽套接字 tcp_server_socket.close() # 配置伺服器靜態資源路徑 DOCUMENTS_ROOT = "./html" if __name__ == "__main__": main()
7.Web靜態伺服器-多程序(multiprocessing)
# html資料夾網盤連結: https://pan.baidu.com/s/1wbZ92KLstff3GurLV_C0lg 密碼: hp9o # 下載完命令列解壓後放到程式的同級路徑下,解壓命令: tar -zxvf 06_html.tar.gz -C ./ import socket import multiprocessing import re class WSGIServer: """定義一個WSGI伺服器的類""" def __init__(self, server_address): # 1.建立資料流套接字 self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 2.設定當伺服器先close 即伺服器端4次揮手之後資源能夠立即釋放,保證下次執行程式時可以立即繫結7890埠 self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 3.繫結伺服器地址 self.tcp_server_socket.bind(server_address) # 4.套接字由主動改為監聽,並設定佇列長度 self.tcp_server_socket.listen(128) def serve_forever(self): """迴圈執行伺服器,等待客戶端連結併為客戶端服務""" while True: # 5.等待客戶端連結 client_socket, client_addr = self.tcp_server_socket.accept() # 6.為客戶端服務 new_process = multiprocessing.Process(target=self.handle_request, args=(client_socket,)) new_process.start() # 因為子程序已經複製了父程序的套接字等資源,所以父程序呼叫close不會將他們對應的這個連結關閉的 client_socket.close() def handle_request(self, client_socket): """用一個新的程序為客戶端服務""" # 1.接收瀏覽器傳送過來的HTTP請求 recv_data = client_socket.recv(1024).decode('utf-8', errors="ignore") request_header_lines = recv_data.splitlines() for line in request_header_lines: print(line) # GET /index.html HTTP/1.1 http_request_line = request_header_lines[0] get_file_name = re.match("[^/]+(/[^ ]*)", http_request_line).group(1) print("file name is ===>%s" % get_file_name) # for test # 如果沒有指定訪問哪個頁面,則預設訪問index.html # GET / HTTP/1.1 if get_file_name == "/": get_file_name = DOCUMENTS_ROOT + "/index.html" else: get_file_name = DOCUMENTS_ROOT + get_file_name print("file name is ===2>%s" % get_file_name) # for test # 2.返回HTTP格式的資料給瀏覽器 try: f = open(get_file_name, "rb") except IOError: # 404表示沒有這個頁面 response_headers = "HTTP/1.1 404 not found\r\n" response_headers += "\r\n" response_body = "====sorry, file not found====".encode("utf-8") else: # 2.1 準備傳送給瀏覽器的資料-->header response_headers = "HTTP/1.1 200 OK\r\n" # 2.2 用一個空的行與body進行隔開 response_headers += "\r\n" # 2.3 準備傳送給瀏覽器的資料-->body response_body = f.read() f.close() finally: # 因為頭資訊在組織的時候,是按照字串組織的,不能與以二進位制開啟檔案讀取的資料合併,因此分開發送 # 先發送response的頭資訊 client_socket.send(response_headers.encode("utf-8")) # 再發送body client_socket.send(response_body) # 關閉套接字 client_socket.close() # 設定伺服器的地址 SERVER_ADDR = ("", 7890) # 設定伺服器靜態資源的路徑 DOCUMENTS_ROOT = "./html" def main(): http_server = WSGIServer(SERVER_ADDR) print("web Server: Serving HTTP on port %d ...\n" % SERVER_ADDR[1]) http_server.serve_forever() if __name__ == "__main__": main()
8.Web靜態伺服器-多程序(os.fork)
# html資料夾網盤連結: https://pan.baidu.com/s/1wbZ92KLstff3GurLV_C0lg 密碼: hp9o # 下載完命令列解壓後放到程式的同級路徑下,解壓命令: tar -zxvf 06_html.tar.gz -C ./ import socket import os import signal import re class WSGIServer: """定義一個WSGI伺服器的類""" def __init__(self, server_address): # 1.建立資料流套接字 self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 2.設定當伺服器先close 即伺服器端4次揮手之後資源能夠立即釋放,保證下次執行程式時可以立即繫結7890埠 self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 3.繫結伺服器地址 self.tcp_server_socket.bind(server_address) # 4.套接字由主動改為監聽,並設定佇列長度 self.tcp_server_socket.listen(128) def serve_forever(self): """迴圈執行伺服器,等待客戶端連結併為客戶端服務""" print("父程序PID[%s]等待客戶端的連結" % os.getpid()) while True: try: # 5.等待客戶端連結 client_socket, client_addr = self.tcp_server_socket.accept() except KeyboardInterrupt: # 使用者中斷執行(通常是輸入^C) raise except Exception: continue # 6.處理子程序,避免成為殭屍程序-->SIGCHLD: 子程序改變狀態時,父程序會收到這個訊號;SIG_IGN: 忽略這個訊號 signal.signal(signal.SIGCHLD, signal.SIG_IGN) # 7.父程序為客戶端建立新的程序 pid = os.fork() if pid < 0: print("建立子程序失敗...") # 關閉客戶端套接字 client_socket.close() elif pid == 0: # 子程序中不需要使用監聽套接字,因此關閉子程序中的監聽套接字 self.tcp_server_socket.close() # 8.為客戶端服務 self.handle_request(client_socket) else: # 父程序中不需要使用客戶端套接字,因此關不父程序中的客戶端套接字 client_socket.close() continue def handle_request(self, client_socket): """用一個新的程序為客戶端服務""" # 1.接收瀏覽器傳送過來的HTTP請求 recv_data = client_socket.recv(1024).decode('utf-8', errors="ignore") request_header_lines = recv_data.splitlines() for line in request_header_lines: print(line) # GET /index.html HTTP/1.1 http_request_line = request_header_lines[0] get_file_name = re.match("[^/]+(/[^ ]*)", http_request_line).group(1) print("file name is ===>%s" % get_file_name) # for test # 如果沒有指定訪問哪個頁面,則預設訪問index.html # GET / HTTP/1.1 if get_file_name == "/": get_file_name = DOCUMENTS_ROOT + "/index.html" else: get_file_name = DOCUMENTS_ROOT + get_file_name print("file name is ===2>%s" % get_file_name) # for test # 2.返回HTTP格式的資料給瀏覽器 try: f = open(get_file_name, "rb") except IOError: # 404表示沒有這個頁面 response_headers = "HTTP/1.1 404 not found\r\n" response_headers += "\r\n" response_body = "====sorry, file not found====".encode("utf-8") else: # 2.1 準備傳送給瀏覽器的資料-->header response_headers = "HTTP/1.1 200 OK\r\n" # 2.2 用一個空的行與body進行隔開 response_headers += "\r\n" # 2.3 準備傳送給瀏覽器的資料-->body response_body = f.read() f.close() finally: # 因為頭資訊在組織的時候,是按照字串組織的,不能與以二進位制開啟檔案讀取的資料合併,因此分開發送 # 先發送response的頭資訊 client_socket.send(response_headers.encode("utf-8")) # 再發送body client_socket.send(response_body) # 關閉套接字 client_socket.close() # 設定伺服器的地址 SERVER_ADDR = ("", 7890) # 設定伺服器靜態資源的路徑 DOCUMENTS_ROOT = "./html" def main(): http_server = WSGIServer(SERVER_ADDR) print("web Server: Serving HTTP on port %d ...\n" % SERVER_ADDR[1]) http_server.serve_forever() if __name__ == "__main__": main()
9.Web靜態伺服器-多程序(整合模組 socketserver)
# html資料夾網盤連結: https://pan.baidu.com/s/1wbZ92KLstff3GurLV_C0lg 密碼: hp9o # 下載完命令列解壓後放到程式的同級路徑下,解壓命令: tar -zxvf 06_html.tar.gz -C ./ from socketserver import ForkingMixIn from socketserver import TCPServer from socketserver import StreamRequestHandler import re # class WSGIServer(ForkingTCPServer): # 寫法一 'ForkingTCPServer' = 'ForkingMixIn' + 'TCPServer' class WSGIServer(ForkingMixIn, TCPServer): # 寫法二 """定義一個建立伺服器類繼承自模組中的建立伺服器類""" pass class HandleRequest(StreamRequestHandler): """定義一個處理請求類繼承模組的處理請求類""" def handle(self): # 固定的入口方法 # 等待客戶端連線 self.request等同於accept()建立一個新的客戶端連線 client_socket = self.request client_addr = self.request.getpeername() # for test print("客戶端 %s 連線上伺服器" % str(client_addr)) # for test while True: # 1.接收瀏覽器傳送過來的HTTP請求 recv_data = client_socket.recv(1024).decode('utf-8', errors="ignore") request_header_lines = recv_data.splitlines() for line in request_header_lines: print(line) # GET /index.html HTTP/1.1 http_request_line = request_header_lines[0] get_file_name = re.match("[^/]+(/[^ ]*)", http_request_line).group(1) print("file name is ===>%s" % get_file_name) # for test # 如果沒有指定訪問哪個頁面,則預設訪問index.html # GET / HTTP/1.1 if get_file_name == "/": get_file_name = DOCUMENTS_ROOT + "/index.html" else: get_file_name = DOCUMENTS_ROOT + get_file_name print("file name is ===2>%s" % get_file_name) # for test # 2.返回HTTP格式的資料給瀏覽器 try: f = open(get_file_name, "rb") except IOError: # 404表示沒有這個頁面 response_body = "====sorry, file not found====".encode("utf-8") response_headers = "HTTP/1.1 404 not found\r\n" response_headers += "Content-Type: text/html; charset=utf-8\r\n" response_headers += "Content-Length: %d\r\n" % (len(response_body)) response_headers += "\r\n" else: # 2.1 準備傳送給瀏覽器的資料-->body response_body = f.read() f.close() # 2.2 準備傳送給瀏覽器的資料-->header response_headers = "HTTP/1.1 200 OK\r\n" # 2.3 傳送body資料長度,讓伺服器根據長度確認body資料已完成接收,實現長連線 response_headers += "Content-Length: %d\r\n" % (len(response_body)) # 2.4 用一個空的行與body進行隔開 response_headers += "\r\n" finally: # 因為頭資訊在組織的時候,是按照字串組織的,不能與以二進位制開啟檔案讀取的資料合併,因此分開發送 # 先發送response的頭資訊 client_socket.send(response_headers.encode("utf-8")) # 再發送body client_socket.send(response_body) # 長連線下不需要關閉套接字,而是在header頭中宣告要傳送的body長度 # client_socket.close() # 設定伺服器的地址 SERVER_ADDR = ("", 7890) # 設定伺服器靜態資源的路徑 DOCUMENTS_ROOT = "./html" def main(): http_server = WSGIServer(SERVER_ADDR, HandleRequest) print("web Server: Serving HTTP on port %d ...\n" % SERVER_ADDR[1]) http_server.serve_forever() if __name__ == "__main__": main()
10.Web靜態伺服器-多執行緒(threading)
# html資料夾網盤連結: https://pan.baidu.com/s/1wbZ92KLstff3GurLV_C0lg 密碼: hp9o # 下載完命令列解壓後放到程式的同級路徑下,解壓命令: tar -zxvf 06_html.tar.gz -C ./ import socket import threading import re class WSGIServer: """定義一個WSGI伺服器的類""" def __init__(self, server_address): # 1.建立資料流套接字 self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 2.設定當伺服器先close 即伺服器端4次揮手之後資源能夠立即釋放,保證下次執行程式時可以立即繫結7890埠 self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 3.繫結伺服器地址 self.tcp_server_socket.bind(server_address) # 4.套接字由主動改為監聽,並設定佇列長度 self.tcp_server_socket.listen(128) def serve_forever(self): """迴圈執行伺服器,等待客戶端連結併為客戶端服務""" while True: # 5.等待客戶端連結 client_socket, client_addr = self.tcp_server_socket.accept() # 6.為客戶端服務 new_thread = threading.Thread(target=self.handle_request, args=(client_socket,)) new_thread.start() # 因為執行緒是共享同一個套接字,所以主執行緒不能關閉,否則子執行緒就不能再使用這個套接字 # client_socket.close() def handle_request(self, client_socket): """用一個新的程序為客戶端服務""" # 1.接收瀏覽器傳送過來的HTTP請求 recv_data = client_socket.recv(1024).decode('utf-8', errors="ignore") request_header_lines = recv_data.splitlines() for line in request_header_lines: print(line) # GET /index.html HTTP/1.1 http_request_line = request_header_lines[0] get_file_name = re.match("[^/]+(/[^ ]*)", http_request_line).group(1) print("file name is ===>%s" % get_file_name) # 如果沒有指定訪問哪個頁面,則預設訪問index.html # GET / HTTP/1.1 if get_file_name == "/": get_file_name = DOCUMENTS_ROOT + "/index.html" else: get_file_name = DOCUMENTS_ROOT + get_file_name print("file name is ===2>%s" % get_file_name) # 2.返回HTTP格式的資料給瀏覽器 try: f = open(get_file_name, "rb") except IOError: # 404表示沒有這個頁面 response_headers = "HTTP/1.1 404 not found\r\n" response_headers += "\r\n" response_body = "====sorry, file not found====".encode("utf-8") else: # 2.1 準備傳送給瀏覽器的資料-->header response_headers = "HTTP/1.1 200 OK\r\n" # 2.2 用一個空的行與body進行隔開 response_headers += "\r\n" # 2.3 準備傳送給瀏覽器的資料-->body response_body = f.read() f.close() finally: # 因為頭資訊在組織的時候,是按照字串組織的,不能與以二進位制開啟檔案讀取的資料合併,因此分開發送 # 先發送response的頭資訊 client_socket.send(response_headers.encode("utf-8")) # 再發送body client_socket.send(response_body) # 關閉套接字 client_socket.close() # 設定伺服器的地址 SERVER_ADDR = ("", 7890) # 設定伺服器靜態資源的路徑 DOCUMENTS_ROOT = "./html" def main(): http_server = WSGIServer(SERVER_ADDR) print("web Server: Serving HTTP on port %d ...\n" % SERVER_ADDR[1]) http_server.serve_forever() if __name__ == "__main__": main()
11.Web靜態伺服器-多協程(gevent + monkey)
# html資料夾網盤連結: https://pan.baidu.com/s/1wbZ92KLstff3GurLV_C0lg 密碼: hp9o # 下載完命令列解壓後放到程式的同級路徑下,解壓命令: tar -zxvf 06_html.tar.gz -C ./ import socket import re import gevent from gevent import monkey monkey.patch_all() class WSGIServer: """定義一個WSGI伺服器的類""" def __init__(self, server_address): # 1.建立資料流套接字 self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 2.設定當伺服器先close 即伺服器端4次揮手之後資源能夠立即釋放,保證下次執行程式時可以立即繫結7890埠 self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 3.繫結伺服器地址 self.tcp_server_socket.bind(server_address) # 4.套接字由主動改為監聽,並設定佇列長度 self.tcp_server_socket.listen(128) def serve_forever(self): """迴圈執行伺服器,等待客戶端連結併為客戶端服務""" while True: # 5.等待客戶端連結 client_socket, client_addr = self.tcp_server_socket.accept() # 6.為客戶端服務 gevent.spawn(self.handle_request, client_socket) def handle_request(self, client_socket): """用協程為客戶端服務""" # 1.接收瀏覽器傳送過來的HTTP請求 recv_data = client_socket.recv(1024).decode('utf-8', errors="ignore") request_header_lines = recv_data.splitlines() for line in request_header_lines: print(line) # GET /index.html HTTP/1.1 http_request_line = request_header_lines[0] get_file_name = re.match("[^/]+(/[^ ]*)", http_request_line).group(1) print("file name is ===>%s" % get_file_name) # 如果沒有指定訪問哪個頁面,則預設訪問index.html # GET / HTTP/1.1 if get_file_name == "/": get_file_name = DOCUMENTS_ROOT + "/index.html" else: get_file_name = DOCUMENTS_ROOT + get_file_name print("file name is ===2>%s" % get_file_name) # 2.返回HTTP格式的資料給瀏覽器 try: f = open(get_file_name, "rb") except IOError: # 404表示沒有這個頁面 response_headers = "HTTP/1.1 404 not found\r\n" response_headers += "\r\n" response_body = "====sorry, file not found====".encode("utf-8") else: # 2.1 準備傳送給瀏覽器的資料-->header response_headers = "HTTP/1.1 200 OK\r\n" # 2.2 用一個空的行與body進行隔開 response_headers += "\r\n" # 2.3 準備傳送給瀏覽器的資料-->body response_body = f.read() f.close() finally: # 因為頭資訊在組織的時候,是按照字串組織的,不能與以二進位制開啟檔案讀取的資料合併,因此分開發送 # 先發送response的頭資訊 client_socket.send(response_headers.encode("utf-8")) # 再發送body client_socket.send(response_body) # 關閉套接字 client_socket.close() # 設定伺服器的地址 SERVER_ADDR = ("", 7890) # 設定伺服器靜態資源的路徑 DOCUMENTS_ROOT = "./html" def main(): http_server = WSGIServer(SERVER_ADDR) print("web Server: Serving HTTP on port %d ...\n" % SERVER_ADDR[1]) http_server.serve_forever() if __name__ == "__main__": main()
12.Web靜態伺服器-非阻塞IO(基於非阻塞狀態的單程序單執行緒長連線)
# html資料夾網盤連結: https://pan.baidu.com/s/1wbZ92KLstff3GurLV_C0lg 密碼: hp9o # 下載完命令列解壓後放到程式的同級路徑下,解壓命令: tar -zxvf 06_html.tar.gz -C ./ import sys import time import socket import re class WSGIServer: """定義一個WSGI伺服器的類""" def __init__(self, server_addr): # 1.建立資料流套接字 self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 2.設定當伺服器先close 即伺服器端4次揮手之後資源能夠立即釋放,保證下次執行程式時可以立即繫結7890埠 self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 3.繫結伺服器地址 self.tcp_server_socket.bind(server_addr) # 4.套接字由主動改為監聽,並設定佇列長度 self.tcp_server_socket.listen(128) # 5.設定為非堵塞後,如果accept時,恰巧沒有客戶端connect,那麼accept會產生一個異常,所以需要try來進行處理 self.tcp_server_socket.setblocking(False) # 將監聽套接字設定為非堵塞 self.client_socket_list = list() # 建立客戶端client_socket輪詢列表 def serve_forever(self): """迴圈執行伺服器,等待客戶端連結併為客戶端服務""" while True: time.sleep(1) # for test try: # 6.等待客戶端連結 client_socket, client_addr = self.tcp_server_socket.accept() except Exception as ret: print("--->1<---沒有客戶端到來: ", ret) # for test else: # 7.當有客戶端連線時,將客戶端的套接字加入到輪詢列表 client_socket.setblocking(False) # 將客戶端套接字設定為非堵塞 self.client_socket_list.append(client_socket) # 8.遍歷列表看是否有收到客戶端套接字傳送的訊息,如果收到訊息則為客戶端服務 for client_socket in self.client_socket_list: try: # 1.接收瀏覽器傳送過來的HTTP請求 request = client_socket.recv(1024).decode("utf-8", errors="ignore") except Exception as ret: print("--->2<---這個客戶端沒有傳送過來請求: ", ret) # for test else: if request: # 2.1 為這個客戶端服務 self.handle_request(request, client_socket) else: # 2.2 客戶端呼叫了close導致recv返回,服務端將關閉客戶端套接字並從輪詢列表中刪除 client_socket.close() self.client_socket_list.remove(client_socket) print(self.client_socket_list) def handle_request(self, request, client_socket): """為客戶端服務""" # 1.提取客戶端傳送過來的資料 request_header_lines = request.splitlines() for i, line in enumerate(request_header_lines): print(i, line) # GET /index.html HTTP/1.1 http_request_line = request_header_lines[0] ret = re.match(r"([^/]*)([^ ]+)", http_request_line) if ret: print("正則提取資料:", ret.group(1)) print("正則提取資料:", ret.group(2)) get_file_name = ret.group(2) # 如果沒有指定訪問哪個頁面,則預設訪問index.html # GET / HTTP/1.1 if get_file_name == "/": get_file_name = DOCUMENTS_ROOT + "/index.html" else: get_file_name = DOCUMENTS_ROOT + get_file_name print("要訪問的完整路徑是: %s" % get_file_name) # 2.返回HTTP格式的資料給瀏覽器 try: f = open(get_file_name, "rb") except IOError: # 404表示沒有這個頁面 response_body = "====sorry, file not found====".encode("utf-8") response_headers = "HTTP/1.1 404 not found\r\n" response_headers += "Content-Type: text/html; charset=utf-8\r\n" response_headers += "Content-Length: %d\r\n" % (len(response_body)) response_headers += "\r\n" else: # 2.1 準備傳送給瀏覽器的資料-->body response_body = f.read() f.close() # 2.2 準備傳送給瀏覽器的資料-->header response_headers = "HTTP/1.1 200 OK\r\n" # 2.3 傳送body資料長度,讓伺服器根據長度確認body資料已完成接收,實現長連線 response_headers += "Content-Length: %d\r\n" % (len(response_body)) # 2.4 用一個空的行與body進行隔開 response_headers += "\r\n" finally: # 因為頭資訊在組織的時候,是按照字串組織的,不能與以二進位制開啟檔案讀取的資料合併,因此分開發送 # 先發送response的頭資訊 client_socket.send(response_headers.encode("utf-8")) # 再發送body client_socket.send(response_body) # 長連線下不需要關閉套接字,而是在header頭中宣告要傳送的body長度 # client_socket.close() # 設定伺服器靜態資源的路徑 DOCUMENTS_ROOT = "./html" def main(): # python3 xxx.py 7890 if len(sys.argv) == 2: port = sys.argv[1] if port.isdigit(): port = int(port) else: print("執行方式如: python3 xxx.py 7890") return server_addr = ("", port) http_server = WSGIServer(server_addr) print("web Server: Serving HTTP on port %d ...\n" % port) http_server.serve_forever() if __name__ == "__main__": main()
13.Web靜態伺服器-IO多路複用(基於epoll的單程序單執行緒長連線高併發)
# html資料夾網盤連結: https://pan.baidu.com/s/1wbZ92KLstff3GurLV_C0lg 密碼: hp9o # 下載完命令列解壓後放到程式的同級路徑下,解壓命令: tar -zxvf 06_html.tar.gz -C ./ import sys import socket import re import select class WsgiServer: """定義一個WSGI伺服器的類""" def __init__(self, server_addr): # 1.建立資料流套接字 self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 2.設定當伺服器先close 即伺服器端4次揮手之後資源能夠立即釋放,保證下次執行程式時可以立即繫結7890埠 self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 3.繫結伺服器地址 self.tcp_server_socket.bind(server_addr) # 4.套接字由主動改為監聽,並設定佇列長度 self.tcp_server_socket.listen(128) # 5.建立epoll物件 self.epoll = select.epoll() # 6.將tcp伺服器的監聽套接對應的fd註冊到epoll中進行監聽,如果fd已經註冊過,則會發生異常 self.epoll.register(self.tcp_server_socket.fileno(), select.EPOLLIN | select.EPOLLET) # 7.建立新增的fd對應的套接字 self.fd_socket = dict() def serve_forever(self): """迴圈執行伺服器,等待客戶端連結併為客戶端服務""" # 等待新的客戶端連線或者已連線的客戶端傳送過來資料 while True: # epoll進行fd掃描的地方,未指定超時時間則為阻塞等待,直到系統檢測到資料到來,通過事件通知方式告訴程式才會解阻塞 epoll_list = self.epoll.poll() # epoll_list列表結構: [(fd, event),] # fd: 套接字對應的檔案描述符 # event: 這個檔案描述符到底是什麼事件,例如可以呼叫recv接收等 # 對事件進行判斷 for fd, event in epoll_list: # 如果是伺服器的監聽套接字被啟用可以收資料,那麼意味著可以進行accept,即有新的客戶端連線 if fd == self.tcp_server_socket.fileno(): new_socket, new_addr = self.tcp_server_socket.accept() # 向epoll中註冊連線socket的可讀事件,EPOLLIN(可讀),EPOLLOUT(可寫),EPOLLET(ET模式) # LT模式: 當epoll檢測到描述符事件發生並將此事件通知應用程式 # LT模式下應用程式可以不立即處理該事件,下次呼叫epoll時,會再次響應應用程式並通知此事件 # ET模式: 當epoll檢測到描述符事件發生並將此事件通知應用程式 # LT模式下應用程式必須立即處理該事件,如果不處理,下次呼叫epoll時,不會再次響應應用程式並通知此事件 self.epoll.register(new_socket.fileno(), select.EPOLLIN | select.EPOLLET) # 記錄這個資訊 self.fd_socket[new_socket.fileno()] = new_socket # 如果是客戶端傳送資料,客戶端套接字將被啟用,伺服器可以接收到客服端傳送的資料 elif event == select.EPOLLIN: # # 從啟用 fd 上接收 request = self.fd_socket[fd].recv(1024).decode("utf-8") if request: self.handle_request(request, self.fd_socket[fd]) else: # 在epoll中登出客戶端的資訊 self.epoll.unregister(fd) # 關閉客戶端的檔案控制代碼 self.fd_socket[fd].close() # 在字典中刪除與已關閉客戶端相關的資訊 del self.fd_socket[fd] def handle_request(self, request, client_socket): """為客戶端服務""" # 1.接收瀏覽器傳送過來的HTTP請求 request_header_lines = request.splitlines() for i, line in enumerate(request_header_lines): print(i, line) # GET /index.html HTTP/1.1 http_request_line = request_header_lines[0] ret = re.match(r"([^/]*)([^ ]+)", http_request_line) if ret: print("正則提取資料:", ret.group(1)) print("正則提取資料:", ret.group(2)) get_file_name = ret.group(2) # 如果沒有指定訪問哪個頁面,則預設訪問index.html # GET / HTTP/1.1 if get_file_name == "/": get_file_name = DOCUMENTS_ROOT + "/index.html" else: get_file_name = DOCUMENTS_ROOT + get_file_name print("要訪問的完整路徑是: %s" % get_file_name) # 2.返回HTTP格式的資料給瀏覽器 try: f = open(get_file_name, "rb") except IOError: # 404表示沒有這個頁面 response_body = "====sorry, file not found====".encode("utf-8") response_headers = "HTTP/1.1 404 not found\r\n" response_headers += "Content-Type: text/html; charset=utf-8\r\n" response_headers += "Content-Length: %d\r\n" % (len(response_body)) response_headers += "\r\n" else: # 2.1 準備傳送給瀏覽器的資料-->body response_body = f.read() f.close() # 2.2 準備傳送給瀏覽器的資料-->header response_headers = "HTTP/1.1 200 OK\r\n" # 2.3 傳送body資料長度,讓伺服器根據長度確認body資料已完成接收,實現長連線 response_headers += "Content-Length: %d\r\n" % (len(response_body)) # 2.4 用一個空的行與body進行隔開 response_headers += "\r\n" finally: # 因為頭資訊在組織的時候,是按照字串組織的,不能與以二進位制開啟檔案讀取的資料合併,因此分開發送 # 先發送response的頭資訊 client_socket.send(response_headers.encode("utf-8")) # 再發送body client_socket.send(response_body) # 長連線下不需要關閉套接字,而是在header頭中宣告要傳送的body長度 # client_socket.close() # 設定伺服器靜態資源的路徑 DOCUMENTS_ROOT = "./html" def main(): # python3 xxx.py 7890 if len(sys.argv) == 2: port = sys.argv[1] if port.isdigit(): port = int(port) else: print("執行方式如: python3 xxx.py 7890") return server_addr = ("", port) http_server = WsgiServer(server_addr) print("web Server: Serving HTTP on port %d ...\n" % port) http_server.serve_forever() if __name__ == "__main__": main()
14.C10K問題(單機1萬網路併發連線和資料處理能力)
1.C10K問題的本質:
C10K問題本質上是作業系統的問題,對於Web1.0/2.0時代的作業系統而言, 傳統的同步阻塞I/O模型都是一樣的
處理的方式都是requests per second併發10K和100的區別關鍵在於CPU
建立的程序執行緒多了,資料拷貝頻繁(快取I/O,核心將資料拷貝到使用者程序空間,阻塞)
程序/執行緒上下文切換消耗大,導致作業系統崩潰,這就是C10K問題的本質
2.C10K解決方案: 每個程序/執行緒同時處理多個連線(IO多路複用)
1.儘量避免伺服器處理超過1萬個的併發連線
2.通過改進作業系統核心以及用事件驅動伺服器(典型技術實現如:Nginx和Node)代替執行緒伺服器(典型代表:Apache)
3.epoll能達到10k併發,但是epoll有Linux,Unix平臺限制
3.使用libevent庫開發,libevent庫是對/dev/poll, kqueue, event ports, select, poll和epoll介面的封裝
3.檔案控制代碼限制
1.問題概述:
在linux下編寫網路伺服器程式的朋友肯定都知道每一個tcp連線都要佔一個檔案描述符
一旦這個檔案描述符使用完了,新的連線到來返回給我們的錯誤是"Socket/File:Can't open so many files"
這是因為作業系統對可以開啟的最大檔案數的限制
2.程序限制
執行 ulimit -n 輸出 1024, 說明對於一個程序而言最多隻能開啟1024個檔案
要採用此預設配置最多也就可以併發上千個TCP連線,臨時修改: ulimit -n 1000000
但是這種臨時修改只對當前登入使用者目前的使用環境有效,系統重啟或使用者退出後就會失效
重啟後失效的修改,編輯 /etc/security/limits.conf 檔案修改後內容為
soft nofile 1000000
hard nofile 1000000
永久修改: 編輯/etc/rc.local 在其後新增如下內容
ulimit -SHn 1000000
3.全侷限制
執行 cat /proc/sys/fs/file-nr
輸出 3040 0 194572
輸出釋義: 已經分配的檔案控制代碼數, 已經分配但沒有使用的檔案控制代碼數, 最大檔案控制代碼數
但在kernel 2.6版本中第二項的值總為0,這並不是一個錯誤,它實際上是已經分配的檔案描述符無一浪費的都已經被使用了
我們可以把這個數值改大些,用 root 許可權修改 /etc/sysctl.conf 檔案
fs.file-max = 1000000
net.ipv4.ip_conntrack_max = 1000000
net.ipv4.netfilter.ip_conntrack_max = 1000000
4.埠號範圍限制
問題誤區:
作業系統上埠號1024以下是系統保留的,從1024-65535是使用者使用的
由於每個TCP連線都要佔一個埠號,所以我們最多可以有60000多個併發連線
如何標識一個TCP連線
系統用一個4四元組來唯一標識一個TCP連線: {local ip, local port,remote ip,remote port}
《UNIX網路程式設計: 卷一》第四章中對accept的講解來看看概念性的東西,第二個引數cliaddr代表了客戶端的ip地址和埠號
而我們作為服務端實際只使用了bind時這一個埠,說明埠號65535並不是併發量的限制
server最大tcp連線數
server通常固定在某個本地埠上監聽,等待client的連線請求
不考慮地址重用(unix的SO_REUSEADDR選項)的情況下,即使server端有多個ip,本地監聽埠也是獨佔的
因此server端tcp連線4元組中只有remote ip(也就是client ip)和remote port(客戶端port)是可變的
得出最大tcp連線為客戶端ip數×客戶端port數
對IPV4,不考慮ip地址分類等因素,最大tcp連線數約為2的32次方(ip數) × 2的16次方(port數)
得出server端單機最大tcp連線數約為2的48次方
5.檔案控制代碼限制和埠範圍限制小結
上述得出的是理論上的單機TCP併發連線數,實際上單機併發連線數要受硬體資源(記憶體),網路資源(頻寬)的限制
6.C10K問題參考自如下連結:
http://www.52im.net/thread-561-1-1.html
http://www.52im.net/thread-566-1-1.html
15.C10M問題(單機1000萬網路併發連線和資料處理能力)
1.C10M問題的本質:
1.硬體上完全可以處理1000萬個以上的併發連線,如果它們不能,那是因為你選擇了錯誤的軟體,而不是底層硬體的問題
2.OS的核心不是解決C10M問題的辦法,恰恰相反OS的核心正是導致C10M問題的關鍵所在
3.不要讓OS核心執行所有繁重的任務:
將資料包處理,記憶體管理,處理器排程等任務從核心轉移到應用程式高效地完成
讓諸如Linux這樣的OS只處理控制層,資料層完全交給應用程式來處理
2.C10M問解決方案:
1.CPU親和性 & 記憶體局域性
2.RSS, RPS, RFS, XPS
3.IRQ 優化
4.Kernel 優化
5. CPU 記憶體 網路
3.C10M問題參考自如下連結:
http://www.52im.net/thread-568-1-1.html
http://www.52im.net/thread-578-1-1.html