跳转至

第11章 Web的概念与原理

11.1 Web 概念与开发技术

11.1.1 Web 的概念

  • Web
  • 是万维网(World Wide Web)的简称
  • 是一个通过互联网(Internet)进行访问的由大量相互链接的超文件组成的系统
  • 是一个客户机/服务器模式的分布式系统
    • 客户端:浏览器、移动APP
    • 浏览器/服务器
    • 服务器端
  • Web核心技术
  • HTML:用于定义超文本文档的结构和格式
  • URI:用于唯一的定位Web中的每个资源,包括文档、图片、视频、声音等等
  • HTTP:用于规定客户端和服务器之间的交互、通信标准

  • Web 2.0

    • Web的应用趋于交互化、社交化
    • 以互动、分享和关系为核心
    • 典型的Web 2.0 应用包括博客、微博客、短视频、百科站点、社交网络、即时通讯等
    • 影响
    • 巨大的业务数据
    • 深深改变了人们的生活方式
    • 巨大的商业价值
    • 技术特点
    • Web 2.0本质上并有引入革命性的技术,其核心依旧是HTML、URI和HTTP协议
    • 服务器端应用程序起着更为核心的作用,Web页面不再是静态的
    • 要求强大的数据库系统和业务逻辑系统的支持,与传统软件的应用服务器越来越相似
  • Web 3.0

    • 超高的带宽
    • 人工智能技术的应用
    • 云计算
    • 语义网技术
    • 虚拟现实/增强现实
  • Web的工作原理



11.1.2 Web 页面的访问过程

  • 静态网页的运行过程



  • 动态网页的运行过程


11.1.3 Web 开发技术栈


11.2 统一资源标识符

  • URI
  • 全称是通一资源标识符(Uniform Resource Identifie),用来唯一地标识一个资源
  • 不要求其所标识的资源是网络资源
  • URL
  • 统一资源定位符(Uniform Resource Locator,URL)
  • URN
  • 统一资源名称(Uniform Resource Name,URN)
  • 利用命名空间来确保URN的合局唯一性,其形式为urn:<NID>:<NSS>
  • 例:urn:isbn:0451450523,表示一本书的编号

11.2.1 统一资源定位符

  • URL的标准格式
  • 端口号可以省略,默认取值为80;文件名、查询、片段ID都是可选项
1
[协议类型]://[服务器地址]:[端口号]/[资源层级路径][文件名]?[查询]#[片段ID]
  • 例:
1
http://www.exmaple.com:9000/path/contents?key1=value1&key2=value2#anchorid

11.2.2 URL 的解析

urllib.parse

  • urllib.parse模块提供了解析或构造URL的功能
  • urlparse
  • urlunparse
  • urljoin
1
2
3
4
from urllib.parse import urlparse, parse_qs
url = 'http://www.exmaple.com:9000/path/contents?key1=value1&key2=value21&key2=value22#anchorid'
rst = urlparse(url)
rst.scheme
1
'http'
1
rst.hostname
1
'www.exmaple.com'
1
rst.port
1
9000
1
rst.netloc
1
'www.exmaple.com:9000'
1
rst.path
1
'/path/contents'
1
rst.query
1
'key1=value1&key2=value21&key2=value22'
1
rst.fragment
1
'anchorid'
1
parse_qs(rst.query)
1
{'key1': ['value1'], 'key2': ['value21', 'value22']}
  • urlunparse的功能与urlparse相反,是将各个组成部分组装为一个合法的URL
1
2
from urllib.parse import urlunparse
tuple(rst)
1
2
3
4
5
6
('http',
 'www.exmaple.com:9000',
 '/path/contents',
 '',
 'key1=value1&key2=value21&key2=value22',
 'anchorid')
1
urlunparse(tuple(rst))
1
'http://www.exmaple.com:9000/path/contents?key1=value1&key2=value21&key2=value22#anchorid'
  • urljoin的作用是将原URL中的服务器根路径与一个新的路径合并为一个新的URL
1
2
from urllib.parse import urljoin
urljoin(url, '/path1/contents1')
1
'http://www.exmaple.com:9000/path1/contents1'
1
urljoin(url, 'path1/contents1')
1
'http://www.exmaple.com:9000/path/path1/contents1'

11.3 超文本标记语言

11.3.1 HTML的结构

  • HTML示例

     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
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>Python编程</title>
    </head>
    <body>
        <div class="continer">
            <div class="box">
                <h1>课后作业提交</h1>
                <form action="/" method="post" accept-charset="utf-8">
                    <div>
                        <span>学号:</span><input type="text" name="usertag" />
                        <span></span>
                        <span>密码:</span><input type="password" name="password" />
                    </div>
                    <br>
                    <div>
                        <textarea name="code" id="codebox" rows="10" wrap="off">
                            代码拷到这里!
                        </textarea>
                    </div>
                    <br>
                    <div>
                        <input type="submit" value="提交代码" />
                    </div>
                </form>
            </div>
            <br>
        </div>
    </body>
    </html>
    

  • 常用 HTML 标签

