并发编程(四)—— 并发网络通信模型、IO并发、协程

目录

  • 并发网络通信模型
    • 常见模型分类
    • 基于fork的多进程网络并发模型
    • 基于threading的多线程网络并发模型
    • 基于multiprocessing的多进程网络并发模型
    • 扩展:集成模块完成多进程/多线程网并发
    • ftp 文件服务器
  • IO并发
    • IO分类
    • 阻塞IO
    • 非阻塞IO
    • IO多路复用
      • select方法
      • @@扩展:位运算
      • poll方法
      • epoll方法
  • 协程技术
    • 基础概念
    • 扩展延伸@标准库协程的实现
    • 第三方协程模块
  • HTTPServer v2.0

并发网络通信模型

常见模型分类

  1. 循环服务器模型 :循环接收客户端请求,处理请求。同一时刻只能处理一个请求,处理完毕后再处理下一个。(就是TCP 和 UDP基本示例程序)

优点:实现简单,占用资源少
缺点:无法同时处理多个客户端请求

适用情况:处理的任务可以很快完成,客户端无需长期占用服务端程序。udp比tcp更适合循环。

  1. IO并发模型:利用IO多路复用,异步IO等技术,同时处理多个客户端IO请求。

优点 : 资源消耗少,能同时高效处理多个IO行为
缺点 : 只能处理并发产生的IO事件,无法处理cpu计算

适用情况:HTTP请求,网络传输等都是IO行为。

  1. 多进程/线程网络并发模型:每当一个客户端连接服务器,就创建一个新的进程/线程为该客户端服务,客户端退出时再销毁该进程/线程。

优点:能同时满足多个客户端长期占有服务端需求,可以处理各种请求
缺点: 资源消耗较大

适用情况:客户端同时连接量较少,需要处理行为较复杂情况。

基于fork的多进程网络并发模型

实现步骤

  1. 创建监听套接字
  2. 等待接收客户端请求
  3. 客户端连接创建新的进程处理客户端请求
  4. 原进程继续等待其他客户端连接
  5. 如果客户端退出,则销毁对应的进程

重点代码 (fork_server.py):

from socket import *
import os, sys
import signal

def handle(c):
    print("客户端:", c.getpeername())
    while True:
        data = c.recv(1024)
        if not data:
            break
        print(data.decode())
        c.send(b'OK')
    c.close()

# 创建监听套接字
HOST = "0.0.0.0"
PORT = 8888
ADDR = (HOST, PORT)

s = socket()  # tcp套接字
s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)  # 设置端口立即重用
s.bind(ADDR)
s.listen(3)

# 僵尸进程处理
signal.signal(signal.SIGCHLD, signal.SIG_IGN)

print("Listen the port 8888...")
# 循环等待客户端连接
while True:
    try:
        c, addr = s.accept()
    except KeyboardInterrupt:
        sys.exit('服务器退出')
    except Exception as e:
        print(e)
        continue

    # 创建子进程处理客户端请求
    pid = os.fork()
    if pid == 0:
        s.close()  # 子进程不需要s
        handle(c)  # 具体处理客户端请求
        os._exit(0)
    # 父进程其实只用来处理客户端连接
    else:
        c.close()  # 父进程不需要c

基于threading的多线程网络并发模型

实现步骤

  1. 创建监听套接字
  2. 循环接收客户端连接请求
  3. 当有新的客户端连接创建线程处理客户端请求
  4. 主线程继续等待其他客户端连接
  5. 当客户端退出,则对应分支线程退出

重点代码(thread_server.py):

from socket import *
from threading import Thread
import sys

# 客户端处理
def handle(c):
    print("客户端:", c.getpeername())
    while True:
        data = c.recv(1024)
        if not data:
            break
        print(data.decode())
        c.send(b'OK')
    c.close()

# 创建监听套接字
HOST = '0.0.0.0'
PORT = 8888
ADDR = (HOST, PORT)

