乐趣区

关于python:用-Python-撸一个-Web-服务器第2章Hello-World

从一个 Hello World 程序说起

要编写 Web 服务器,须要用到一个 Python 内置库 socket。Socket 是一个比拟形象的概念,中文叫套接字,它代表一个网络连接。两台计算机之间要进行通信,大略分为三个步骤:建设连贯,传输数据,敞开连贯。而 socket 库为咱们提供了这个能力。

依照国际惯例,咱们将通过编写一个 Hello World 程序来开始 Web 服务器的学习。

首先要创立一个基于 TCPsocket 对象:

# 导入 socket
import socket

# 创立 socket 对象
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

socket.socket() 办法用来创立一个 socket 对象。同时咱们给它传递了两个参数:socket.AF_INET 示意应用 IPv4 协定,socket.SOCK_STREAM 示意这是一个基于 TCPsocket 对象。这两个参数也是默认参数,都能够不传。

HTTP 协定是基于申请 —— 响应模型的,申请只能够是客户端发动的,服务器进行响应。服务器并不具备被动发动申请的能力,然而它须要被动的期待客户端的申请。所以当初有了 socket 对象当前咱们接下来要做的就是监听客户端的申请:

# 绑定 IP 和端口
sock.bind(('127.0.0.1', 8000))
# 开始监听
sock.listen(5)

socket 对象的 bind 办法用来绑定监听的 IP 地址和端口,它接管一个由 IP 和端口组成的 tuple 作为参数,127.0.0.1 代表本机 IP,只有运行在本机上的浏览器能力连贯。端口号容许范畴在 0~65535 之间,然而小于 1024 的端口号须要管理员权限才可应用。sock.listen(5) 用来开启监听,期待连贯的最大数量指定为 5

开启监听当前,就能够期待接管客户端的申请了:

client, addr = sock.accept()

sock.accept() 会阻塞程序,期待客户端的连贯,一旦有客户端连贯上来,它会别离返回客户端连贯对象和客户端的地址。

与客户端建设好连贯后,接下来就是接管客户端发来的申请数据:

data = b''
while True:
    chunk = client.recv(1024)
    data += chunk
    if len(chunk) < 1024:
        break

接管客户端申请数据须要调用客户端连贯对象的 recv 办法,参数为每一次接管的数据长度。socket 通信过程中的数据都为 Python 的 bytes 类型。这里每次接管 1024 个字节,期待数据全副接管实现退出循环。

接管到客户端发来的数据后,就须要对数据进行解决,而后返回响应给客户端的浏览器:

# 打印从客户端接管的数据
print(f'data: {data}')
# 给客户端发送响应数据
client.sendall(b'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<h1>Hello World</h1>')

为了简略起见,在接管到客户端发来的数据后间接进行打印,并没有做进一步的解析解决。接着就是服务器给客户端发送响应数据。发送的数据同样为 bytes 类型。数据依照 HTTP 协定的标准进行组装,首先是状态行 HTTP/1.1 200 OK,紧跟是着一个换行符 \r\n,而后通过响应头 Content-Type: text/html 指定响应后果为 HTML 类型,接下来是两个间断的 \r\n\r\n,留神因为在响应头和响应报文之间隔着一个空行,所以才会呈现两个间断的 \r\n\r\n,最初就是响应体局部 <h1>Hello World</h1>

在发送完响应数据后,咱们须要敞开客户端连贯对象和服务端 socket 对象:

# 敞开客户端连贯对象
client.close()
# 敞开 socket 对象
sock.close()

至此,一个 Hello World 服务器程序编写实现,上面是残缺代码:

# server.py

import socket


def main():
    # 创立 socket 对象
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 容许端口复用
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # 绑定 IP 和端口
    sock.bind(('127.0.0.1', 8000))
    # 开始监听
    sock.listen(5)

    # 期待客户端申请
    client, addr = sock.accept()
    print(f'client type: {type(client)}\naddr: {addr}')

    # 接管客户端发来的数据
    data = b''
    while True:
        chunk = client.recv(1024)
        data += chunk
        if len(chunk) < 1024:
            break

    # 打印从客户端接管的数据
    print(f'data: {data}')
    # 给客户端发送响应数据
    client.sendall(b'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<h1>Hello World</h1>')

    # 敞开客户端连贯对象
    client.close()
    # 敞开 socket 对象
    sock.close()


if __name__ == '__main__':
    main()

将以上代码写入到 server.py 文件中。而后在终端中应用 Python 运行此文件:python3 server.py

关上浏览器,地址栏输出 http://127.0.0.1:8000,你将失去如下后果:

Hello World!浏览器胜利渲染出了服务器的响应后果。

回到终端能够查看打印进去的客户端申请信息:

能够发现,客户端连贯对象实际上也是一个 socket 对象,客户端 IP 地址为 127.0.0.1 端口为 50510。最初是客户端申请数据,只有申请行和申请头,因为没有申请体,所以最初以两个间断的 \r\n\r\n 完结。

仔细的读者可能曾经发现在最初给出的残缺的 Hello World 程序代码中,在创立 socket 对象后有一行:

sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

后面并没有介绍这行代码的作用,实际上它的作用是容许端口复用。如果不写这行代码,那么在程序运行实现后须要马上重启程序时,因为上次的端口还在占用,会导致程序抛出异样,端口须要在距离一段时间后才会被开释容许应用。加上这行代码就不会呈现此问题,不便调试。

