Python 提供低階與高階兩種網路存取 API. 在低階部分這些 API 透過底層作業系統的支援基本的 Socket 存取以實作連接式 (Connection-oriented, TCP) 與非連接式 (Connectionless) 的客戶端 (Client) 與伺服端 (Server) 應用; 而在高階部分, 也提供了特定應用層如 HTTP, FTP 等通訊協定的存取.
Socket (網路插槽) 是兩個主機透過 IP 網路連線時在雙向通訊通道兩端的端點, 它是主機應用層與傳輸層之間的介面, 兩個不同的程序透過 IP (網路位址) 與 Port (通訊埠) 組成的網路位址來找到通訊的對象 (程序), 這是因為一個主機上有多個程序共用一個 IP, 必須加上 Port 才能區別 不同的程序 (0~1024 為系統保留埠號, 1025~65535 可自由使用). 事實上 Port 是用來讓作業系統比對收到的封包是要分派給哪一個程序的 Socket.
不同的通道型態 (channel type) 需建立不同的 Socket, 例如常用的 TCP scoket 是用在連接式的連線通道; 而 UDP Socket 則用在非連接式通道. 連接式協定需要確認程序, 例如 TCP 即透過三向交握才能建立一條連線通道, 而 UDP 則不需要, 因此 TCP 較費時. 透過 Socket 可以讓網路的存取就跟檔案 I/O 一樣, 基本上就是讀 (接收) 與寫 (傳送) 而已, 參考 :
# Bear實驗室:網路通訊之什麼是Socket?
一般的 Socket 只能讀取傳輸層 (例如 TCP/UDP) 以上的資訊, 但有一種用在 sniffer 程式設計的 Raw Socket 則可以讀取包含傳輸層以下的封包資訊, 例如 TCP/UDP 層, IP 層, 甚至鏈路層的資訊, , 參考 :
# Beej's Guide to Network Programming (很棒的 UNIX Networking 教學)
對於 Server-Client 架構來說, Socket 可以分成 Server socket 與 Client Socket, 作為伺服器的主機在建立 Server socket 後必須監聽 (listen) 特定網路位址 (IP+Port) 以等候客戶端連線; 而作為客戶端的主機在建立 Client socket 後則須連線 (connect) 遠端主機.
Python 提供了 socket 模組來支援低階網路應用程式存取 BSD Socket 介面, 其 API 參考 :
# https://docs.python.org/3/library/socket.html#
# python socket编程详细介绍
而在 MicroPython 則是使用 usocket 模組, 使用 Socket 功能前須先匯入此模組 :
import usocket
當然基於移植性考慮還是可以匯入 socket 模組, 如同其他 u 開頭的改寫模組, MicroPython 找不到 socket 模組時會自動匯入 usocket 模組, 此機制稱為 Fall-back. 此外, 為了效率與一致性, MicroPython 直接在 usocket 物件實作了與檔案處理類似的串流介面 (stream interface), 因此不需要使用 makefile() 將 socket 物件轉換成類似檔案的物件, 不過基於可移植性, 在 MicroPython 中仍然可以使用 makefile().
以下我以燒錄 1.9 版韌體的 ESP-01 模組 (1M) 按圖索驥測試 Socket 模組功能, 參考 :
# 5. Network - TCP sockets
# class socket
# usocket – socket module
# http://docs.micropython.org/en/latest/micropython-esp8266.pdf (2.1.4 usocket module)
在測試 Socket 之前, 首先必須先將 ESP8266 模組透過 WiFi 連線 Internet, 參考本系列之前測試紀錄中的 WiFi 連線篇 :
# MicroPython on ESP8266 (二) : 數值型別測試
# MicroPython on ESP8266 (三) : 序列型別測試
# MicroPython on ESP8266 (四) : 字典與集合型別測試
# MicroPython on ESP8266 (五) : WiFi 連線與 WebREPL 測試
# MicroPython on ESP8266 (六) : 檔案系統測試
# MicroPython on ESP8266 (七) : 時間日期測試
# MicroPython on ESP8266 (八) : GPIO 測試
# MicroPython on ESP8266 (九) : PIR 紅外線移動偵測
MicroPython 文件參考 :
# MicroPython tutorial for ESP8266 (官方教學)
# http://docs.micropython.org/en/latest/micropython-esp8266.pdf
# http://docs.micropython.org/en/latest/pyboard/library/usocket.html#class-socket
# http://docs.micropython.org/en/v1.8.7/esp8266/library/usocket.html#module-usocket
# https://gist.github.com/xyb/9a4c8d7fba92e6e3761a (驅動程式)
簡言之只要三個指令就可以讓 ESP8266 模組透過附近的 WiFi 基地台連上 Internet :
import network
sta=network.WLAN(network.STA_IF)
sta.connect('H30-L02-webbot', '1234567890')
sta.ifconfig()
最後一個指令 ifconfig() 只是用來確認是否連線成功取得 IP, 實際測試如下 :
MicroPython v1.9-8-gfcaadf92 on 2017-05-26; ESP module with ESP8266
Type "help()" for more information.
>>> import network
>>> sta=network.WLAN(network.STA_IF)
>>> sta.connect('H30-L02-webbot', '1234567890')
>>> sta.ifconfig()
('192.168.43.72', '255.255.255.0', '192.168.43.1', '192.168.43.1')
>>> import socket
參考 :
# 4.1. Configuration of the WiFi
可見已獲得 DHCP 指派 IP 為 192.168.43.72 了. 連線 Internet 之後即可匯入 socket 模組並呼叫其建構式 socket() 來建立 socket 物件, 此建構式可傳入三個可有可無的參數如下 :
socket.socket([family[, type[, proto]]])
其中 family 是指位址家族 (address family), 可用 socket.AF_INET (預設), 或 socket.AF_INET6. type 是 socket 類型, 可用 socket.SOCK_STREAM (TCP, 預設) 或 socket.SOCK_DGRAM (UDP). 而 proto 為協定編號, 可用 socket.IPPROTO_UDP 或 socket.IPPROTO_TCP, 通常設為 0.
參考 :
# Socket address format(s)
因此下面的指令會在本機建立一個 TCP socket :
import socket
s=socket.socket() #預設建立 TCP socket
測試結果如下 :
>>> import socket
>>> s=socket.socket()
>>> s
<socket state=0 timeout=-1 incoming=0 off=0>
但是第三參數傳入 socket.IPPROTO_TCP 卻報錯 (無 socket.IPPROTO_TCP 屬性) :
>>> s=socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'module' object has no attribute 'IPPROTO_TCP'
照下面這個文件, 第三參數通常為 0 (預設), 測試是 OK 的 :
# https://docs.python.org/2/library/socket.html#socket.socket
"The protocol number is usually zero and may be omitted in that case."
>>> s=socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
>>> s
<socket state=0 timeout=-1 incoming=0 off=0>
如果是作為 Client, 則建立 socket 物件 (Client socket) 後, 就可以呼叫 connect(address) 方法與遠端主機的 socket 連線, 此方法必須傳入遠端 Socket 位址, 即 IP 與對方 Port 組成之 tuple, 且必須使用 IP 位址, 不能用網域名稱 (Domain Name). 但要如何得知遠端網域名稱的 IP 呢? 這就要用到 socket 物件的 getaddrinfo() 方法了, 此方法會傳回一個五個元素的 tuple 組成的串列, 參考 :
# http://docs.micropython.org/en/latest/pyboard/library/usocket.html#usocket.socket.getaddrinfo
# https://docs.python.org/3/library/socket.html#socket.getaddrinfo
例如查詢 MicroPython 首頁 micropython.org 的網址資訊, 會得到 IP 位址為 176.58.119.26 :
MicroPython v1.9-8-gfcaadf92 on 2017-05-26; ESP module with ESP8266
Type "help()" for more information.
>>> import socket
>>> addr_info=socket.getaddrinfo('micropython.org',80)
>>> addr_info
[(2, 1, 0, '', ('176.58.119.26', 80))]
>>> addr=addr_info[0][-1]
>>> addr
('176.58.119.26', 80)
建立起 socket 物件後, 應用層與傳輸層 (TCP/UDP) 的通道就建立起來, 這樣便可以用 socket 物件的送收方法 send() 與 recv() 與伺服端進行應用層的交易了.
一. Telnet 協定 :
下面測試 1 程式取自 "5. Network - TCP sockets" 教學文件中的 Telnet 連線範例, Telnet 是 TCP/IP 的一種應用層協定, 使用系統保留埠 Port 23, 主要用於本地主機透過帳號密碼登錄遠端主機以便遙控遠端主機之用. 此例中的遠端主機 towel.blinkenlights.nl 會向與其連線的用戶端主機傳送字元動畫.
測試 1 : Telnet 協定測試 (字元動畫)
import socket
import network
sta=network.WLAN(network.STA_IF)
sta.connect('H30-L02-webbot', '1234567890') #這行要自己改
s=socket.socket()
s.connect(socket.getaddrinfo("towel.blinkenlights.nl", 23)[0][-1])
while True:
data=s.recv(500)
print(str(data,'utf8'),end='')
此程式以 Telnet 與遠端主機連線後, 在無窮迴圈內用 socket 的 recv() 方法讀取遠端主機傳來的 byte 串流, 用 str() 以 unicode 編碼轉成文字後印出來, 結果是如下的動畫 :
recv() 方法會從 Socket 接收 byte 串流資料, 須傳入一個必要參數 bufsize (緩衝區大小), 用來指定一次所能接收的資料 byte 數; 而 flag 為, 預設是 0 :
socket.recv(bufsize)
bufsize 最好是 2 的冪次整數, 例如 512, 1024, 或 4096, 參考 :
# http://docs.micropython.org/en/latest/pyboard/library/usocket.html#usocket.socket.recv
在 CPython 此 recv() 方法還有一個可有可無的參數 flag (預設是 0), 參考 :
# https://docs.python.org/3/library/socket.html#socket.socket.recv
但在 MicroPython 並未實作, 傳 0 進去會出現錯誤 :
>>> s=socket.socket()
>>> s.connect(socket.getaddrinfo("towel.blinkenlights.nl", 23)[0][-1])
>>> data=s.recv(500,0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: function takes 2 positional arguments but 3 were given
若不用無窮迴圈, 而是手動讀取 Socket 接收緩衝區, 可以看到每次收到的串流 :
>>> data=s.recv(500)
>>> print(data)
b'\x1b[H\x1b[J'
>>> data=s.recv(500)
>>> print(data)
b'\x1b[H\r\n\r\n\r\n\r\n\r\n\r\n \r\n Original Work : Simon Jansen ( http://www.asciimation.co.nz/ ) \r\n Telnetification : Sten Spans ( http://blinkenlights.nl/ ) \r\n Terminal Tricks : Mike Edwards (pf-asciimation@mirkwood.net) \r\n \r\n The hard work was done by Simon and Mike, \r\n I just placed it online in a '
二. HTTP 協定 :
HTTP 是 WWW 所使用的應用層協定, 它跟 Telnet 一樣也是底層採用 TCP 協定, 預設使用系統保留埠 80, 也可以改為任何自訂埠, 例如 8080 埠. WWW 是英國科學家 Tim Berners-Lee 為了協助 CERN (歐洲粒子物理實驗室) 與物理學者社群交換研究資料於 1989 年所提出的計畫, 整個 WWW 的運作是基於 Tim Berners-Lee 的三個核心設計 : HTTP 協定, HTML 語言, 以及 URL 資源定位方式. Tim Berners-Lee 於 2016 年獲得有電腦諾貝爾獎之稱的圖靈獎.
HTTP 是一種交易導向 (Transaction-based) 的 Client-Server 互動架構, 一次 TCP 連線代表一個交易, 主要用在瀏覽器 (Client) 向 WWW 網頁伺服器進行資源存取的操作上, 例如瀏覽網頁或資料庫讀寫等. 但瀏覽器只是 WWW 的一個使用者代理 (User Agent) 而已, 任何實作 HTTP 的應用程式都是 User Agent.
HTTP 以 Client-Server 模式運作, 請求 (Request) 訊息由客戶端發出, 而伺服端則在收到後進行處理並發出回應 (Response) 訊息. 在 HTTP 1.1 版以前, 所使用的 TCP 連線為非持續性連線 (Non-persistent), 一次只能傳送一個資源, 若一個網頁含有 2 個圖檔, 則連同網頁本身必須先後建立 3 個 TCP 連線才能取得全部資源. HTTP 1.1 版則支援持續性連線, 即可以在一次 TCP 連線中回應多個資源, 傳輸效率較高.
HTTP 是一種無狀態的協定, 當伺服器對一個請求做出回應後, 伺服器不會儲存這個交易的狀態或訊息 (無記憶性), 亦即每一個 Request之間都是彼此獨立, 即使客戶端馬上發出同樣的 Request, 伺服端不會記得與前面的 Request 相同, 還是重新回應一次. 有狀態 (Stateful) 的應用層協定例如 FTP.
HTTP 協定是文本的 (Text-based), 由 ASCII 純文字構成, 其訊息結構主要分成三部分 :
- 請求或狀態行 : 在 Request 訊息為請求行, 在 Response 訊息則為狀態行
- 標頭 : 由通用標頭, 請求或回應標頭, 以及實體標頭 (有實體的話) 組成
- 實體內容 : 應用層訊息例如 HTML
注意, 標頭與實體內容之間必須有一個空行, 且此空行就只是單純的 \r\n (即跳行字元
# 工具篇 - HTTP协议报文结构及示例03
# HTTP报文结构图解
# https://www.slideshare.net/waqas1234/dictributed-application-by-waqas-presentation
HTTP 定義了客戶端向伺服端提出請求的 8 種方法 (Method), 也就是要求伺服器操作資源的 8 種動作, 一個 HTTP 伺服器至少要實作 GET 與 HEAD 這兩種方法 (注意, 方法的名稱一定要大寫), 不一定要實作全部 :
方法 | 說明 |
GET | 在網址上傳送訊息來擷取伺服器上的資料 |
POST | 在 HTTP 內容中傳送訊息來擷取伺服器上的資料 |
HEAD | 與 GET 相同但伺服器只回應標頭 |
OPTIONS | 查詢伺服器可用的通訊選項 |
PUT | 上傳資料給伺服器以取代原來的資料 |
DELETE | 刪除伺服器上客戶端指定之資料 (可能被伺服器拒絕) |
PATCH | 上傳與伺服器原始資料不同之處進行更新 |
TRACE | 要求伺服器回傳所收到之訊息並記錄所經過之 Proxy |
CONNECT | 要求 Proxy 伺服器建立連線轉送 HTTP 訊息 |
這 8 種方法中最常用的是 GET 與 POST 這兩種, 都是用來向伺服器提出資源要求 (Request), 其主要差別在於請求參數的傳遞方式, GET 是把要傳送給伺服器的資訊組成 key=value 字串 (例如 k1=v1&k2=v2) 放在請求行中資源 URL 後面傳遞 (以 ? 號隔開), 因為 URL 長度限制為 255 個字元 (1K Bytes), 因此傳送的資訊大小有限. POST 則是將要傳遞給伺服器的資訊放在 HTTP 訊息的 Entity body (實體本文) 中, 最大可傳送 2M Bytes 的數據, 參考 :
# 淺談 HTTP Method:表單中的 GET 與 POST 有什麼差別
# GET 與 POST 的區別與優缺點
# HTTP 方法:GET 对比 POST
# Http GET、POST Method
# HTTP - Requests
HTTP 訊息結構的第一行為請求或狀態行, 如果是 Request 訊息, 此行即為請求行; 若為 Response 訊息則為狀態行. 每欄用一個空格 (SP) 隔開, 以歸位 (CR) 與換列 (LF) 字元結束 :
GET /myweb/test.htm HTTP/1.1
如果是動態網頁, 則可以在網址後面以 ? 號附掛參數, 每一個參數以 key=value 方式傳送, 多個參數則用 & 串接, 例如 :
GET /myweb/test.php?k1=v1&k2=v2&k3=v3 HTTP/1.1
而伺服器的 Response 訊息第一行為狀態行, 第一欄位 Version 為 HTTP 版本 "HTTP/1.1", 第二欄位為狀態碼, 表示伺服器對該請求之處理狀態, 一般正常是 200, 代表該請求已被成功執行, 常見的不正常狀態是 404, 表示所要求的資源不存在, 例如網址不正確或網頁不存在. 如果請求沒有成功執行 (即不是 200 OK), 則伺服器可能在第三欄位 Reason 註明原因.
可能的狀態代碼如下表 :
代碼 | 狀態 | 說明 |
100 | Continue | 通知客戶端繼續傳送 HTTP Request 訊息的本文 (Body) |
101 | Switching protocol | 同意用戶端要求, 更換為標頭中指定之協定 |
200 | OK | Request 執行成功 |
201 | Created | 建立了一個新的網路資源 |
202 | Accepted | Request 已被接受但尚未執行完畢 |
204 | No content | Request 執行完畢, 但無回傳訊息 |
301 | Multiple choices | 因所要求之網路資源指定到多個地點而無法決定 |
302 | Moved permanently | 所要求之網路資源已被永久移除無法取得 |
304 | Moved temporariy | 所要求之網路資源暫時移除無法取得 |
400 | Bad request | Request 語法錯誤無法辨識 |
401 | Unauthorized | Request 未獲授權 |
403 | Forbidden | 拒絕提供服務 |
404 | Not found | 找不到所要求之資源 |
405 | Method not allowed | 要求之方法不被允許 |
406 | Not acceptable | Request 的格式不被接受 |
500 | Internal server error | 伺服器內部有問題 |
501 | Not implemented | 所要求之方法未實作 |
503 | Service unavailable | 伺服器無法提供服務 |
HTTP 的三類標頭中, 通用標頭不管是請求或回應訊息都可以用; 請求標頭只有請求訊息才會用; 回應標頭只用在回應訊息中; 而實體標頭則只在訊息含有實體本文 (Entity body) 時才會用到. 標頭的定義參考 :
# HTTP頭欄位列表 (完整)
# HTTP - Header Fields
# HTTP 標頭參照
# HTTP協議頭部與Keep-Alive模式詳解
以 MicroPython 的測試網頁為例, 它是一個單純的 HTML 靜態網頁, 使用瀏覽器連線此網頁, 瀏覽器會以 GET 方法提出請求, 伺服器會回應簡單的文字頁面 :
其網頁原始碼如下 :
<!DOCTYPE html>
<html lang="en">
<head>
<title>Test</title>
</head>
<body>
<h1>Test</h1>
It's working if you can read this!
</body>
</html>
這個網頁內容會被放在回應訊息的實體本文中傳回給客戶端 (瀏覽器). 在 FireFox 中按 F12, 切到 "網路" 點選 test.htm, 在右方 "檔頭" 頁籤按 "原始檔頭" 即可顯示標頭訊息, 切到 "回應" 標籤可看到放在實體內容中的回應網頁如下 :
不過在 MicroPython 中要擷取此網頁不需要傳送這麼多標頭, 只要傳送請求標頭 Host 即可. 在 GET 訊息中最重要的是請求行以及請求標頭中的 Host 標頭, 請求行是用來告訴伺服器所要求的資源位置 (URL) 在哪裡 ; 而請求標頭的 Host 標頭則是所連線的遠端伺服器之 Socket 位址 (IP, Port). 因此在 MicroPython 中只要用 send() 方法向伺服端傳送下面兩行 GET 請求即可 :
GET /ks/test.html HTTP/1.1
Host: micropython.org
寫成 HTTP 請求訊息字串如下 (注意結尾是兩個跳行) :
msg='GET /ks/test.html HTTP/1.0\r\nHost: micropython.org\r\n\r\n'
與上面 Telnet 協定不同的是, WWW 資源的 URL (網頁位址) 是階層式結構, 例如 MicroPython 測試網頁的位址 "http://micropython.org/ks/test.html" 是在主機域名 micropython.org 的 ks 目錄下的 test.htm 檔案, 因此在連線遠端網頁伺服器前, 須利用如 split() 函數將主機位址 (host) 與網頁路徑 (path) 從完整的 URL 中擷取出來, 參考 :
函數 split() 的 API 如下 :
split([sep[, maxsplit]])
它會以第一參數為界拆分字串 (預設是空格), 並將拆出來的子字串以串列傳回, 例如 :
>>> url='http://micropython.org/ks/test.html'
>>> url.split('/')
['http:', '', 'micropython.org', 'ks', 'test.html']
可見主機位址 (Host) 在索引 2, 而網頁路徑 ks/test.html 卻被分解了. 不過在 HTTP 協定中我們要的是網頁完整的路徑, 這可以利用 split() 的第二參數 maxsplit 來達成, 以上例來說只要拆分 3 次就可以了, 例如 :
>>> url.split('/', 3)
['http:', '', 'micropython.org', 'ks/test.html']
官網教學文件的範例是以多重指派來取得所需的域名 host 與資源路徑 path :
_, _, host, path=url.split('/', 3) #拆解 url 字串, 取出索引 2 的 host 與索引 3 的 path
addr=socket.getaddrinfo(host, 80)[0][-1] #傳回域名 (host,port) 的位址資訊 (IP, Port)
s=socket.socket() #建立 socket 物件
s.connect(addr) #連線遠端伺服器的 socket
TCP 連線成功後便可以用 send() 方法傳送 HTTP 訊息向伺服器請求資源, 即路徑 ks/test.html 所指的網頁, 其 API 如下 :
socket.send(bytes)
參考 :
# http://docs.micropython.org/en/latest/pyboard/library/usocket.html#usocket.socket.send
這裡傳入參數是 bytes 類型的二進位資料, 因 TCP 網路串流傳送的是 Byte stream 二進位資料 (TCP 是傳輸層, 只負責在兩台連線主機間搬移 byte, 這些資料的意義 TCP 不懂, 必須應用層如 HTTP 才懂), 因此必須先將 ASCII 編碼的 HTTP 請求訊息字串轉成 byte 型態資料才能傳送. Python 有內建函數 bytes() 與 bytearray() 可以處理二進位資料, bytes() 的傳回值是類似元組的 bytes 類型資料 (不可變), 而 bytearray() 的傳回值則是類似串列的 bytearray 類型資料, 跟串列或元組一樣是透過索引存取. bytes() 與 bytearray() 之 API 如下 :
bytearray([source[, encoding[, errors]]])
bytes([source[, encoding[, errors]]])
參考 :
# https://docs.python.org/3/library/stdtypes.html#bytearray
注意, 如果參數 Source 是字串, 一定要指定編碼參數 Encoding, 例如 'utf-8'.
例如 :
>>> blist=[1,2,3,255]
>>> the_bytes=bytes(blist)
>>> the_bytes
b'\x01\x02\x03\xff'
>>> the_bytes[0]
1
>>> the_bytes[1]
2
>>> the_bytes[2]
3
>>> the_bytes[3]
255
>>> the_bytes[2:3]
b'\x03'
>>> the_bytes[1:3]
b'\x02\x03'
>>> the_bytes[4] #索引超過範圍
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: bytes index out of range
>>> the_bytes[3]=127 # bytes 類型為不可變
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'bytes' object does not support item assignment
>>> the_bytes=bytes(range(0,256)) #將全部 ASCII 碼轉成 Byte
>>> the_bytes
b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff'
而 bytearray() 的傳回值是可變的, 例如 :
>>> the_byte_array=bytearray(blist)
>>> the_byte_array
bytearray(b'\x01\x02\x03\xff')
>>> the_byte_array[0]
1
>>> the_byte_array[3]
255
>>> the_byte_array[3]=127
>>> the_byte_array
bytearray(b'\x01\x02\x03\x7f')
>>> the_byte_array[3]
127
傳送 HTTP 請求訊息之後, 就可以用 socket 物件的 recv() 方法來接收伺服端的回應訊息, 通常用無窮迴圈來讀取接收緩衝器, 直到緩衝器無資料可讀為止. recv() 的 API 如下 :
socket.recv(bufsize)
此處傳入參數為緩衝器一次能接收的最大 byte 數, 傳回值為一個 byte 的二進位資料, 需用 str() 函數轉成 ASCII 編碼的字串.
# http://docs.micropython.org/en/latest/pyboard/library/usocket.html#usocket.socket.recv
下列測試 2 是把 ESP8266 當作一個客戶端, 透過所建立的 Client socket 傳送 HTTP 請求訊息去下載 MicroPython 官網 (Server socket) 的測試網頁 :
# http://micropython.org/ks/test.html
測試 2 : HTTP 協定測試 (下載網頁)
#main.py
import network
import socket
def connect_wifi(ssid, pwd):
wlan=network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
print('connecting to network ...')
wlan.connect(ssid, pwd)
while not wlan.isconnected():
pass
print('Connected:', wlan.ifconfig())
def get_ip():
return network.WLAN(network.STA_IF).ifconfig()[0]
def http_get(url):
_, _, host, path=url.split('/', 3)
addr=socket.getaddrinfo(host, 80)[0][-1]
s=socket.socket()
s.connect(addr)
s.send(bytes('GET /%s HTTP/1.0\r\nHost: %s\r\n\r\n' % (path, host), 'utf8'))
while True:
data=s.recv(100)
if data:
print(str(data, 'utf8'), end='')
else:
break
s.close()
將上面的程式存成 main.py, 用 ampy 上傳 ESP8266 模組後按 Ctrl-D 重開機, 先呼叫 connect_wifi() 連線 WiFi 基地台, 再呼叫 http_get() 函數, 傳入測試網頁網址 "http://micropython.org/ks/test.html" 即可收到伺服器回應之網頁內容了 :
MicroPython v1.9.1-8-g7213e78d on 2017-06-12; ESP module with ESP8266
Type "help()" for more information.
>>>
PYB: soft reboot
#8 ets_task(40100164, 3, 3fff829c, 4)
MicroPython v1.9.1-8-g7213e78d on 2017-06-12; ESP module with ESP8266
Type "help()" for more information.
>>> connect_wifi('EDIMAX-tony','1234567890')
Connected: ('192.168.2.112', '255.255.255.0', '192.168.2.1', '168.95.1.1')
>>> get_ip()
'192.168.2.112'
>>> http_get('http://micropython.org/ks/test.html')
HTTP/1.1 200 OK
Server: nginx/1.8.1
Date: Mon, 12 Jun 2017 23:18:35 GMT
Content-Type: text/html
Content-Length: 180
Last-Modified: Tue, 03 Dec 2013 00:16:26 GMT
Connection: close
Vary: Accept-Encoding
ETag: "529d22da-b4"
Accept-Ranges: bytes
<!DOCTYPE html>
<html lang="en">
<head>
<title>Test</title>
</head>
<body>
<h1>Test</h1>
It's working if you can read this!
</body>
</html>
>>>
可是當我測試 Google 首頁 'http://www.google.com.tw' 時發現測試 2 程式處理 url 有瑕疵, 其中的 split() 方法拆分 '/' 第三次時會出錯, 因為此 url 只能拆兩次 :
>>> url="http://www.google.com.tw"
>>> _, _, host, path=url.split('/', 3) # 這一行會出錯
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: need more than 3 values to unpack
解決辦法是全拆, path 的部分 (索引 3 之後) 再重新用 '/' 聚合 :
url=url.split('/')
host=url[2]
path='/'.join(url[3:])
這樣就可以順利處理 path 的問題了. 上面測試 2 程式修改如下 :
#main.py
import network
import socket
def connect_wifi(ssid, pwd):
wlan=network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
print('connecting to network ...')
wlan.connect(ssid, pwd)
while not wlan.isconnected():
pass
print('Connected:', wlan.ifconfig())
def get_ip():
return network.WLAN(network.STA_IF).ifconfig()[0]
def http_get(url):
url=url.split('/')
host=url[2]
path='/'.join(url[3:])
addr=socket.getaddrinfo(host, 80)[0][-1]
s=socket.socket()
s.connect(addr)
s.send(bytes('GET /%s HTTP/1.0\r\nHost: %s\r\n\r\n' % (path, host), 'utf8'))
while True:
data=s.recv(100)
if data:
print(str(data, 'utf8'), end='')
else:
break
s.close()
藍色為修改的部分, 這樣下面這個函數呼叫就不會出錯了 (但回應內容很長喔) :
>>>htt_get('http://www.google.com.tw')
如果只是要取得伺服器回應的標頭, 可以向伺服器提出 HEAD 請求, 其 HTTP 訊息格式與 GET 一樣, 只要把 GET 改成 HEAD 即可 :
HEAD / HTTP/1.1
伺服器只回應標頭, 而不會傳回資源內容. 我在 main.py 裡面複製 http_get() 為 http_head(), 將其中 GET 改成 HEAD 如下 :
#main.py
import network
import socket
def connect_wifi(ssid, pwd):
wlan=network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
print('connecting to network ...')
wlan.connect(ssid, pwd)
while not wlan.isconnected():
pass
print('Connected:', wlan.ifconfig())
def get_ip():
return network.WLAN(network.STA_IF).ifconfig()[0]
def http_get(url):
url=url.split('/')
host=url[2]
path='/'.join(url[3:])
addr=socket.getaddrinfo(host, 80)[0][-1]
s=socket.socket()
s.connect(addr)
s.send(bytes('GET /%s HTTP/1.0\r\nHost: %s\r\n\r\n' % (path, host), 'utf8'))
while True:
data=s.recv(100)
if data:
print(str(data, 'utf8'), end='')
else:
break
s.close()
def http_head(url):
url=url.split('/')
host=url[2]
path='/'.join(url[3:])
addr=socket.getaddrinfo(host, 80)[0][-1]
s=socket.socket()
s.connect(addr)
s.send(bytes('HEAD /%s HTTP/1.0\r\nHost: %s\r\n\r\n' % (path, host), 'utf8'))
while True:
data=s.recv(100)
if data:
print(str(data, 'utf8'), end='')
else:
break
s.close()
用 ampy 上傳 ESP8266 後按 Ctrl-D 軟開機, 呼叫 http_head() 查詢 MicroPython 測試網頁與 Google 首頁標頭 :
>>>
PYB: soft reboot
#7 ets_task(40100164, 3, 3fff829c, 4)
MicroPython v1.9.1-8-g7213e78d on 2017-06-12; ESP module with ESP8266
Type "help()" for more information.
>>>
>>> http_head('http://micropython.org/ks/test.html')
HTTP/1.1 200 OK
Server: nginx/1.8.1
Date: Thu, 15 Jun 2017 01:32:08 GMT
Content-Type: text/html
Content-Length: 180
Last-Modified: Tue, 03 Dec 2013 00:16:26 GMT
Connection: close
Vary: Accept-Encoding
ETag: "529d22da-b4"
Accept-Ranges: bytes
>>> http_head('http://www.google.com.tw')
HTTP/1.0 200 OK
Date: Thu, 15 Jun 2017 01:33:03 GMT
Expires: -1
Cache-Control: private, max-age=0
Content-Type: text/html; charset=Big5
P3P: CP="This is not a P3P policy! See https://www.google.com/support/accounts/answer/151657?hl=en for more info."
Server: gws
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Set-Cookie: NID=105=RWTxicR6Kgq4IYlTYBvoLoirOP_rLjL4y_ka8k4GHHQ-3l2C0uymV1hIMtxC5d2_DJaqIghQqa4DIoGITMXvgeAxu_bKkxoBgWJUNTiVYg6ah2-f2noikme3aAe-AYr7; expires=Fri, 15-Dec-2017 01:33:03 GMT; path=/; domain=.google.com.tw; HttpOnly
Accept-Ranges: none
Vary: Accept-Encoding
>>>
接下來測試 POST 請求的傳送方法, 參考 :
如果只是要取得伺服器回應的標頭, 可以向伺服器提出 HEAD 請求, 其 HTTP 訊息格式與 GET 一樣, 只要把 GET 改成 HEAD 即可 :
HEAD / HTTP/1.1
伺服器只回應標頭, 而不會傳回資源內容. 我在 main.py 裡面複製 http_get() 為 http_head(), 將其中 GET 改成 HEAD 如下 :
#main.py
import network
import socket
def connect_wifi(ssid, pwd):
wlan=network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
print('connecting to network ...')
wlan.connect(ssid, pwd)
while not wlan.isconnected():
pass
print('Connected:', wlan.ifconfig())
def get_ip():
return network.WLAN(network.STA_IF).ifconfig()[0]
def http_get(url):
url=url.split('/')
host=url[2]
path='/'.join(url[3:])
addr=socket.getaddrinfo(host, 80)[0][-1]
s=socket.socket()
s.connect(addr)
s.send(bytes('GET /%s HTTP/1.0\r\nHost: %s\r\n\r\n' % (path, host), 'utf8'))
while True:
data=s.recv(100)
if data:
print(str(data, 'utf8'), end='')
else:
break
s.close()
def http_head(url):
url=url.split('/')
host=url[2]
path='/'.join(url[3:])
addr=socket.getaddrinfo(host, 80)[0][-1]
s=socket.socket()
s.connect(addr)
s.send(bytes('HEAD /%s HTTP/1.0\r\nHost: %s\r\n\r\n' % (path, host), 'utf8'))
while True:
data=s.recv(100)
if data:
print(str(data, 'utf8'), end='')
else:
break
s.close()
用 ampy 上傳 ESP8266 後按 Ctrl-D 軟開機, 呼叫 http_head() 查詢 MicroPython 測試網頁與 Google 首頁標頭 :
>>>
PYB: soft reboot
#7 ets_task(40100164, 3, 3fff829c, 4)
MicroPython v1.9.1-8-g7213e78d on 2017-06-12; ESP module with ESP8266
Type "help()" for more information.
>>>
>>> http_head('http://micropython.org/ks/test.html')
HTTP/1.1 200 OK
Server: nginx/1.8.1
Date: Thu, 15 Jun 2017 01:32:08 GMT
Content-Type: text/html
Content-Length: 180
Last-Modified: Tue, 03 Dec 2013 00:16:26 GMT
Connection: close
Vary: Accept-Encoding
ETag: "529d22da-b4"
Accept-Ranges: bytes
>>> http_head('http://www.google.com.tw')
HTTP/1.0 200 OK
Date: Thu, 15 Jun 2017 01:33:03 GMT
Expires: -1
Cache-Control: private, max-age=0
Content-Type: text/html; charset=Big5
P3P: CP="This is not a P3P policy! See https://www.google.com/support/accounts/answer/151657?hl=en for more info."
Server: gws
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Set-Cookie: NID=105=RWTxicR6Kgq4IYlTYBvoLoirOP_rLjL4y_ka8k4GHHQ-3l2C0uymV1hIMtxC5d2_DJaqIghQqa4DIoGITMXvgeAxu_bKkxoBgWJUNTiVYg6ah2-f2noikme3aAe-AYr7; expires=Fri, 15-Dec-2017 01:33:03 GMT; path=/; domain=.google.com.tw; HttpOnly
Accept-Ranges: none
Vary: Accept-Encoding
>>>
接下來測試 POST 請求的傳送方法, 參考 :
# Python socket client Post parameters
def http_post(url, parameters):
_, _, host, path=url.split('/', 3)
addr=socket.getaddrinfo(host, 80)[0][-1]
s=socket.socket()
s.connect(addr)
request='POST /%s HTTP/1.0\r\n' % path
headers='Host: %s\r\n' % host
headers += 'Content-Length: %s\r\n' % str(len(parameters))
headers += 'Content-Type: application/x-www-form-urlencoded\r\n\r\n'
s.send(bytes(request + headers + parameters + '\r\n', 'utf8'))
while True:
data=s.recv(256)
if data:
print(str(data, 'utf8'), end='')
else:
break
s.close()
http_post('http://httpbin.org/post','a=1&b=2')
測試結果如下 :
PYB: soft reboot
#7 ets_task(40100164, 3, 3fff829c, 4)
MicroPython v1.9.1-8-g7213e78d on 2017-06-12; ESP module with ESP8266
Type "help()" for more information.
>>> http_post('http://httpbin.org/post','a=1&b=2')
HTTP/1.1 200 OK
Connection: close
Server: meinheld/0.6.1
Date: Thu, 15 Jun 2017 23:23:14 GMT
Content-Type: application/json
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
X-Powered-By: Flask
X-Processed-Time: 0.000968217849731
Content-Length: 341
Via: 1.1 vegur
{
"args": {},
"data": "",
"files": {},
"form": {
"a": "1",
"b": "2"
},
"headers": {
"Connection": "close",
"Content-Length": "7",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbin.org"
},
"json": null,
"origin": "223.138.253.129",
"url": "http://httpbin.org/post"
}
在上面的範例中, POST 訊息中包含了三個請求標頭 : Host, Content-Type, 以及 Content-Length, 其中 Host 在 Internet 存取中是一定要的標頭, 否則 Proxy 會不知道要將此要求丟給誰, 參考 :
# Python socket client Post parameters
"A client MUST include a Host header field in all HTTP/1.1 request messages . If the requested URI does not include an Internet host name for the service being requested, then the Host header field MUST be given with an empty value. An HTTP/1.1 proxy MUST ensure that any request message it forwards does contain an appropriate Host header field that identifies the service being requested by the proxy. All Internet-based HTTP/1.1 servers MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message which lacks a Host header field."
參考 :
# urllib / urequests for Micropython
# micropython/micropython-lib
# NTP update micropython time
# Get time from NTP Server
# micropython-lib/smtplib
# micropython sending mail
# HTTP協議頭部與Keep-Alive模式詳解
# Beej's Guide to Network Programming 正體中文版 (推薦)
# usocket UDP and sockaddr questions
# python socket编程详细介绍
# m@rcus 學習筆記
# HTTP - Responses
# Learn Python in 24 Hours - Sunny Chanday
# Python3.pdf
# Core Python Programming 2nd Edition 2006 (TCP/UDP)
_, _, host, path=url.split('/', 3)
addr=socket.getaddrinfo(host, 80)[0][-1]
s=socket.socket()
s.connect(addr)
request='POST /%s HTTP/1.0\r\n' % path
headers='Host: %s\r\n' % host
headers += 'Content-Length: %s\r\n' % str(len(parameters))
headers += 'Content-Type: application/x-www-form-urlencoded\r\n\r\n'
s.send(bytes(request + headers + parameters + '\r\n', 'utf8'))
while True:
data=s.recv(256)
if data:
print(str(data, 'utf8'), end='')
else:
break
s.close()
http_post('http://httpbin.org/post','a=1&b=2')
測試結果如下 :
PYB: soft reboot
#7 ets_task(40100164, 3, 3fff829c, 4)
MicroPython v1.9.1-8-g7213e78d on 2017-06-12; ESP module with ESP8266
Type "help()" for more information.
>>> http_post('http://httpbin.org/post','a=1&b=2')
HTTP/1.1 200 OK
Connection: close
Server: meinheld/0.6.1
Date: Thu, 15 Jun 2017 23:23:14 GMT
Content-Type: application/json
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
X-Powered-By: Flask
X-Processed-Time: 0.000968217849731
Content-Length: 341
Via: 1.1 vegur
{
"args": {},
"data": "",
"files": {},
"form": {
"a": "1",
"b": "2"
},
"headers": {
"Connection": "close",
"Content-Length": "7",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbin.org"
},
"json": null,
"origin": "223.138.253.129",
"url": "http://httpbin.org/post"
}
在上面的範例中, POST 訊息中包含了三個請求標頭 : Host, Content-Type, 以及 Content-Length, 其中 Host 在 Internet 存取中是一定要的標頭, 否則 Proxy 會不知道要將此要求丟給誰, 參考 :
# Python socket client Post parameters
"A client MUST include a Host header field in all HTTP/1.1 request messages . If the requested URI does not include an Internet host name for the service being requested, then the Host header field MUST be given with an empty value. An HTTP/1.1 proxy MUST ensure that any request message it forwards does contain an appropriate Host header field that identifies the service being requested by the proxy. All Internet-based HTTP/1.1 servers MUST respond with a 400 (Bad Request) status code to any HTTP/1.1 request message which lacks a Host header field."
# urllib / urequests for Micropython
# micropython/micropython-lib
# NTP update micropython time
# Get time from NTP Server
# micropython-lib/smtplib
# micropython sending mail
# HTTP協議頭部與Keep-Alive模式詳解
# Beej's Guide to Network Programming 正體中文版 (推薦)
# usocket UDP and sockaddr questions
# python socket编程详细介绍
# m@rcus 學習筆記
# HTTP - Responses
# Learn Python in 24 Hours - Sunny Chanday
# Python3.pdf
# Core Python Programming 2nd Edition 2006 (TCP/UDP)
沒有留言:
張貼留言