了解http協議
http請求頭
GET / HTTP/1.1
Host: www.baidu.com
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Sec-Fetch-Site: none
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
最主要的頭兩行分析如下:
- GET表示一個讀取請求,將從服務器獲得網頁數據,/表示URL的路徑,URL總是以/開頭,/就表示首頁,最后的HTTP/1.1指示采用的HTTP協議版本是1.1。
- 目前HTTP協議的版本就是1.1,但是大部分服務器也支持1.0版本,主要區別在于1.1版本允許多個HTTP請求復用一個TCP連接,以加快傳輸速度。
- Host: www.baidu.com表示請求的域名是www.baidu.com。如果一臺服務器有多個網站,服務器就需要通過Host來區分瀏覽器請求的是哪個網站。
http響應頭
HTTP/1.1 200 OK
Bdpagetype: 2
Bdqid: 0x8ef7ae5901149cf7
Cache-Control: private
Connection: Keep-Alive
Content-Encoding: gzip
Content-Type: text/html;charset=utf-8
Date: Wed, 28 Aug 2019 01:59:49 GMT
Expires: Wed, 28 Aug 2019 01:59:48 GMT
Server: BWS/1.1
Set-Cookie: BDSVRTM=249; path=/
Set-Cookie: BD_HOME=1; path=/
Set-Cookie: H_PS_PSSID=1426_21111_20697_29522_29518_29099_29568_29220_26350; path=/; domain=.baidu.com
Strict-Transport-Security: max-age=172800
X-Ua-Compatible: IE=Edge,chrome=1
Transfer-Encoding: chunked
說明:
- 200表示一個成功的響應,后面的OK是說明。
- Content-Type指示響應的內容,這里是text/html表示HTML網頁。
- 請求頭和響應頭通過\r\n來換行。
- 響應頭和body響應體中也通過\r\n來分隔。
簡單的http服務器
有多簡單呢?運行程序后打開瀏覽器,只能顯示hello world。
import socket
def service_client(new_socket):
# 接受瀏覽器發過來的http請求
# GET / HTTP/1.1
request = new_socket.recv(1024)
print(request)
# 返回http響應
resposne = "HTTP/1.1 200 OK\r\n"
resposne += "\r\n"
resposne += "
hello world
"
new_socket.send(resposne.encode("utf-8"))
# 關閉套接字
new_socket.close()
def main():
# 創建套接字
http_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 防止端口被占用無法啟動程序
http_server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
# 綁定端口
http_server.bind(("", 80))
# 變為監聽套接字
http_server.listen(128)
while True:
# 等在新客戶端連接
client, info = http_server.accept()
# 為這個客戶端服務
service_client(client)
if __name__ == "__main__":
main()
單進程http服務器
它上之前有一個升級就是可以返回靜態的html頁面。
import socket
import re
def service_client(new_socket):
# 接受瀏覽器發過來的http請求
# GET / HTTP/1.1
request = new_socket.recv(1024).decode("utf-8")
# print(request)
request_lines = request.splitlines()
req = re.match(r"[^/]+(/\S*)", request_lines[0])
file_name: str = ""
if req:
file_name = req.group(1)
if file_name == "/":
file_name = "/index.html"
print(file_name)
# print(request_lines)
# 返回http響應
try:
# 打開要請求的html文件,并返回給客戶端。網頁在當前路徑的html文件夾里面。
with open("./html" + file_name, "r", encoding="utf-8") as f:
resposne = "HTTP/1.1 200 OK\r\n"
resposne += "\r\n"
# resposne += "
hello world
"
resposne += f.read()
except Exception as e:
resposne = "HTTP/1.1 400 NOT FOUND\r\n"
resposne += "\r\n"
resposne += "--file not found--"
new_socket.send(resposne.encode("utf-8"))
# 關閉套接字
new_socket.close()
def main():
# 創建套接字
http_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 防止端口被占用無法啟動程序
http_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 綁定端口
http_server.bind(("", 80))
# 變為監聽套接字
http_server.listen(128)
while True:
# 等在新客戶端連接
client, info = http_server.accept()
# 為這個客戶端服務
service_client(client)
if __name__ == "__main__":
main()
多進程服務器
http服務器是完成了,但是如果同時有好多人訪問的話它反應就會非常慢,所有又在之前的基礎上做了升級,增加服務器的并發能力。
import socket
import re
from multiprocessing import Process
def service_client(new_socket):
# 接受瀏覽器發過來的http請求
# GET / HTTP/1.1
request = new_socket.recv(1024).decode("utf-8")
# print(request)
request_lines = request.splitlines()
req = re.match(r"[^/]+(/\S*)", request_lines[0])
file_name: str = ""
if req:
file_name = req.group(1)
if file_name == "/":
file_name = "/index.html"
print(file_name)
# print(request_lines)
# 返回http響應
try:
with open("./html" + file_name, "r", encoding="utf-8") as f:
resposne = "HTTP/1.1 200 OK\r\n"
resposne += "\r\n"
# resposne += "
hello world
"
resposne += f.read()
except Exception as e:
resposne = "HTTP/1.1 400 NOT FOUND\r\n"
resposne += "\r\n"
resposne += "--file not found--"
new_socket.send(resposne.encode("utf-8"))
# new_socket.send(body)
# 關閉套接字
new_socket.close()
def main():
# 創建套接字
http_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 防止端口被占用無法啟動程序
http_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 綁定端口
http_server.bind(("", 80))
# 變為監聽套接字
http_server.listen(128)
while True:
# 等在新客戶端連接
client, info = http_server.accept()
# 開啟一個子進程為這個客戶端服務
p = Process(target=service_client,args=(client,))
p.start()
# 子進程會復制主進程發的資源,故把主進程的socket關閉。
client.close()
if __name__ == "__main__":
main()
多線程服務器
我們知道進程耗費資源是非常大的,所以這次使用了耗費資源小的線程來實現多任務。
import socket
import re
from threading import Thread
def service_client(new_socket):
# 接受瀏覽器發過來的http請求
# GET / HTTP/1.1
request = new_socket.recv(1024).decode("utf-8")
# print(request)
request_lines = request.splitlines()
req = re.match(r"[^/]+(/\S*)", request_lines[0])
file_name: str = ""
if req:
file_name = req.group(1)
if file_name == "/":
file_name = "/index.html"
print(file_name)
# print(request_lines)
# 返回http響應
try:
with open("./html" + file_name, "r", encoding="utf-8") as f:
resposne = "HTTP/1.1 200 OK\r\n"
resposne += "\r\n"
# resposne += "
hello world
"
resposne += f.read()
except Exception as e:
resposne = "HTTP/1.1 400 NOT FOUND\r\n"
resposne += "\r\n"
resposne += "--file not found--"
new_socket.send(resposne.encode("utf-8"))
# new_socket.send(body)
# 關閉套接字
new_socket.close()
def main():
# 創建套接字
http_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 防止端口被占用無法啟動程序
http_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 綁定端口
http_server.bind(("", 80))
# 變為監聽套接字
http_server.listen(128)
while True:
# 等在新客戶端連接
client, info = http_server.accept()
# 開啟一個子線程為這個客戶端服務
p = Thread(target=service_client, args=(client,))
p.start()
if __name__ == "__main__":
main()
gevent協程版的服務器
協程在一個線程中執行,減少了線程之間的切換,多線程的升級版,擁有更好的處理能力。
import socket
import re
import gevent
from gevent import monkey
monkey.patch_all()
def service_client(new_socket):
# 接受瀏覽器發過來的http請求
# GET / HTTP/1.1
request = new_socket.recv(1024).decode("utf-8")
# print(request)
request_lines = request.splitlines()
req = re.match(r"[^/]+(/\S*)", request_lines[0])
file_name: str = ""
if req:
file_name = req.group(1)
if file_name == "/":
file_name = "/index.html"
print(file_name)
# print(request_lines)
# 返回http響應
try:
with open("./html" + file_name, "r", encoding="utf-8") as f:
resposne = "HTTP/1.1 200 OK\r\n"
resposne += "\r\n"
# resposne += "
hello world
"
resposne += f.read()
except Exception as e:
resposne = "HTTP/1.1 400 NOT FOUND\r\n"
resposne += "\r\n"
resposne += "--file not found--"
new_socket.send(resposne.encode("utf-8"))
# new_socket.send(body)
# 關閉套接字
new_socket.close()
def main():
# 創建套接字
http_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 防止端口被占用無法啟動程序
http_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 綁定端口
http_server.bind(("", 80))
# 變為監聽套接字
http_server.listen(128)
while True:
# 等在新客戶端連接
client, info = http_server.accept()
# 為這個客戶端服務
gevent.spawn(service_client, client)
if __name__ == "__main__":
main()
單進程非堵塞版長連接的服務器
從這個例子中來引入epoll版,它的性能應該要比協程的好,與之前所有服務器的不同之處就是采用了長連接,通過響應頭中的Content-Length來指定響應體的長度,從而讓瀏覽器知道頁面數據傳輸完成以后自動在同一個套接字連接中繼續發送其他資源文件的請求,效率較高。之前的都是短連接,只要傳輸完當前文件就關閉這個套接字。
import socket
import re
def service_client(new_socket: object, request: str):
# 接受瀏覽器發過來的http請求
# GET / HTTP/1.1
# request = new_socket.recv(1024).decode("utf-8")
# print(request)
request_lines = request.splitlines()
req = re.match(r"[^/]+(/\S*)", request_lines[0])
file_name: str = ""
if req:
file_name = req.group(1)
if file_name == "/":
file_name = "/index.html"
print(file_name)
# print(request_lines)
# 返回http響應
try:
with open("./html" + file_name, "r", encoding="utf-8") as f:
resposne_body: str = f.read()
resposne_header: str = "HTTP/1.1 200 OK\r\n"
resposne_header += "Content-Length:%d\r\n" % len(resposne_body)
resposne_header += "\r\n"
# resposne += "
hello world
"
resposne = resposne_header + resposne_body
except Exception as e:
resposne = "HTTP/1.1 400 NOT FOUND\r\n"
resposne += "\r\n"
resposne += "--file not found--"
new_socket.send(resposne.encode("utf-8"))
# new_socket.send(body)
# 關閉套接字
# new_socket.close()
def main():
# 創建套接字
http_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 防止端口被占用無法啟動程序
http_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 綁定端口
http_server.bind(("", 80))
# 變為監聽套接字
http_server.listen(128)
# 設置套接字為非堵塞方式
http_server.setblocking(False)
socket_list: list = []
while True:
try:
# 等在新客戶端連接
client, info = http_server.accept()
# 為這個客戶端服務
# gevent.spawn(service_client, client)
except Exception as e:
# print(e)
pass
else:
client.setblocking(False)
socket_list.append(client)
for socket_client in socket_list:
try:
recv_data: str = socket_client.recv(1024).decode("utf-8")
except Exception as e:
# print(e)
pass
else:
if recv_data:
service_client(socket_client, recv_data)
else:
socket_list.remove(socket_client)
socket_client.close()
if __name__ == "__main__":
main()
eopll版的服務器
這個版本是性能最高的服務器。它的基本原理就是select,poll,epoll這個function會不斷的輪詢所負責的所有socket,當某個socket有數據到達了,就通知用戶進程。select/epoll的好處就在于單個process就可以同時處理多個網絡連接的IO。
import socket
import re
import select
def service_client(new_socket: object, request: str):
# 接受瀏覽器發過來的http請求
# GET / HTTP/1.1
# request = new_socket.recv(1024).decode("utf-8")
# print(request)
request_lines = request.splitlines()
req = re.match(r"[^/]+(/\S*)", request_lines[0])
file_name: str = ""
if req:
file_name = req.group(1)
if file_name == "/":
file_name = "/index.html"
print(file_name)
# print(request_lines)
# 返回http響應
try:
with open("./html" + file_name, "r", encoding="utf-8") as f:
resposne_body: str = f.read()
resposne_header: str = "HTTP/1.1 200 OK\r\n"
resposne_header += "Content-Length:%d\r\n" % len(resposne_body)
resposne_header += "\r\n"
# resposne += "
hello world
"
resposne = resposne_header + resposne_body
except Exception as e:
resposne = "HTTP/1.1 400 NOT FOUND\r\n"
resposne += "\r\n"
resposne += "--file not found--"
new_socket.send(resposne.encode("utf-8"))
# new_socket.send(body)
# 關閉套接字
# new_socket.close()
def main():
# 創建套接字
http_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 防止端口被占用無法啟動程序
http_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 綁定端口
http_server.bind(("", 80))
# 變為監聽套接字
http_server.listen(128)
# 設置套接字為非堵塞方式
http_server.setblocking(False)
# 創建一個epoll對象
epl = select.epoll()
# 將監聽套接字對應的fd(文件描述符)注冊到epoll中
epl.register(http_server.fileno(), select.EPOLLIN)
# 存儲fd文件描述符和套接字的對應關系
fd_event_dict: dict = {}
while True:
# 默認會堵塞,知道os檢測到數據到來,通過事件通知方式告訴這個程序,此時才會解堵塞
fd_event_list: list = epl.poll() # [(套接字對應的文件描述符,這個文件描述符是什么事件),...]
for fd, event in fd_event_list:
# 如果是監聽套接字有數據過來,即等待新的客戶端連接
if fd == http_server.fileno():
client, info = http_server.accept()
# 將新的套接字注冊到epoll中
epl.register(client.fileno(), select.EPOLLIN)
# 把文件描述符和套接字的對應關系存入字典
fd_event_dict[client.fileno()] = client
elif event == select.EPOLLIN:
# 判斷已連接的套接字是否有數據發過來
recv_data: str = fd_event_dict[fd].recv(1024).decode("utf-8")
if recv_data:
service_client(fd_event_dict[fd], recv_data)
else:
fd_event_dict[fd].close()
epl.unregister(fd)
del fd_event_dict[fd]
if __name__ == '__main__':
main()
I/O 多路復用的特點:
通過一種機制使一個進程能同時等待多個文件描述符,而這些文件描述符(套接字描述符)其中的任意一個進入讀就緒狀態,epoll()函數就可以返回。 所以, IO多路復用,本質上不會有并發的功能,因為任何時候還是只有一個進程或線程進行工作,它之所以能提高效率是因為select\epoll 把進來的socket放到他們的 '監視' 列表里面,當任何socket有可讀可寫數據立馬處理,那如果select\epoll 手里同時檢測著很多socket, 一有動靜馬上返回給進程處理,總比一個一個socket過來,阻塞等待,處理高效率。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