s = socket()  # tcp套接字
s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)  # 设置端口立即重用
s.bind(ADDR)
s.listen(3)

# 循环等待客户端连接
while True:
    try:
        c, addr = s.accept()
    except KeyboardInterrupt:
        sys.exit('服务器退出')
    except Exception as e:
        print(e)
        continue
    # 创建新的线程处理客户端请求
    t = Thread(target=handle, args=(c,))
    t.setDaemon(True)  # 分支线程随主线程退出(这句话可加可不加)
    t.start()

基于multiprocessing的多进程网络并发模型

实现步骤(实现步骤与“基于fork的多进程网络并发模型”实现步骤相同)

  1. 创建监听套接字
  2. 等待接收客户端请求
  3. 客户端连接创建新的进程处理客户端请求
  4. 原进程继续等待其他客户端连接
  5. 如果客户端退出,则销毁对应的进程

重点代码(multi_server.py):

from socket import *
from multiprocessing import Process
import sys, signal

# 客户端处理
def handle(c):
    print("客户端:", c.getpeername())
    while True:
        data = c.recv(1024)
        if not data:
            break
        print(data.decode())
        c.send(b'OK')
    c.close()

# 创建监听套接字
HOST = '0.0.0.0'
PORT = 8888
ADDR = (HOST, PORT)

s = socket()  # tcp套接字
s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)  # 设置端口立即重用
s.bind(ADDR)
s.listen(3)

# 僵尸进程处理
signal.signal(signal.SIGCHLD, signal.SIG_IGN)

# 循环等待客户端连接
while True:
    try:
        c, addr = s.accept()
    except KeyboardInterrupt:
        sys.exit('服务器退出')
    except Exception as e:
        print(e)
        continue
    # 创建新的线程处理客户端请求
    p = Process(target=handle, args=(c,))
    p.daemon = True  # 子进程随父进程退出
    p.start()

扩展:集成模块完成多进程/多线程网并发

这部分感兴趣自己看一下

  1. import sockectserver
    通过模块提供的不同的类的组合来完成多进程或者多线程,tcp 或udp的网路并发模型

  2. 常用类说明
    TCPServer:创建TCP服务端套接字
    UDPServer:创建UDP服务端套接字

    StreamRequestHandler:处理TCP客户端请求
    DatageramRequestHandler:处理udp客户端请求

    ForkingMinIn:创建多进程并发
    ForkingTCPServer:ForkingMinIn + TCPServer
    ForkingUDPServer:ForkingMinIn + UDPServer

    ThreadingMixIn:创建多线程并发
    ThreadingTCPServer :ThreadingMixIn + TCPServer
    ThreadingUDPServer:ThreadingMixIn + UDPServer

  3. 步骤
    【1】创建服务器类,通过选择继承的类,决定创建TCP或者UDP,多进程或者多线程确定定法类型
    【2】创建请求处理类,根据服务类型选择stream处理类还是Datager处理类。重写handle方法,做具体的请求处理
    【3】通过服务器类创建服务器对象,并绑定请求处理类
    【4】通过富强武器对象,调用server_forever()启动服务

ftp 文件服务器

功能

【1】 分为服务端和客户端,要求可以有多个客户端同时操作。
【2】 客户端可以查看服务器文件库中有什么文件。
【3】 客户端可以从文件库中下载文件到本地。
【4】 客户端可以上传一个本地文件到文件库。
【5】 使用print在客户端打印命令输入提示,引导操作。

ftp文件服务器思路分析:

  1. 技术点分析
  • 并发模型:多线程并发模式
  • 数据传输:tcp传输
  1. 结构设计
  • 客户端发起请求,打印请求提示界面
  • 文件传输功能封装为类
  1. 功能分析
  • 网络搭建
  • 查看文件库信息
  • 下载文件
  • 上传文件
  • 客户端退出
  1. 协议
  • L–表示请求文件列表
  • Q–表示退出
  • G–表示下载
  • P–表示上传

