中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

如何用socket構建一個簡單的Web Server

發布時間:2020-07-05 07:41:24 來源:網絡 閱讀:2826 作者:omtty 欄目:編程語言

背景

現代社會網絡應用隨處可見,不管我們是在瀏覽網頁、發送電子郵件還是在線游戲都離不開網絡應用程序,網絡編程正在變得越來越重要

目標

了解web server的核心思想,然后自己構建一個tiny web server,它可以為我們提供簡單的靜態網頁

最終效果

完整的事例代碼可以查看這里

如何用socket構建一個簡單的Web Server

如何運行

python3 index.py

注意

我們假設你已經學習過Python的系統IO、網絡編程、Http協議,如果對此不熟悉,可以點擊這里的Python教程進行學習,可以點擊這里的Http協議進行學習,事例基于Python 3.7.2編寫。

TinyWeb實現

首先我們給出TinyWebServer的主結構

import socket

# 創建socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 綁定地址和端口
server.bind(("127.0.0.1", 3000))
server.listen(5)

while True:
    # 等待客戶端請求
    client, addr = server.accept()
    # 處理請求
    process_request(client, addr)

上面代碼的核心邏輯是socket等待客戶端請求,一旦接受到客戶端請求就處理請求。

接下來我們主要工作就是實現process_request函數,我們都知道Http協議,Http請求主要包含4部分請求行、請求頭、空行、請求體,于是我們可以抽象process_request的過程如下:

讀取請求行--->讀取請求頭--->讀取請求體--->處理請求--->關閉請求

具體的Python代碼如下所示:

def process_request(client, addr):
    try:
        # 獲取請求行
        request_line = read_request_line(client)
        # 獲取請求頭
        request_headers = read_request_headers(client)
        # 獲取請求體
        request_body = read_request_body(
            client, request_headers[b"content-length"])
        # 處理客戶端請求
        do_it(client, request_line, request_headers, request_body)
    except BaseException as error:
        # 錯誤處理
        handle_error(client, error)
    finally:
        # 關閉客戶端請求
        client.close()

為什么我們不用單獨解析空行,因為空行是用來表示整個http請求頭的結束,除此之外空行對我們來說沒有什么作用,關于如何解析Http消息,首先我們先來看一下Http消息結構:

如何用socket構建一個簡單的Web Server

從上面的消息結構我們可以看出,要解析http消息,其中有一個關鍵的步驟是從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只是對socket.recv的一個包裝,具體代碼如下:

def recv(socket, count):
    if count > 0:
        recv_buffer = socket.recv(count)
        if recv_buffer == b"":
            raise TinyWebException("socket.rect調用失敗!")
        return recv_buffer
    return b""

在上面的封裝中我們主要是處理了socket.recv返回錯誤和count小于0的異常情況,然后我們自己定義了一個TinyWebException用來表示我們的錯誤,TinyWebException的代碼如下:

class TinyWebException(BaseException):
    pass
解析請求行:

請求行的解析從上面的結構中我們知道只要從請求數據中讀取第一行,然后通過空格把他們分開就可以了,具體代碼如下所示:

def read_request_line(socket):
    """
    讀取http請求行
    """
    # 讀取行并把\r\n替換成空字符,最后以空格分離
    values = read_line(socket).replace(b"\r\n", b"").split(b" ")
    return dict({
        # 請求方法
        b'method': values[0],
        # 請求路徑
        b'path': values[1],
        # 協議版本
        b'protocol': values[2]
    })
解析請求頭:

請求頭的解析要稍微復雜一點,它要不停得讀取行,直到遇到單獨的\r\n行結束,具體代碼如下:

def read_request_headers(socket):
    """
    讀取http請求頭
    """
    headers = dict()
    line = read_line(socket)
    while line != b"\r\n":
        keyValuePair = line.replace(b"\r\n", b"").split(b": ")
        # 統一header中的可以為小寫,方便后面使用
        keyValuePair[0] = keyValuePair[0].decode(
            encoding="utf-8").lower().encode("utf-8")
        if keyValuePair[0] == b"content-length":
            # 如果是cotent-length我們需要把結果轉化為整數,方便后面讀取body
            headers[keyValuePair[0]] = bytesToInt(keyValuePair[1])
        else:
            headers[keyValuePair[0]] = keyValuePair[1]
        line = read_line(socket)
    # 如果heander中沒有content-length,我們就手動把cotent-length設置為0
    if not headers.__contains__(b"content-length"):
        headers[b"content-length"] = 0
    return headers
解析請求體:

請求體的讀取相對也簡單,只要連續讀取conetnt-length個bytes

def read_request_body(socket, content_length):
    """
    讀取http請求體
    """
    return recv(socket, content_length)

完成了Http數據解析以后我們需要實現核心的do_it,它主要是基于Http數據處理請求,我們在上面說過,tiny web server主要是實現了靜態資源的讀取,讀取資源首先我們要定位資源,資源的定位主要是基于path的,在解析path的時候,我們用到了urllib.parse模塊的urlparse功能,只要我們解析到了具體的資源,我們直接向瀏覽器輸出響應就可以了。在輸出具體的代碼之前,我們需要簡單說明一個Http消息響應的格式,HTTP響應也由四個部分組成,分別是:狀態行、消息報頭、空行和響應正文,下面給出一個簡單的事例:
如何用socket構建一個簡單的Web Server

