老齐教室

剖析Web技术栈(二)

作者:Leonardo Giordani

翻译:老齐

与本文相关书籍推荐:《跟老齐学Python:Django实战》

在本系列的第一篇中,已经对web技术栈的基本做了介绍,本节将重点介绍scoket及其实现。

1 套接字

1.1 基本原理

TCP/IP是一种使用套接字(socket)的网络协议。Socket是包括IP地址(在网络中是唯一的)和端口(对于特定的IP地址是唯一的)的元组,计算机使用IP地址和端口与其他计算机通信。Socket类似文件,可以打开和关闭,也可以读写。Socket编程是一种低级的网络编程,但你需要知道,计算机中提供网络访问的每个软件最终都必须处理Socket(不过,很可能是通过某些库来处理)。

因为我们是从头开始构建,所以要先实现一个小的Python程序,它打开一个socket连接,接收HTTP请求,并返回对这个HTTP请求的响应。由于端口80是一个“低级端口”(一个小于1024的数字),通常没有权限打开那里的socket,所以我将使用端口8080。这暂时不是问题,因为任何端口上都可以提供HTTP服务。

1.2 实现

创建文件server.py并键入下面的代码。是的,输入它,不要只是复制和粘贴,否则你不会学到任何东西。

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
import socket

# Create a socket instance
# AF_INET: use IP protocol version 4
# SOCK_STREAM: full-duplex byte stream
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Allow reuse of addresses
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# Bind the socket to any address, port 8080, and listen
s.bind(('', 8080))
s.listen()

# Serve forever
while True:
# Accept the connection
conn, addr = s.accept()

# Receive data from this socket using a buffer of 1024 bytes
data = conn.recv(1024)

# Print out the data
print(data.decode('utf-8'))

# Close the connection
conn.close()

这个小程序接受8080端口上的连接,并在终端上打印接收到的数据。你可以执行这个程序,然后在另一个终端中运行curl localhost:8080,应该看到类似下面的内容:

1
2
3
4
5
$ python3 server.py 
GET / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.65.3
Accept: */*

服务器一直在while循环中运行代码,如果要终止运行,必须使用Ctrl+C来完成。到目前为止还不错,但这还不是一个HTTP服务器,因为它没有发送任何响应;实际上,你应该会从curl接收到一条错误消息,上面写着“curl: (52) Empty reply from server”。

返回标准响应非常简单,我们只需要调用conn.sendall,传递原始字节。最小的HTTP响应包含协议和状态、空行和实际内容,例如:

1
2
3
HTTP/1.1 200 OK

Hi there!

我们的服务器变成

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
import socket

# Create a socket instance
# AF_INET: use IP protocol version 4
# SOCK_STREAM: full-duplex byte stream
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Allow reuse of addresses
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# Bind the socket to any address, port 8080, and listen
s.bind(('', 8080))
s.listen()

# Serve forever
while True:
# Accept the connection
conn, addr = s.accept()

# Receive data from this socket using a buffer of 1024 bytes
data = conn.recv(1024)

# Print out the data
print(data.decode('utf-8'))

conn.sendall(bytes("HTTP/1.1 200 OK\n\nHi there!\n", 'utf-8'))

# Close the connection
conn.close()

但此时,我们并没有真正响应用户的请求。如果尝试使用不同的curl命令行,如curl localhost:8080/index.htmlcurl localhost:8080/main.css,你总是收到相同的响应。我们应该尝试找到用户请求的资源并将其随同响应的内容返回。

此版本的HTTP服务器正确地提取资源并尝试从当前目录加载它,返回结果或成功或失败

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
import socket
import re

# Create a socket instance
# AF_INET: use IP protocol version 4
# SOCK_STREAM: full-duplex byte stream
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Allow reuse of addresses
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# Bind the socket to any address, port 8080, and listen
s.bind(('', 8080))
s.listen()

HEAD_200 = "HTTP/1.1 200 OK\n\n"
HEAD_404 = "HTTP/1.1 404 Not Found\n\n"

# Serve forever
while True:
# Accept the connection
conn, addr = s.accept()

# Receive data from this socket using a buffer of 1024 bytes
data = conn.recv(1024)

request = data.decode('utf-8')

# Print out the data
print(request)

resource = re.match(r'GET /(.*) HTTP', request).group(1)
try:
with open(resource, 'r') as f:
content = HEAD_200 + f.read()
print('Resource {} correctly served'.format(resource))
except FileNotFoundError:
content = HEAD_404 + "Resource /{} cannot be found\n".format(resource)
print('Resource {} cannot be loaded'.format(resource))

print('--------------------')

conn.sendall(bytes(content, 'utf-8'))

# Close the connection
conn.close()

正如你所看到的,这个实现非常简单。如果你使用下面的内容创建一个简单的本地文件,文件名为index.html

1
2
3
4
5
6
7
<head>
<title>This is my page</title>
<link rel="stylesheet" href="main.css">
</head>
<html>
<p>Some random content</p>
</html>

运行curl localhost:8080/index.html,你将看到文件的内容。此时,你甚至可以使用浏览器打开http://localhost:8080/index.html,你还可以看到页面的标题和内容。Web浏览器是一种能够发送HTTP请求并解释响应内容的软件,只要这些内容是HTML文件(以及许多其他文件类型,如图像或视频)。因此,浏览器可以呈现返回信息的内容。浏览器还负责对渲染所需的缺失资源进行检索。因此,当你在页面的HTML代码中提供指向带有<link><script>标记的样式表或JS脚本的链接时,你也是在指示浏览器为这些文件发送HTTP GET请求。

访问http://localhost:8080/index.html时,server.py的输出是

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
GET /index.html HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:72.0) Gecko/20100101 Firefox/72.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Pragma: no-cache
Cache-Control: no-cache


Resource index.html correctly served
--------------------
GET /main.css HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:72.0) Gecko/20100101 Firefox/72.0
Accept: text/css,*/*;q=0.1
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Referer: http://localhost:8080/index.html
Pragma: no-cache
Cache-Control: no-cache


