14_Web服务器-并发服务器

1.服务器概述

    1.硬件服务器(IBM,HP): 能够给用户提供服务的主机,集群

    2.软件服务器(HTTPserver Apache Tomcat IIS Nginx)
        软件服务器是一个基于网络服务器,能够接收用户请求并给出响应的程序或者架构
        软件服务器是一个能在后端提供网络功能逻辑处理数据处理的程序或者架构

    3.服务器架构
        c/s(客户端client服务器server): b/s架构充分发挥了PC机的性能
        b/s(浏览器browser服务器server): 隶属于c/s架构,b/s架构统一了应用的接口

    4.服务器追求: 处理速度快,数据更安全,并发量大
        硬件: 更高配置,更多主机,集成,分布
        软件: 程序占有更少的资源,更流畅的运行,处理更多的并发

2.服务器模型

    1.基本的服务器模型
        1.循环服务器模型
            模式: 单线程程序,循环接收连接或者请求然后处理,处理后继续循环
            缺点: 不能同时处理多个客户端的并行,不允许某个客户端长期占有服务器
            优点: 结构比较简单,适用于UDP程序,要求处理请求可以很快完成
        2.IO多路复用服务器模型
            模式: 通过同时监控多个IO来达到IO并发的目的
            缺点: 也是单线程,不能长期阻塞,不适用处理大量CPU占有高的程序
            优点: 开销小,比较适合IO密集型的服务端程序
        3.并发服务器模型
            模式:
                每有一个客户端链接请求就创建一个新的进程或线程处理客户端的请求
                而主进程或主线程可以继续接收其它客户端的连接
            缺点: 资源消耗比较大,客户端需要长期占有服务器的情况
            优点: 多任务,高并发

    2.并发服务器-多进程(fork, multiprocessing)
        1.创建套接字,绑定,监听
        2.接收客户端请求
        3.创建子进程处理客户端请求,父进程继续准备接受新的客户端连接
        4.客户端退出,销毁相应的子进程

    3.并发服务器-多线程(threading)
        1.相比多进程服务器的优缺点
            缺点: 需要用到同步互斥,可能受到GIL的影响(网络IO线程并发可以)
            优点: 资源消耗比较少
        2.使用模块: threading socket
        3.步骤
            1.创建套接字,绑定,监听
            2.接收客户端连接请求,创建新的线程
            3.主线程继续接收下一个客户端连接请求,分支线程处理客户端事件
            4.处理事件结束退出线程关闭套接字

    4.并发服务器-多协程(gevent + monkey)

    5.使用集成模块完成网络并发-socketserver模块
        实现进程tcp并发的类
            'ForkingMixIn','TCPServer', 'StreamRequestHandler'
        实现进程udp并发的类
            'ForkingMixIn', 'UDPServer', 'DatagramRequestHandler',
        实现线程tcp并发的类
            'ThreadingMixIn', 'TCPServer', 'StreamRequestHandler'
        实现线程udp并发的类
            'ThreadingMixIn', 'UDPServer','DatagramRequestHandler',
        其他类
            'ForkingTCPServer' = 'ForkingMixIn' + 'TCPServer'
            'ForkingUDPServer' = 'ForkingMixIn' + 'UDPServer'
            'ThreadingTCPServer' = 'ThreadingMixIn' + 'TCPServer'
            'ThreadingUDPServer' = 'ThreadingMixIn' + 'UDPServer'

3.HTTP超文本传输协议概述

    1.在Web应用中,服务器把网页传给浏览器,实际上就是把网页的HTML代码发送给浏览器让浏览器显示出

    2.浏览器和服务器之间的传输协议是HTTP,HTTP是在网络上传输HTML的协议,用于浏览器和服务器的通信

    3.HTML是一种用来定义网页的文本,会HTML,就可以编写网页

    4.用途:
        1.网站中网页的传输和数据传输
        2.基于HTTP协议的编程传输数据

    5.特点:
        1.应用层协议,传输层使用TCP连接
        2.简单灵活接口使用方便
        3.几乎支持所有的数据类型
        4.是无状态的
        5.长连接(HTTP 1.1)

4.Web网站概述

1.WEB: 表示用户可以浏览的网页内容(HTML,CSS,JS)

2.浏览器访问网站过程

        1.域名解析

        2.向服务器发送三次握手

        3.客户端(浏览器)发起HTTP请求

        4.传输层使用TCP协议建立连接,层层打包将请求内容发送给服务器

        5.web服务器解包后解析HTTP请求,交给后端应用程序处理

        6.后端应用得到结果,通过web服务器回发给前端

        7.发送tcp四次回送

        8.浏览器访问网站流程图: https://www.processon.com/view/link/5efcac3507912929cb6b33df

3.请求(request)的结构格式

        1.请求行(确定具体的请求类型):
            GET(请求方法) /index.thml(请求资源) HTTP/1.1(协议版本)
            # 请求方法
            GET: 获取网络资源
            POST: 提交一定的附加数据,得到返回结果
            HEAD: 获取响应的头信息
            PUT: 更新服务器资源
            DELETE: 删除服务器资源
            TRACE: 用于测试
            CONNECT: 保留方法
            OPTIONS: 请求获取服务器性能和信息

        3.请求头(对请求内容的信息描述)
            # 选项: 值
            Accept: text/html,application/xhtml+xml  # 浏览器可以接收的文本格式
            Accept-Encoding: gzip, deflate  # 浏览器可以接收的压缩格式
            Accept-Language: zh-CN,zh;q=0.9  # 浏览器可以接收的语言
            Connection: keep-alive  # 长连接
            Cookie: BAIDUID=8A4DA4339C1B8A74DD251F7D9F834C76:FG=1
            Host: news.baidu.com  # 请求的服务器地址
            Referer: https://www.baidu.com/  # 防盗链,防止恶意请求
            Upgrade-Insecure-Requests: 1
            User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64)  # 发送请求的浏览器的版本

        3.空行

        4.请求体(具体请求参数):
            get请求: get参数 &a=1&b=2
            post请求: post提交的内容

4.响应(response)的结构格式

        1.响应行(反馈响应的情况): HTTP/1.1(协议版本) 200(响应码) OK(信息)
            # 响应码
            1xx: 提示信息,表示请求已经接收,正在处理
            2xx: 请求响应成功
            3xx: 重定向,完成任务需要其它操作
            4xx: 客户端错误
            5xx: 服务端错误

            # HTTP响应状态码划分
            100-199: 表示成功接收请求,要求客户端继续提交下一次请求才能完成整个处理过程
            200-299: 表示成功接收请求并已完成整个处理过程,常用200
            300-399: 未完成请求,客户需进一步细化请求,常用302,307,304
            400-499: 客户端的请求有错误,常用404
            500-599: 服务器端出现错误,常用500

            # 响应码示例
            200 成功
            401 没有访问权限
            404 资源不存在
            500 服务器发生未知错误
            503 服务器暂时无法执行

        2.响应头(对响应的具体描述)
            # 选项: 值
            Accept: text/html,application/xhtml+xml,application/xml
            Accept-Encoding: gzip, deflate
            Accept-Language: zh-CN,zh;q=0.9
            Connection: keep-alive
            Cookie: BAIDUID=8A4DA4339C1B8A74DD251F7D9F834C76:FG=1
            Host: news.baidu.com
            Referer: https://www.baidu.com/
            Upgrade-Insecure-Requests: 1
            User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64)

        3.空行

        4.响应体(具体返回给客户的内容): 文件,图片等