def do_it(socket, request_line, request_headers, request_body):
    """
    處理http請求
    """
    # 生成靜態資源的目標地址,在這里我們所有的靜態文件都統一放在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"))

    # 如果靜態資源存在就向客戶端提供靜態文件
    if os.path.exists(file_path):
        serve_static(socket, file_path)
    else:
        # 靜態文件不存在,向客戶展示404頁面
        serve_static(socket, os.path.join(current_dir, "static/404.html"))

do_it最核心的邏輯是serve_static,serve_static主要就是實現了讀取靜態文件并以Htt的響應格式返回給客戶端,下面是serve_static的主要代碼

def serve_static(socket, path):
    # 檢查是否有path讀的權限和具體path對應的資源是否是文件
    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響應
        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"
        # 發送http響應頭
        socket.send(response_headers)
        # 以二進制的方式讀取文件
        with open(path, "rb") as f:
            # 發送http消息體
            socket.send(f.read())
    else:
        raise TinyWebException("沒有訪問權限")

在serve_static中首先我們需要判斷我們是否有文件的讀全權,并且我們指定的資源是文件,而不是文件夾,如果不是合法文件我們直接提示沒有訪問權限,我們還需要直到文件的格式,因為客戶端需要通過content-type來決定如何處理資源,然后我們需要文件大小,用來確定content-length,文件格式主要是通過后綴名簡單判斷,我們單獨提供了static_type來生成content-type,文件的大小只要通過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"

完整的tiny web server 代碼


#!/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調用失敗!")
        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請求行
    """
    # 讀取行并把\r\n替換成空字符,最后以空格分離
    values = read_line(socket).replace(b"\r\n", b"").split(b" ")
    return dict({
        # 請求方法
        b'method': values[0],
        # 請求路徑
        b'path': values[1],
        # 協議版本
        b'protocol': values[2]
    })

def bytesToInt(bs):
    """
    把bytes轉化為int
    """
    return int(bs.decode(encoding="utf-8"))

def read_request_headers(socket):
    """
    讀取http請求頭
    """
    headers = dict()
    line = read_line(socket)
    while line != b"\r\n":
        keyValuePair = line.replace(b"\r\n", b"").split(b": ")
        # 統一header中的可以為小寫,方便后面使用
        keyValuePair[0] = keyValuePair[0].decode(
            encoding="utf-8").lower().encode("utf-8")
        if keyValuePair[0] == b"content-length":
            # 如果是cotent-length我們需要把結果轉化為整數,方便后面讀取body
            headers[keyValuePair[0]] = bytesToInt(keyValuePair[1])
        else:
            headers[keyValuePair[0]] = keyValuePair[1]
        line = read_line(socket)
    # 如果heander中沒有content-length,我們就手動把cotent-length設置為0
    if not headers.__contains__(b"content-length"):
        headers[b"content-length"] = 0
    return headers

def read_request_body(socket, content_length):
    """
    讀取http請求體
    """
    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讀的權限和具體path對應的資源是否是文件
    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響應
        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"
        # 發送http響應頭
        socket.send(response_headers)
        # 以二進制的方式讀取文件
        with open(path, "rb") as f:
            # 發送http消息體
            socket.send(f.read())
    else:
        raise TinyWebException("沒有訪問權限")

def do_it(socket, request_line, request_headers, request_body):
    """
    處理http請求
    """

    # 生成靜態資源的目標地址,在這里我們所有的靜態文件都統一放在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"))

    # 如果靜態資源存在就向客戶端提供靜態文件
    if os.path.exists(file_path):
        serve_static(socket, file_path)
    else:
        # 靜態文件不存在,向客戶展示404頁面
        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:
        # 獲取請求行
        request_line = read_request_line(client)
        # 獲取請求頭
        request_headers = read_request_headers(client)
        # 獲取請求體
        request_body = read_request_body(
            client, request_headers[b"content-length"])
        # 處理客戶端請求
        do_it(client, request_line, request_headers, request_body)
    except BaseException as error:
        # 打印錯誤信息
        handle_error(client, error)
    finally:
        # 關閉客戶端請求
        client.close()

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("127.0.0.1", 3000))
server.listen(5)

print("啟動tiny web server,port = 3000")

while True:
    client, addr = server.accept()
    print("請求地址:%s" % str(addr))
    # 處理請求
    process_request(client, addr)

最后想說的

上面的tiny web server只是實現了很簡單的功能,在實際的應用中比這復雜得多,這里只是體現了web server的核心思想

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

屏边| 利辛县| 东明县| 常熟市| 新疆| 同江市| 抚顺县| 大理市| 金华市| 包头市| 荣昌县| 嘉义县| 邳州市| 台山市| 原平市| 南安市| 衡阳市| 慈溪市| 宁国市| 罗山县| 红河县| 江达县| 康乐县| 砚山县| 凤庆县| 林州市| 余江县| 海城市| 东丽区| 平利县| 东兴市| 大同市| 加查县| 巴林左旗| 弥渡县| 上蔡县| 左云县| 滦平县| 淳化县| 穆棱市| 通城县|