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占有高的程式
優點: 開銷小,比較適合IO密集型的服務端程式
3.并發服務器模型
模式:
每有一個客戶端鏈接請求就創建一個新的行程或執行緒處理客戶端的請求
而主行程或主執行緒可以繼續接收其它客戶端的連接
缺點: 資源消耗比較大,客戶端需要長期占有服務器的情況
優點: 多任務,高并發
2.并發服務器-多行程(fork, multiprocessing)
1.創建套接字,系結,監聽
2.接收客戶端請求
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 = https://www.cnblogs.com/tangxuecheng/p/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 = https://www.cnblogs.com/tangxuecheng/p/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 = https://www.cnblogs.com/tangxuecheng/p/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 = https://www.cnblogs.com/tangxuecheng/p/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 = https://www.cnblogs.com/tangxuecheng/p/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 = https://www.cnblogs.com/tangxuecheng/p/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
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/5082.html
標籤:Python
上一篇:Python爬取各大汽車銷量資訊
下一篇:u"Field 'download_file_content' doesn't have a default value"錯誤解決
