Webサーバーとの単純なソケットを構築する方法

背景

私たちは、電子メールやオンラインゲームを送信し、ウェブを閲覧しているかどうかを、現代のどこでもソーシャルネットワーキングアプリケーションは、ネットワークアプリケーションから不可分である、ネットワークプログラミングはますます重要になってきています

目標

その後、コアのWebサーバーを理解するという考え方、そして自分自身に小さなWebサーバを構築、それは簡単な静的なページを私たちに提供することができます

最終結果

完全なコード例を見ることができ、ここで

結果は

実行する方法

python3 index.py

注意を払います

私たちは、あなたがPythonのシステムIO、ネットワークプログラミングは、http契約を学んできたことを前提とし、if'reは、あなたがクリックすることができますに慣れていないここでは、 Pythonのチュートリアルを学んでいる、あなたがクリックすることができ、ここではPython 3.7.2で書かれた小説に基づいて、HTTPプロトコルを学んでいます。

達成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)

上記のコードのコアロジックは、クライアント要求を受信すると、クライアントソケット要求を待つ処理要求です。

次は、メインジョブをprocess一機能を達成することで、我々は、HTTPプロトコルを知っている、HTTPリクエストにはメイン部4リクエストライン、リクエストヘッダ、ブランク行、要求の本体を備えるので、次のように我々は、手続き抽象化をprocess一ことができます。

要求ラインを読む---> ---読み出し要求ヘッダー>リクエストボディを読む---> ---要求処理>オフ要求

具体的な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要求の端部全体を表していないために使用されているので、私たちは一人で空白行を解決しないのはなぜ構造:

HTTPメッセージの構造

それが\ r \ nに遭遇するまで、私たちは、ソケットからの読み出しラインにおける重要なステップであるHTTPメッセージを、解析するために、上記の構成メッセージから見ることができ、我々は、読むことを続けることができ、我々はソケットからできるように、完全な行を読みます

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

以下のように唯一のパッケージsocket.recv 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""

次のように上記パッケージには、私たちは主にエラーに対処し、リターンは、我々は我々の過ちを表現する独自のTinyWebExceptionを定義し、異常カウントが0未満のsocket.recv、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バイトなど、比較的単純で読みます

def read_request_body(socket, content_length):
    """
    读取http请求体
    """
    return recv(socket, content_length)

私たちは、HTTPリクエストを処理するデータに主に基づいてコアを実装する必要がdo_itのHttpの解析データが終了した後、我々は、我々がリソースを検索するために必要なリソースを読んで、小さなWebサーバーが読むために静的リソースを達成することである、上記述べましたリソースを見つけ、主パスに基づいて、パスを解析するとき、我々はそれに直接ブラウザに特定のリソースに、我々の出力応答を解決する限り、urlparse urllib.parse機能モジュールを使用しています。特定のコードの出力前に、我々は単にメッセージHTTPレスポンスのフォーマットを示し、HTTP応答は、4つの部分、すなわち、から成る:ステータスライン、簡単な例は、メッセージヘッダーを与えられ、そして身体に応答し、空白行、以下の通りです。
HTTPレスポンス

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まず最初に私たちは、文書がいっぱい読んでいるかどうかを判断する必要がある、と私たちは、リソースは、ファイル、フォルダではなく、そうでない場合は、私たちが直接のヒントがアクセス権を持っていない法的文書で指定された、我々は、ファイル形式まで、必要なクライアント理由主に、単にコンテンツタイプによって判断ファイル形式の接尾辞を通じて、私たちは限りすることにより、ファイルサイズなど、コンテンツ・タイプを生成するために単独でstatic_typeを提供し、コンテンツの長さを決定するために使用する、そして我々はサイズを提出する必要があり、資源をどのように処理するかを決定する必要がありますPythonのはos.statそれは得ることができ、最終的に我々はそれにHTTPレスポンスに組み立て、すべての情報を入れました。

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"

小さなWebサーバのコードを完了


#!/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)

最後のポイント

上記の小さなウェブサーバは、ちょうどここだけで、Webサーバのコアアイデアを反映している、実際のアプリケーションに比べてはるかに複雑で、非常に簡単な機能を、達成するために

おすすめ

転載: blog.51cto.com/14378833/2404932