您好,登錄后才能下訂單哦!
現(xiàn)代社會(huì)網(wǎng)絡(luò)應(yīng)用隨處可見,不管我們是在瀏覽網(wǎng)頁(yè)、發(fā)送電子郵件還是在線游戲都離不開網(wǎng)絡(luò)應(yīng)用程序,網(wǎng)絡(luò)編程正在變得越來(lái)越重要
了解web server的核心思想,然后自己構(gòu)建一個(gè)tiny web server,它可以為我們提供簡(jiǎn)單的靜態(tài)網(wǎng)頁(yè)
完整的事例代碼可以查看這里
python3 index.py
我們假設(shè)你已經(jīng)學(xué)習(xí)過(guò)Python的系統(tǒng)IO、網(wǎng)絡(luò)編程、Http協(xié)議,如果對(duì)此不熟悉,可以點(diǎn)擊這里的Python教程進(jìn)行學(xué)習(xí),可以點(diǎn)擊這里的Http協(xié)議進(jìn)行學(xué)習(xí),事例基于Python 3.7.2編寫。
首先我們給出TinyWebServer的主結(jié)構(gòu)
import socket
# 創(chuàng)建socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 綁定地址和端口
server.bind(("127.0.0.1", 3000))
server.listen(5)
while True:
# 等待客戶端請(qǐng)求
client, addr = server.accept()
# 處理請(qǐng)求
process_request(client, addr)
上面代碼的核心邏輯是socket等待客戶端請(qǐng)求,一旦接受到客戶端請(qǐng)求就處理請(qǐng)求。
接下來(lái)我們主要工作就是實(shí)現(xiàn)process_request函數(shù),我們都知道Http協(xié)議,Http請(qǐng)求主要包含4部分請(qǐng)求行、請(qǐng)求頭、空行、請(qǐng)求體,于是我們可以抽象process_request的過(guò)程如下:
讀取請(qǐng)求行--->讀取請(qǐng)求頭--->讀取請(qǐng)求體--->處理請(qǐng)求--->關(guān)閉請(qǐng)求
具體的Python代碼如下所示:
def process_request(client, addr):
try:
# 獲取請(qǐng)求行
request_line = read_request_line(client)
# 獲取請(qǐng)求頭
request_headers = read_request_headers(client)
# 獲取請(qǐng)求體
request_body = read_request_body(
client, request_headers[b"content-length"])
# 處理客戶端請(qǐng)求
do_it(client, request_line, request_headers, request_body)
except BaseException as error:
# 錯(cuò)誤處理
handle_error(client, error)
finally:
# 關(guān)閉客戶端請(qǐng)求
client.close()
為什么我們不用單獨(dú)解析空行,因?yàn)榭招惺怯脕?lái)表示整個(gè)http請(qǐng)求頭的結(jié)束,除此之外空行對(duì)我們來(lái)說(shuō)沒(méi)有什么作用,關(guān)于如何解析Http消息,首先我們先來(lái)看一下Http消息結(jié)構(gòu):
從上面的消息結(jié)構(gòu)我們可以看出,要解析http消息,其中有一個(gè)關(guān)鍵的步驟是從socket中讀取行,我們可以不斷地從socket中讀取直到遇到\r\n,這樣我們就可以讀取到完整的行
def read_line(socket):
recv_buffer = b''
while True:
recv_buffer += recv(socket, 1)
if recv_buffer.endswith(b"\r\n"):
break
return recv_buffer
上面的recv只是對(duì)socket.recv的一個(gè)包裝,具體代碼如下:
def recv(socket, count):
if count > 0:
recv_buffer = socket.recv(count)
if recv_buffer == b"":
raise TinyWebException("socket.rect調(diào)用失敗!")
return recv_buffer
return b""
在上面的封裝中我們主要是處理了socket.recv返回錯(cuò)誤和count小于0的異常情況,然后我們自己定義了一個(gè)TinyWebException用來(lái)表示我們的錯(cuò)誤,TinyWebException的代碼如下:
class TinyWebException(BaseException):
pass
請(qǐng)求行的解析從上面的結(jié)構(gòu)中我們知道只要從請(qǐng)求數(shù)據(jù)中讀取第一行,然后通過(guò)空格把他們分開就可以了,具體代碼如下所示:
def read_request_line(socket):
"""
讀取http請(qǐng)求行
"""
# 讀取行并把\r\n替換成空字符,最后以空格分離
values = read_line(socket).replace(b"\r\n", b"").split(b" ")
return dict({
# 請(qǐng)求方法
b'method': values[0],
# 請(qǐng)求路徑
b'path': values[1],
# 協(xié)議版本
b'protocol': values[2]
})
請(qǐng)求頭的解析要稍微復(fù)雜一點(diǎn),它要不停得讀取行,直到遇到單獨(dú)的\r\n行結(jié)束,具體代碼如下:
def read_request_headers(socket):
"""
讀取http請(qǐng)求頭
"""
headers = dict()
line = read_line(socket)
while line != b"\r\n":
keyValuePair = line.replace(b"\r\n", b"").split(b": ")
# 統(tǒng)一header中的可以為小寫,方便后面使用
keyValuePair[0] = keyValuePair[0].decode(
encoding="utf-8").lower().encode("utf-8")
if keyValuePair[0] == b"content-length":
# 如果是cotent-length我們需要把結(jié)果轉(zhuǎn)化為整數(shù),方便后面讀取body
headers[keyValuePair[0]] = bytesToInt(keyValuePair[1])
else:
headers[keyValuePair[0]] = keyValuePair[1]
line = read_line(socket)
# 如果heander中沒(méi)有content-length,我們就手動(dòng)把cotent-length設(shè)置為0
if not headers.__contains__(b"content-length"):
headers[b"content-length"] = 0
return headers
請(qǐng)求體的讀取相對(duì)也簡(jiǎn)單,只要連續(xù)讀取conetnt-length個(gè)bytes
def read_request_body(socket, content_length):
"""
讀取http請(qǐng)求體
"""
return recv(socket, content_length)
完成了Http數(shù)據(jù)解析以后我們需要實(shí)現(xiàn)核心的do_it,它主要是基于Http數(shù)據(jù)處理請(qǐng)求,我們?cè)谏厦嬲f(shuō)過(guò),tiny web server主要是實(shí)現(xiàn)了靜態(tài)資源的讀取,讀取資源首先我們要定位資源,資源的定位主要是基于path的,在解析path的時(shí)候,我們用到了urllib.parse模塊的urlparse功能,只要我們解析到了具體的資源,我們直接向?yàn)g覽器輸出響應(yīng)就可以了。在輸出具體的代碼之前,我們需要簡(jiǎn)單說(shuō)明一個(gè)Http消息響應(yīng)的格式,HTTP響應(yīng)也由四個(gè)部分組成,分別是:狀態(tài)行、消息報(bào)頭、空行和響應(yīng)正文,下面給出一個(gè)簡(jiǎn)單的事例:
def do_it(socket, request_line, request_headers, request_body):
"""
處理http請(qǐng)求
"""
# 生成靜態(tài)資源的目標(biāo)地址,在這里我們所有的靜態(tài)文件都統(tǒng)一放在static目錄下面
parse_result = urlparse(request_line[b"path"])
current_dir = os.path.dirname(os.path.realpath(__file__))
file_path = os.path.join(current_dir, "static" +
parse_result.path.decode(encoding="utf-8"))
# 如果靜態(tài)資源存在就向客戶端提供靜態(tài)文件
if os.path.exists(file_path):
serve_static(socket, file_path)
else:
# 靜態(tài)文件不存在,向客戶展示404頁(yè)面
serve_static(socket, os.path.join(current_dir, "static/404.html"))
do_it最核心的邏輯是serve_static,serve_static主要就是實(shí)現(xiàn)了讀取靜態(tài)文件并以Htt的響應(yīng)格式返回給客戶端,下面是serve_static的主要代碼
def serve_static(socket, path):
# 檢查是否有path讀的權(quán)限和具體path對(duì)應(yīng)的資源是否是文件
if os.access(path, os.R_OK) and os.path.isfile(path):
# 文件類型
content_type = static_type(path)
# 文件大小
content_length = os.stat(path).st_size
# 拼裝Http響應(yīng)
response_headers = b"HTTP/1.0 200 OK\r\n"
response_headers += b"Server: Tiny Web Server\r\n"
response_headers += b"Connection: close\r\n"
response_headers += b"Content-Type: " + content_type + b"\r\n"
response_headers += b"Content-Length: %d\r\n" % content_length
response_headers += b"\r\n"
# 發(fā)送http響應(yīng)頭
socket.send(response_headers)
# 以二進(jìn)制的方式讀取文件
with open(path, "rb") as f:
# 發(fā)送http消息體
socket.send(f.read())
else:
raise TinyWebException("沒(méi)有訪問(wèn)權(quán)限")
在serve_static中首先我們需要判斷我們是否有文件的讀全權(quán),并且我們指定的資源是文件,而不是文件夾,如果不是合法文件我們直接提示沒(méi)有訪問(wèn)權(quán)限,我們還需要直到文件的格式,因?yàn)榭蛻舳诵枰ㄟ^(guò)content-type來(lái)決定如何處理資源,然后我們需要文件大小,用來(lái)確定content-length,文件格式主要是通過(guò)后綴名簡(jiǎn)單判斷,我們單獨(dú)提供了static_type來(lái)生成content-type,文件的大小只要通過(guò)Python的os.stat獲取就可以,最后我們只要把所有信息拼裝成Http Response就可以了。
def static_type(path):
if path.endswith(".html"):
return b"text/html; charset=UTF-8"
elif path.endswith(".png"):
return b"image/png; charset=UTF-8"
elif path.endswith(".jpg"):
return b"image/jpg; charset=UTF-8"
elif path.endswith(".jpeg"):
return b"image/jpeg; charset=UTF-8"
elif path.endswith(".gif"):
return b"image/gif; charset=UTF-8"
elif path.endswith(".js"):
return b"application/javascript; charset=UTF-8"
elif path.endswith(".css"):
return b"text/css; charset=UTF-8"
else:
return b"text/plain; charset=UTF-8"
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import socket
from urllib.parse import urlparse
import os
class TinyWebException(BaseException):
pass
def recv(socket, count):
if count > 0:
recv_buffer = socket.recv(count)
if recv_buffer == b"":
raise TinyWebException("socket.rect調(diào)用失??!")
return recv_buffer
return b""
def read_line(socket):
recv_buffer = b''
while True:
recv_buffer += recv(socket, 1)
if recv_buffer.endswith(b"\r\n"):
break
return recv_buffer
def read_request_line(socket):
"""
讀取http請(qǐng)求行
"""
# 讀取行并把\r\n替換成空字符,最后以空格分離
values = read_line(socket).replace(b"\r\n", b"").split(b" ")
return dict({
# 請(qǐng)求方法
b'method': values[0],
# 請(qǐng)求路徑
b'path': values[1],
# 協(xié)議版本
b'protocol': values[2]
})
def bytesToInt(bs):
"""
把bytes轉(zhuǎn)化為int
"""
return int(bs.decode(encoding="utf-8"))
def read_request_headers(socket):
"""
讀取http請(qǐng)求頭
"""
headers = dict()
line = read_line(socket)
while line != b"\r\n":
keyValuePair = line.replace(b"\r\n", b"").split(b": ")
# 統(tǒng)一header中的可以為小寫,方便后面使用
keyValuePair[0] = keyValuePair[0].decode(
encoding="utf-8").lower().encode("utf-8")
if keyValuePair[0] == b"content-length":
# 如果是cotent-length我們需要把結(jié)果轉(zhuǎn)化為整數(shù),方便后面讀取body
headers[keyValuePair[0]] = bytesToInt(keyValuePair[1])
else:
headers[keyValuePair[0]] = keyValuePair[1]
line = read_line(socket)
# 如果heander中沒(méi)有content-length,我們就手動(dòng)把cotent-length設(shè)置為0
if not headers.__contains__(b"content-length"):
headers[b"content-length"] = 0
return headers
def read_request_body(socket, content_length):
"""
讀取http請(qǐng)求體
"""
return recv(socket, content_length)
def send_response():
print("send response")
def static_type(path):
if path.endswith(".html"):
return b"text/html; charset=UTF-8"
elif path.endswith(".png"):
return b"image/png; charset=UTF-8"
elif path.endswith(".jpg"):
return b"image/jpg; charset=UTF-8"
elif path.endswith(".jpeg"):
return b"image/jpeg; charset=UTF-8"
elif path.endswith(".gif"):
return b"image/gif; charset=UTF-8"
elif path.endswith(".js"):
return b"application/javascript; charset=UTF-8"
elif path.endswith(".css"):
return b"text/css; charset=UTF-8"
else:
return b"text/plain; charset=UTF-8"
def serve_static(socket, path):
# 檢查是否有path讀的權(quán)限和具體path對(duì)應(yīng)的資源是否是文件
if os.access(path, os.R_OK) and os.path.isfile(path):
# 文件類型
content_type = static_type(path)
# 文件大小
content_length = os.stat(path).st_size
# 拼裝Http響應(yīng)
response_headers = b"HTTP/1.0 200 OK\r\n"
response_headers += b"Server: Tiny Web Server\r\n"
response_headers += b"Connection: close\r\n"
response_headers += b"Content-Type: " + content_type + b"\r\n"
response_headers += b"Content-Length: %d\r\n" % content_length
response_headers += b"\r\n"
# 發(fā)送http響應(yīng)頭
socket.send(response_headers)
# 以二進(jìn)制的方式讀取文件
with open(path, "rb") as f:
# 發(fā)送http消息體
socket.send(f.read())
else:
raise TinyWebException("沒(méi)有訪問(wèn)權(quán)限")
def do_it(socket, request_line, request_headers, request_body):
"""
處理http請(qǐng)求
"""
# 生成靜態(tài)資源的目標(biāo)地址,在這里我們所有的靜態(tài)文件都統(tǒng)一放在static目錄下面
parse_result = urlparse(request_line[b"path"])
current_dir = os.path.dirname(os.path.realpath(__file__))
file_path = os.path.join(current_dir, "static" +
parse_result.path.decode(encoding="utf-8"))
# 如果靜態(tài)資源存在就向客戶端提供靜態(tài)文件
if os.path.exists(file_path):
serve_static(socket, file_path)
else:
# 靜態(tài)文件不存在,向客戶展示404頁(yè)面
serve_static(socket, os.path.join(current_dir, "static/404.html"))
def handle_error(socket, error):
print(error)
error_message = str(error).encode("utf-8")
response = b"HTTP/1.0 500 Server Internal Error\r\n"
response += b"Server: Tiny Web Server\r\n"
response += b"Connection: close\r\n"
response += b"Content-Type: text/html; charset=UTF-8\r\n"
response += b"Content-Length: %d\r\n" % len(error_message)
response += b"\r\n"
response += error_message
socket.send(response)
def process_request(client, addr):
try:
# 獲取請(qǐng)求行
request_line = read_request_line(client)
# 獲取請(qǐng)求頭
request_headers = read_request_headers(client)
# 獲取請(qǐng)求體
request_body = read_request_body(
client, request_headers[b"content-length"])
# 處理客戶端請(qǐng)求
do_it(client, request_line, request_headers, request_body)
except BaseException as error:
# 打印錯(cuò)誤信息
handle_error(client, error)
finally:
# 關(guān)閉客戶端請(qǐng)求
client.close()
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("127.0.0.1", 3000))
server.listen(5)
print("啟動(dòng)tiny web server,port = 3000")
while True:
client, addr = server.accept()
print("請(qǐng)求地址:%s" % str(addr))
# 處理請(qǐng)求
process_request(client, addr)
上面的tiny web server只是實(shí)現(xiàn)了很簡(jiǎn)單的功能,在實(shí)際的應(yīng)用中比這復(fù)雜得多,這里只是體現(xiàn)了web server的核心思想
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。