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

news/2025/3/12 1:51:13/文章来源:https://www.cnblogs.com/markhoo/p/18236370

从一个 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

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

1

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.pyimport socketdef 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

运行 Hello World 程序

运行 Hello World 程序

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

Hello World

Hello World

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.pyimport socketdef 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,果然浏览器发送了两个请求。

Chrome 请求

Chrome 请求

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

Hello World 请求头及响应头信息

Hello World 请求头及响应头信息

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

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

\# threading_server_forever.pyimport socket  
import threadingdef 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

原文出处: https://jianghushinian.cn

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/721286.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

go pool

来自: sync.Pool 原理 sync.Pool 核心对象有三个New:函数,负责对象初始化 Get:获取 Pool 中的对象,如果 Pool 中对象不存在则会调用 New Put:将对象放入 Pool 中New func Pool 的结构很简单,就 5 个字段 type Pool struct { ... New func() interface{} }字段 New 是…

CSP历年复赛题-P2119 [NOIP2016 普及组] 魔法阵

原题链接:https://www.luogu.com.cn/problem/P2119 题意解读:在一组数里找出所有的Xa,Xb,Xc,Xd的组合,使得满足Xa<Xb<Xc<Xd, Xb-Xa=2(Xd-Xc), Xb-Xa<(Xc-Xb)/3,并统计出每个数作为A,B,C,D出现的次数。 解题思路: 1、枚举(O(n^4)) 首先想到的是通过4重循环枚…

应用解析 | 面向智能网联汽车的产教融合解决方案

经纬恒润融合二十多年的行业经验,将实际的工程应用、项目需求引入到产教融合中,打破院校与企业之间的界限,做到所学即所用,毕业即就业。 背景介绍随着科技的飞速发展,智能网联汽车已成为汽车产业的新宠,引领着未来出行的潮流。然而,行业的高速发展也带来了对高素质技术技…

PTA大作业4-6总结

前言 这三次大作业使用到的新知识点并不是很多,主要还是以继承与多态为主,虽然在新知识点上并没有增加许多,但明显在类的构造,类与类之间的关联设计方面难度明显有较大的增加,题量方面明显还是和之前三次大作业一样,每次大作业都是以一题为主,其余的题目都是为了巩固基础…

.NET之Hangfire快速入门和使用

原文地址:.NET之Hangfire快速入门和使用 - 追逐时光者 - 博客园 (cnblogs.com)前言:定时任务调度问题,是一个老生常谈的问题。网上有许多定时任务调度的解决方案,对于我而言很早以前主要是使用Window计划和Window服务来做任务定时执行,然后就开始使用定时任务调度框架Quar…

插值技术研究

过采样与欠采样&图像重采样(上采样&下采样) 研究图像插值技术:2016年苏州大学的钟宝江等人《图像插值技术综述》[1]图像插值利用图像已知采样点的灰度值估计未知采样点的灰度值,是图像数据的一种生成过程 。 图像插值的目的在于通过升采样的方式提高图像的分辨率。…

科研日记4【2024-06-06】

实验高度向稀疏采样造成的整行缺失使得在高度向上出现严重混叠现象, 对高度向稀疏采样的数据首先利用线性插值恢复补全,再将部分插值去掉,以模拟二维随机降采样。 实验结果说明上述方法并不好。 高度向50%稀疏直接成像:高度向50%稀疏-不动迭代成像:高度向50%稀疏-线性插值…

活动预热丨在 AGI Playground 2024 遇见一群 RTE+AI 的 Builders

6 月 22、23 日,北京。AGI Playground 2024,这个夏日最火热的 AGI 盛会。王小川、杨植麟等 AGI 创业者悉数参加。RTE 开发者社区的 builders 和 RTE Open Day 也将在现场!我们将为大家呈现两大板块: 01 实时开发挑战 Workshop RTE 开发者社区将联合「零一万物」发起 worksh…

Yearning外置工单通知实现思路

背景概述 上篇我们讲解了一下Yearning如何使用飞书发送工单通知,但是我最初的想法不仅仅是飞书、钉钉这些媒介,更多的是希望可以自定义集成渠道,因此想到了PrometheusAlert,这样我们就不用重新造轮子了。 大致配置image-20240606110115965 我们拿到Yearning发送的数据,当然…

一加七Pro刷Lineageos21(kernelsu+MicroG)

前言 前几天我已经刷了Lineageos21(一加七Pro刷lineageos21(kernelsu+gapps)),但是体验下来有两个很严重的bug:无法接打电话,没有声音。 收短信倒是很正常,但电话打进来只会显示号码,不会显示接听界面(对方那边显示正在通话中,也就是挂断了),拨打电话会显示已结束。 测试…

教务管理系统

1 项目简介 教务管理系统主要模块: 邮箱注册:用户根据邮箱发送验证码注册用户信息 邮箱登录:用户根据注册的邮箱登录 教师管理:主要包含教师的全部查询,教师信息的修改,添加教师信息,删除教师信息,分页查询教师信息 多条件查询教师信息(教师编号,名称,性别,入职日期…

网络隔离后的跨网投递需求,要这样做才能让需求落地

为了保护企业的核心数字资产、隔离有害的网络安全威胁、保障数据信息在可信网络内进行安全交互,越来越多的企业在网络建设时,选择进行网络隔离。应用较为普遍的网络隔离手段包括物理隔离、协议隔离、应用隔离等,而常见的状态是企业进行内部网络和外部互联网隔离,或者企业内…