一、socket的http套路
web應用本質上是一個socket服務端,用戶的瀏覽器是一個socket客戶端。socket處在應用層與傳輸層之間,是操作系統中I/O系統的延伸部分(接口),負責系統進程和應用之間的通信。
HTTP協議又稱超文本傳輸協議。
1 //瀏覽器發送一個HTTP請求; 2 //服務器收到請求,根據請求信息,進行函數處理,生成一個HTML文檔; 3 //服務器把HTML文檔作為HTTP響應的Body發送給瀏覽器; 4 //瀏覽器收到HTTP響應,從HTTP Body取出HTML文檔並顯示;
1、客戶端套路解析
1 import socket, ssl 2 # socket 是操作系統用來進行網絡通信的底層方案,用來發送/接收數據 3 4 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 5 # socket.AF_INET表示是ipv4協議,socket.SOCK_STREAM表示是tcp協議,這兩個值是默認的。 6 # s = socket.socket() 7 # 上面只能連接http,如果連接https,需要用s = ssl.wrap_socket(socket.socket()) 8 9 host, port = ("g.cn", 80) 10 s.connect((host, port)) # 連接主機,參數是主機的和端口 11 12 ip, port = s.getsockname() 13 print('本機 ip 和 port {} {}'.format(ip, port)) # 查看本機的ip和端口 14 15 # 構造一個http請求 16 http_request = 'GET / HTTP/1.1\r\nhost:{}\r\n\r\n'.format(host) 17 18 # 發送HTTP請求給服務器 19 # send 函數只接收bytes作為參數,所以要重編碼為utf-8。實際上,web數據傳送都是utf-8編碼的字節流數據。 20 21 request = http_request.encode(encoding='utf_8', errors='strict') 22 print("發送請求", request) 23 s.send(request) 24 25 26 # 接收服務器的相應數據 27 response = s.recv(1024) # buffer_size=1024,只接收1024個字節,多余的數據就不接收了。粘包。 28 29 # 輸出響應的數據,bytes類型 30 # print("響應", response) 31 # 再講response的utf-8編碼的字節流數據進行解碼(實際上是轉換成unicode編碼的字符串,因為讀進了內存)。 32 print("響應的 str 格式: ", end="\r\n") 33 print(response.decode(encoding='utf-8'))
2、服務器套路解析
1 import socket 2 3 host, port = '', 2000 4 # 服務器的host為空字符串,表示接受任意ip地址的連接 5 6 s = socket.socket() 7 s.bind((host, port)) # 監聽 8 9 # 用一個無線循環來接受數據 10 while True: 11 print("before listen") 12 s.listen(5) 13 14 # 接收請求 15 connection, address = s.accept() 16 # connection是一個socket.socket對象 17 print("after listen") 18 19 # 接收請求 20 request = connection.recv(1024) # 只接收1024個字節 21 print("ip and request, {}\n{}".format(address, request.decode("utf-8"))) 22 23 # 構造響應 24 response = b"HTTP/1.1 200 OK\r\n\r\n<h1>Hello World!</h1>" 25 26 # 用sendall發送響應數據 27 connection.sendall(response) 28 29 # 關閉連接 30 connection.close() 31
在瀏覽器中輸入localhost:2000,看到Hello World。后台打印的結果如下:
1 before listen 2 after listen 3 ip and request, ('127.0.0.1', 54275) 4 GET / HTTP/1.1 5 Host: localhost:2000 6 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:59.0) Gecko/20100101 Firefox/59.0 7 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 9 Accept-Encoding: gzip, deflate 10 Cookie: username-localhost-8888="2|1:0|10:1524886551|23:username-localhost-8888|44:ZTc2ZjE3MjIxMTMwNDIxYzg3OTZmMDlkMDdhNzhjMjI=|b432650e1e450be30083dd567068aebd47dc8d5b7167b068610af60db4c5a35d"; _xsrf=2|fc2cd1bd|88c10f771944fea069e65d5e767b6621|1524882104 11 Connection: keep-alive 12 Upgrade-Insecure-Requests: 1
粘包處理
1 粘包處理: 2 3 buffer_size = 1023 4 r = b'' 5 while True: 6 request = connection.recv(buffer_size) 7 r += request 8 if len(request) < buffer_size: 9 break
3、HTTP請求內容解析
1 """ 2 HTTP頭 3 http://localhost:2000/,瀏覽器默認會隱藏http://和末尾的/ 4 5 GET / HTTP/1.1 6 # GET表示請求方式, / 表示請求的資源路徑, HTTP/1.1 協議版本 7 8 Host: localhost:2000 9 # 主機地址和端口 10 11 Connection: keep-alive 12 # keep-alive保持連接狀態,它表示http連接(不用TCP請求斷開再請求);close每次離開就關閉連接 13 14 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:59.0) Gecko/20100101 Firefox/59.0 15 # 用戶代理,瀏覽器標識。可以偽造瀏覽器。 16 17 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 18 # 瀏覽器接收的數據類型。左邊是數據類型,右邊是解析時的權重(優先級)。 19 20 Accept-Encoding: gzip, deflate 21 # 可解壓的壓縮文件類型;只表示能解壓該類型的壓縮文件,不代表拒絕接收其它類型的壓縮文件。 22 23 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 24 # 支持的語言類型及其解析權重。 25 26 Cookie: username-localhost-8888="2|1:0|10:1524886551|23:username-localhost-8888|44:ZTc2ZjE3MjIxMTMwNDIxYzg3OTZmMDlkMDdhNzhjMjI=|b432650e1e450be30083dd567068aebd47dc8d5b7167b068610af60db4c5a35d"; _xsrf=2|fc2cd1bd|88c10f771944fea069e65d5e767b6621|1524882104 27 # 瀏覽器緩存。 28 29 /favicon.ico 30 # url地址圖標,非必須。 31 32 # Header里可以添加任意的內容。 33 """
二、socket的udp和tcp套路
1、udp的客戶端和服務端寫法
客戶端
1 import socket 2 # upd鏈接 3 # SOCK_DGRAM:數據報套接字,主要用於UDP協議 4 udpSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 5 6 # 關閉防火牆 7 # 同一網段(局域網)下,主機的ip地址和端口號. 8 sendAddr = ('192.168.10.247', 8080) 9 10 # 綁定端口:寫的是自己的ip和固定的端口,一般是寫在sever端. 11 udpSocket.bind(('', 9900)) 12 13 # sendData = bytes(input('請輸入要發送的數據:'), 'gbk') 14 # gbk, utf8, str 15 sendData = input('請輸入要發送的數據:').encode('gbk') 16 17 # 使用udp發送數據,每一次發送都需要寫上接收方的ip地址和端口號 18 udpSocket.sendto(sendData, sendAddr) 19 # udpSocket.sendto(b'hahahaha', ('192.168.10.247', 8080)) 20 21 udpSocket.close()
服務端
1 import socket 2 3 udpSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 4 5 # 接收方一般需要綁定端口 6 # ''表示自己電腦的任何一個ip,即無線和有限同時連接或者電腦有不同的網卡(橋接),會有多個ip. 7 # 綁定自己的端口 8 bindAddr = ('', 7788) 9 udpSocket.bind(bindAddr) 10 11 recvData = udpSocket.recvfrom(1024) 12 # print(recvData) 13 print(recvData[0].decode('gbk')) 14 15 udpSocket.close() 16 # recvData的格式:(data, ('ip', 端口)).它是一個元組,前面是數據,后面是一個包含ip和端口的元組.
2、tcp的客戶端和服務端寫法
客戶端
1 import socket 2 3 tcpClient = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 4 5 serverAddr = ('192.168.10.247', 8899) 6 7 # tcp的三次握手,寫進了這一句話 8 tcpClient.connect(serverAddr) 9 10 sendData = input('') 11 12 # 直接用send就行了,udp是用sendto 13 tcpClient.send(sendData.encode('gbk')) 14 15 recvData = tcpClient.recv(1024) 16 17 print('接收到的數據為:%s' % recvData.decode('gbk')) 18 19 tcpClient.close() 20 21 # 為什么用send而不是sendto?因為tcp連接是事先鏈接好了,后面就直接發就行了。前面的connect已經連接好了,后面直接用send發送即可。 22 # 而udp必須用sendto,是發一次數據,連接一次。必須要指定對方的ip和port。 23 # 相同的道理,在tcpServer端,要寫recv,而不是recvfrom來接收數據
服務端
1 import socket 2 tcpServer = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 3 tcpServer.bind(('', 8899)) 4 tcpServer.listen(5) 5 6 # tcp的三次握手,寫進了這一句話當中 7 tcpClient, tcpClientInfo = tcpServer.accept() 8 # tcpServer.accept(),不需要寫ip,可以接收多個客戶端的。但事先要綁定端口和接入的客戶端的數量 9 # client 表示接入的新的客戶端 10 # clientInfo 表示接入的新的客戶端的ip和端口port 11 12 recvData = tcpClient.recv(1024) 13 print('%s: %s' % (str(tcpClientInfo), recvData.decode('gbk'))) 14 15 # tcp的四次握手,寫進了這一句話 16 tcpClient.close() 17 tcpServer.close() 18 19 # tcpServer.accept():等待客戶端的接入,自帶堵塞功能:即必須接入客戶端,然后往下執行 20 # tcpClient.recv(1024): 也是堵塞,不輸入數據就一直等待,不往下執行. 21 # tcpServer創建了兩個套接字,一個是Server,另一個是tcpClient.Server負責監聽接入的Client,再為其創建專門的tcpClient進行通信.
3、服務端開啟循環和多線程模式
開啟循環
1 import socket 2 3 Server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 4 5 Server.bind(('', 9000)) 6 Server.listen(10) 7 8 while True: 9 # 如果有新的客戶端來鏈接服務器,那么就產生一個新的套接字專門為這個客戶端服務 10 serverThisClient, ClientInfo = Server.accept() 11 print('Waiting connect......') 12 13 # 如果客戶發送的數據是空的,那么斷開連接 14 while True: 15 recvData = serverThisClient.recv(1024) 16 if len(recvData) > 1: 17 18 print('recv: %s' % recvData.decode('gbk')) 19 20 sendData = input('send: ') 21 serverThisClient.send(sendData.encode('gbk')) 22 else: 23 print('再見!') 24 break 25 serverThisClient.close()
多線程寫法
1 from threading import Thread 2 import socket 3 # 收數據,然后打印 4 def recvData(): 5 while True: 6 recvInfo = udpSocket.recvfrom(1024) 7 print('%s:%s' % (str(recvInfo[1]), recvInfo[0].decode('gbk'))) 8 9 # 檢測鍵盤,發數據 10 def sendData(): 11 while True: 12 sendInfo = input('') 13 udpSocket.sendto(sendInfo.encode('gbk'), (destIp, destPort)) 14 15 udpSocket = None 16 destIp = '' 17 destPort = 0 18 # 多線程 19 def main(): 20 21 global udpSocket 22 global destIp 23 global destPort 24 25 destIp = input('對方的ip: ') 26 destPort = int(input('對方的端口:')) 27 28 udpSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 29 udpSocket.bind(('', 45678)) 30 31 tr = Thread(target=recvData) 32 ts = Thread(target=sendData) 33 34 tr.start() 35 ts.start() 36 37 tr.join() 38 ts.join() 39 if __name__ == '__main__': 40 main()
三、socket 獲取 html
目標:獲取https://movie.douban.com/top250整個網頁。
過程:
1.構造GET請求頭,包括: https協議處理,movie.douban.com為host,port默認為443,/top250為請求的url。
2.粘包接收字節流並進行解析,返回狀態碼、響應頭、響應體。
實例代碼:
1 import socket, ssl 2 3 4 """ 5 https 請求的默認端口是 443,https 的 socket 連接需要 import ssl,並且使用 s = ssl.wrap_socket(socket.socket()) 來初始化 6 7 HTTP 協議的 301 狀態會在 HTTP 頭的 Location 部分告訴你應該轉向的 URL 8 如果遇到 301, 就請求新地址並且返回 9 HTTP/1.1 301 Moved Permanently 10 ... 11 Location: https://movie.douban.com/top250 12 """ 13 14 15 def parse_url(url): 16 protocol = 'http' 17 18 if url[:7] == 'http://': 19 u = url.split("://")[1] 20 elif url[:8] == "https://": 21 protocol = "https" 22 u = url.split("://")[1] 23 else: 24 u = url 25 26 # https://g.cn:1234/hello/world 27 # 這里host就是g.cn:1234 28 # 這里path就是/hello/world 29 30 # 檢查host和path 31 i = u.find('/') 32 if i == -1: 33 host = u 34 path = '/' 35 else: 36 host = u[:i] 37 path = u[i:] 38 39 # 檢查端口 40 port_dict = dict( 41 http=80, 42 https=443, 43 ) 44 # 默認端口 45 port = port_dict[protocol] 46 if ":" in host: 47 h = host.split(":") 48 host = h[0] # 到這里獲取ip 49 port = int(h[1]) # 獲取端口 50 return protocol, host, port, path 51 52 53 def socket_by_protocol(protocol): 54 """根據協議返回一個socket實例""" 55 if protocol == 'http': 56 s = socket.socket() 57 else: 58 # https協議要用到ssl 59 s = ssl.wrap_socket(socket.socket()) 60 return s 61 62 def response_by_socket(s): 63 """s是一個socket實例,返回這個socket讀取的所有數據""" 64 response = b'' 65 buffer_size = 1024 66 while True: 67 r = s.recv(buffer_size) 68 if len(r) == 0: 69 break 70 response += r 71 return response 72 73 def parsed_response(r): 74 """ 75 把response解析出 狀態碼 headers body 返回 76 狀態碼是 int 77 headers是 dict 78 body是 str 79 """ 80 header, body = r.split('\r\n\r\n', 1) # split 1 只分割1次 81 h = header.split('\r\n') 82 83 # HTTP/1.1 200 OK 84 status_code = h[0].split()[1] # 空格切分,取中間的狀態碼 85 status_code= int(status_code) 86 87 headers = {} 88 for line in h[1: ]: 89 k, v = line.split(": ") 90 headers[k] = v 91 return status_code, headers, body 92 93 def get(url): 94 """用get請求url並返回響應""" 95 protocol, host, port, path = parse_url(url) 96 print(protocol, host, port, path) 97 # 根據protocol確定socket方式 98 s = socket_by_protocol(protocol) 99 s.connect((host, port)) 100 101 # 構造請求,注意不要用keep-alive,因為服務器不會主動關閉連接 102 request = 'GET {} HTTP/1.1\r\nhost: {}\r\nconnection: close\r\n\r\n'.format(path, host) 103 104 # 發送請求 105 s.send(request.encode(encoding='utf_8', errors='strict')) 106 107 # 獲取響應 108 response = response_by_socket(s) # 接收所有的數據 109 r = response.decode('utf-8') 110 111 status_code, headers, body = parsed_response(r) # 解析狀態碼,請求頭和請求體 112 113 if status_code in [301, 302]: # 301和302是重定向,要遞歸進行尋址 114 url = headers["Location"] 115 return get(url) 116 return status_code, headers, body 117 118 def main(): 119 url = 'https://movie.douban.com/top250' 120 status_code, headers, body = get(url) 121 print(status_code, headers, body) 122 123 if __name__ == '__main__': 124 main() 125
缺陷:這里沒有對html文檔進行處理,包括html文檔中所需內容(node節點)的解析和保存。可以使用bs4進一步處理。
注意:request請求必須要確保無誤,否則會很容易出錯。
四、socket寫web服務
這里的內容極其重要。一切web框架的本質是socket。Django和Flask的框架都是在下面最簡功能上進行拆分解耦建立起來的。
1 import socket, functools 2 from docutils.parsers.rst.directives import encoding 3 4 # 包裹print,禁止它的一些額外的功能 5 def log(*args, **kwargs): 6 print("LOG: ", *args, **kwargs) 7 # 禁止函數的默認返回, func = lambda x: x, print = func(print)。這個在flask的werkzeug.local中用了幾十次。 8 9 10 def route_index(): 11 """主頁的處理函數, 返回響應""" 12 header = 'HTTP/1.x 200 OK \r\nContent-Type: text/html\r\n' 13 body = '<h1>Hello World.</h1><img src="dog.gif"/>' 14 r = header + '\r\n' + body 15 return r.encode(encoding='utf-8') 16 17 def route_image(): 18 """返回一個圖片""" 19 with open('dog.gif', mode='rb') as f: 20 header = b'HTTP/1.x 200 OK\r\nContent-Type: image/gif\r\n\r\n' 21 img = header + f.read() 22 return img 23 24 def page(html): 25 with open(html, encoding="utf-8") as f: 26 return f.read() 27 28 29 def route_msg(): 30 """返回一個html文件""" 31 header = 'HTTP/1.x 200 OK \r\nContent-Type: text/html\r\n' 32 body = page("html_basic.html") 33 r = header + '\r\n' + body 34 return r.encode(encoding='utf-8') 35 36 37 def error(code=404): 38 e = { 39 404: b'HTTP/1.x 404 NOT FOUND\r\n\r\n<h1>Page Not Found</h1>', 40 } 41 return e.get(code, b'') 42 43 44 def response_for_path(path): 45 """根據path調用相應的處理函數,沒有處理的path會返回404""" 46 r = { 47 '/': route_index, 48 '/dog.gif': route_image, 49 '/msg': route_msg, 50 } 51 response = r.get(path, error) # 注意,這里用dict的get方法設置了不存在時的默認值 52 return response() 53 54 def run(host="", port=3000): 55 """啟動服務器""" 56 with socket.socket() as s: 57 # 使用with可以保證程序終端的時候正確關閉socket,釋放占用的端口 58 s.bind((host, port)) 59 60 while True: 61 s.listen(5) 62 63 connection, address = s.accept() 64 request = connection.recv(1024) 65 request = request.decode('utf-8') 66 log('ip and request, {}\n{}'.format(address, request)) 67 68 try: 69 path = request.split()[1] 70 response = response_for_path(path) # 用response_for_path函數來根據不同的path,生成不同的響應內容 71 connection.sendall(response) 72 73 except Exception as e: 74 log("error ", e) 75 76 connection.close() 77 78 79 def main(): 80 config = dict( 81 host='', 82 port=4000, 83 ) 84 run(**config) 85 86 if __name__ == '__main__': 87 main()
用到的圖片直接放在當前.py同一目錄下即可。html也是同級目錄,內容如下:
1 <!DOCTYPE html> 2 <!-- 注釋是這樣的, 不會被顯示出來 --> 3 <!-- 4 html 格式是瀏覽器使用的標准網頁格式 5 簡而言之就是 標簽套標簽 6 --> 7 <!-- html 中是所有的內容 --> 8 <html> 9 <!-- head 中是放一些控制信息, 不會被顯示 --> 10 <head> 11 <!-- meta charset 指定了頁面編碼, 否則中文會亂碼 --> 12 <meta charset="utf-8"> 13 <!-- title 是瀏覽器顯示的頁面標題 --> 14 <title>例子 1</title> 15 </head> 16 <!-- body 中是瀏覽器要顯示的內容 --> 17 <body> 18 <!-- html 中的空格是會被轉義的, 所以顯示的和寫的是不一樣的 --> 19 <!-- 代碼寫了很多空格, 顯示的時候就只有一個 --> 20 很 好普通版 21 <h1>很好 h1 版</h1> 22 <h2>很好 h2 版</h2> 23 <h3>很好 h3 版</h3> 24 <!-- form 是用來給服務器傳遞數據的 tag --> 25 <!-- action 屬性是 path --> 26 <!-- method 屬性是 HTTP方法 一般是 get 或者 post --> 27 <!-- get post 的區別上課會講 --> 28 <form action="/" method="get"> 29 <!-- textarea 是一個文本域 --> 30 <!-- name rows cols 都是屬性 --> 31 <textarea name="message" rows="8" cols="40"></textarea> 32 <!-- button type=submit 才可以提交表單 --> 33 <button type="submit">GET 提交</button> 34 </form> 35 <form action="/" method="post"> 36 <textarea name="message" rows="8" cols="40"></textarea> 37 <button type="submit">POST 提交</button> 38 </form> 39 </body> 40 </html>
運行上述py程序,訪問localhost:4000/msg,在get和post輸入框里分別輸入內容並點擊發送。可以看到,get請求的參數包含在請求頭里,post請求的參數包含在請求體里。這需要分別處理並獲取參數。
五、從socket到web框架
有了上述內容,整個url請求的處理流程如下圖所示。