程序下载

IO并发

只针对IO行为

IO分类

IO分类:阻塞IO ,非阻塞IO,IO多路复用,异步IO等

阻塞IO

  1. 定义:在执行IO操作时如果执行条件不满足则阻塞。阻塞IO是IO的默认形态。
  2. 效率:阻塞IO是效率很低的一种IO。但是由于逻辑简单所以是默认IO行为。
  3. 阻塞情况:(占用较少CPU)
  • 因为某种执行条件没有满足造成的函数阻塞
    e.g. accept input recv
  • 处理IO的时间较长产生的阻塞状态
    e.g. 网络传输,大文件读写

非阻塞IO

定义 :通过修改IO属性行为,使原本阻塞(因为某种执行条件没有满足造成的函数阻塞)的IO变为非阻塞的状态。

  • 设置套接字为非阻塞IO

sockfd.setblocking(bool)
功能:设置套接字为非阻塞IO
参数:默认为True,表示套接字IO阻塞;设置为False则套接字IO变为非阻塞

  • 超时检测 :设置一个最长阻塞时间,超过该时间后则不再阻塞等待。

sockfd.settimeout(sec)
功能:设置套接字的超时时间
参数:设置的时间

并发编程(四)—— 并发网络通信模型、IO并发、协程_第1张图片

重点代码:

from socket import *
from time import sleep, ctime

f = open('log.txt', 'a+')

# tcp 套接字
sockfd = socket()
sockfd.bind(('127.0.0.1', 8888))
sockfd.listen(3)

# 设置套接字为非阻塞
# sockfd.setblocking(False) # 2s 写一条日志

# 超时检测
sockfd.settimeout(3) # 3+2 S 写一条日志

while True:
    print("Waiting for connect...")
    try:
        connfd, addr = sockfd.accept()
    except (BlockingIOError, timeout) as e:
        # 每隔2s写入一条日志
        sleep(2)
        f.write("%s: %s\n" % (ctime(), e))
        f.flush()
    else:  
        data = connfd.recv(1024).decode()
        print(data)

IO多路复用

  1. 定义
    同时监控多个IO事件,当哪个IO事件准备就绪就执行哪个IO事件。以此形成可以同时处理多个IO的行为,避免一个IO阻塞造成其他IO均无法执行,提高了IO执行效率。

  2. 具体方案

  • select方法 : windows linux unix
  • poll方法: linux unix
  • epoll方法: linux
    并发编程(四)—— 并发网络通信模型、IO并发、协程_第2张图片

select方法

from select import select
rs, ws, xs = select(rlist, wlist, xlist[, timeout])
功能:监控IO事件,阻塞等待IO发生
参数:rlist  列表  存放关注的等待发生(被动发生的)的IO事件(例如:套接字等待连接accept)
     wlist  列表  存放关注的要主动处理的IO事件(例如:TCP中的send)
     xlist  列表  存放关注的出现异常要处理的IO(基本不用)
     timeout  超时时间(可以无参)

返回值: rs 列表  rlist中准备就绪(已经发生的瞬间事件)的IO
        ws 列表  wlist中准备就绪的IO
	    xs 列表  xlist中准备就绪的IO

示例代码:

from select import select
from socket import *

# 做几个IO用作监控
s = socket()
s.bind(('0.0.0.0', 8888))
s.listen(3)

fd = open('log.txt', 'a+')

print("开始提交监控的IO")
rs, ws, xs = select([s], [fd], [])

print("rs:", rs)
print("ws:", ws)
print("xs:", xs)

select 实现tcp服务

【1】将关注的IO放入对应的监控类别列表
【2】通过select函数进行监控
【3】遍历select返回值列表,确定就绪IO事件
【4】处理发生的IO事件

注意:

  • wlist中如果存在IO事件,则select立即返回给ws
  • 处理IO过程中不要出现死循环占有服务端的情况
  • IO多路复用消耗资源较少,效率较高