Resource main.css cannot be loaded
--------------------
GET /favicon.ico HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:72.0) Gecko/20100101 Firefox/72.0
Accept: image/webp,*/*
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache


Resource favicon.ico cannot be loaded
--------------------

如你所见,浏览器发送大量的HTTP请求,其中包含大量的header信息,自动请求HTML代码中提到的CSS文件,并自动尝试检索网站图标。

1.3 参考资源

这些资源对本节所讨论的主题提供了更详细的信息

本文例子的源代码可以在这里找到:https://github.com/lgiordani/dissecting-a-web-stack-code/tree/master/1_sockets_and_parsers)

1.4 问题

上面的程序让你有点了点成就感,你从头开始构建了一个项目,然后发现它可以与你每天使用的浏览器等成熟的软件顺利地协同工作。我还认为,有趣的是,像HTTP这样的技术,现在基本上遍布全世界了,但它们的核心非常简单。

在上面的操作中,HTTP的许多特性都没有在简单socket 编程中涉及到。首先,HTTP/1.0在GET之后引入了其他方法,比如POST,它对于今天的网站来说是至关重要的。这些网站的用户通过表单不断地向服务器发送信息。要实现所有这9个HTTP方法,我们需要正确地解析传入的请求并向代码中添加相关函数。

不过,在这一点上,你可能会注意到,我们正在处理协议的许多低级细节,而这些通常不是我们业务的核心。通过HTTP构建一个服务时,我们有足够的知识来正确实现一些代码。这些代码可以简化特定的过程,比如搜索其他网站、购买书籍或与朋友共享图片。我们不想花时间去理解TCP/IP套接字(socket)的微妙之处,也不想为请求——响应协议编写解析器。很高兴看到这些技术的工作原理,但是在日常工作中,我们需要关注更高层次的东西。

由于HTTP是无状态协议,小型HTTP服务器的情况可能会恶化。该协议不提供任何连接两个连续请求的方法,因此可以跟踪通信状态,这是现代互联网的基石。每当我们在一个网站上进行身份验证,并且我们想访问其他页面时,需要服务器记住我们是谁,这意味着要跟踪连接的状态。

长话短说:要成为一个正常运行的HTTP服务器,我们的代码此时应该实现所有HTTP方法和cookies管理,还需要支持其他协议,如Websockets。这些都是些微不足道的任务,所以我们肯定需要在整个系统中添加一些组件,让我们专注于业务逻辑,而不是应用程序协议的低级细节。

(未完,待续)

搜索技术问答的公众号:老齐教室

在公众号中回复:老齐,可查看所有文章、书籍、课程。

觉得好看,就点这里👇👇👇

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

关注微信公众号,读文章、听课程,提升技能