創建python package。創建以下幾個py文件。(https://github.com/ZJingyu/socket_learning)
- web
- static: 靜態文件
- templates: html文件
- server: 主要負責監聽socket連接和接收請求,並對請求進行解析,傳遞給不同的處理函數。
- routes: url對應的處理函數。
- models: 數據交互的處理。
當然,對於models和routes都是可以再拆分。如:
- web
- static
- templates
- db
- views.py: 將routes改名為views.py,刪掉route_dict。
- urls.py: 把route_dict放進來,從views.py中導入所有視圖函數。
- models:新建名為models的python package,創建Message.py和User.py,將原models.py中的三個類分開,Model放進__init__.py里。
__init__.py: Model類
- Message.py: Message類
- User.py: User類
1 # __init__.py 2 3 import json 4 5 def save(data, path): 6 """把一個dict或者list寫入文件""" 7 8 # indent是縮進, ensure_ascii=False用於保存中文 9 s = json.dumps(data, indent=2, ensure_ascii=False) 10 with open(path, 'w+', encoding='utf-8') as f: 11 print('save', path, s, data) 12 f.write(s) 13 14 15 def load(path): 16 """從一個文件中載入數據並轉化為dict或者list""" 17 with open(path, 'r', encoding='utf-8') as f: 18 s = f.read() 19 print('load', s) 20 return json.loads(s) 21 22 23 # Model是用於存儲數據的基類 24 class Model(object): 25 @classmethod 26 def db_path(cls): 27 classname = cls.__name__ 28 path = '{}.txt'.format(classname) 29 return path 30 31 @classmethod 32 def new(cls, form): 33 m = cls(form) # 初始化實例 m = Model(form) 34 return m 35 36 @classmethod 37 def all(cls): 38 path = cls.db_path() 39 models = load(path) 40 ms = [cls.new(m) for m in models] 41 return ms 42 43 def save(self): 44 models = self.all() 45 print('models', models) 46 models.append(self) 47 l = [m.__dict__ for m in models] 48 path = self.db_path() 49 save(l, path) 50 51 def __repr__(self): 52 classname = self.__class__.__name__ 53 properties = ['{}: ({})'.format(k, v) for k, v in self.__dict__.items()] 54 s = '\n'.join(properties) 55 return '< {}\n{} >\n'.format(classname, s)
1 # Message.py 2 from . import Model 3 4 # 定義一個class用於保存message 5 class Message(Model): 6 def __init__(self, form): 7 self.author = form.get('author', "") 8 self.message = form.get('message', "")
1 # User.py 2 from . import Model 3 4 # 以下兩個類用於實際的數據處理 5 class User(Model): 6 def __init__(self, form): 7 self.username = form.get('username', "") 8 self.password = form.get('password', "") 9 10 def validate_login(self): 11 return self.username == "gua" and self.password == "123" 12 13 def validate_register(self): 14 return len(self.username) > 2 and len(self.password) > 2
當然,,server也可以再拆分成server和manage的。將serve.py中的if main 放到manage.py去執行。
上面這些雖然簡陋,但是實現了socket搭建web服務器的基本邏輯和功能,有助於梳理web服務的流程。原理在手,一切的web框架不外乎如是。