5.访问百度的信息详解: 描述信息/应答信息/请求信息

    1.描述信息

General  # 简单的描述
Request URL: https://www.baidu.com/  # 请求网址
Request Method: GET  # 请求方法
Status Code: 200 OK  # 状态代码 有数据 成功
Remote Address: 14.215.177.39:443  # 远程地址:服务器地址
Referrer Policy: no-referrer-when-downgrade  # 表示从https协议降为http协议时不发送referrer给跳转网站的服务器

    2.应答信息

Response Headers    view source  # 响应头  源始头
Bdpagetype: 1  # 百度服务器定义的特定值
Bdqid: 0xedfaa2040000c90c  # 百度服务器定义的特定值
Cache-Control: private  # 缓存控制:私有
Connection: Keep-Alive  # 连接:tpc socket的可靠连接
Content-Encoding: gzip  # 服务器通过这个头告诉浏览器数据的压缩格式
Content-Type: text/html  # 服务器通过这个头告诉浏览器回送数据的类型
Cxy_all: baidu+d83b58345e15ab078f816c529e1cef79
Date: Sun, 14 Apr 2019 02:47:43 GMT  # 时间
Expires: Sun, 14 Apr 2019 02:47:31 GMT  # 到期时间
Server: BWS/1.1  # 服务器通过这个头告诉浏览器服务器的型号
Set-Cookie: delPer=0; path=/; domain=.baidu.com
Set-Cookie: BDSVRTM=0; path=/
Set-Cookie: BD_HOME=0; path=/
Set-Cookie: H_PS_PSSID=1427_28827_21091_28768_28722_28558_28832_28585_28604_28606; path=/; domain=.baidu.com
"""Set-Cookie
    服务器与客户端验证身份的信息
    其中path=/; domain=.baidu.com为域名判断条件,如果满足该条件,才可以向服务器发送cookie
"""
Strict-Transport-Security: max-age=172800  # 通知浏览器,这个网站禁止使用HTTP方式加载,浏览器应该自动把所有尝试使用HTTP的请求自动替换为HTTPS请求
Transfer-Encoding: chunked  # Transfer-Encoding,是一个 HTTP 头部字段(响应头域),字面意思是传输编码,最新的 HTTP规范里,只定义了一种编码传输:分块编码(chunked),数据分解成一系列数据块,并以一个或多个块发送,这样服务器可以发送数据而不需要预先知道发送内容的总大小
Vary: Accept-Encoding  # 告诉代理服务器缓存两种版本的资源:压缩和非压缩
X-Ua-Compatible: IE=Edge,chrome=1  # 这是一个文档兼容模式的定义,主要用于加强代码对IE的兼容性,强制IE使用当前本地最新版标准模式渲染或者用chrome内核渲染


Response Headers    view parsed  # 响应头  已解析源始头
HTTP/1.1 200 OK  # 使用HTTP协议进行传输的中协议版本号为1.1请求状态码为200 OK
Bdpagetype: 1  # 百度服务器定义的特定值
Bdqid: 0xfa9b2b6a00029507  # 百度服务器定义的特定值
Cache-Control: private  # 对数据进行缓存
Connection: keep-alive  # # 连接:tpc socket的可靠连接
Content-Encoding: gzip  # 传输资源的文件压缩格式
Content-Type: text/html;charset=utf-8  # 传输资源的文件类型和编码类型
Date: Thu, 04 Jun 2020 08:56:33 GMT  # 资源的发送时间
Expires: Thu, 04 Jun 2020 08:56:29 GMT  # 资源过期时间
Server: BWS/1.1  # 服务器的系统版本,其中BWS为百度自有服务器(Baidu Web Server)
Set-Cookie: BDSVRTM=0; path=/
Set-Cookie: BD_HOME=1; path=/
Set-Cookie: H_PS_PSSID=31728_1460_31326_21078_31069_31762_31605_31322_30824; path=/; domain=.baidu.com
"""Set-Cookie
    服务器与客户端验证身份的信息
    其中path=/; domain=.baidu.com为域名判断条件,如果满足该条件,才可以向服务器发送cookie
"""
Strict-Transport-Security: max-age=172800
"""Strict-Transport-Security: max-age=172800
    使用https安全传输,防止在浏览器默认http请求和服务器https资源之间进行转换的时候,遭受中间人攻击
    浏览器收到该条响应字段,会在max-age秒之内自动将http转换成https的安全访问形式,过期后会重复之前的步骤
"""
Traceid: 1591260993276042138618058074865138832647  # 标记了浏览器发起的某个请求,这个id可在服务端从接收请求到 响应请求中流转,甚至接力传递给下游应用中流转,用于唯一标记和定外这次请求,一般用于定位请求日志
X-Ua-Compatible: IE=Edge,chrome=1  # # 这是一个文档兼容模式的定义,主要用于加强代码对IE的兼容性,强制IE使用当前本地最新版标准模式渲染或者用chrome内核渲染
Transfer-Encoding: chunked   # Transfer-Encoding,是一个 HTTP 头部字段(响应头域),字面意思是传输编码,最新的 HTTP规范里,只定义了一种编码传输:分块编码(chunked),数据分解成一系列数据块,并以一个或多个块发送,这样服务器可以发送数据而不需要预先知道发送内容的总大小

    3.请求信息

Request Headers view source  # 请求头  源始头
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
"""Accept
    客户端希望接受的文件类型,后面的q=0.9(0<q<1)代表权重系数,越大表示越期待
"""
Accept-Encoding: gzip, deflate, br  # 设置接受的编码格式
Accept-Language: zh-CN,zh;q=0.9  # 浏览器支持的语言为简体中文和中文,优先支持简体中文
Cache-Control: max-age=0  # 缓存控制
Connection: keep-alive
"""Connection: keep-alive
    该字段可以承载三种不同类型的标签:
    HTTP首部字段名,列出了只与此连接有关的首部任意标签值,用于描述此连接的非标准选项值(close)表示的是TCP连接是否关闭,HTTP数据传输采用的TCP三次握手协议
    如果值为close,则该次传输数据结束关闭TCP连接,下次传输数据,要重新进行三次握手
    如果值为默认的keep-alive,则保持数据通道,下次数据传输可以直接传递,省去三次握手的过程
    一般我们只用值close来控制TCP连接状态
"""
    Cookie: BIDUPSID=D97A5AC4617B3E6A2486D73D9F89F5AF; PSTM=1590403180; BAIDUID=D97A5AC4617B3E6AA36FE3B792FD9646:FG=1; BD_UPN=12314353; cflag=13%3A3; BD_HOME=1; H_PS_PSSID=31728_1460_31326_21078_31069_31762_31605_31322_30824; delPer=0; BD_CK_SAM=1; BDORZ=B490B5EBF6F3CD402E515D22BCDA1598; yjs_js_security_passport=64f53ded3515c88a93fa8fb6ab0e4dafa11418ec_1591253192_js; PSINO=5; COOKIE_SESSION=1708_0_9_9_24_19_0_3_9_7_4_6_1708_0_6_0_1591254838_0_1591254844%7C9%230_0_1590910191%7C1
    Host: www.baidu.com  # # 浏览器发给服务器,声明浏览器访问哪台主机
    Upgrade-Insecure-Requests: 1
