HTTP, Socket, TCP/IP
HTTP 由 Header 和 Body 兩部分組成,發(fā)送 HTTP 請(qǐng)求(Request)的叫客戶(hù)端,接受到 HTTP 請(qǐng)求并返回信息(Response)的叫服務(wù)器?,F(xiàn)時(shí)流行的 HTTP 協(xié)議版本是 1.1,當(dāng)然也有用 HTTP 2 的,不表。最常用的兩種 method 是 GET 和 POST。PUT現(xiàn)在也會(huì)被提到不少。一般的 HTTP 頭是這樣的:
GET / HTTP/1.1
Host: vip.cocode.cc
Connection: close
Content-Type: text/html
GET表示我們所用的方式,/login表示我們?cè)讷@取這個(gè)網(wǎng)站根目錄下的 login 的數(shù)據(jù),HTTP/1.1表示所用的 HTTP 協(xié)議。
當(dāng)然,我們寫(xiě)的時(shí)候?yàn)榱丝招袝?huì)這樣寫(xiě):
- http_client.py
import socket
# 創(chuàng)建 Socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立連接
s.connect(('vip.cocode.cc', 80))
# 發(fā)起 HTTP 請(qǐng)求
s.send(b'GET /login HTTP/1.1\r\nHost:vip.cocode.cc\r\nConnection:close\r\nContent-Type:text/html\r\n\r\n')
# 接收數(shù)據(jù)
buffer = []
while True:
d = s.recv(1024)
if d:
buffer.append(d)
else:
break
data = b''.join(buffer)
s.close()
print(data)
Socket 是一個(gè)高大上名詞,具體這里不解釋。創(chuàng)建 Socket 是一個(gè)套路,第一個(gè)參數(shù)socket.AF_INET代表著在這里我們是用 IPv4 模式,而第二個(gè)參數(shù)socket.SOCK_STREAM意思是這里我們用 TCP 協(xié)議。
建立連接,給s.connect傳入一個(gè)tuple,分別是 address 和 port 兩個(gè)參數(shù),一般都是 80,因?yàn)?HTTP 默認(rèn)就是 80,套路。
建立連接后,我們就向 server 發(fā)起 HTTP 請(qǐng)求,要注意\r\n和\r\n\r\n,規(guī)定的套路,如果不按照這個(gè)來(lái),這就不是一個(gè)合規(guī)的 HTTP 請(qǐng)求,會(huì)導(dǎo)致你無(wú)法獲得你想要的首頁(yè)內(nèi)容。如果沒(méi)問(wèn)題,我們就可以接收服務(wù)器返回的數(shù)據(jù)了。
接收數(shù)據(jù)的這段代碼的意思是,s.recv(1024)每次最多接受 1024 字節(jié)的數(shù)據(jù),然后嵌套在一個(gè)while循環(huán)內(nèi),當(dāng)s.recv()返回空數(shù)據(jù),證明數(shù)據(jù)都被接收過(guò)來(lái)了,這時(shí)候就可以結(jié)束循環(huán)。
s.close()用作關(guān)閉 socket ,和服務(wù)器的一次通信就此結(jié)束。
最后,返回的數(shù)據(jù)是這樣的:
b'HTTP/1.1 200 OK\r\nDate: Fri, 01 Jul 2016 04:58:34 GMT\r\nServer: Apache/2.4.7 (Ubuntu)\r\nContent-Length: 1181\r\nVary: Accept-Encoding\r\nConnection: close\r\nContent-Type: text/html; charset=utf-8\r\n\r\n<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <title>\xe7\x99\xbb\xe5\xbd\x95\xe9\xa1\xb5\xe9\x9d\xa2</title>\n <link rel="stylesheet" >\n</head>\n<body>\n <ul class=flashes>\n \n <h1>\xe7\x99\xbb\xe5\xbd\x95</h1>\n <form class="pure-form" action="/login" method="POST">\n <input name="username" class="form-control" placeholder="\xe8\xbe\x93\xe5\x85\xa5\xe7\x94\xa8\xe6\x88\xb7\xe5\x90\x8d" />\n <br>\n <input name="password" class="form-control" placeholder="\xe8\xbe\x93\xe5\x85\xa5\xe5\xaf\x86\xe7\xa0\x81" />\n <br>\n <button class="pure-button pure-button-primary" type="submit">\xe7\x99\xbb\xe5\xbd\x95</button>\n </form>\n <hr>\n <!--<h1>\xe6\xb3\xa8\xe5\x86\x8c</h1>-->\n <!--<form class="pure-form" action="/register" method="POST">-->\n <!--<input name="username" class="form-control" placeholder="\xe8\xbe\x93\xe5\x85\xa5\xe7\x94\xa8\xe6\x88\xb7\xe5\x90\x8d" />-->\n <!--<br>-->\n <!--<input name="password" class="form-control" placeholder="\xe8\xbe\x93\xe5\x85\xa5\xe5\xaf\x86\xe7\xa0\x81" />-->\n <!--<br>-->\n <!--<br>-->\n <!--<input name="note" class="form-control" placeholder="\xe8\xbe\x93\xe5\x85\xa5\xe4\xb8\xaa\xe6\x80\xa7\xe7\xad\xbe\xe5\x90\x8d" />-->\n <!--<br>-->\n <!--<button class="pure-button pure-button-primary" type="submit">\xe6\xb3\xa8\xe5\x86\x8c</button>-->\n <!--</form>-->\n</body>\n</html>'
雖然看上去很亂,但是相信你可以看出,這里既包括了 HTTP 頭的數(shù)據(jù),也包括了網(wǎng)頁(yè)(Body)數(shù)據(jù),可以用代碼把它們分離一下:
header, body = data.split('\r\n\r\n')
print(header.decode('utf-8'), body.decode('utf-8'))
最后,我們就得到了一個(gè)比較直觀(guān)的數(shù)據(jù):
# HTTP 頭
HTTP/1.1 200 OK
Date: Fri, 01 Jul 2016 05:07:06 GMT
Server: Apache/2.4.7 (Ubuntu)
Content-Length: 1181
Vary: Accept-Encoding
Connection: close
Content-Type: text/html; charset=utf-8
# Body
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登錄頁(yè)面</title>
<link rel="stylesheet" >
</head>
<body>
<ul class=flashes>
<h1>登錄</h1>
<form class="pure-form" action="/login" method="POST">
<input name="username" class="form-control" placeholder="輸入用戶(hù)名" />
<br>
<input name="password" class="form-control" placeholder="輸入密碼" />
<br>
<button class="pure-button pure-button-primary" type="submit">登錄</button>
</form>
<hr>
<!--<h1>注冊(cè)</h1>-->
<!--<form class="pure-form" action="/register" method="POST">-->
<!--<input name="username" class="form-control" placeholder="輸入用戶(hù)名" />-->
<!--<br>-->
<!--<input name="password" class="form-control" placeholder="輸入密碼" />-->
<!--<br>-->
<!--<br>-->
<!--<input name="note" class="form-control" placeholder="輸入個(gè)性簽名" />-->
<!--<br>-->
<!--<button class="pure-button pure-button-primary" type="submit">注冊(cè)</button>-->
<!--</form>-->
</body>
</html>
當(dāng)然,我們也可以利用socket庫(kù)構(gòu)造一個(gè) HTTP 服務(wù)器。
- http_server.py
import socket
def index():
html = b'HTTP/1.x 200 OK\r\nContent-Type: text/html\r\n\r\n<h1>Hello World</h1>'
return html
host = ''
port = 3000
s = socket.socket()
s.bind((host, port))
s.listen(5)
while True:
s.listen(3)
connection, address = s.accept()
request = connection.recv(1024)
request = request.decode('utf-8')
connection.sendall(response)
connection.close()
運(yùn)行它,它就會(huì)一直處于監(jiān)聽(tīng)狀態(tài)。然后修改之前的客戶(hù)端代碼給我們這個(gè)自己造的服務(wù)器就 OK。
還有一些知識(shí)點(diǎn)是,手寫(xiě)路徑,手寫(xiě)解析GET的查詢(xún)字符串(query string),先挖坑,以后填。GET和POST簡(jiǎn)單的區(qū)別就是,一個(gè)顯式(在地址欄上),一個(gè)隱式(在 Body 里),所以HTTPS協(xié)議配合POST方法,這樣傳送隱私數(shù)據(jù)就能保證安全。
Cookie
服務(wù)器確認(rèn)你的身份是利用 Cookie,比方說(shuō)驗(yàn)證你的登錄狀態(tài)。你給服務(wù)器提交了用戶(hù)名密碼,它驗(yàn)證 OK 了,它會(huì)給你一段 Cookie,從來(lái)在后面你發(fā)起的 HTTP 請(qǐng)求里驗(yàn)證你的身份。因此,Cookie 不可以是明文的(譬如說(shuō) username=arischow,這安全性就太差了),因?yàn)?HTTP 的請(qǐng)求頭是可以愛(ài)寫(xiě)啥寫(xiě)啥的(之前的代碼里面就是手寫(xiě)的 HTTP 請(qǐng)求頭),假設(shè)是明文的,對(duì)方把 Cookie 改成 username=admin,那樣它就可以偽造成管理員身份做壞事,這會(huì)產(chǎn)生安全問(wèn)題。簡(jiǎn)單的解決方法是,造一個(gè)無(wú)規(guī)律高強(qiáng)度的隨機(jī)字符作為 Cookie,導(dǎo)致無(wú)規(guī)律可循。
數(shù)據(jù)庫(kù)
其實(shí)數(shù)據(jù)也可以用文本文件保存:
Aris, 123456, xxx@xxx.com
Alex, 566555, alex@126.com
Susan, 455721, susan@163.com
數(shù)據(jù)庫(kù)儲(chǔ)存數(shù)據(jù)更有條理,更方便查詢(xún)和調(diào)用特定部分。
知識(shí)點(diǎn):SQL 的 CRUD
Flask
了解上面所羅列的一些知識(shí)點(diǎn)之后,來(lái)看 Flask。用了 Flask,上面很多掏糞的事情都變得簡(jiǎn)單,具體到render_template, url_for, flash, request, redirect那樣的沒(méi)什么好講。MVC 的概念,我這么理解:
- Model - 數(shù)據(jù)請(qǐng)求 / 操作 (像現(xiàn)在用到的 Flask-SQLAlchemy, Flask-WTForms 的東西, 我都放在這里)
- View - 視圖展示 / 操作 (給 View 傳入數(shù)據(jù), 再寫(xiě)一些簡(jiǎn)單的判斷, 決定視圖怎么顯示, 譬如用戶(hù)已經(jīng)登錄到系統(tǒng)了, 導(dǎo)航欄就不可能還顯示"登錄"這個(gè)按鈕, 這個(gè)可以把 Session 傳進(jìn) View, 然后 View 根據(jù) Session 的值判斷應(yīng)該展示登錄還是登出按鈕)
- Controller - 事件綁定 (建立路由, 判斷什么時(shí)候該調(diào)用什么, 譬如用戶(hù)注冊(cè), 用戶(hù)輸入的帳號(hào)密碼郵箱是需要判斷格式/長(zhǎng)度是否正確的, 這時(shí)候涉及到數(shù)據(jù)的操作, Controller 就去調(diào)用 Model 里面的某一個(gè)函數(shù), 這個(gè)函數(shù)返回值后, Controller 根據(jù)值作出下一步?jīng)Q定:如果合規(guī),調(diào)用 Model 里面的函數(shù),保存數(shù)據(jù), 如果不合規(guī), 返回錯(cuò)誤……)
SQLAlchemy
在 Flask 里面我們會(huì)用到 SQLAlchemy,它做好了 API 接口,用了它我們不用裸寫(xiě) SQL 語(yǔ)句。
難點(diǎn):對(duì)應(yīng)關(guān)系是一個(gè)難點(diǎn),比較常用的是一對(duì)多關(guān)系。假設(shè)我們有兩張表,一張是users,里面的字段有id, username, password, 還有一張posts,里面的字段有id, title, content,我們要為這兩張表建立連接:一個(gè)博客帖子只會(huì)有一個(gè)作者(用戶(hù)),而一個(gè)作者(用戶(hù))可以有很多博客帖子。
# ...
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String, unique=True)
password = db.Column(db.String, nullable=False)
# 下面這行重點(diǎn)
posts = db.relationship('Post', backref='user')
def __repr__(self):
return u'<User: {}>'.format(self.username)
class Post(db.Model):
__tablename__ = 'posts'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String, nullable=False)
content = db.Column(db.Text)
# 下面這行重點(diǎn)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
def __repr__(self):
return u'<Post: {}>'.format(self.title)
外鍵部分待編輯...