标签 功能 常用子标签
html HTML 文档的最外层标签 head, body
head HTML 文档的头部 title, link, script, style, meta
body HTML 文档的主体 head 之外的大多数标签
title HTML 文档的标题
talbe 表格 tr, td
tr 表格的一行 td
td 表格的一个单元格 大多数能放入 body 的标签
form 表单 inputselect 等输入控件,table, div
textarea 文本域 ——
div 块元素,用于组织文档内容 大多数能放入 body的标签
span 行内块元素 a
ul 无序列表 li
ol 有序列表 li
li 列表元素 a, span
a 超链接 ——
img 图片 ——
br 换行 ——
h1-h6 六个级别的标题 a,span
link 用于链接外部样式表 ——
style 内部样式表 ——
script JavaScript 脚本 ——
audio 音频内容 ——
video 视频内容 ——
canvas 图形容器 ——
  • 页面显示如下



11.3.2 HTML 文档的修饰与控制



  • HTML样式控制
  • 层叠样式表(Cascading Style Sheets,CSS)
1
# 例 11-2
 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
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Python编程</title>
    <style>
        .continer {
            margin: 0 auto;
            width: 80%;
        }
        .box {
            margin: 0 auto;
            width: 100%;
            text-align: center;
        }
    </style>
</head>
<body>
    <div class="continer">
        <div class="box">
            <h1>课后作业提交</h1>
            <form action="/" method="post" accept-charset="utf-8">
                <div>
                    <span>学号:</span>
                    <input type="text" name="usertag" style="width: 20%;" />
                    <span style="width: 5%; display:inline-block"></span>
                    <span>密码:</span>
                    <input type="password" name="password" style="width: 20%;" />
                </div>
                <br>
                <div>
                    <textarea name="code" id="codebox" rows="10" wrap="off"
                          style="width: 100%; overflow-y:scroll; overflow:scroll;">
                        代码拷到这里!
                    </textarea>
                </div>
                <br>
                <div style="margin: 0 auto; text-align: center;">
                    <input type="submit" value="提交代码" 
                         style="width:100px; line-height:99px;display:inline-block" />
                </div>
            </form>
        </div>
    </div>
</body>
</html>

页面显示为:


  • HTML行为控制
  • JavaScript
1
# 例 11-3
 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
<!DOCTYPE html>
<html>
<head>
    ... 
    <script type="text/javascript">
        function clear_code(id) {
            if (document.getElementById(id).value == "代码拷到这里!") {
                document.getElementById(id).value = "";
            }
        }
    </script>
</head>
<body>
    <div class="continer">
        <div class="box">
            <h1>课后作业提交</h1>
            <form action="/" method="post" accept-charset="utf-8">
                ...
                <div>
                    <textarea name="code" id="codebox" rows="10" wrap="off"
                        style="width: 100%; overflow-y:scroll; overflow:scroll;"
                        onclick="clear_code(this.id)">
                        代码拷到这里!
                    </textarea>
                </div>
                ...
            </form>
        </div>
    </div>
</body>
</html>

页面显示为:


11.4 超文本传输协议

  • HTTP
  • Web服务器与客户端之间的传输HTML文档的协议
  • 处于OSI/ISO参考模型的最顶层应用层,在TCP/IP参考模型中位于传输层之上,通常基于TCP协议实现


  • HTTP协议的特点
  • 无连接性:一般情况下,为了节省传输时间降低网络开销,每次TCP连接只处理一个请求/响应过程,客户端接收到服务器的响应好TCP连接就被断开
  • 无状态性:无状态是指协议没有对事务处理的记忆能力,服务器需要使用其他的手段来判断不同请求是否来自相同的客户端
  • 媒体内容独立性:HTTP协议并不要求传输内容一定是HTML文档,实际上任何数据都可以通过HTTP协议传输,传输内容的类型和格式由MIME类型来指定

11.4.1 HTTP 请求

HTTP请求的格式



HTTP请求格式

请求方法

方法 描述
GET 请求服务器中的资源,请求的查询参数包含在URL之中
HEAD 类似于 GET 请求,只不过返回的响应中没有具体的内容,用于获取请求头中的信息,通常常并不单独使用
POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件),数据包含在请求正文之中。POST 请求往往意味着资源的创建或修改
PUT 从客户端向服务器传送的数据取代指定的文档的内容
DELETE 请求服务器删除指定的资源
PATCH 是对 PUT 方法的补充,用来对已知资源进行局部更新
CONNECT 将服务器作为代理来访问其他Web资源
OPTIONS 查看服务器的性能
TRACE 回溯服务器收到的请求,主要用于测试或诊断

协议版本

  • HTTP 1.0
  • 支持GET、POST和HEAD三种方法;传输内容不再仅限于HTML文档,可以利用Content-Type支持多种格式;支持浏览器缓存;
  • HTTP 1.1
  • 引入了持久连接(Persistent Connection),即一次TCP连接可以被多个HTTP请求重复使用;新增了多种请求方法类型;
  • HTTP 2.0
  • 增加了双工模式,即客户端同时发出多个请求的同时,服务器端也可以同时处理多个请求;增加了服务器推送功能。

HTTP请求头

Header 解释 示例
Accept 客户端能够接收的内容类型 Accept: text/plain, text/html
Accept-Charset 客户端能够接受的字符编码集 Accept-Charset: utf-8
Accept-Encoding 客户端能够支持的内容压缩编码类型 Accept-Encoding: compress, gzip
Accept-Language 客户端能够接受的语言 Accept-Language: en,zh
Cache-Control 指定请求和响应遵循的缓存机制 Cache-Control: no-cache
Connection 是否允许持久连接(HTTP 1.1默认允许) Connection: close
Cookie 客户端发出请求时,会把保存在相同请求域名下的
所有cookie值一起发送给web服务器
Cookie: user=name; type=vip;
Content-Length 请求正文的长度 Content-Length: 520
Date 请求发送的日期和时间 Date: Tue, 2 JUN 2020 12:15:36 GMT
Host 指定请求的服务器的域名和端口号 Host: www.baidu.com
Referer 上次请求的URL Referer: https://www.baidu.com/s?wd=http&ie=utf-8
User-Agent 关于客户端的一些信息 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4)