以上,咱们实现了一个简略的可能返回 Hello World 的服务器程序。

让服务器永恒运行

下面实现的 Hello World 服务器程序运行一次就退出了。通常来说,服务器端的程序是永恒运行的程序。因为你不晓得客户端什么时候发送申请,所以就须要服务器端始终处在监听状态。这样能力保障任何时候客户端发送申请都能被服务器端接管到。

# server_forever.py

import socket


def main():
    # 创立 socket 对象
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 容许端口复用
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # 绑定 IP 和端口
    sock.bind(('127.0.0.1', 8000))
    # 开始监听
    sock.listen(5)

    while True:
        # 期待客户端申请
        client, addr = sock.accept()
        print(f'client type: {type(client)}\naddr: {addr}')

        # 接管客户端发来的数据
        data = b''
        while True:
            chunk = client.recv(1024)
            data += chunk
            if len(chunk) < 1024:
                break

        # 打印从客户端接管的数据
        print(f'data: {data}')
        # 给客户端发送响应数据
        client.sendall(b'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<h1>Hello World</h1>')

        # 敞开客户端连贯对象
        client.close()


if __name__ == '__main__':
    main()

下面的程序中退出了一个 while True 有限循环,在解决完一个客户端连贯对象当前程序马上执行到下一次循环,开始期待新的客户端连贯,这样就实现了服务器程序永恒运行。并且删除了 main 函数最初一行 sock.close() 代码,因为既然要让程序永恒运行上来,那么也就不须要敞开服务器端 socket 连贯了。

将以上代码保留到 server_forever.py 文件中,同样在命令行终端应用 Python 运行此程序,浏览器多刷新几次页面,仍然可能失常加载 Hello World

不过,此时如果在终端查看打印信息,会发现每次刷新浏览器时,浏览器并不是一次只发送一个申请,而是两个申请。

关上 Chrome 控制台查看 Network,果然浏览器发送了两个申请。

第一个申请门路为 /,依据浏览器申请及响应记录来看是合乎预期的。

第二个申请门路为 /favicon.ico,这个申请的响应后果同样为 <h1>Hello World</h1>

实际上,这个申请是 Chrome 浏览器自主发动的,是为了获取网站图标用的。当在浏览器中关上京东网站首页时,浏览器标签栏就会加载出京东网站的图标。

咱们本人编写的 Hello World 服务器因为没有返回正确的图标文件,而是返回了一个 <h1>Hello World</h1> 字符串,所以浏览器并不能将其辨认为图标。最终在 Hello World 页面标签栏也就不会有像京东网站相似的图标了。这个问题目前来说咱们并不需要关怀,在之后实现 Todo List 程序时再来解决。

有些读者可能会纳闷为什么 Hello World 服务器返回的是一个不残缺的 HTML 页面,只是一个带有 h1 标签的字符串 <h1>Hello World</h1>,浏览器就可能失常渲染页面,并对 Hello World 做加粗解决。这其实是 Chrome 浏览器的容错机制,如果检测到 HTML 标签不全,那么它会主动补全短少的标签。以达到更好的渲染成果。

当初如果要完结服务器程序,只须要在程序运行终端按组合键 Ctrl + C 即可。

让服务器同时反对多个客户端连贯

咱们当初实现的 Hello World 服务器程序因为是单线程的,所以服务器一次只能解决一个申请。但咱们应用的京东等网站实际上同时会有很多客户端在连贯的,如果一次只能解决一个申请,那么客户端体验将十分差。

为了让咱们的程序也能反对同时解决多个客户端连贯,须要将其改成多线程版本。

# threading_server_forever.py

import socket
import threading


def process_connection(client):
    """解决客户端连贯"""
    # 接管客户端发来的数据
    data = b''
    while True:
        chunk = client.recv(1024)
        data += chunk
        if len(chunk) < 1024:
            break

    # 打印从客户端接管的数据
    print(f'data: {data}')
    # 给客户端发送响应数据
    client.sendall(b'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<h1>Hello World</h1>')

    # 敞开客户端连贯对象
    client.close()


def main():
    # 创立 socket 对象
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 容许端口复用
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # 绑定 IP 和端口
    sock.bind(('127.0.0.1', 8000))
    # 开始监听
    sock.listen(5)

    while True:
        # 期待客户端申请
        client, addr = sock.accept()
        print(f'client type: {type(client)}\naddr: {addr}')

        # 创立新的线程来解决客户端连贯
        t = threading.Thread(target=process_connection, args=(client,))
        t.start()


if __name__ == '__main__':
    main()

改成多线程版本当前,服务器每接管到一个客户端连贯,就将其交给一个新的子线程来解决,主线程继续执行到下一轮循环期待新的客户端连贯。这样,就实现了让服务器同时反对多个客户端连贯。

本章通过编写一个 Hello World 程序学习了 Web 服务器的开发。如果你是编程老手,对 socket 编程了解起来还是略有艰难,那么你能够类比 Python 的文件操作来进行比照学习。文件解决通常也是三个步骤:关上文件、读写数据、敞开文件。通过这样利用已有常识来类比学习新技术也是一个不错的办法。

本章源码:chapter2

分割我:

  • 微信:jianghushinian
  • 邮箱:jianghushinian007@outlook.com
  • 博客地址:https://jianghushinian.cn/
退出移动版