用户通过浏览器浏览网站的过程:
用户浏览器(socket客户端)
客户端往服务端发消息
客户端接收消息
关闭
网站服务器(socket服务端)
启动,监听
等待客户端连接
服务端收消息
服务端回消息
关闭(一般都不会关闭)
下面,我们先写一个服务端程序,来模拟浏览器服务器访问过程。
你会发现,运行程序之后并且用浏览器访问 127.0.0.1:8001 ,程序会报错,浏览器显示“该网页无法正常运作”,如下图
为什么呢?这时候就要引出
HTTP 协议
了。
HTTP协议
HTTP是一个客户端终端(用户)和服务器端(网站)请求和应答的标准(TCP)。
HTTP请求/响应步骤:
1. 客户端连接到Web服务器
一个HTTP客户端,通常是浏览器,与Web服务器的HTTP端口(默认为80)建立一个TCP套接字连接。
2. 发送HTTP请求
通过TCP套接字,客户端向Web服务器发送一个文本的请求报文,一个请求报文由请求行、请求头部、空行和请求数据4部分组成。
3. 服务器接受请求并返回HTTP响应
Web服务器解析请求,定位请求资源。服务器将资源复本写到TCP套接字,由客户端读取。一个响应由状态行、响应头部、空行和响应数据4部分组成。
4. 释放连接TCP连接
若connection 模式为close,则服务器主动关闭TCP连接,客户端被动关闭连接,释放TCP连接;若connection 模式为keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求;
5. 客户端浏览器解析HTML内容
客户端浏览器首先解析状态行,查看表明请求是否成功的状态代码。然后解析每一个响应头,响应头告知以下为若干字节的HTML文档和文档的字符集。客户端浏览器读取响应数据HTML,根据HTML的语法对其进行格式化,并在浏览器窗口中显示。
浏览器和服务端通信都要遵循一个HTTP协议(消息的格式要求)
关于HTTP协议:
1. 浏览器往服务端发的叫 请求(request)
请求的消息格式:
请求方法 路径 HTTP/1.1\r\n
k1:v1\r\n
k2:v2\r\n
\r\n
请求数据
2. 服务端往浏览器发的叫 响应(response)
响应的消息格式:
HTTP/1.1 状态码 状态描述符\r\n
k1:v1\r\n
k2:v2\r\n
\r\n
响应正文
HTTP请求报文格式:
HTTP响应报文格式:
再回到我们刚才的程序,程序报错的原因是接收到了浏览器的访问报文请求,但是我们的服务器程序在响应的时候并没有按照HTTP响应格式(一个响应由状态行、响应头部、空行和响应数据4部分组成)进行回应,所以浏览器在处理服务器的响应的时候就会出错。
因此,我们要在发送给浏览器的响应中按照HTTP响应格式加上 状态行、响应头部、空行和响应数据 这四部分。
这时候,在浏览器上面就可以看到正确的页面了,并且可以调出Chrome的开发者工具查看到我们传过来的HTTP响应格式。
根据不同的路径返回不同的内容
细心的你可能会发现,现在无论我们输出什么样的路径,只要保持 IP 和端口号不变,浏览器页面显示的都是同样的内容,这不太符合我们日常的使用场景。
如果我想根据不同的路径返回不同的内容,应该怎么办呢?
这时候就需要我们把服务器收到的请求报文进行解析,读取到其中的访问路径。
观察收到的HTTP请求,会发现,它们的请求行、请求头部、请求数据是以 \r\n 进行分隔的,所以我们可以根据 \r\n 对收到的请求进行分隔,取出我们想要的访问路径。
-
"""
-
完善的web服务端示例
-
根据不同的路径返回不同的内容
-
"""
-
-
import
socket
-
-
# 生成socket实例对象
-
sk
=
socket
.
socket
()
-
# 绑定IP和端口
-
sk
.
bind
((
"127.0.0.1"
,
8001
))
-
# 监听
-
sk
.
listen
()
-
-
# 写一个死循环,一直等待客户端来连接
-
while
1
:
-
# 获取与客户端的连接
-
conn
,
_
=
sk
.
accept
()
-
# 接收客户端发来消息
-
data
=
conn
.
recv
(
8096
)
-
# 把收到的数据转成字符串类型
-
data_str
=
str
(
data
,
encoding
=
"utf-8"
)
# bytes("str", enconding="utf-8")
-
# print(data_str)
-
# 用\r\n去切割上面的字符串
-
l1
=
data_str
.
split
(
"\r\n"
)
-
# l1[0]获得请求行,按照空格切割上面的字符串
-
l2
=
l1
[
0
].
split
()
-
# 请求行格式为:请求方法 URL 协议版本,因此 URL 是 l2[1]
-
url
=
l2
[
1
]
-
# 给客户端回复消息
-
conn
.
send
(
b
'http/1.1 200 OK\r\ncontent-type:text/html; charset=utf-8\r\n\r\n'
)
-
# 想让浏览器在页面上显示出来的内容都是响应正文
-
-
# 根据不同的url返回不同的内容
-
if
url
==
"/yimi/"
:
-
response
=
b
'
hello yimi!
'
-
elif
url
==
"/xiaohei/"
:
-
response
=
b
'
hello xiaohei!
'
-
else
:
-
response
=
b
'
404! not found!
'
-
conn
.
send
(
response
)
-
# 关闭
-
conn
.
close
()
-
sk
.
close
()
这时候,我们访问不同的路径,例如 http://127.0.0.1:8001/yimi/ http://127.0.0.1:8001/xiaohei/ 会在浏览器上显示不一样的内容
可以看到,我们现在的程序逻辑不是很清晰,我们可以改一下,url 用一个列表存起来,url 对应的响应分别写成一个个函数,通过函数调用进行 url 访问,你会发现,这跟某个框架的处理方式很像很像(偷笑罒ω罒~~~)
-
"""
-
完善的web服务端示例
-
函数版根据不同的路径返回不同的内容
-
进阶函数版 不写if判断了,用url名字去找对应的函数名
-
"""
-
-
import
socket
-
-
# 生成socket实例对象
-
sk
=
socket
.
socket
()
-
# 绑定IP和端口
-
sk
.
bind
((
"127.0.0.1"
,
8001
))
-
# 监听
-
sk
.
listen
()
-
-
# 定义一个处理/yimi/的函数
-
def
yimi
(
url
):
-
ret
=
'
hello {}
'
.
format
(
url
)
-
# 因为HTTP传的是字节,所以要把上面的字符串转成字节
-
return
bytes
(
ret
,
encoding
=
"utf-8"
)
-
-
-
# 定义一个处理/xiaohei/的函数
-
def
xiaohei
(
url
):
-
ret
=
'
hello {}
'
.
format
(
url
)
-
return
bytes
(
ret
,
encoding
=
"utf-8"
)
-
-
-
# 定义一个专门用来处理404的函数
-
def
f404
(
url
):
-
ret
=
"
你访问的这个{} 找不到
"
.
format
(
url
)
-
return
bytes
(
ret
,
encoding
=
"utf-8"
)
-
-
-
url_func
=
[
-
(
"/yimi/"
,
yimi
),
-
(
"/xiaohei/"
,
xiaohei
),
-
]
-
-
-
# 写一个死循环,一直等待客户端来连我
-
while
1
:
-
# 获取与客户端的连接
-
conn
,
_
=
sk
.
accept
()
-
# 接收客户端发来消息
-
data
=
conn
.
recv
(
8096
)
-
# 把收到的数据转成字符串类型
-
data_str
=
str
(
data
,
encoding
=
"utf-8"
)
# bytes("str", enconding="utf-8")
-
# print(data_str)
-
# 用\r\n去切割上面的字符串
-
l1
=
data_str
.
split
(
"\r\n"
)
-
# print(l1[0])
-
# 按照空格切割上面的字符串
-
l2
=
l1
[
0
].
split
()
-
url
=
l2
[
1
]
-
# 给客户端回复消息
-
conn
.
send
(
b
'http/1.1 200 OK\r\ncontent-type:text/html; charset=utf-8\r\n\r\n'
)
-
# 想让浏览器在页面上显示出来的内容都是响应正文
-
-
# 根据不同的url返回不同的内容
-
# 去url_func里面找对应关系
-
for
i
in
url_func
:
-
if
i
[
0
]
==
url
:
-
func
=
i
[
1
]
-
break
-
# 找不到对应关系就默认执行f404函数
-
else
:
-
func
=
f404
-
-
# 拿到函数的执行结果
-
response
=
func
(
url
)
-
# 将函数返回的结果发送给浏览器
-
conn
.
send
(
response
)
-
# 关闭连接
-
conn
.
close
()
返回具体的 HTML 页面
现在,你可能会在想,目前我们想要返回的内容是通过函数进行返回的,返回的都是一些简单地字节,如果我想要返回一个已经写好的精美的 HTML 页面应该怎么办呢?
我们可以把写好的 HTML 页面以二进制的形式读取进来,返回给浏览器,浏览器再进行解析,这就可以啦!
-
"""
-
完善的web服务端示例
-
函数版根据不同的路径返回不同的内容
-
进阶函数版 不写if判断了,用url名字去找对应的函数名
-
返回html页面
-
"""
-
-
import
socket
-
-
# 生成socket实例对象
-
sk
=
socket
.
socket
()
-
# 绑定IP和端口
-
sk
.
bind
((
"127.0.0.1"
,
8001
))
-
# 监听
-
sk
.
listen
()
-
-
-
# 定义一个处理/yimi/的函数
-
def
yimi
(
url
):
-
# 以二进制的形式读取
-
with
open
(
"yimi.html"
,
"rb"
)
as
f
:
-
ret
=
f
.
read
()
-
return
ret
-
-
-
# 定义一个处理/xiaohei/的函数
-
def
xiaohei
(
url
):
-
with
open
(
"xiaohei.html"
,
"rb"
)
as
f
:
-
ret
=
f
.
read
()
-
return
ret
-
-
-
# 定义一个专门用来处理404的函数
-
def
f404
(
url
):
-
ret
=
"
你访问的这个{} 找不到
"
.
format
(
url
)
-
return
bytes
(
ret
,
encoding
=
"utf-8"
)
-
-
-
# 用户访问的路径和后端要执行的函数的对应关系
-
url_func
=
[
-
(
"/yimi/"
,
yimi
),
-
(
"/xiaohei/"
,
xiaohei
),
-
]
-
-
-
# 写一个死循环,一直等待客户端来连我
-
while
1
:
-
# 获取与客户端的连接
-
conn
,
_
=
sk
.
accept
()
-
# 接收客户端发来消息
-
data
=
conn
.
recv
(
8096
)
-
# 把收到的数据转成字符串类型
-
data_str
=
str
(
data
,
encoding
=
"utf-8"
)
# bytes("str", enconding="utf-8")
-
# print(data_str)
-
# 用\r\n去切割上面的字符串
-
l1
=
data_str
.
split
(
"\r\n"
)
-
# print(l1[0])
-
# 按照空格切割上面的字符串
-
l2
=
l1
[
0
].
split
()
-
url
=
l2
[
1
]
-
# 给客户端回复消息
-
conn
.
send
(
b
'http/1.1 200 OK\r\ncontent-type:text/html; charset=utf-8\r\n\r\n'
)
-
# 想让浏览器在页面上显示出来的内容都是响应正文
-
-
# 根据不同的url返回不同的内容
-
# 去url_func里面找对应关系
-
for
i
in
url_func
:
-
if
i
[
0
]
==
url
:
-
func
=
i
[
1
]
-
break
-
# 找不到对应关系就默认执行f404函数
-
else
:
-
func
=
f404
-
# 拿到函数的执行结果
-
response
=
func
(
url
)
-
# 将函数返回的结果发送给浏览器
-
conn
.
send
(
response
)
-
# 关闭连接
-
conn
.
close
()
返回动态 HTML 页面
这时候,你可能又会纳闷,现在返回的都是些静态的、固定的 HTML 页面,如果我想返回一个动态的 HTML 页面,应该怎么办?
动态的网页,本质上都是字符串的替换,字符串替换发生服务端,替换完再返回给浏览器。
这里,我们通过返回一个当前时间,来模拟动态 HTML 页面的返回过程。
-
"""
-
完善的web服务端示例
-
函数版根据不同的路径返回不同的内容
-
进阶函数版 不写if判断了,用url名字去找对应的函数名
-
返回html页面
-
返回动态的html页面
-
"""
-
-
import
socket
-
import
time
-
-
# 生成socket实例对象
-
sk
=
socket
.
socket
()
-
# 绑定IP和端口
-
sk
.
bind
((
"127.0.0.1"
,
8001
))
-
# 监听
-
sk
.
listen
()
-
-
# 定义一个处理/yimi/的函数
-
def
yimi
(
url
):
-
with
open
(
"yimi.html"
,
"r"
,
encoding
=
"utf-8"
)
as
f
:
-
ret
=
f
.
read
()
-
# 得到替换后的字符串
-
ret2
=
ret
.
replace
(
"@@xx@@"
,
str
(
time
.
ctime
()))
-
return
bytes
(
ret2
,
encoding
=
"utf-8"
)
-
-
-
# 定义一个处理/xiaohei/的函数
-
def
xiaohei
(
url
):
-
with
open
(
"xiaohei.html"
,
"rb"
)
as
f
:
-
ret
=
f
.
read
()
-
return
ret
-
-
-
# 定义一个专门用来处理404的函数
-
def
f404
(
url
):
-
ret
=
"你访问的这个{} 找不到"
.
format
(
url
)
-
return
bytes
(
ret
,
encoding
=
"utf-8"
)
-
-
-
url_func
=
[
-
(
"/yimi/"
,
yimi
),
-
(
"/xiaohei/"
,
xiaohei
),
-
]
-
-
-
# 写一个死循环,一直等待客户端来连我
-
while
1
:
-
# 获取与客户端的连接
-
conn
,
_
=
sk
.
accept
()
-
# 接收客户端发来消息
-
data
=
conn
.
recv
(
8096
)
-
# 把收到的数据转成字符串类型
-
data_str
=
str
(
data
,
encoding
=
"utf-8"
)
# bytes("str", enconding="utf-8")
-
# print(data_str)
-
# 用\r\n去切割上面的字符串
-
l1
=
data_str
.
split
(
"\r\n"
)
-
# print(l1[0])
-
# 按照空格切割上面的字符串
-
l2
=
l1
[
0
].
split
()
-
url
=
l2
[
1
]
-
# 给客户端回复消息
-
conn
.
send
(
b
'http/1.1 200 OK\r\ncontent-type:text/html; charset=utf-8\r\n\r\n'
)
-
# 想让浏览器在页面上显示出来的内容都是响应正文
-
-
# 根据不同的url返回不同的内容
-
# 去url_func里面找对应关系
-