"""Upgrade-Insecure-Requests: 1
    该字段用于http和https格式的过渡,一般如果请求http时,服务器资源为https,或是相反,浏览器会提示或报错
    而Google浏览器使用该字段向服务器表明,浏览器本身可以将http转换成https,服务器可以随意发,浏览器会自动将http://该成https://
"""
    User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36
"""User-Agent
    用户Agent代理,即用户发出HTTP请求的客户端程序(例:浏览器Mozilla/5.0版本,Chrome/73.0.3683.86版本,spider爬虫,Web robot等)
"""


Request Headers view parsed  # 请求头  已解析源始头
GET / HTTP/1.1  # 请求方式 请求目录 协议版本
Host: www.baidu.com  # 浏览器发给服务器,声明浏览器访问哪台主机
Connection: keep-alive  # 浏览器发给服务器,声明请求完后是断开链接还是维持链接
Cache-Control: max-age=0  # 缓存控制
Upgrade-Insecure-Requests: 1  # 浏览器发给服务器,声明自己支持这种操作,也就是我能读懂你服务器发过来的上面这条信息,并且在以后发请求的时候不用http而用https
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36  # 浏览器发给服务器,声明支持的浏览器及版本
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3  # 浏览器发给服务器,声明浏览器所支持的数据类型
Accept-Encoding: gzip, deflate, br  # 浏览器发给服务器,声明浏览器支持的编码类型
Accept-Language: zh-CN,zh;q=0.9  # 浏览器发给服务器,声明浏览器支持的语言
Cookie: BAIDUID=BF236BEC6E4EF4DCF1AD838C2D217739:FG=1; PSTM=1555138860; BIDUPSID=88B1B87B29B6B36C8E48F551D3574BFE; BD_UPN=12314353; BDORZ=B490B5EBF6F3CD402E515D22BCDA1598; H_PS_PSSID=1427_28827_21091_28768_28722_28558_28832_28585_28604_28606; BDUSS=HU1UVhKRnNDMEhiYjlOVjNPQUhYfkxlZ2xTejV-c0Y0c2RzUVFSQUxtTVBOZHBjSUFBQUFBJCQAAAAAAAAAAAEAAABZiE1gQW5nZWxzxbe6wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA-oslwPqLJcW; delPer=0; BD_CK_SAM=1; PSINO=6; H_PS_645EC=c374KRUZaisqZePbVCOq52p9mRyTUyW7oNYJp%2Bbf2HRkfC7pCZ%2F5tPvURnM; ZD_ENTRY=baidu; BD_HOME=1

6.Web静态服务器-单任务

# html文件夹网盘链接: https://pan.baidu.com/s/1wbZ92KLstff3GurLV_C0lg  密码: hp9o
# 下载完命令行解压后放到程序的同级路径下,解压命令: tar -zxvf 06_html.tar.gz -C ./
import socket
import re


def handle_request(client_socket):
    """为一个客户端进行服务"""
    # 1.接收浏览器发送过来的HTTP请求
    recv_data = client_socket.recv(1024).decode('utf-8', errors="ignore")
    request_header_lines = recv_data.splitlines()
    for line in request_header_lines:
        print(line)

    # GET /index.html HTTP/1.1
    http_request_line = request_header_lines[0]
    get_file_name = re.match("[^/]+(/[^ ]*)", http_request_line).group(1)
    print("file name is ===>%s" % get_file_name)  # for test

    # 如果没有指定访问哪个页面,则默认访问index.html
    # GET / HTTP/1.1
    if get_file_name == "/":
        get_file_name = DOCUMENTS_ROOT + "/index.html"
    else:
        get_file_name = DOCUMENTS_ROOT + get_file_name
    print("file name is ===2>%s" % get_file_name)  # for test

    # 2.返回HTTP格式的数据给浏览器
    try:
        f = open(get_file_name, "rb")
    except IOError:
        # 404表示没有这个页面
        response_headers = "HTTP/1.1 404 not found\r\n"
        response_headers += "\r\n"
        response_body = "====sorry, file not found====".encode("utf-8")
    else:
        # 2.1 准备发送给浏览器的数据-->header
        response_headers = "HTTP/1.1 200 OK\r\n"
        # 2.2 用一个空的行与body进行隔开
        response_headers += "\r\n"
        # 2.3 准备发送给浏览器的数据-->body
        response_body = f.read()
        f.close()
    finally:
        # 因为头信息在组织的时候,是按照字符串组织的,不能与以二进制打开文件读取的数据合并,因此分开发送
        # 先发送response的头信息
        client_socket.send(response_headers.encode("utf-8"))
        # 再发送body
        client_socket.send(response_body)
        # 关闭套接字
        client_socket.close()


def main():
    """作为程序的主控制入口"""
    # 1.创建数据流套接字
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 2.设置当服务器先close 即服务器端4次挥手之后资源能够立即释放,保证下次运行程序时可以立即绑定7890端口
    tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # 3.绑定服务器地址
    tcp_server_socket.bind(("", 7890))
    # 4.套接字由主动改为监听,并设置队列长度
    tcp_server_socket.listen(128)
    while True:
        # 5.等待客户端连接
        client_socket, client_addr = tcp_server_socket.accept()
        # 6.为客户端服务
        handle_request(client_socket)

    # 7.关闭监听套接字
    tcp_server_socket.close()


# 配置服务器静态资源路径
DOCUMENTS_ROOT = "./html"

if __name__ == "__main__":
    main()

7.Web静态服务器-多进程(multiprocessing)

# html文件夹网盘链接: https://pan.baidu.com/s/1wbZ92KLstff3GurLV_C0lg  密码: hp9o
# 下载完命令行解压后放到程序的同级路径下,解压命令: tar -zxvf 06_html.tar.gz -C ./
import socket
import multiprocessing
import re