请求正文

  • GET请求的信息包含在URL之中,因此不包含请求正文
  • POST请求的信息则是包含在正文之中的

示例

1
2
3
4
5
6
7
8
9
GET / HTTP/1.1
Host: 47.100.5.4:9527
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Upgrade-Insecure-Requests: 1
Cookie: _p_ake_mcjuw_oxyuwk_ksnqjcl_skw_xk_ksms_plwkd_=eyJfcGVybWFuZW50Ijp0cnVlLCJsb2dpbl9rZXkiOiJsb2dpbnN1Y2Nlc3MifQ.XtW69g.2zbtCqYnQWMRq6iu0QU91HqxrII
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Safari/605.1.15
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
Connection: keep-alive

11.4.2 HTTP 响应

HTTP响应格式


状态码

  • HTTP响应的状态码是以1、2、3、4或5开头的三位整数
  • 1XX:提示信息,表示服务器收到请求,需要客户端继续执行操作
  • 2XX:响应成功,请求被成功接收并处理
  • 3XX:重定向
  • 4XX:客户端错误,请求包含错误或无法完成请求
  • 5XX:服务器错误

  • 常见的状态码

状态码 描述 含义
200 OK 响应成功
400 Bad Request HTTP请求格式不正确
404 Not Found 文件不存在
405 Method Not Allowed 服务器不支持请求方法
500 Internal Server Error 服务器内部错误

响应头

  • 常用的响应头信息
Header 解释 示例
Allow 服务器允许的请求方法 Allow: GET, POST, HEAD
Content-Encoding 响应正文的压缩编码类型 Content-Encoding: gzip
Content-Language 响应正文的语言 Content-Language: en,zh
Content-Length 响应正文的长度 Content-Length: 263
Content-Type 响应正文内容的MIME类型 Content-Type: text/html; charset=utf-8
Date 响应时间 Date: Tue, 2 JUN 2020 12:15:36 GMT
Expires 响应过期不再缓存的时间 Expires: WED, 3 JUN 2020 12:15:36 GMT
Last-Modified 请求资源的最后修改时间 Last-Modified: Tue, 2 JUN 2020 12:15:36 GMT
Location 重定向的URL Location: https://www.baidu.com
refresh 重定向后的页刷新时间 Refresh: 5; url=https://www.baidu.com/s?wd=http&ie=utf-8
Server Web服务器软件名 Server: Apache/1.3.27 (Unix) (Red-Hat/Linux)
Set-Cookie 设置 Cookie Set-Cookie: UserID=admin; Max-Age=3600; Version=1

MIME协议

  • MIME
  • Multipurpose Internet Mail Extension
  • HTTP请求和响应正文的MIME类型用请求头或响应头部的Content-Type来指定
  • MIME协议不是一个独立的协议,它可以用于各种应用层协议之上用于规范网络传输数据的格式

    • 它与SMTP协议相结合,使得Email能够发送各种格式的文件
  • 常用MIME类型

扩展名 MIME类型
avi video/x-msvideo
bin, exe application/octet-stream
bmp image/bmp
css text/css
gif image/gif
gz application/x-gzip
html, htm, c, txt text/html
jpeg, jpg image/jpeg
js application/x-javascript
mp3 audio/mpeg
mpeg video/mpeg
pdf application/pdf
tar application/x-tar
txt, c text/plain
wav audio/x-wav
zip application/zip

示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
HTTP/1.1 200 OK
Content-Length: 2994
Content-Type: text/html; charset=utf-8
Date: Tue, 02 Jun 2020 03:31:21 GMT
Server: waitress
Set-Cookie: _p_ake_mcjuw_oxyuwk_ksnqjcl_skw_xk_ksms_plwkd_=eyJfcGVybWFuZW50Ijp0cnVlLCJsb2dpbl9rZXkiOiJsb2dpbnN1Y2Nlc3MifQ.XtXICQ.KtDU8d0d--nYNjz0C2XP-viv98o; Expires=Fri, 03-Jul-2020 03:31:21 GMT; HttpOnly; Path=/

<!DOCTYPE html>
<html>
<head>
    ...
</head>
<body>
    ...
</body>
</html>

11.4.3 HTTP 协议解析

1
# 例 11-4
 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
from urllib.parse import parse_qs