重点代码(select_server.py):

from socket import *
from select import select

#  创建一个监听套接字作为关注的IO
s = socket()
s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)  # 设置端口可以重用
s.bind(('0.0.0.0', 8888))
s.listen(5)

#  设置关注列表
rlist = [s]
wlist = []
xlist = []

while True:
    #  循环监控IO的发生
    rs, ws, xs = select(rlist, wlist, xlist)
    # 遍历三个返回列表,判断哪个IO发生
    for r in rs:
        # 如果是套接字就绪则处理连接
        if r is s:
            c, addr = r.accept()
            print("Connect from", addr)
            rlist.append(c)  # 增加新的关注IO事件
        # else为客户端套接字就绪情况
        else:  # r is c
            data = r.recv(1024)
            # 客户端退出
            if not data:
                rlist.remove(r)  # 从关注列表移除
                r.close()
                continue  # 继续处理其他就绪IO
            print("Receive:", data.decode())
            # r.send(b'OK')
            #  我们希望主动处理这个IO对象
            wlist.append(r)

    for w in ws:
        w.send(b'OK')
        wlist.remove(w)  # 使用后移除

    for x in xs:
        pass

@@扩展:位运算

  • 定义:将整数转换为二进制,按二进制位进行运算
  • 运算符号:
    & 按位与
    | 按位或
    ^ 按位异或
    << 左移
    >> 右移
e.g. 14 --> 01110
	 19 --> 10011
14 & 19 = 00010 = 2 一0则0
14 | 19 = 11111 = 31 一1则1
14 ^ 19 = 11101 = 29 相同为0不同为1
14 << 2 = 111000 = 56 向左移动低位补0
14 >> 2 = 11 = 3 向右移动去掉低位

poll方法

p = select.poll()

功能 : 创建poll对象
返回值: poll对象
p.register(fd,event)   

功能: 注册关注的IO事件
参数:fd 要关注的IO
	 event 要关注的IO事件类型
		   常用类型:POLLIN 读IO事件(rlist)
				   POLLOUT 写IO事件 (wlist)
				   POLLERR 异常IO (xlist)
				   POLLHUP 断开连接 
 			 e.g.  p.register(sockfd,POLLIN|POLLERR)  同时关注多个事件
p.unregister(fd)

功能:取消对IO的关注
参数:IO对象或者IO对象的fileno
events = p.poll()

功能: 阻塞等待监控的IO事件发生
返回值: 返回发生的IO
		events格式 [(fileno,event),()....]
		每个元组为一个就绪IO,元组第一项是该IO的fileno,第二项为该IO就绪的事件类型

poll_server 步骤

【1】创建套接字
【2】将套接字register
【3】创建查找字典,并维护
【4】循环监控IO发生
【5】处理发生的IO

次重点代码(poll_server.py):

from socket import *
from select import *

#  设置套接字为关注IO
s = socket()
s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
s.bind(('0.0.0.0', 8888))
s.listen(5)

# 创建poll对象关注s
p = poll()

#  建立查找字典{fileno:io_obj},用于通过文件描述符fileno查找IO对象
fdmap = {s.fileno(): s}

# 设置关注IO
p.register(s, POLLIN | POLLERR)

#  循环监控IO事件发生
while True:
    events = p.poll()  # 阻塞等待IO发生
    #  循环遍历发生的事件 fd-->fileno
    for fd, event in events:
        #  区分事件进行处理
        if fd == s.fileno():
            c, addr = fdmap[fd].accept()
            print("Connect from", addr)
            #  添加新的关注IO
            p.register(c, POLLIN | POLLERR)
            fdmap[c.fileno()] = c  # 维护字典
        # elif event & POLLHUP:  # 客户端断开
        #     print("客户端退出")
        #     p.unregister(fd)  # 取消关注
        #     fdmap[fd].close()
        #     del fdmap[fd]  # 从字典删除
        elif event & POLLIN:  # 客户端发消息
            data = fdmap[fd].recv(1024)
            # 断开(POLLERR)发生时data得到空,此时POLLIN也会就绪
            if not data:
                p.unregister(fd)  # 取消关注
                fdmap[fd].close()
                del fdmap[fd]  # 从字典删除
                continue
            print("Receive:", data.decode())
            fdmap[fd].send(b'OK')