class WSGIServer:
    """定义一个WSGI服务器的类"""
    def __init__(self, server_address):
        # 1.创建数据流套接字
        self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 2.设置当服务器先close 即服务器端4次挥手之后资源能够立即释放,保证下次运行程序时可以立即绑定7890端口
        self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # 3.绑定服务器地址
        self.tcp_server_socket.bind(server_address)
        # 4.套接字由主动改为监听,并设置队列长度
        self.tcp_server_socket.listen(128)

    def serve_forever(self):
        """循环运行服务器,等待客户端链接并为客户端服务"""
        while True:
            # 5.等待客户端链接
            client_socket, client_addr = self.tcp_server_socket.accept()
            # 6.为客户端服务
            new_process = multiprocessing.Process(target=self.handle_request, args=(client_socket,))
            new_process.start()

            # 因为子进程已经复制了父进程的套接字等资源,所以父进程调用close不会将他们对应的这个链接关闭的
            client_socket.close()

    def handle_request(self, client_socket):
        """用一个新的进程为客户端服务"""
        # 1.接收浏览器发送过来的HTTP请求
        recv_data = client_socket.recv(1024).decode('utf-8', errors="ignore")
        request_header_lines = recv_data.splitlines()
        for line in request_header_lines:
            print(line)

        # GET /index.html HTTP/1.1
        http_request_line = request_header_lines[0]
        get_file_name = re.match("[^/]+(/[^ ]*)", http_request_line).group(1)
        print("file name is ===>%s" % get_file_name)  # for test

        # 如果没有指定访问哪个页面,则默认访问index.html
        # GET / HTTP/1.1
        if get_file_name == "/":
            get_file_name = DOCUMENTS_ROOT + "/index.html"
        else:
            get_file_name = DOCUMENTS_ROOT + get_file_name
        print("file name is ===2>%s" % get_file_name)  # for test

        # 2.返回HTTP格式的数据给浏览器
        try:
            f = open(get_file_name, "rb")
        except IOError:
            # 404表示没有这个页面
            response_headers = "HTTP/1.1 404 not found\r\n"
            response_headers += "\r\n"
            response_body = "====sorry, file not found====".encode("utf-8")
        else:
            # 2.1 准备发送给浏览器的数据-->header
            response_headers = "HTTP/1.1 200 OK\r\n"
            # 2.2 用一个空的行与body进行隔开
            response_headers += "\r\n"
            # 2.3 准备发送给浏览器的数据-->body
            response_body = f.read()
            f.close()
        finally:
            # 因为头信息在组织的时候,是按照字符串组织的,不能与以二进制打开文件读取的数据合并,因此分开发送
            # 先发送response的头信息
            client_socket.send(response_headers.encode("utf-8"))
            # 再发送body
            client_socket.send(response_body)
            # 关闭套接字
            client_socket.close()


# 设定服务器的地址
SERVER_ADDR = ("", 7890)
# 设置服务器静态资源的路径
DOCUMENTS_ROOT = "./html"


def main():
    http_server = WSGIServer(SERVER_ADDR)
    print("web Server: Serving HTTP on port %d ...\n" % SERVER_ADDR[1])
    http_server.serve_forever()


if __name__ == "__main__":
    main()

8.Web静态服务器-多进程(os.fork)

# html文件夹网盘链接: https://pan.baidu.com/s/1wbZ92KLstff3GurLV_C0lg  密码: hp9o
# 下载完命令行解压后放到程序的同级路径下,解压命令: tar -zxvf 06_html.tar.gz -C ./
import socket
import os
import signal
import re


class WSGIServer:
    """定义一个WSGI服务器的类"""

    def __init__(self, server_address):
        # 1.创建数据流套接字
        self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 2.设置当服务器先close 即服务器端4次挥手之后资源能够立即释放,保证下次运行程序时可以立即绑定7890端口
        self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # 3.绑定服务器地址
        self.tcp_server_socket.bind(server_address)
        # 4.套接字由主动改为监听,并设置队列长度
        self.tcp_server_socket.listen(128)

    def serve_forever(self):
        """循环运行服务器,等待客户端链接并为客户端服务"""
        print("父进程PID[%s]等待客户端的链接" % os.getpid())
        while True:
            try:
                # 5.等待客户端链接
                client_socket, client_addr = self.tcp_server_socket.accept()
            except KeyboardInterrupt:  # 用户中断执行(通常是输入^C)
                raise
            except Exception:
                continue
            # 6.处理子进程,避免成为僵尸进程-->SIGCHLD: 子进程改变状态时,父进程会收到这个信号;SIG_IGN: 忽略这个信号
            signal.signal(signal.SIGCHLD, signal.SIG_IGN)
            # 7.父进程为客户端创建新的进程
            pid = os.fork()
            if pid < 0:
                print("创建子进程失败...")
                # 关闭客户端套接字
                client_socket.close()
            elif pid == 0:
                # 子进程中不需要使用监听套接字,因此关闭子进程中的监听套接字
                self.tcp_server_socket.close()
                # 8.为客户端服务
                self.handle_request(client_socket)
            else:
                # 父进程中不需要使用客户端套接字,因此关不父进程中的客户端套接字
                client_socket.close()
                continue

    def handle_request(self, client_socket):
        """用一个新的进程为客户端服务"""
        # 1.接收浏览器发送过来的HTTP请求
        recv_data = client_socket.recv(1024).decode('utf-8', errors="ignore")
        request_header_lines = recv_data.splitlines()
        for line in request_header_lines:
            print(line)

        # GET /index.html HTTP/1.1
        http_request_line = request_header_lines[0]
        get_file_name = re.match("[^/]+(/[^ ]*)", http_request_line).group(1)
        print("file name is ===>%s" % get_file_name)  # for test

        # 如果没有指定访问哪个页面,则默认访问index.html
        # GET / HTTP/1.1
        if get_file_name == "/":
            get_file_name = DOCUMENTS_ROOT + "/index.html"
        else:
            get_file_name = DOCUMENTS_ROOT + get_file_name
        print("file name is ===2>%s" % get_file_name)  # for test

        # 2.返回HTTP格式的数据给浏览器
        try:
            f = open(get_file_name, "rb")
        except IOError:
            # 404表示没有这个页面
            response_headers = "HTTP/1.1 404 not found\r\n"
            response_headers += "\r\n"
            response_body = "====sorry, file not found====".encode("utf-8")
        else:
            # 2.1 准备发送给浏览器的数据-->header
            response_headers = "HTTP/1.1 200 OK\r\n"
            # 2.2 用一个空的行与body进行隔开
            response_headers += "\r\n"
            # 2.3 准备发送给浏览器的数据-->body
            response_body = f.read()
            f.close()
        finally:
            # 因为头信息在组织的时候,是按照字符串组织的,不能与以二进制打开文件读取的数据合并,因此分开发送
            # 先发送response的头信息
            client_socket.send(response_headers.encode("utf-8"))
            # 再发送body
            client_socket.send(response_body)
            # 关闭套接字
            client_socket.close()