class Parser:
    def __init__(self, content=b''):
        self.reset()
        self.append(content)

    def reset(self):                            # 初始化/重置解析器
        self.__dict__ = {}
        self._buff = b''                        # 缓冲区
        self.top = b''                          # 首行内容
        self.head = b''                         # 头部内容
        self.body = b''                         # 正文内容
        self._head_ok = False                   # 头部是否接收完毕
        self.head_dict = dict()                # 头部信息字典

    def content(self):                          # 获取全部协议数据
        return b''.join([self.top, b'\r\n', self.head,
                         b'\r\n'*2, self.body]).decode('utf-8')

    def append(self, recved):                   # 添加新的数据
        if self._head_ok:
            self.body = b''.join([self.body, recved])
        else:
            self._buff = b''.join([self._buff, recved])
            if b'\r\n\r\n' in recved:
                top_head, self.body = self._buff.split(b'\r\n\r\n', 1)
                self.top, self.head = top_head.split(b'\r\n', 1)
                self._head_ok = True
                self._buff = b''
                self._parse_top()
                self._parse_head()

    def _parse_top(self):                       # 解析协议首行
        items = self.top.decode('utf-8').split(' ')
        if items[0].startswith('HTTP'):         # 响应
            self.type = 'RESPONSE'
            self.version = items[0]             # 协议版本
            self.resp_status = items[1]         # 响应状态码
            self.resp_desc = items[2]           # 响应状态描述
        else:
            self.type = 'REQUEST'
            self.req_method = items[0]          # 请求方法
            self.req_url = items[1]             # 请求URL
            self.query_string = ''
            if '?' in self.req_url:
                self.req_url, self.query_string = self.req_url.split('?')
            self.version = items[2]             # 协议版本

    def _parse_head(self):                      # 解析头部
        items = self.head.decode('utf-8').split('\r\n')
        for item in items:
            key, value = self._cut_kv(item, ':')
            if key == '':
                continue
            self.head_dict[key.strip().lower()] = value.strip()
        if'Cookie' in self.head_dict:          # 解析cookie
            cookies = dict()
            for cookie in self.head_dict['Cookie'].split(';'):
                key, value = self._cut_kv(cookie, '=')
                if key == '':
                    continue
                cookies[key.strip()] = value.strip()
            self.head_dict['Cookie'] = cookies

    def _cut_kv(self, item, sym):
        if sym in item:
            key, value = item.split(sym, 1)
        else:
            key, value = item, True
        return key.strip(), value

    def cookie(self, key):                      # 获取cookie
        cookies = self.head_dict.get('Cookie', False)
        if not cookies:
            return False
        return cookies.get(key, False)

    def __getattr__(self, key):                 # 获取头部数据
        key = key.lower().replace('_', '-')
        return self.head_dict.get(key, False)

    def is_ok(self):                            # 数据接收是否完成
        if not self._head_ok:
            return False
        if self.type == 'REQUEST' and self.req_method == 'GET':
            return True
        if len(self.body) >= int(self.content_length):
            return True
        return False

    def req_params(self):                       # 解析请求参数
        if self.type == 'RESPONSE':
            return None
        if self.req_method == 'GET':
            return parse_qs(self.query_string)
        elif self.req_method == 'POST':
            return parse_qs(self.body.decode('utf-8'))
        return None

11.5 Web 服务器的工作原理

11.5.1 基于套接字的 Web 服务器端

1
# 例 11-5
 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
from socketserver import ThreadingTCPServer, StreamRequestHandler
from httplib import Parser

class TCPHandler(StreamRequestHandler):
    def __init__(self, *args, **kwargs):
        self.parser = Parser()
        super().__init__(*args, **kwargs)

    def handle(self):
        while True:                               # 通信循环
            data_recv = self.request.recv(1024)   # 接收数据
            self.parser.append(data_recv)
            if self.parser.is_ok():
                break
        print(f'{self.client_address} {self.parser.req_method} 请求')
        if self.parser.req_method == 'GET':
            send_data = self.do_GET()             # 处理GET请求
        elif self.parser.req_method == 'POST':
            send_data = self.do_POST()            # 处理POST请求
        else:
            send_data = self.do_error()           # 处理错误
        self.request.sendall(send_data)           # 发送数据
        self.request.close()

    def do_GET(self):                             # 处理GET请求
        html = self.load_file()
        if html is None:
            return self.do_error()
        else:
            body = html.encode('utf8')
            head = self.make_head('200', len(body))
            return head + body

    def do_POST(self):                            # 处理POST请求
        html = self.load_file()
        if html is None:
            return self.do_error()
        else:
            info = f'你的请求参数是:{self.parser.req_params()}'
            html = html.replace('<span id="info"/>', info)
            body = html.encode('utf8')
            head = self.make_head('200', len(body))
            return head + body

    def make_head(self, code, content_len):       # 生成响应头
        head = b''
        if code == '200':
            head += b'HTTP/1.1 200 OK\r\n'
        elif code == '404':
            head += b'HTTP/1.1 404 Not Found\r\n'
        head += f'Content-Length: {content_len}\r\n'.encode('utf-8')
        head += b'Content-Type: text/html; charset=utf-8\r\n\r\n'
        return head

    def load_file(self):                          # 加载文件
        try:
            req_url = self.parser.req_url
            path = '/index.html' if req_url == '/' else req_url
            with open(f'.{path}') as f:
                return f.read()
        except Exception:
            return None

    def do_error(self):                           # 处理错误请求
        return self.make_head('404', 18) + '404页面不存在'.encode('utf-8')

if __name__ == '__main__':
    addr = ('127.0.0.1', 9000)
    with ThreadingTCPServer(addr, TCPHandler) as server:
        server.allow_reuse_address = True
        print(f'HTTP服务器启动: http://{addr[0]}:{addr[1]}')
        server.serve_forever()

11.5.2 简单Web服务器

1
# 例 11-6
 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
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs

class RequestHandler(BaseHTTPRequestHandler):
    def do_GET(self):                             # 响应GET请求
        html = self.load_file()
        if html is not None:
            self.make_response(html.encode('utf-8'))
        else:
            self.send_error(404, f'Page not found!')

    def do_POST(self):                            # 响应POST请求
        html = self.load_file()
        if html is not None:
            query = self.rfile.read(int(self.headers['content-length']))
            params = parse_qs(query.decode("utf-8"))
            html = html.replace('<span id="info"/>', str(params))
            self.make_response(html.encode('utf-8'))
        else:
            self.send_error(404, f'Page not found!')

    def load_file(self):                          # 加载文件
        try:
            path = '/index.html' if self.path == '/' else self.path
            with open(f'.{path}') as f:
                return f.read()
        except Exception:
            return None

    def make_response(self, body):                # 构造HTTP响应
        self.send_response(200)
        self.send_header('Content-type', 'text/html')
        self.send_header('Content-Length', len(body))
        self.end_headers()
        self.wfile.write(body)

if __name__ == '__main__':
    addr = ('127.0.0.1', 9000)
    with ThreadingHTTPServer(addr, RequestHandler) as server:
        server.allow_reuse_address = True
        print(f'服务器启动: http://{addr[0]}:{addr[1]}')
        server.serve_forever()

11.6 Web 客户端的工作原理

  • 注意:本小节代码的运行首先需要运行一个Web服务器

11.6.1 基于套接字的 Web 客户端

  • GET请求
1
# 例 11-7
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import socket
skt = socket.socket()
host = '127.0.0.1'
skt.connect((host, 9000))

request = f"GET / HTTP/1.1\r\nhost:{host}\r\n\r\n"

print('HTTP请求:', '-' * 20)
print(request)

request = request.encode('utf-8')
skt.send(request)

response = b''
while True:
    resp = skt.recv(1024)
    if not resp:
        break
    response += resp
skt.close()

print('HTTP响应:', '-' * 20)
print(response.decode('utf-8'))
1
2
3
HTTP请求: --------------------
GET / HTTP/1.1
host:127.0.0.1


HTTP响应: -------------------- HTTP/1.1 200 OK Content-Length: 1675 Content-Type: text/html; charset=utf-8

 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
48
49
50
51
52
53
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Python编程</title>
    <style>
        .continer {
            margin: 0 auto;
            width: 80%;
        }

        .box {
            margin: 0 auto;
            width: 100%;
            text-align: center;
        }
    </style>
    <script type="text/javascript">
        function clear_code(id) {
            if (document.getElementById(id).value == "代码拷到这里!") {
                document.getElementById(id).value = "";
            }
        }
    </script>
</head>

<body>
    <div class="continer">
        <div class="box">
            <h1>课后作业提交</h1>
            <form action="/" method="post" accept-charset="utf-8">
                <div>
                    <span>学号:</span>
                    <input type="text" name="usertag" style="width: 20%;" />
                    <span style="width: 5%; display:inline-block"></span>
                    <span>密码:</span>
                    <input type="password" name="password" style="width: 20%;" />
                </div><br>
                <div>
                    <textarea name="code" id="codebox" style="width:100%;overflow-y:scroll;overflow:scroll;" rows="10"
                        wrap="off" onclick="clear_code(this.id)">代码拷到这里!</textarea>
                </div><br>
                <div style="margin: 0 auto; text-align: center;">
                    <input type="submit" value="提交代码" style="width:100px; line-height:99px;display:inline-block" />
                </div>
            </form>
        </div>
        <div><span id="info" /></div>
    </div>
</body>

</html>
  • POST请求
1
# 例 11-8
 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
import socket
from urllib.parse import quote

skt = socket.socket()
host = '127.0.0.1'
skt.connect((host, 9000))

code = '''print("Hello Web")'''
request_content = f'usertag=test&password=123456&code={quote(code)}\r\n'

request = 'POST / HTTP/1.1\r\n'
request += 'Content-Type: application/x-www-form-urlencoded\r\n'
request += f'Content-Length: {len(request_content.encode("utf-8"))}\r\n'
request += '\r\n'
request += request_content

print('HTTP请求:', '-' * 20)
print(request)

request = request.encode('utf-8')
skt.send(request)

response = b''
while True:
    resp = skt.recv(1024)
    if not resp:
        break
    response += resp
skt.close()

print('HTTP响应:', '-' * 20)
print(response.decode('utf-8'))
 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
HTTP请求: --------------------
POST / HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 64

usertag=test&password=123456&code=print%28%22Hello%20Web%22%29

HTTP响应: --------------------
HTTP/1.1 200 OK
Content-Length: 1675
Content-Type: text/html; charset=utf-8

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Python编程</title>
    <style>
        .continer {
            margin: 0 auto;
            width: 80%;
        }

        .box {
            margin: 0 auto;
            width: 100%;
            text-align: center;
        }
    </style>
    <script type="text/javascript">
        function clear_code(id) {
            if (document.getElementById(id).value == "代码拷到这里!") {
                document.getElementById(id).value = "";
            }
        }
    </script>
</head>

<body>
    <div class="continer">
        <div class="box">
            <h1>课后作业提交</h1>
            <form action="/" method="post" accept-charset="utf-8">
                <div>
                    <span>学号:</span>
                    <input type="text" name="usertag" style="width: 20%;" />
                    <span style="width: 5%; display:inline-block"></span>
                    <span>密码:</span>
                    <input type="password" name="password" style="width: 20%;" />
                </div><br>
                <div>
                    <textarea name="code" id="codebox" style="width:100%;overflow-y:scroll;overflow:scroll;" rows="10"
                        wrap="off" onclick="clear_code(this.id)">代码拷到这里!</textarea>
                </div><br>
                <div style="margin: 0 auto; text-align: center;">
                    <input type="submit" value="提交代码" style="width:100px; line-height:99px;display:inline-block" />
                </div>
            </form>
        </div>
        <div><span id="info" /></div>
    </div>
