共计 5446 个字符,预计需要花费 14 分钟才能阅读完成。
从一个 Hello World 程序说起
要编写 Web 服务器,须要用到一个 Python 内置库 socket
。Socket 是一个比拟形象的概念,中文叫套接字,它代表一个网络连接。两台计算机之间要进行通信,大略分为三个步骤:建设连贯,传输数据,敞开连贯。而 socket
库为咱们提供了这个能力。
依照国际惯例,咱们将通过编写一个 Hello World 程序来开始 Web 服务器的学习。
首先要创立一个基于 TCP
的 socket
对象:
# 导入 socket
import socket
# 创立 socket 对象
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket.socket()
办法用来创立一个 socket
对象。同时咱们给它传递了两个参数:socket.AF_INET
示意应用 IPv4 协定,socket.SOCK_STREAM
示意这是一个基于 TCP
的 socket
对象。这两个参数也是默认参数,都能够不传。
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/