# 设定服务器的地址
SERVER_ADDR = ("", 7890)
# 设置服务器静态资源的路径
DOCUMENTS_ROOT = "./html"


def main():
    http_server = WSGIServer(SERVER_ADDR)
    print("web Server: Serving HTTP on port %d ...\n" % SERVER_ADDR[1])
    http_server.serve_forever()


if __name__ == "__main__":
    main()

9.Web静态服务器-多进程(集成模块 socketserver)

# html文件夹网盘链接: https://pan.baidu.com/s/1wbZ92KLstff3GurLV_C0lg  密码: hp9o
# 下载完命令行解压后放到程序的同级路径下,解压命令: tar -zxvf 06_html.tar.gz -C ./
from socketserver import ForkingMixIn
from socketserver import TCPServer
from socketserver import StreamRequestHandler
import re


# class WSGIServer(ForkingTCPServer):  # 写法一 'ForkingTCPServer' = 'ForkingMixIn' + 'TCPServer'
class WSGIServer(ForkingMixIn, TCPServer):  # 写法二
    """定义一个创建服务器类继承自模块中的创建服务器类"""
    pass


class HandleRequest(StreamRequestHandler):
    """定义一个处理请求类继承模块的处理请求类"""

    def handle(self):  # 固定的入口方法
        # 等待客户端连接 self.request等同于accept()创建一个新的客户端连接
        client_socket = self.request
        client_addr = self.request.getpeername()  # for test
        print("客户端 %s 连接上服务器" % str(client_addr))  # for test

        while True:
            # 1.接收浏览器发送过来的HTTP请求
            recv_data = client_socket.recv(1024).decode('utf-8', errors="ignore")
            request_header_lines = recv_data.splitlines()
            for line in request_header_lines:
                print(line)

            # GET /index.html HTTP/1.1
            http_request_line = request_header_lines[0]
            get_file_name = re.match("[^/]+(/[^ ]*)", http_request_line).group(1)
            print("file name is ===>%s" % get_file_name)  # for test

            # 如果没有指定访问哪个页面,则默认访问index.html
            # GET / HTTP/1.1
            if get_file_name == "/":
                get_file_name = DOCUMENTS_ROOT + "/index.html"
            else:
                get_file_name = DOCUMENTS_ROOT + get_file_name
            print("file name is ===2>%s" % get_file_name)  # for test

            # 2.返回HTTP格式的数据给浏览器
            try:
                f = open(get_file_name, "rb")
            except IOError:
                # 404表示没有这个页面
                response_body = "====sorry, file not found====".encode("utf-8")
                response_headers = "HTTP/1.1 404 not found\r\n"
                response_headers += "Content-Type: text/html; charset=utf-8\r\n"
                response_headers += "Content-Length: %d\r\n" % (len(response_body))
                response_headers += "\r\n"
            else:
                # 2.1 准备发送给浏览器的数据-->body
                response_body = f.read()
                f.close()
                # 2.2 准备发送给浏览器的数据-->header
                response_headers = "HTTP/1.1 200 OK\r\n"
                # 2.3 发送body数据长度,让服务器根据长度确认body数据已完成接收,实现长连接
                response_headers += "Content-Length: %d\r\n" % (len(response_body))
                # 2.4 用一个空的行与body进行隔开
                response_headers += "\r\n"
            finally:
                # 因为头信息在组织的时候,是按照字符串组织的,不能与以二进制打开文件读取的数据合并,因此分开发送
                # 先发送response的头信息
                client_socket.send(response_headers.encode("utf-8"))
                # 再发送body
                client_socket.send(response_body)
                # 长连接下不需要关闭套接字,而是在header头中声明要发送的body长度
                # client_socket.close()


# 设定服务器的地址
SERVER_ADDR = ("", 7890)
# 设置服务器静态资源的路径
DOCUMENTS_ROOT = "./html"


def main():
    http_server = WSGIServer(SERVER_ADDR, HandleRequest)
    print("web Server: Serving HTTP on port %d ...\n" % SERVER_ADDR[1])
    http_server.serve_forever()


if __name__ == "__main__":
    main()

10.Web静态服务器-多线程(threading)

# html文件夹网盘链接: https://pan.baidu.com/s/1wbZ92KLstff3GurLV_C0lg  密码: hp9o
# 下载完命令行解压后放到程序的同级路径下,解压命令: tar -zxvf 06_html.tar.gz -C ./
import socket
import threading
import re


class WSGIServer:
    """定义一个WSGI服务器的类"""
    def __init__(self, server_address):
        # 1.创建数据流套接字
        self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 2.设置当服务器先close 即服务器端4次挥手之后资源能够立即释放,保证下次运行程序时可以立即绑定7890端口
        self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # 3.绑定服务器地址
        self.tcp_server_socket.bind(server_address)
        # 4.套接字由主动改为监听,并设置队列长度
        self.tcp_server_socket.listen(128)

    def serve_forever(self):
        """循环运行服务器,等待客户端链接并为客户端服务"""
        while True:
            # 5.等待客户端链接
            client_socket, client_addr = self.tcp_server_socket.accept()
            # 6.为客户端服务
            new_thread = threading.Thread(target=self.handle_request, args=(client_socket,))
            new_thread.start()

            # 因为线程是共享同一个套接字,所以主线程不能关闭,否则子线程就不能再使用这个套接字
            # client_socket.close()

    def handle_request(self, client_socket):
        """用一个新的进程为客户端服务"""
        # 1.接收浏览器发送过来的HTTP请求
        recv_data = client_socket.recv(1024).decode('utf-8', errors="ignore")
        request_header_lines = recv_data.splitlines()
        for line in request_header_lines:
            print(line)

        # GET /index.html HTTP/1.1
        http_request_line = request_header_lines[0]
        get_file_name = re.match("[^/]+(/[^ ]*)", http_request_line).group(1)
        print("file name is ===>%s" % get_file_name)

        # 如果没有指定访问哪个页面,则默认访问index.html
        # GET / HTTP/1.1
        if get_file_name == "/":
            get_file_name = DOCUMENTS_ROOT + "/index.html"
        else:
            get_file_name = DOCUMENTS_ROOT + get_file_name
        print("file name is ===2>%s" % get_file_name)

        # 2.返回HTTP格式的数据给浏览器
        try:
            f = open(get_file_name, "rb")
        except IOError:
            # 404表示没有这个页面
            response_headers = "HTTP/1.1 404 not found\r\n"
            response_headers += "\r\n"
            response_body = "====sorry, file not found====".encode("utf-8")
        else:
            # 2.1 准备发送给浏览器的数据-->header
            response_headers = "HTTP/1.1 200 OK\r\n"
            # 2.2 用一个空的行与body进行隔开
            response_headers += "\r\n"
            # 2.3 准备发送给浏览器的数据-->body
            response_body = f.read()
            f.close()
        finally:
            # 因为头信息在组织的时候,是按照字符串组织的,不能与以二进制打开文件读取的数据合并,因此分开发送
            # 先发送response的头信息
            client_socket.send(response_headers.encode("utf-8"))
            # 再发送body
            client_socket.send(response_body)
            # 关闭套接字
            client_socket.close()


