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

简介: 用 Python 撸一个 Web 服务器-第2章:Hello World

从一个 Hello World 程序说起

要编写 Web 服务器,需要用到一个 Python 内置库 socket。Socket 是一个比较抽象的概念,中文叫套接字,它代表一个网络连接。两台计算机之间要进行通讯,大概分为三个步骤:建立连接,传输数据,关闭连接。而 socket 库为我们提供了这个能力。

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

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

1
2
3
4
5
# 导入 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 对象以后我们接下来要做的就是监听客户端的请求:

1
2
3
4
# 绑定 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

开启监听以后,就可以等待接收客户端的请求了:

1
client, addr = sock.accept()

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

与客户端建立好连接后,接下来就是接收客户端发来的请求数据:

1
2
3
4
5
6
data = b''
whileTrue:
    chunk = client.recv(1024)
    data += chunk
if len(chunk) < 1024:
break

接收客户端请求数据需要调用客户端连接对象的 recv 方法,参数为每一次接收的数据长度。socket 通讯过程中的数据都为 Python 的 bytes 类型。这里每次接收 1024 个字节,等待数据全部接收完成退出循环。

接收到客户端发来的数据后,就需要对数据进行处理,然后返回响应给客户端的浏览器:

1
2
3
4
# 打印从客户端接收的数据
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 对象:

1
2
3
4
# 关闭客户端连接对象
client.close()
# 关闭 socket 对象
sock.close()

至此,一个 Hello World 服务器程序编写完成,下面是完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# server.py
import socket
defmain():
# 创建 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''
whileTrue:
        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

运行 Hello World 程序

打开浏览器,地址栏输入 http://127.0.0.1:8000,你将得到如下结果:

Hello World

Hello World!浏览器成功渲染出了服务器的响应结果。

回到终端可以查看打印出来的客户端请求信息:

客户端请求信息

可以发现,客户端连接对象实际上也是一个 socket 对象,客户端 IP 地址为 127.0.0.1 端口为 50510。最后是客户端请求数据,只有请求行和请求头,由于没有请求体,所以最后以两个连续的 \r\n\r\n 结束。

细心的读者可能已经发现在最后给出的完整的 Hello World 程序代码中,在创建 socket 对象后有一行:

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

前面并没有介绍这行代码的作用,实际上它的作用是允许端口复用。如果不写这行代码,那么在程序运行完成后需要马上重启程序时,由于上次的端口还在占用,会导致程序抛出异常,端口需要在间隔一段时间后才会被释放允许使用。加上这行代码就不会出现此问题,方便调试。

以上,我们实现了一个简单的能够返回 Hello World 的服务器程序。

让服务器永久运行

上面实现的 Hello World 服务器程序运行一次就退出了。通常来说,服务器端的程序是永久运行的程序。因为你不知道客户端什么时候发送请求,所以就需要服务器端一直处在监听状态。这样才能保证任何时候客户端发送请求都能被服务器端接收到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# server_forever.py
import socket
defmain():
# 创建 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)
whileTrue:
# 等待客户端请求
        client, addr = sock.accept()
        print(f'client type: {type(client)}\naddr: {addr}')
# 接收客户端发来的数据
        data = b''
whileTrue:
            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,果然浏览器发送了两个请求。

Chrome 请求

第一个请求路径为 /,根据浏览器请求及响应记录来看是符合预期的。

Hello World 请求头及响应头信息

Hello World 响应信息

第二个请求路径为 /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 服务器程序由于是单线程的,所以服务器一次只能处理一个请求。但我们使用的京东等网站实际上同时会有很多客户端在连接的,如果一次只能处理一个请求,那么客户端体验将非常差。

为了让我们的程序也能支持同时处理多个客户端连接,需要将其改成多线程版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# threading_server_forever.py
import socket
import threading
defprocess_connection(client):
"""处理客户端连接"""
# 接收客户端发来的数据
    data = b''
whileTrue:
        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()
defmain():
# 创建 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)
whileTrue:
# 等待客户端请求
        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