</body>

</html>

11.6.2 基于 http.client 的 Web 客户端

  • GET请求
1
# 例 11-9
1
2
3
4
5
6
7
8
import http.client

conn = http.client.HTTPConnection("127.0.0.1:9000")
conn.request("GET", '/')
resp = conn.getresponse()
html = resp.read().decode("utf-8")
conn.close()
print(html)
 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
48
49
50
51
52
53
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Python编程</title>
    <style>
        .continer {
            margin: 0 auto;
            width: 80%;
        }

        .box {
            margin: 0 auto;
            width: 100%;
            text-align: center;
        }
    </style>
    <script type="text/javascript">
        function clear_code(id) {
            if (document.getElementById(id).value == "代码拷到这里!") {
                document.getElementById(id).value = "";
            }
        }
    </script>
</head>

<body>
    <div class="continer">
        <div class="box">
            <h1>课后作业提交</h1>
            <form action="/" method="post" accept-charset="utf-8">
                <div>
                    <span>学号:</span>
                    <input type="text" name="usertag" style="width: 20%;" />
                    <span style="width: 5%; display:inline-block"></span>
                    <span>密码:</span>
                    <input type="password" name="password" style="width: 20%;" />
                </div><br>
                <div>
                    <textarea name="code" id="codebox" style="width:100%;overflow-y:scroll;overflow:scroll;" rows="10"
                        wrap="off" onclick="clear_code(this.id)">代码拷到这里!</textarea>
                </div><br>
                <div style="margin: 0 auto; text-align: center;">
                    <input type="submit" value="提交代码" style="width:100px; line-height:99px;display:inline-block" />
                </div>
            </form>
        </div>
        <div><span id="info" /></div>
    </div>
</body>

</html>
  • POST请求
1
# 例 11-10
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import http.client
from urllib.parse import urlencode
conn = http.client.HTTPConnection("127.0.0.1:9000")

post_data = {
    "usertag": "test",
    "password": '123456',
    'code': "print('Hello Web')"
}
head = {
    'Content-Type': 'application/x-www-form-urlencoded'
}
post_data = urlencode(post_data)
conn.request(method="POST", url='/', body=post_data.encode('utf-8'), headers=head)
resp = conn.getresponse()
html = resp.read().decode("utf-8")
conn.close()
print(html)
 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
48
49
50
51
52
53
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Python编程</title>
    <style>
        .continer {
            margin: 0 auto;
            width: 80%;
        }

        .box {
            margin: 0 auto;
            width: 100%;
            text-align: center;
        }
    </style>
    <script type="text/javascript">
        function clear_code(id) {
            if (document.getElementById(id).value == "代码拷到这里!") {
                document.getElementById(id).value = "";
            }
        }
    </script>
</head>

<body>
    <div class="continer">
        <div class="box">
            <h1>课后作业提交</h1>
            <form action="/" method="post" accept-charset="utf-8">
                <div>
                    <span>学号:</span>
                    <input type="text" name="usertag" style="width: 20%;" />
                    <span style="width: 5%; display:inline-block"></span>
                    <span>密码:</span>
                    <input type="password" name="password" style="width: 20%;" />
                </div><br>
                <div>
                    <textarea name="code" id="codebox" style="width:100%;overflow-y:scroll;overflow:scroll;" rows="10"
                        wrap="off" onclick="clear_code(this.id)">代码拷到这里!</textarea>
                </div><br>
                <div style="margin: 0 auto; text-align: center;">
                    <input type="submit" value="提交代码" style="width:100px; line-height:99px;display:inline-block" />
                </div>
            </form>
        </div>
        <div><span id="info" /></div>
    </div>
</body>

</html>

11.6.3 urllib.request与requests

GET请求

  • urllib.request
1
2
3
4
5
from urllib import request

resp = request.urlopen("http://127.0.0.1:9000/")
html = resp.read().decode("utf-8")
print(html)
 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
48
49
50
51
52
53
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Python编程</title>
    <style>
        .continer {
            margin: 0 auto;
            width: 80%;
        }

        .box {
            margin: 0 auto;
            width: 100%;
            text-align: center;
        }
    </style>
    <script type="text/javascript">
        function clear_code(id) {
            if (document.getElementById(id).value == "代码拷到这里!") {
                document.getElementById(id).value = "";
            }
        }
    </script>
</head>

<body>
    <div class="continer">
        <div class="box">
            <h1>课后作业提交</h1>
            <form action="/" method="post" accept-charset="utf-8">
                <div>
                    <span>学号:</span>
                    <input type="text" name="usertag" style="width: 20%;" />
                    <span style="width: 5%; display:inline-block"></span>
                    <span>密码:</span>
                    <input type="password" name="password" style="width: 20%;" />
                </div><br>
                <div>
                    <textarea name="code" id="codebox" style="width:100%;overflow-y:scroll;overflow:scroll;" rows="10"
                        wrap="off" onclick="clear_code(this.id)">代码拷到这里!</textarea>
                </div><br>
                <div style="margin: 0 auto; text-align: center;">
                    <input type="submit" value="提交代码" style="width:100px; line-height:99px;display:inline-block" />
                </div>
            </form>
        </div>
        <div><span id="info" /></div>
    </div>
