結合Python的SimpleHTTPServer源碼來解析socket通訊,simplesocketserver
何謂socket
電腦,顧名思義即是用來做計算。因而也需要輸入和輸出,輸入需要計算的條件,輸出計算結果。這些輸入輸出可以抽象為I/O(input output)。
Unix的電腦處理IO是通過檔案的抽象。電腦不同的進程之間也有輸入輸出,也就是通訊。因此這這個通訊也是通過檔案的抽象檔案描述符來進行。
在同一台電腦,進程之間可以這樣通訊,如果是不同的電腦呢?網路上不同的電腦,也可以通訊,那麼就得使用網路通訊端(socket)。socket就是在不同電腦之間進行通訊的一個抽象。他工作於TCP/IP協議中應用程式層和傳輸層之間的一個抽象。如:
伺服器通訊
socket保證了不同電腦之間的通訊,也就是網路通訊。對於網站,通訊模型是用戶端伺服器之間的通訊。兩個端都建立一個socket對象,然後通過socket對象對資料進行傳輸。通常伺服器處於一個無線迴圈,等待用戶端串連:
socket 通訊執行個體
socket介面是作業系統提供的,叫用作業系統的介面。當然進階語言一般也封裝了好用的函數介面,下面用python代碼寫一個簡單的socket服務端例子:
server.py
import socketHOST = 'localhost' # 伺服器主機地址PORT = 5000 # 伺服器監聽連接埠BUFFER_SIZE = 2048 # 讀取資料大小# 建立一個通訊端sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 綁定主機和連接埠sock.bind((HOST, PORT))# 開啟socket監聽sock.listen(5)print 'Server start, listening {}'.format(PORT)while True: # 建立串連,串連為建立的時候阻塞 conn, addr = sock.accept() while True: # 讀取資料,資料還沒到來阻塞 data = conn.recv(BUFFER_SIZE) if len(data): print 'Server Recv Data: {}'.format(data) conn.send(data) print 'Server Send Data: {}'.format(data) else: print 'Server Recv Over' break conn.close()sock.close()
client.py
import socketHOST = 'localhost'PORT = 5000BUFFER_SIZE = 1024# 建立用戶端通訊端sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)# 串連到伺服器sock.connect((HOST, PORT))try: message = "Hello" # 發起資料給伺服器 sock.sendall(message) amount_received = 0 amount_expected = len(message) while amount_received < amount_expected: # 接收伺服器返回的資料 data = sock.recv(10) amount_received += len(data) print 'Client Received: {}'.format(data)except socket.errno, e: print 'Socket error: {}'.format(e)except Exception, e: print 'Other exception: %s'.format(e)finally: print 'Closing connection to the server' sock.close()
TCP 三向交握
python代碼寫通訊端很簡單。傳說的TCP三向交握又是如何體現的呢?什麼是三向交握呢?
第一握:首先用戶端發送一個syn,請求串連,
第二握:伺服器收到之後確認,並發送一個 syn ack應答
第三握:用戶端接收到伺服器發來的應答之後再給伺服器發送建立串連的確定。
用下面的比喻就是
C:約嗎?
S:約
C:好的
約會
這樣就建立了一個TCP串連會話。如果是要中斷連線,大致過程是:
也很清晰的表明了三向交握的socket具體過程。
- 用戶端socket對象connect調用之後進行阻塞,此過程發送了一個syn。
- 伺服器socket對象調用accept函數之後阻塞,直到用戶端發送來的syn,然後發送syn和ack應答
- 用戶端socket對象收到服務端發送的應答之後,再發送一個ack給伺服器,並返回connect調用,建立串連。
- 伺服器socket對象接受用戶端最後一次握手確定ack返回accept函數,建立串連。
至此,用戶端和伺服器的socket通訊串連建立完成,剩下的就是兩個端的連線物件收發資料,從而完成網路通訊。
SimpleHTTPServer
構建一個簡單的HTTP服務,需要繼承HTTPServer,同時requesthandler也需要繼承BaseHTTPRequestHandler。python已經實現了一個例子,那就是SimpleHTTPServer。因此分析SimpleHTTPServer來查看如何使用前面的一些類構建http服務。
曾經為了表示python的簡潔優雅,經常會舉這樣的例子,python可以一行代碼開啟一個伺服器。
$ python -m SimpleHTTPServer
這裡的SimpleHTTPServer就是實現了HTTPServer的模組。
SimpleHTTPServer通過調用BaseHTTPServer模組的test方法做為入口。
def test(HandlerClass = SimpleHTTPRequestHandler, ServerClass = BaseHTTPServer.HTTPServer): BaseHTTPServer.test(HandlerClass, ServerClass)
test方法做了兩件事,第一件就是使用HTTPServer接受一個監聽地址和requestClass參數,建立了一個執行個體對象,調用server_forever方法開啟服務。
1.SimpleHTTPRequestHandler
根據之前的分析,使用httpserver的服務,我們只需要繼續BaseHTTPRequestHandler,並提供自省的method方法即可。
class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): server_version = "SimpleHTTP/" + __version__ def do_GET(self): f = self.send_head() if f: self.copyfile(f, self.wfile) f.close() def do_HEAD(self): f = self.send_head() if f: f.close()
do_GET 和 do_HEAD 分別實現了http的get請求和head請求的處理。他們調用send_head方法:
def send_head(self): path = self.translate_path(self.path) f = None if os.path.isdir(path): if not self.path.endswith('/'): self.send_response(301) self.send_header("Location", self.path + "/") self.end_headers() return None for index in "index.html", "index.htm": index = os.path.join(path, index) if os.path.exists(index): path = index break else: return self.list_directory(path) ctype = self.guess_type(path) try: f = open(path, 'rb') except IOError: self.send_error(404, "File not found") return None self.send_response(200) self.send_header("Content-type", ctype) fs = os.fstat(f.fileno()) self.send_header("Content-Length", str(fs[6])) self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) self.end_headers() return f
send_head 方法通過uri的path分析得到客戶請求的網路路徑。構造head的mime元資訊並發送到用戶端,然後返回一個開啟path的檔案控制代碼。
2.copyfile
do_GET的下一步就是通過 copyfile方法,將客戶請求的path的檔案資料寫入到緩衝可寫檔案中,發送給用戶端。
3.list_directory
SimpleHTTPServer模組還提供了list_directory方法,用於響應path是一個目錄,而不是檔案的情況。
def list_directory(self, path): try: list = os.listdir(path) except os.error: self.send_error(404, "No permission to list directory") return None list.sort(key=lambda a: a.lower()) f = StringIO() displaypath = cgi.escape(urllib.unquote(self.path)) f.write('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">') f.write("<html>\n<title>Directory listing for %s</title>\n" % displaypath) f.write("<body>\n<h2>Directory listing for %s</h2>\n" % displaypath) f.write("<hr>\n<ul>\n") for name in list: fullname = os.path.join(path, name) displayname = linkname = name # Append / for directories or @ for symbolic links if os.path.isdir(fullname): displayname = name + "/" linkname = name + "/" if os.path.islink(fullname): displayname = name + "@" # Note: a link to a directory displays with @ and links with / f.write('<li><a href="%s">%s</a>\n' % (urllib.quote(linkname), cgi.escape(displayname))) f.write("</ul>\n<hr>\n</body>\n</html>\n") length = f.tell() f.seek(0) self.send_response(200) encoding = sys.getfilesystemencoding() self.send_header("Content-type", "text/html; charset=%s" % encoding) self.send_header("Content-Length", str(length)) self.end_headers() return f
由此可見,處理用戶端的請求,只需要使用 send_reponse, send_header 和 end_headers ,就能向用戶端發送reponse。
4.自訂http服務
定義一個CustomHTTPRequestHadnler繼承自BaseHTTPRequestHandler。在其內實現do_GET 方法來處理get請求。
然後再定義一個CustomHTTPServer繼承自HTTPServer,它接受CustomHTTPRequestHadnler作為自己的handler。簡單的代碼如下:
# -*- coding: utf-8 -*-from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServerclass CustomHTTPRequestHandler(BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write("hello world\r\n")class CustomHTTPServer(HTTPServer): def __init__(self, host, port): HTTPServer.__init__(self, (host, port), CustomHTTPRequestHandler)def main(): server = CustomHTTPServer('127.0.0.1', 8000) server.serve_forever()if __name__ == '__main__': main()
使用curl訪問可以得到
➜ ~ curl http://127.0.0.1:8000hello world➜ ~
控制台會打出訪問的log。
127.0.0.1 - - [01/Jun/2015 11:42:33] "GET / HTTP/1.1" 200 -
從socket的建立,select的IO模式,再到Server和Handler的組合構建服務。我們已經熟悉了python的基本網路編程。python的web開發中,更多是使用WSGI協議。實現該協議的還有 uWSGI和gunicorn等庫。相比那些庫,python內部提供了一個wsgiref模組,實現了一個簡單wsgi服務--simple_server。
接下來將會通過分析simple_server,更好的掌握WSGI協議。