# 设定服务器的地址
SERVER_ADDR = ("", 7890)
# 设置服务器静态资源的路径
DOCUMENTS_ROOT = "./html"


def main():
    http_server = WSGIServer(SERVER_ADDR)
    print("web Server: Serving HTTP on port %d ...\n" % SERVER_ADDR[1])
    http_server.serve_forever()


if __name__ == "__main__":
    main()

11.Web静态服务器-多协程(gevent + monkey)

# html文件夹网盘链接: https://pan.baidu.com/s/1wbZ92KLstff3GurLV_C0lg  密码: hp9o
# 下载完命令行解压后放到程序的同级路径下,解压命令: tar -zxvf 06_html.tar.gz -C ./
import socket
import re
import gevent
from gevent import monkey

monkey.patch_all()


class WSGIServer:
    """定义一个WSGI服务器的类"""
    def __init__(self, server_address):
        # 1.创建数据流套接字
        self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 2.设置当服务器先close 即服务器端4次挥手之后资源能够立即释放,保证下次运行程序时可以立即绑定7890端口
        self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # 3.绑定服务器地址
        self.tcp_server_socket.bind(server_address)
        # 4.套接字由主动改为监听,并设置队列长度
        self.tcp_server_socket.listen(128)

    def serve_forever(self):
        """循环运行服务器,等待客户端链接并为客户端服务"""
        while True:
            # 5.等待客户端链接
            client_socket, client_addr = self.tcp_server_socket.accept()
            # 6.为客户端服务
            gevent.spawn(self.handle_request, client_socket)

    def handle_request(self, client_socket):
        """用协程为客户端服务"""
        # 1.接收浏览器发送过来的HTTP请求
        recv_data = client_socket.recv(1024).decode('utf-8', errors="ignore")
        request_header_lines = recv_data.splitlines()
        for line in request_header_lines:
            print(line)

        # GET /index.html HTTP/1.1
        http_request_line = request_header_lines[0]
        get_file_name = re.match("[^/]+(/[^ ]*)", http_request_line).group(1)
        print("file name is ===>%s" % get_file_name)

        # 如果没有指定访问哪个页面,则默认访问index.html
        # GET / HTTP/1.1
        if get_file_name == "/":
            get_file_name = DOCUMENTS_ROOT + "/index.html"
        else:
            get_file_name = DOCUMENTS_ROOT + get_file_name
        print("file name is ===2>%s" % get_file_name)

        # 2.返回HTTP格式的数据给浏览器
        try:
            f = open(get_file_name, "rb")
        except IOError:
            # 404表示没有这个页面
            response_headers = "HTTP/1.1 404 not found\r\n"
            response_headers += "\r\n"
            response_body = "====sorry, file not found====".encode("utf-8")
        else:
            # 2.1 准备发送给浏览器的数据-->header
            response_headers = "HTTP/1.1 200 OK\r\n"
            # 2.2 用一个空的行与body进行隔开
            response_headers += "\r\n"
            # 2.3 准备发送给浏览器的数据-->body
            response_body = f.read()
            f.close()
        finally:
            # 因为头信息在组织的时候,是按照字符串组织的,不能与以二进制打开文件读取的数据合并,因此分开发送
            # 先发送response的头信息
            client_socket.send(response_headers.encode("utf-8"))
            # 再发送body
            client_socket.send(response_body)
            # 关闭套接字
            client_socket.close()


# 设定服务器的地址
SERVER_ADDR = ("", 7890)
# 设置服务器静态资源的路径
DOCUMENTS_ROOT = "./html"


def main():
    http_server = WSGIServer(SERVER_ADDR)
    print("web Server: Serving HTTP on port %d ...\n" % SERVER_ADDR[1])
    http_server.serve_forever()


if __name__ == "__main__":
    main()

12.Web静态服务器-非阻塞IO(基于非阻塞状态的单进程单线程长连接)

# html文件夹网盘链接: https://pan.baidu.com/s/1wbZ92KLstff3GurLV_C0lg  密码: hp9o
# 下载完命令行解压后放到程序的同级路径下,解压命令: tar -zxvf 06_html.tar.gz -C ./
import sys
import time
import socket
import re


class WSGIServer:
    """定义一个WSGI服务器的类"""
    def __init__(self, server_addr):
        # 1.创建数据流套接字
        self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 2.设置当服务器先close 即服务器端4次挥手之后资源能够立即释放,保证下次运行程序时可以立即绑定7890端口
        self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # 3.绑定服务器地址
        self.tcp_server_socket.bind(server_addr)
        # 4.套接字由主动改为监听,并设置队列长度
        self.tcp_server_socket.listen(128)
        # 5.设置为非堵塞后,如果accept时,恰巧没有客户端connect,那么accept会产生一个异常,所以需要try来进行处理
        self.tcp_server_socket.setblocking(False)  # 将监听套接字设置为非堵塞
        self.client_socket_list = list()  # 创建客户端client_socket轮询列表

    def serve_forever(self):
        """循环运行服务器,等待客户端链接并为客户端服务"""
        while True:
            time.sleep(1)  # for test
            try:
                # 6.等待客户端链接
                client_socket, client_addr = self.tcp_server_socket.accept()
            except Exception as ret:
                print("--->1<---没有客户端到来: ", ret)  # for test
            else:
                # 7.当有客户端连接时,将客户端的套接字加入到轮询列表
                client_socket.setblocking(False)  # 将客户端套接字设置为非堵塞
                self.client_socket_list.append(client_socket)

            # 8.遍历列表看是否有收到客户端套接字发送的消息,如果收到消息则为客户端服务
            for client_socket in self.client_socket_list:
                try:
                    # 1.接收浏览器发送过来的HTTP请求
                    request = client_socket.recv(1024).decode("utf-8", errors="ignore")
                except Exception as ret:
                    print("--->2<---这个客户端没有发送过来请求: ", ret)  # for test
                else:
                    if request:
                        # 2.1 为这个客户端服务
                        self.handle_request(request, client_socket)
                    else:
                        # 2.2 客户端调用了close导致recv返回,服务端将关闭客户端套接字并从轮询列表中删除
                        client_socket.close()
                        self.client_socket_list.remove(client_socket)

            print(self.client_socket_list)

    def handle_request(self, request, client_socket):
        """为客户端服务"""
        # 1.提取客户端发送过来的数据
        request_header_lines = request.splitlines()
        for i, line in enumerate(request_header_lines):
            print(i, line)

        # GET /index.html HTTP/1.1
        http_request_line = request_header_lines[0]
        ret = re.match(r"([^/]*)([^ ]+)", http_request_line)
        if ret:
            print("正则提取数据:", ret.group(1))
            print("正则提取数据:", ret.group(2))
            get_file_name = ret.group(2)
            # 如果没有指定访问哪个页面,则默认访问index.html
            # GET / HTTP/1.1
            if get_file_name == "/":
                get_file_name = DOCUMENTS_ROOT + "/index.html"
            else:
                get_file_name = DOCUMENTS_ROOT + get_file_name
            print("要访问的完整路径是: %s" % get_file_name)

        # 2.返回HTTP格式的数据给浏览器
        try:
            f = open(get_file_name, "rb")
        except IOError:
            # 404表示没有这个页面
            response_body = "====sorry, file not found====".encode("utf-8")
            response_headers = "HTTP/1.1 404 not found\r\n"
            response_headers += "Content-Type: text/html; charset=utf-8\r\n"
            response_headers += "Content-Length: %d\r\n" % (len(response_body))
            response_headers += "\r\n"
        else:
            # 2.1 准备发送给浏览器的数据-->body
            response_body = f.read()
            f.close()
            # 2.2 准备发送给浏览器的数据-->header
            response_headers = "HTTP/1.1 200 OK\r\n"
            # 2.3 发送body数据长度,让服务器根据长度确认body数据已完成接收,实现长连接
            response_headers += "Content-Length: %d\r\n" % (len(response_body))
            # 2.4 用一个空的行与body进行隔开
            response_headers += "\r\n"
        finally:
            # 因为头信息在组织的时候,是按照字符串组织的,不能与以二进制打开文件读取的数据合并,因此分开发送
            # 先发送response的头信息
            client_socket.send(response_headers.encode("utf-8"))
            # 再发送body
            client_socket.send(response_body)
            # 长连接下不需要关闭套接字,而是在header头中声明要发送的body长度
            # client_socket.close()