epoll方法

  1. 使用方法 : 基本与poll相同
    生成对象改为 epoll()
    将所有事件类型改为EPOLL类型

  2. epoll特点:
    epoll 效率比select、poll要高
    epoll 监控IO数量比select要多
    epoll 的触发方式比poll要多 (EPOLLET边缘触发)

次重点代码(epoll_server.py):

from socket import *
from select import *

#  创建套接字
s = socket()
s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
s.bind(('0.0.0.0', 8888))
s.listen(3)

# 创建epoll对象关注s
ep = epoll()

#  建立查找字典,用于通过fileno查找IO对象
fdmap = {s.fileno(): s}

# 关注s
ep.register(s, EPOLLIN | EPOLLERR)

#  循环监控
while True:
    events = ep.poll()
    print(events)
    #  循环遍历发生的事件 fd-->fileno
    for fd, event in events:
        #  区分事件进行处理
        if fd == s.fileno():
            c, addr = fdmap[fd].accept()
            print("Connect from", addr)
            #  添加新的关注IO
            #  将触发方式变为边缘触发(EPOLLET)
            ep.register(c, EPOLLIN | EPOLLERR | EPOLLET)
            fdmap[c.fileno()] = c  # 维护字典
        # #  按位与判定是EPOLLIN就绪
        # elif event & EPOLLIN:
        #     data = fdmap[fd].recv(1024)
        #     if not data:
        #         ep.unregister(fd)  # 取消关注
        #         fdmap[fd].close()
        #         del fdmap[fd]  # 从字典中删除
        #         continue
        #     print("Receive:", data.decode())
        #     fdmap[fd].send(b'OK')

协程技术

基础概念

  1. 定义:纤程,微线程。是为非抢占式多任务产生子程序的计算机组件。协程允许不同入口点在不同位置暂停或开始,简单来说,协程就是可以暂停执行的函数。(什么是协程:在应用层通过函数间的暂停跳转实现多任务同时操作,消耗较少的资源)
  2. 协程原理 : 记录一个函数的上下文栈帧,协程调度切换时会将记录的上下文保存,在切换回来时进行调取,恢复原有的执行内容,以便从上一次执行位置继续执行。
  3. 协程优缺点
    优点:
    【1】协程完成多任务占用计算资源很少
    【2】由于协程的多任务切换在应用层完成,因此切换开销少
    【3】协程为单线程程序,无需进行共享资源同步互斥处理
    
    缺点:协程的本质是一个单线程,无法利用计算机多核资源
    

扩展延伸@标准库协程的实现

python3.5以后,使用标准库asyncio和async/await 语法来编写并发代码。asyncio库通过对异步IO行为的支持完成python的协程。

  • 同步是指完成事务的逻辑,先执行第一个事务,如果阻塞了,会一直等待,直到这个事务完成,再执行第二个事务,顺序执行
  • 异步和同步是相对的,异步是指在处理调用这个事务的之后,不会等待这个事务的处理结果,直接处理第二个事务去了,通过状态、通知、回调来通知调用者处理结果。

虽然官方说asyncio是未来的开发方向,但是由于其生态不够丰富,大量的客户端不支持awaitable需要自己去封装,所以在使用上存在缺陷。更多时候只能使用已有的异步库(asyncio等),功能有限

程序实现(async_test.py 了解即可):

import asyncio
import time

now = lambda: time.time()

async def do_work(x):
    print("Waiting:", x)
    await asyncio.sleep(x)  # 阻塞自动跳转(无法使用日常的IO阻塞性为,所以该标准库日常中几乎不使用)
    return "Done after %s s" % x