</body>

</html>
  • requests
  • pip install requests
1
2
3
4
import requests

resp = requests.get("http://127.0.0.1:9000/")
print(resp.text)
 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
48
49
50
51
52
53
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Python编程</title>
    <style>
        .continer {
            margin: 0 auto;
            width: 80%;
        }

        .box {
            margin: 0 auto;
            width: 100%;
            text-align: center;
        }
    </style>
    <script type="text/javascript">
        function clear_code(id) {
            if (document.getElementById(id).value == "代码拷到这里!") {
                document.getElementById(id).value = "";
            }
        }
    </script>
</head>

<body>
    <div class="continer">
        <div class="box">
            <h1>课后作业提交</h1>
            <form action="/" method="post" accept-charset="utf-8">
                <div>
                    <span>学号:</span>
                    <input type="text" name="usertag" style="width: 20%;" />
                    <span style="width: 5%; display:inline-block"></span>
                    <span>密码:</span>
                    <input type="password" name="password" style="width: 20%;" />
                </div><br>
                <div>
                    <textarea name="code" id="codebox" style="width:100%;overflow-y:scroll;overflow:scroll;" rows="10"
                        wrap="off" onclick="clear_code(this.id)">代码拷到这里!</textarea>
                </div><br>
                <div style="margin: 0 auto; text-align: center;">
                    <input type="submit" value="提交代码" style="width:100px; line-height:99px;display:inline-block" />
                </div>
            </form>
        </div>
        <div><span id="info" /></div>
    </div>
</body>

</html>

POST请求

  • urllib.request
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from urllib import request
from urllib.parse import urlencode

post_data = {
    "usertag": "test",
    "password": '123456',
    'code': "print('Hello Web')"
}
head = {
    'Content-Type': 'application/x-www-form-urlencoded'
}

requ = request.Request("http://127.0.0.1:9000/", data=urlencode(post_data).encode('utf-8'), headers=head)

resp = request.urlopen(requ)
print(resp.read().decode("utf-8"))
 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
48
49
50
51
52
53
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Python编程</title>
    <style>
        .continer {
            margin: 0 auto;
            width: 80%;
        }

        .box {
            margin: 0 auto;
            width: 100%;
            text-align: center;
        }
    </style>
    <script type="text/javascript">
        function clear_code(id) {
            if (document.getElementById(id).value == "代码拷到这里!") {
                document.getElementById(id).value = "";
            }
        }
    </script>
</head>

<body>
    <div class="continer">
        <div class="box">
            <h1>课后作业提交</h1>
            <form action="/" method="post" accept-charset="utf-8">
                <div>
                    <span>学号:</span>
                    <input type="text" name="usertag" style="width: 20%;" />
                    <span style="width: 5%; display:inline-block"></span>
                    <span>密码:</span>
                    <input type="password" name="password" style="width: 20%;" />
                </div><br>
                <div>
                    <textarea name="code" id="codebox" style="width:100%;overflow-y:scroll;overflow:scroll;" rows="10"
                        wrap="off" onclick="clear_code(this.id)">代码拷到这里!</textarea>
                </div><br>
                <div style="margin: 0 auto; text-align: center;">
                    <input type="submit" value="提交代码" style="width:100px; line-height:99px;display:inline-block" />
                </div>
            </form>
        </div>
        <div><span id="info" /></div>
    </div>
</body>

</html>
  • requests
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import requests
from urllib.parse import urlencode

post_data = {
    "usertag": "test",
    "password": '123456',
    'code': "print('Hello Web')"
}
head = {
    'Content-Type': 'application/x-www-form-urlencoded'
}
res = requests.post("http://127.0.0.1:9000/", data=urlencode(post_data), headers=head)
print(res.text)  # 转为字典格式
 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
48
49
50
51
52
53
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Python编程</title>
    <style>
        .continer {
            margin: 0 auto;
            width: 80%;
        }

        .box {
            margin: 0 auto;
            width: 100%;
            text-align: center;
        }
    </style>
    <script type="text/javascript">
        function clear_code(id) {
            if (document.getElementById(id).value == "代码拷到这里!") {
                document.getElementById(id).value = "";
            }
        }
    </script>
</head>

<body>
    <div class="continer">
        <div class="box">
            <h1>课后作业提交</h1>
            <form action="/" method="post" accept-charset="utf-8">
                <div>
                    <span>学号:</span>
                    <input type="text" name="usertag" style="width: 20%;" />
                    <span style="width: 5%; display:inline-block"></span>
                    <span>密码:</span>
                    <input type="password" name="password" style="width: 20%;" />
                </div><br>
                <div>
                    <textarea name="code" id="codebox" style="width:100%;overflow-y:scroll;overflow:scroll;" rows="10"
                        wrap="off" onclick="clear_code(this.id)">代码拷到这里!</textarea>
                </div><br>
                <div style="margin: 0 auto; text-align: center;">
                    <input type="submit" value="提交代码" style="width:100px; line-height:99px;display:inline-block" />
                </div>
            </form>
        </div>
        <div><span id="info" /></div>
    </div>
</body>

</html>

11.7 WebSocket 协议 *

  • HTTP协议的缺点
  • 服务器不能主动向客户端发送消息

11.7.1 WebSocket 的工作过程

  • 握手
  • 基于HTTP协议
  • 协议升级
  • 通信
  • 双向通信