# 设置服务器静态资源的路径
DOCUMENTS_ROOT = "./html"


def main():
    # python3 xxx.py 7890
    if len(sys.argv) == 2:
        port = sys.argv[1]
        if port.isdigit():
            port = int(port)
    else:
        print("运行方式如: python3 xxx.py 7890")
        return
    server_addr = ("", port)
    http_server = WSGIServer(server_addr)
    print("web Server: Serving HTTP on port %d ...\n" % port)
    http_server.serve_forever()


if __name__ == "__main__":
    main()

13.Web静态服务器-IO多路复用(基于epoll的单进程单线程长连接高并发)

# html文件夹网盘链接: https://pan.baidu.com/s/1wbZ92KLstff3GurLV_C0lg  密码: hp9o
# 下载完命令行解压后放到程序的同级路径下,解压命令: tar -zxvf 06_html.tar.gz -C ./
import sys
import socket
import re
import select


class WsgiServer:
    """定义一个WSGI服务器的类"""
    def __init__(self, server_addr):
        # 1.创建数据流套接字
        self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 2.设置当服务器先close 即服务器端4次挥手之后资源能够立即释放,保证下次运行程序时可以立即绑定7890端口
        self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # 3.绑定服务器地址
        self.tcp_server_socket.bind(server_addr)
        # 4.套接字由主动改为监听,并设置队列长度
        self.tcp_server_socket.listen(128)

        # 5.创建epoll对象
        self.epoll = select.epoll()
        # 6.将tcp服务器的监听套接对应的fd注册到epoll中进行监听,如果fd已经注册过,则会发生异常
        self.epoll.register(self.tcp_server_socket.fileno(), select.EPOLLIN | select.EPOLLET)
        # 7.创建添加的fd对应的套接字
        self.fd_socket = dict()

    def serve_forever(self):
        """循环运行服务器,等待客户端链接并为客户端服务"""
        # 等待新的客户端连接或者已连接的客户端发送过来数据
        while True:
            # epoll进行fd扫描的地方,未指定超时时间则为阻塞等待,直到系统检测到数据到来,通过事件通知方式告诉程序才会解阻塞
            epoll_list = self.epoll.poll()  # epoll_list列表结构: [(fd, event),]
            # fd: 套接字对应的文件描述符
            # event: 这个文件描述符到底是什么事件,例如可以调用recv接收等

            # 对事件进行判断
            for fd, event in epoll_list:
                # 如果是服务器的监听套接字被激活可以收数据,那么意味着可以进行accept,即有新的客户端连接
                if fd == self.tcp_server_socket.fileno():
                    new_socket, new_addr = self.tcp_server_socket.accept()

                    # 向epoll中注册连接socket的可读事件,EPOLLIN(可读),EPOLLOUT(可写),EPOLLET(ET模式)
                    # LT模式: 当epoll检测到描述符事件发生并将此事件通知应用程序
                    # LT模式下应用程序可以不立即处理该事件,下次调用epoll时,会再次响应应用程序并通知此事件
                    # ET模式: 当epoll检测到描述符事件发生并将此事件通知应用程序
                    # LT模式下应用程序必须立即处理该事件,如果不处理,下次调用epoll时,不会再次响应应用程序并通知此事件
                    self.epoll.register(new_socket.fileno(), select.EPOLLIN | select.EPOLLET)
                    # 记录这个信息
                    self.fd_socket[new_socket.fileno()] = new_socket
                # 如果是客户端发送数据,客户端套接字将被激活,服务器可以接收到客服端发送的数据
                elif event == select.EPOLLIN:
                    # # 从激活 fd 上接收
                    request = self.fd_socket[fd].recv(1024).decode("utf-8")
                    if request:
                        self.handle_request(request, self.fd_socket[fd])
                    else:
                        # 在epoll中注销客户端的信息
                        self.epoll.unregister(fd)
                        # 关闭客户端的文件句柄
                        self.fd_socket[fd].close()
                        # 在字典中删除与已关闭客户端相关的信息
                        del self.fd_socket[fd]

    def handle_request(self, request, client_socket):
        """为客户端服务"""
        # 1.接收浏览器发送过来的HTTP请求
        request_header_lines = request.splitlines()
        for i, line in enumerate(request_header_lines):
            print(i, line)

        # GET /index.html HTTP/1.1
        http_request_line = request_header_lines[0]
        ret = re.match(r"([^/]*)([^ ]+)", http_request_line)
        if ret:
            print("正则提取数据:", ret.group(1))
            print("正则提取数据:", ret.group(2))
            get_file_name = ret.group(2)
            # 如果没有指定访问哪个页面,则默认访问index.html
            # GET / HTTP/1.1
            if get_file_name == "/":
                get_file_name = DOCUMENTS_ROOT + "/index.html"
            else:
                get_file_name = DOCUMENTS_ROOT + get_file_name
            print("要访问的完整路径是: %s" % get_file_name)

        # 2.返回HTTP格式的数据给浏览器
        try:
            f = open(get_file_name, "rb")
        except IOError:
            # 404表示没有这个页面
            response_body = "====sorry, file not found====".encode("utf-8")
            response_headers = "HTTP/1.1 404 not found\r\n"
            response_headers += "Content-Type: text/html; charset=utf-8\r\n"
            response_headers += "Content-Length: %d\r\n" % (len(response_body))
            response_headers += "\r\n"
        else:
            # 2.1 准备发送给浏览器的数据-->body
            response_body = f.read()
            f.close()
            # 2.2 准备发送给浏览器的数据-->header
            response_headers = "HTTP/1.1 200 OK\r\n"
            # 2.3 发送body数据长度,让服务器根据长度确认body数据已完成接收,实现长连接
            response_headers += "Content-Length: %d\r\n" % (len(response_body))
            # 2.4 用一个空的行与body进行隔开
            response_headers += "\r\n"
        finally:
            # 因为头信息在组织的时候,是按照字符串组织的,不能与以二进制打开文件读取的数据合并,因此分开发送
            # 先发送response的头信息
            client_socket.send(response_headers.encode("utf-8"))
            # 再发送body
            client_socket.send(response_body)
            # 长连接下不需要关闭套接字,而是在header头中声明要发送的body长度
            # client_socket.close()


