併發編程(四)—— 併發網絡通信模型、IO併發、協程

併發網絡通信模型

常見模型分類

  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)
功能:設置套接字的超時時間
參數:設置的時間

在這裏插入圖片描述

重點代碼:

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
    在這裏插入圖片描述

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服務
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章