11.7.2 握手

  • WebSocket 握手请求
  • 重要的几个请求头
    • Connection: 取值必须为Upgrade表示客户端希望升级协议
    • Upgrade: 取值必须为websocket表示客户端希望升级的协议为 WebSocket 协议
    • Sec-Websocket-Key: 客户端随机生成的一串字符
    • Sec-WebSocket-Version: WebSocket 协议版本,RFC6455 规定该值必须为 13


  • 示例
1
2
3
4
5
GET / HTTP/1.1
Connection:Upgrade
Upgrade:websocket
Host: 127.0.0.1:9000
Origin: null Sec-Websocket-Key:wxUqKAAwIh+5ZR19JCA+jw== Sec-WebSocket-Version:13
  • WebSocket 握手响应
  • 响应代码为 101,表示服务器同意客户端协议转换请求
  • 握手响应中同样包含了特殊的响应头 和取值,主要有三个ConnectionUpgradeSec-WebSocket-Accept
    • ConnectionUpgrade与请求头一致
  • Sec-WebSocket-Accept
  • 将请求中Sec-Websocket-Key的值与“Magic String”拼接
    • 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
  • SHA-1 编码
  • Base64 编码


  • 示例
1
2
3
4
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 2k2O9g8lUREQjApXs63/dmxhH7U=
  • WebSocket 握手的请求解析与响应构造
1
# 例 11-13
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from base64 import b64encode
from hashlib import sha1

def hand_shake(conn, http_parser):               # 握手
    http_parser.reset()
    while True:                                  # 通信循环
        data_recv = conn.recv(1024)              # 接收数据
        http_parser.append(data_recv)            # 解析HTTP请求
        if http_parser.is_ok(): break
    send_data = handshake_resp(http_parser.sec_websocket_key)
    conn.send(send_data.encode('utf-8'))

def handshake_resp(key):                         # 构造握手的HTTP响应
    magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' # Magic String   
    hashed = sha1(key.encode('utf-8') + magic_string.encode('utf-8'))
    resp_key = b64encode(hashed.digest()).strip().decode('utf-8')
    response = ["HTTP/1.1 101 Switching Protocols\r\n",
                "Upgrade: websocket\r\n", 
                "Connection: Upgrade\r\n",
                f"Sec-WebSocket-Accept: {resp_key}\r\n", "\r\n"]
    return ''.join(response)

11.7.3 WebSocket 协议解析

  • 协议标识为ws
  • 例: ws://www.test.com/websocket
  • WebSocket 协议中数据传输的单位为帧(Frame),每帧能够携带的最大数据量为 264 − 1 字节


11.7.4 WebSocket 服务器

  • 服务器端
1
# 例 11-16
 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
from socketserver import ThreadingTCPServer, StreamRequestHandler
from httplib import Parser
from websocketlib import WSParser, WSBuilder, hand_shake

class TCPHandler(StreamRequestHandler):
    def __init__(self, *args, **kwargs):
        self.http_parser = Parser()                    # HTTP解析器
        self.ws_parser = WSParser()                    # WebSocket解析器
        self.ws_builder = WSBuilder()                  # WebSocket构造器
        super().__init__(*args, **kwargs)

    def handle(self):
        hand_shake(self.request, self.http_parser)     # 握手
        self.ws_parser.init(self.request)
        self.ws_builder.init(self.request)
        i = 1
        while True:
            self.ws_parser.parse()                     # 接收并解析WS消息
            print(f'接收到客户端消息:{self.ws_parser.data}')
            send_data = f'msg-第{i:>2}次交互'       
            send_data = send_data.encode('utf-8')
            self.ws_builder.build(text_data=send_data) # 发送第一条消息
            send_data = f'msg-服务器收到:{self.ws_parser.data}'
            if self.ws_parser.data == '':
                send_data = '服务器收到空消息,连接关闭!'
            send_data = send_data.encode('utf-8')
            self.ws_builder.build(text_data=send_data) # 发送第二条消息
            if self.ws_parser.data == '': break
            i += 1

if __name__ == '__main__':
    addr = ('127.0.0.1', 9000)
    with ThreadingTCPServer(addr, TCPHandler) as server:
        server.allow_reuse_address = True
        print(f'WebSocket服务器启动: ws://{addr[0]}:{addr[1]}')
        server.serve_forever()
  • 基于网页的客户端
1
# 例 11-17
 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
<html>
<head>
    <title>WebSocket客户端</title>
    <script type="text/javascript">
        var ws_client;
        ws_client = new WebSocket("ws://127.0.0.1:9000/");
        ws_client.onopen = function () {           // 握手成功回调函数
            print('--- 握手成功!')
        };
        ws_client.onmessage = function (e) {       // 接收消息回调函数
            print("<<< 收到消息: " + e.data);
        };
        ws_client.onerror = function(e) {          // 运行错误回调函数
            print(e.value);
        };
        function send_msg() {                      // 发送消息
            print("------------------")
            var msg = document.getElementById("input");
            ws_client.send(msg.value);
            print(">>> 发送消息:" + msg.value);
            msg.value = "";
            msg.focus();
        }
        function print(str) {                      // 输出消息
            var info = document.getElementById("info");
            info.innerHTML = str + "<br>" + info.innerHTML;
        }
    </script>
</head>
<body>
    <input type="text" id="input">
    <button onclick="send_msg()">发送消息</button>
    <div id="info"></div>
</body>
</html>