start = now()

# 生成协程对象
cor1 = do_work(1)
cor2 = do_work(2)
cor3 = do_work(3)

# 将协程对象生成一个可轮寻操作的对象列表
tasks = [
    asyncio.ensure_future(cor1),
    asyncio.ensure_future(cor2),
    asyncio.ensure_future(cor3)
]

# 得到轮寻对象调用run启动协程执行
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

print("Time:", now() - start)

第三方协程模块

  1. greenlet模块
  • 安装 : sudo pip3 install greenlet
  • 函数
    greenlet.greenlet(func)
    功能:创建协程对象
    参数:协程函数
    
    g.switch()
    功能:选择要执行的协程函数
    

实现代码(greenlet_test.py):

from greenlet import greenlet

def test1():
  print("执行test1")
  gr2.switch()
  print("结束test1")
  gr2.switch()

def test2():
  print("执行test2")
  gr1.switch()
  print("结束test2")

# 将函数变成协程
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()  # 选择执行协程1

# 返回结果
'''
执行test1
执行test2
结束test1
结束test2
'''
  1. gevent模块
  • 安装:sudo pip3 install gevent
  • 函数
    gevent.spawn(func,argv)
    功能: 生成协程对象
    参数:func  协程函数
         argv  给协程函数传参(不定参)
    返回值: 协程对象
    
    gevent.joinall(list,[timeout])
    功能: 阻塞等待协程执行完毕
    参数:list  协程对象列表
         timeout 超时时间
    
    gevent.sleep(sec)
    功能: gevent睡眠阻塞
    参数:睡眠时间
    

* gevent协程只有在遇到gevent指定的阻塞行为时才会自动在协程之间进行跳转,如:gevent.joinall(),gevent.sleep()带来的阻塞

实现代码(gevent_test.py):

import gevent

# 协程函数
def foo(a,b):
  print("Running foo ...",a,b)
  gevent.sleep(2)
  print("Foo again")

def bar():
  print("Running bar ...")
  gevent.sleep(3)
  print("bar again")

# 将函数封装为协程,遇到gevent阻塞自动执行
f = gevent.spawn(foo,1,2)
b = gevent.spawn(bar)

gevent.joinall([f,b]) # 阻塞等待[]中的协成结束
  • monkey脚本

作用:在gevent协程中,协程只有遇到gevent指定类型的阻塞才能跳转到其他协程,因此,我们希望将普通的IO阻塞行为转换为可以触发gevent协程跳转的阻塞,以提高执行效率。

转换方法:gevent 提供了一个脚本程序monkey,可以修改底层解释IO阻塞的行为,将很多普通阻塞转换为gevent阻塞。

使用方法:

【1】导入monkey

from gevent import monkey

【2】运行相应的脚本,例如转换socket中所有阻塞

monkey.patch_socket()

【3】 如果将所有可转换的IO阻塞全部转换则运行all

monkey.patch_all()

【4】注意:脚本运行函数需要在对应模块导入前执行

实现代码(gevent_server.py):

import gevent
from gevent import monkey

monkey.patch_all()  # 该句执行在导入socket前
from socket import *

# 处理客户端请求
def handle(c):
    while True:
        data = c.recv(1024)
        if not data:
            break
        print(data.decode())
        c.send(b'OK')
    c.close()

# 创建TCP套接字
s = socket()
s.bind(('0.0.0.0', 8888))
s.listen(5)
while True:
    c, addr = s.accept()
    print("Connect from", addr)
    # handle(c)  # 循环方案
    gevent.spawn(handle, c)  # 协程方案

s.close()