相关文章
|
9天前
|
SQL 安全 JavaScript
告别Web安全小白!Python实战指南:抵御SQL注入、XSS、CSRF的秘密武器!
【9月更文挑战第12天】在Web开发中,安全漏洞如同暗礁,尤其对初学者而言,SQL注入、跨站脚本(XSS)和跨站请求伪造(CSRF)是常见挑战。本文通过实战案例,展示如何利用Python应对这些威胁。首先,通过参数化查询防止SQL注入;其次,借助Jinja2模板引擎自动转义机制抵御XSS攻击;最后,使用Flask-WTF库生成和验证CSRF令牌,确保转账功能安全。掌握这些技巧,助你构建更安全的Web应用。
14 5
|
11天前
|
安全 Python
使用Python实现简单的Web服务器
使用Python实现简单的Web服务器
19 6
|
21天前
|
存储 数据库 开发者
Web2py的神秘力量:如何用Python打造快速原型设计与开发,让你的项目一鸣惊人?
【8月更文挑战第31天】在现代软件开发中,快速原型设计至关重要。Web2py作为一款Python Web框架,凭借其简洁的语法和高效开发流程受到开发者青睐。本文通过在线调查问卷系统的案例,展示Web2py在快速原型设计中的应用,包括需求分析、数据库设计、表单创建及路由实现,并提供示例代码,帮助读者理解其最佳实践。
14 1
|
21天前
|
开发者 Python
Web2py的神秘力量:如何用Python打造高效Web应用,让你一鸣惊人?
【8月更文挑战第31天】本文探讨了从热门的Django和Flask框架迁移到Web2py的过程,详细阐述了Web2py的三大优势:简单易学、快速开发与功能丰富。文章按步骤指导读者完成迁移,包括理解基本概念、编写迁移脚本、重构代码及测试调试,并提供了示例代码以展示Web2py的应用。此外,还分享了最佳实践建议,帮助开发者顺利过渡到Web2py,提升Web开发效率。
9 1
|
21天前
|
运维 监控 数据库
自动化运维:使用Python脚本实现服务器监控
【8月更文挑战第31天】在这篇文章中,我们将探索如何利用Python编写简单的脚本来实现对服务器的基本监控。通过学习和应用这些技术,你可以快速检测服务器的状态,包括CPU使用率、内存占用和磁盘空间等关键指标。这不仅有助于及时发现问题,还能提升运维效率。文章将逐步引导你理解监控的重要性,并展示如何从零开始构建自己的监控工具。
|
20天前
|
API C# 开发框架
WPF与Web服务集成大揭秘:手把手教你调用RESTful API,客户端与服务器端优劣对比全解析!
【8月更文挑战第31天】在现代软件开发中,WPF 和 Web 服务各具特色。WPF 以其出色的界面展示能力受到欢迎,而 Web 服务则凭借跨平台和易维护性在互联网应用中占有一席之地。本文探讨了 WPF 如何通过 HttpClient 类调用 RESTful API,并展示了基于 ASP.NET Core 的 Web 服务如何实现同样的功能。通过对比分析,揭示了两者各自的优缺点:WPF 客户端直接处理数据,减轻服务器负担,但需处理网络异常;Web 服务则能利用服务器端功能如缓存和权限验证,但可能增加服务器负载。希望本文能帮助开发者根据具体需求选择合适的技术方案。
56 0
|
20天前
|
Rust 安全 开发者
惊爆!Xamarin 携手机器学习,开启智能应用新纪元,个性化体验与跨平台优势完美融合大揭秘!
【8月更文挑战第31天】随着互联网的发展,Web应用对性能和安全性要求不断提高。Rust凭借卓越的性能、内存安全及丰富生态,成为构建高性能Web服务器的理想选择。本文通过一个简单示例,展示如何使用Rust和Actix-web框架搭建基本Web服务器,从创建项目到运行服务器全程指导,帮助读者领略Rust在Web后端开发中的强大能力。通过实践,读者可以体验到Rust在性能和安全性方面的优势,以及其在Web开发领域的巨大潜力。
29 0
|
20天前
|
存储 运维 监控
自动化运维:使用Python脚本进行服务器监控
【8月更文挑战第31天】在数字化时代,服务器的稳定运行对于企业至关重要。本文将介绍如何使用Python编写一个简单的服务器监控脚本,帮助运维人员及时发现并解决潜在问题。我们将从基础的服务器资源监控开始,逐步深入到日志分析与报警机制的实现。通过实际代码示例和操作步骤,使读者能够快速掌握自动化监控的技能,提升工作效率。
|
20天前
|
Java 数据库 API
JSF与JPA的史诗级联盟:如何编织数据持久化的华丽织锦,重塑Web应用的荣耀
【8月更文挑战第31天】JavaServer Faces (JSF) 和 Java Persistence API (JPA) 分别是构建Java Web应用的用户界面组件框架和持久化标准。结合使用JSF与JPA,能够打造强大的数据驱动Web应用。首先,通过定义实体类(如`User`)和配置`persistence.xml`来设置JPA环境。然后,在JSF中利用Managed Bean(如`UserBean`)管理业务逻辑,通过`EntityManager`执行数据持久化操作。
32 0
|
21天前
|
数据采集 存储 数据挖掘
构建高效Web爬虫:Python与BeautifulSoup实战指南
【8月更文挑战第31天】本文将引导读者步入Web爬虫的世界,通过Python编程语言和BeautifulSoup库的强强联手,解锁数据抓取的艺术。文章不仅提供代码示例,还将深入探讨如何设计高效、可维护且符合伦理的爬虫程序。