# 设置服务器静态资源的路径
DOCUMENTS_ROOT = "./html"


def main():
    # python3 xxx.py 7890
    if len(sys.argv) == 2:
        port = sys.argv[1]
        if port.isdigit():
            port = int(port)
    else:
        print("运行方式如: python3 xxx.py 7890")
        return
    server_addr = ("", port)
    http_server = WsgiServer(server_addr)
    print("web Server: Serving HTTP on port %d ...\n" % port)
    http_server.serve_forever()


if __name__ == "__main__":
    main()

14.C10K问题(单机1万网络并发连接和数据处理能力)

    1.C10K问题的本质:
        C10K问题本质上是操作系统的问题,对于Web1.0/2.0时代的操作系统而言, 传统的同步阻塞I/O模型都是一样的
        处理的方式都是requests per second并发10K和100的区别关键在于CPU
        创建的进程线程多了,数据拷贝频繁(缓存I/O,内核将数据拷贝到用户进程空间,阻塞)
        进程/线程上下文切换消耗大,导致操作系统崩溃,这就是C10K问题的本质

    2.C10K解决方案: 每个进程/线程同时处理多个连接(IO多路复用)
        1.尽量避免服务器处理超过1万个的并发连接
        2.通过改进操作系统内核以及用事件驱动服务器(典型技术实现如:Nginx和Node)代替线程服务器(典型代表:Apache)
        3.epoll能达到10k并发,但是epoll有Linux,Unix平台限制
        3.使用libevent库开发,libevent库是对/dev/poll, kqueue, event ports, select, poll和epoll接口的封装

    3.文件句柄限制
        1.问题概述:
            在linux下编写网络服务器程序的朋友肯定都知道每一个tcp连接都要占一个文件描述符
            一旦这个文件描述符使用完了,新的连接到来返回给我们的错误是"Socket/File:Can't open so many files"
            这是因为操作系统对可以打开的最大文件数的限制
        2.进程限制
            执行 ulimit -n 输出 1024, 说明对于一个进程而言最多只能打开1024个文件
            要采用此默认配置最多也就可以并发上千个TCP连接,临时修改: ulimit -n 1000000
            但是这种临时修改只对当前登录用户目前的使用环境有效,系统重启或用户退出后就会失效
            重启后失效的修改,编辑 /etc/security/limits.conf 文件修改后内容为
                soft nofile 1000000
                hard nofile 1000000
            永久修改: 编辑/etc/rc.local 在其后添加如下内容
                ulimit -SHn 1000000
        3.全局限制
            执行 cat /proc/sys/fs/file-nr
            输出 3040    0      194572
            输出释义: 已经分配的文件句柄数, 已经分配但没有使用的文件句柄数, 最大文件句柄数
            但在kernel 2.6版本中第二项的值总为0,这并不是一个错误,它实际上是已经分配的文件描述符无一浪费的都已经被使用了
            我们可以把这个数值改大些,用 root 权限修改 /etc/sysctl.conf 文件
                fs.file-max = 1000000
                net.ipv4.ip_conntrack_max = 1000000
                net.ipv4.netfilter.ip_conntrack_max = 1000000

    4.端口号范围限制
        问题误区:
            操作系统上端口号1024以下是系统保留的,从1024-65535是用户使用的
            由于每个TCP连接都要占一个端口号,所以我们最多可以有60000多个并发连接
        如何标识一个TCP连接
            系统用一个4四元组来唯一标识一个TCP连接: {local ip, local port,remote ip,remote port}
            《UNIX网络编程: 卷一》第四章中对accept的讲解来看看概念性的东西,第二个参数cliaddr代表了客户端的ip地址和端口号
            而我们作为服务端实际只使用了bind时这一个端口,说明端口号65535并不是并发量的限制
        server最大tcp连接数
            server通常固定在某个本地端口上监听,等待client的连接请求
            不考虑地址重用(unix的SO_REUSEADDR选项)的情况下,即使server端有多个ip,本地监听端口也是独占的
            因此server端tcp连接4元组中只有remote ip(也就是client ip)和remote port(客户端port)是可变的
            得出最大tcp连接为客户端ip数×客户端port数

            对IPV4,不考虑ip地址分类等因素,最大tcp连接数约为2的32次方(ip数) × 2的16次方(port数)
            得出server端单机最大tcp连接数约为2的48次方

    5.文件句柄限制和端口范围限制小结
        上述得出的是理论上的单机TCP并发连接数,实际上单机并发连接数要受硬件资源(内存),网络资源(带宽)的限制

    6.C10K问题参考自如下链接:
        http://www.52im.net/thread-561-1-1.html
        http://www.52im.net/thread-566-1-1.html

15.C10M问题(单机1000万网络并发连接和数据处理能力)

    1.C10M问题的本质:
        1.硬件上完全可以处理1000万个以上的并发连接,如果它们不能,那是因为你选择了错误的软件,而不是底层硬件的问题
        2.OS的内核不是解决C10M问题的办法,恰恰相反OS的内核正是导致C10M问题的关键所在
        3.不要让OS内核执行所有繁重的任务:
            将数据包处理,内存管理,处理器调度等任务从内核转移到应用程序高效地完成
            让诸如Linux这样的OS只处理控制层,数据层完全交给应用程序来处理

    2.C10M问解决方案:
        1.CPU亲和性 & 内存局域性
        2.RSS, RPS, RFS, XPS
        3.IRQ 优化
        4.Kernel 优化
        5. CPU 内存 网络

    3.C10M问题参考自如下链接:
        http://www.52im.net/thread-568-1-1.html
        http://www.52im.net/thread-578-1-1.html

posted @ 2020-09-01 22:33  唐雪成  阅读(947)  评论(0编辑  收藏  举报