HTTPServer v2.0

  1. 主要功能 :
    【1】 接收客户端(浏览器)请求
    【2】 解析客户端发送的请求
    【3】 根据请求组织数据内容
    【4】 将数据内容形参http响应格式返回给浏览器

  2. 升级点 :
    【1】 采用IO并发,可以满足多个客户端同时发起请求情况
    【2】 做基本的请求解析,根据具体请求返回具体内容,同时满足客户端简单的非网页请求情况
    【3】 通过类接口形式进行功能封装

httpserver 2.0

技术点:
【1】使用tcp通信
【2】select io多路复用

结构:采用类封装

类的接口设计:
【1】在用户使用角度进行工作流程设计
【2】尽可能提供全面的功能,能为用户决定的在类中实现
【3】不能替用户决定的变量可以通过实例化对象传入类中
【4】不能替用户决定的复杂功能,可以通过重写让用户自己决定

程序实现(httpserver.py):

from socket import *
from select import select


#  将具体http server功能封装
class HTTPServer:
    def __init__(self, server_address, static_dir):
        # 添加属性
        self.server_address = server_address
        self.static_dir = static_dir
        self.rlist = []
        self.wlist = []
        self.xlist = []
        self.create_socket()
        self.bind()

    # 创建套接字
    def create_socket(self):
        self.sockfd = socket()
        self.sockfd.setsockopt(SOL_SOCKET,
                               SO_REUSEADDR, 1)

    def bind(self):
        self.sockfd.bind(self.server_address)
        self.ip = self.server_address[0]
        self.port = self.server_address[1]

    #  启动服务
    def serve_forever(self):
        self.sockfd.listen(5)
        print("Listen the port %d" % self.port)
        self.rlist.append(self.sockfd)
        while True:
            rs, ws, xs = select(self.rlist, self.wlist,
                                self.xlist)
            for r in rs:
                if r is self.sockfd:
                    c, addr = r.accept()
                    print("Connect from", addr)
                    self.rlist.append(c)
                else:
                    #  处理浏览器请求
                    self.handle(r)

    # 具体处理请求
    def handle(self, connfd):
        #  接收http请求
        request = connfd.recv(4096)
        #  防止客户端断开
        if not request:
            self.rlist.remove(connfd)
            connfd.close()
            return

        # 请求解析
        request_line = request.splitlines()[0]
        info = request_line.decode().split(' ')[1]
        print(connfd.getpeername(), ":", info)

        #  info分为访问网页或者其他内容
        if info == '/' or info[-5:] == '.html':
            self.get_html(connfd, info)
        else:
            self.get_data(connfd, info)
            
        self.rlist.remove(connfd)
        connfd.close()

    # 处理网页请求
    def get_html(self, connfd, info):
        if info == '/':
            filename = self.static_dir + "/index.html"
        else:
            filename = self.static_dir + info
        try:
            fd = open(filename)
        except Exception:
            #  没有网页
            responseHeaders = "HTTP/1.1 404 Not Found\r\n"
            responseHeaders += '\r\n'
            responseBody = "Sorry,Not found the page"
        else:
            #  存在网页
            responseHeaders = "HTTP/1.1 200 OK\r\n"
            responseHeaders += '\r\n'
            responseBody = fd.read()
        finally:
            response = responseHeaders + responseBody
            connfd.send(response.encode())
    
    # 其他情况 
    def get_data(self, connfd, info):
        responseHeaders = "HTTP/1.1 200 OK\r\n"
        responseHeaders += '\r\n'
        responseBody = "Waiting for httpserver 3.0"
        response = responseHeaders + responseBody
        connfd.send(response.encode())


if __name__ == "__main__":
    """
    希望通过HTTPServer类快速搭建http服务
   用以展示自己的网页
    """

    # 用户自己决定的内容:地址、内容
    server_addr = ('0.0.0.0', 8000) # 服务器地址
    static_dir = './static' # 网页存放位置

    httpd = HTTPServer(server_addr, static_dir)  # 生成实例对象
    httpd.serve_forever()  # 启动http服务

你可能感兴趣的:(多任务并发编程,并发网络通信模型,IO并发,协程)