Linux web服務前言
特點:
- HTTP叫超文本傳輸協議,基於請求/響應模式的!
- HTTP是無狀態協議。
為了方便認識http的請求和響應協議,用一段py來抓包分析下
import socket import time def handle_request(client): time.sleep(10) buf = client.recv(1024) print(buf.decode(‘utf-8‘)) client.send(bytes("HTTP/1.1 200 OK\r\n\r\n",encoding=‘utf-8‘)) client.send(bytes(‘Hello World‘, encoding=‘utf-8‘)) def main(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind((‘192.168.1.102‘, 8000)) sock.listen(2) while True: connection, address = sock.accept() handle_request(connection) connection.close() if __name__ == ‘__main__‘: main()
請求協議
get請求協議的格式如下:
請求首行; // 請求方式 請求路徑 協議和版本,例如:GET /index.html HTTP/1.1
請求頭信息;// 請求頭名稱:請求頭內容,即為key:value格式,例如:Host:localhost
空行; // 用來與請求體分隔開
請求體。 // GET沒有請求體,只有POST有請求體。
get抓包分析
- GET / HTTP/1.1
#請求主機ip:port- Host: 192.168.1.102:8000
#客戶端支持的鏈接方式,保持一段時間鏈接- Connection: keep-alive
#表明客戶端不願意接受緩存請求,它需要的是最即時的資源。- Pragma: no-cache
#沒有緩存- Cache-Control: no-cache
#更加支持用https- Upgrade-Insecure-Requests: 1
#與瀏覽器和OS相關的信息。有些網站會顯示用戶的系統版本和瀏覽器版本信息,這都是通過獲取User-Agent頭信息而來的;- User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36
- Accept:
#告訴服務器,當前客戶端可以接收的文檔類型,其實這裏包含了/,就表示什麽都可以接收;- text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8
#告訴服務器,當前客戶端可以接收的文檔類型,其實這裏包含了/,就表示什麽都可以接收;- Accept-Encoding: gzip, deflate
#當前客戶端支持的語言,可以在瀏覽器的工具?選項中找到語言相關信息;- Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,cy;q=0.7
get請求特點:
- 沒有請求體
- 數據必須在1K之內!
- GET請求數據會暴露在瀏覽器的地址欄中
post請求
#python 發送一個post請求
import requests
test = {‘key1‘: ‘value1‘, ‘key2‘: ‘value2‘}
ret = requests.post("http://192.168.1.102:8000", data=test)
print(ret)
print(ret.text)
抓包分析
#post請求
- POST / HTTP/1.1
- Host: 192.168.1.102:8000
- User-Agent: python-requests/2.18.4
- Accept-Encoding: gzip, deflate
- Accept: /
- Connection: keep-alive
#請求body長度
- Content-Length: 23
- Content-Type: application/x-www-form-urlencoded
#請求體
- key1=value1&key2=value2
post請求特點
- 數據不會出現在地址欄中
- 數據的大小沒有上限
- 有請求體
- 請求體中如果存在中文,會使用URL編碼!
響應
- HTTP/1.1 200 OK
Hello World
響應協議的格式如下:
響應首行;
響應頭信息;
空行;
響應體。
響應協議說明
HTTP/1.1 200 OK:響應協議為HTTP1.1,狀態碼為200,表示請求成功,OK是對狀態碼的解釋;
Server:WSGIServer/0.2 CPython/3.5.2:服務器的版本信息;
Content-Type: text/html;charset=UTF-8:響應體使用的編碼為UTF-8;
Content-Length: 724:響應體為724字節;
Set-Cookie: JSESSIONID=C97E2B4C55553EAB46079A4F263435A4; Path=/hello:響應給客戶端的Cookie;
Date: Wed, 25 Sep 2012 04:15:03 GMT:響應的時間,這可能會有8小時的時區差;
tcp三次握手四次斷開說明
服務端代碼
import socket
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
back_log = 5
buffer_size = 1024
ip_port = (‘192.168.1.102‘, 8000)
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
server.bind(ip_port)
server.listen(back_log)
print(‘waiting.....‘)
doing = 1
while doing > 0:
conn, addr = server.accept()
while doing > 0:
data = conn.recv(buffer_size).decode(‘utf-8‘)
if not data :
break
print(‘recv:‘,data)
print(‘test:‘)
if data == ‘exit‘:
conn.close()
break
if data == ‘exitall‘:
doing=0
break
else:
if data:
conn.send(data.upper().encode(‘utf-8‘))
server.close()
客戶端代碼
import socket
client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
buffer_size = 1024
ip_port = (‘192.168.1.102‘, 8000)
client.connect(ip_port)
while True:
msg = input(‘input>>>\n‘).strip()
if not msg:
continue
client.send(bytes(msg,encoding=‘utf-8‘))
data = client.recv(buffer_size).decode(encoding=‘utf-8‘)
print(data)
if msg == ‘exit‘:
break
client.close()
1、開啟一個監聽在8000端口的tpc進程
[root@ns1 conf.d]# ss -antp|grep 8000
LISTEN 0(當前等待的已經是ESTAB的連接數量,排除正在通信的) 5(表示接收連接最大數) 192.168.1.102:8000 *:* users:(("python3.6",pid=29528,fd=3))
我們用watch來看服務器的連接情況
2、連接一個客戶端,進程觀察
其實這個階段有很多狀態變化,服務端會從SYN_RECV到ESTAB
Every 1.0s: ss -antp|grep 8000 Sat May 12 10:36:44 2018
LISTEN 0 5 192.168.1.102:8000 *:* users:(("python3.6",pid=39882,fd=3))
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:58132 users:(("python3.6",pid=39882,fd=4))
2.1斷開連接,進程觀察
客戶端斷開,TIME-WAIT是等待服務器斷開
Every 1.0s: ss -antp|grep 8000 Sat May 12 10:41:39 2018
LISTEN 0 5 192.168.1.102:8000 *:* users:(("python3.6",pid=41041,fd=3))
TIME-WAIT 0 0 192.168.1.102:8000 192.168.1.101:59438
幾秒之後客戶端就確認斷開了 這裏為什麽會有延遲,是因為有可能服務器端發送到客戶端的數據還沒有發完,所以服務器要確認
Every 1.0s: ss -antp|grep 8000 Sat May 12 10:43:50 2018
LISTEN 0 5 192.168.1.102:8000 *:* users:(("python3.6",pid=41041,fd=3))
這裏還有一種情況,出現close-wait,這種情況是因為客戶端斷開連接,而服務器還連接著到客戶端的連接,有可能是服務器的bug(正常情況這個階段是很快被服務器確認的)
Every 1.0s: ss -antp|grep 8000 Sat May 12 10:47:46 2018
LISTEN 0 5 192.168.1.102:8000 *:* users:(("python3.6",pid=41041,fd=3))
CLOSE-WAIT 0 0 192.168.1.102:8000 192.168.1.101:61056 users:(("python3.6",pid=41041,fd=4))
另外一組測試,測試多個連接
3、先連接3個客戶端,進程觀察
一共3個ESTAB連接 1給非阻塞,2個阻塞
Every 1.0s: ss -antp|grep 8000 Sat May 12 10:51:28 2018
LISTEN 2 5 192.168.1.102:8000 *:* users:(("python3.6",pid=43041,fd=3))
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62088
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62079
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62070 users:(("python3.6",pid=43041,fd=4))
3.1 在連接3個客戶端,一共6個,進程觀察
6個建立ESTAB連接,1個非阻塞,5個阻塞
Every 1.0s: ss -antp|grep 8000 Sat May 12 10:52:02 2018
LISTEN 5 5 192.168.1.102:8000 *:* users:(("python3.6",pid=43041,fd=3))
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62249
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62088
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62233
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62242
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62079
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62070 users:(("python3.6",pid=43041,fd=4))
3.2 繼續添加3個客戶端,一共9個連接 ,進程觀察
一共9個連接,1個非阻塞ESTAB,6個阻塞ESTAB,2個等待服務器回應SYN-RECV
Every 1.0s: ss -antp|grep 8000 Sat May 12 10:52:24 2018
LISTEN 6 5 192.168.1.102:8000 *:* users:(("python3.6",pid=43041,fd=3))
SYN-RECV 0 0 192.168.1.102%if355331399:8000 192.168.1.101:62369
SYN-RECV 0 0 192.168.1.102:8000 192.168.1.101:62364
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62249
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62088
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62233
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62355
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62242
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62079
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62070 users:(("python3.6",pid=43041,fd=4))
幾秒之後,服務器一直沒有響應客戶端的請求,服務器斷開連接
Every 1.0s: ss -antp|grep 8000 Sat May 12 10:53:33 2018
LISTEN 6 5 192.168.1.102:8000 *:* users:(("python3.6",pid=43041,fd=3))
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62249
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62088
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62233
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62355
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62242
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62079
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62070 users:(("python3.6",pid=43041,fd=4))
3.3 客戶端斷開第一個連接,就是非阻塞那個, 進行觀察
1個等待服務器確認斷開,1個非阻塞,5個阻塞。之前的62070非阻塞斷開,第二個阻塞狀態轉為非阻塞
Every 1.0s: ss -antp|grep 8000 Sat May 12 10:54:25 2018
LISTEN 5 5 192.168.1.102:8000 *:* users:(("python3.6",pid=43041,fd=3))
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62249
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62088
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62233
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62355
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62242
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62079 users:(("python3.6",pid=43041,fd=4))
TIME-WAIT 0 0 192.168.1.102:8000 192.168.1.101:62070
3.4繼續斷開2個連接,觀察
2個等待確認斷開,1個非阻塞,3個阻塞
Every 1.0s: ss -antp|grep 8000 Sat May 12 10:56:25 2018
LISTEN 3 5 192.168.1.102:8000 *:* users:(("python3.6",pid=43041,fd=3))
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62249
TIME-WAIT 0 0 192.168.1.102:8000 192.168.1.101:62088
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62233 users:(("python3.6",pid=43041,fd=4))
ESTAB 4 0 192.168.1.102:8000 192.168.1.101:62355
ESTAB 0 0 192.168.1.102:8000 192.168.1.101:62242
TIME-WAIT 0 0 192.168.1.102:8000 192.168.1.101:62079
- 由此分析三次握手四次斷開:
1、在三次握手中,客戶端先發起請求,syn seq請求信號
2、服務器接收到信號轉為SYN-RECV
3、服務器查看半連接池,如果還有數量可以接收,就給客戶端確認,並且發出服務器請求信號,狀態還是SYN-RECV
4、客戶端接收到服務器的同意,建立客戶端到服務器的連接(DDOS攻擊,這個階段不給確認),客戶端同意服務器的連接,然後確認信息
5、服務器接收到客戶端的確認,建立服務器到客戶端連接,狀態由SYN-RECV到ESTAB
6、三次握手成功,期間雙方可以友好交流,每次交流都是發出請求,確認收到。
7、客戶端發送斷開請求,狀態轉為close_wait
8、服務器確認同意,客戶端到服務器的請求斷開
9、服務器確認可以關閉請求,發送請求
10、客戶端確認,斷開服務器到客戶端連接
常見IO模型介紹
介紹IO模型因為配置web服務高並發,提供一些基礎理解。
還有是進程線程的一些概念,大概就是進程之前數據相互獨立,切換進程比切換線程消耗大很多,線程是能共享進程資源,這裏不做詳細解釋了
1、阻塞IO 上述服務端socekt就是阻塞io
2、非阻塞IO
import socket
import time
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
buffer_size = 1024
ip_port = (‘127.0.0.1‘, 8000)
server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
server.bind(ip_port)
server.listen(5)
server.setblocking(False) #非阻塞
print(‘waiting.....‘)
doing = 1
while doing > 0:
try:
conn, addr = server.accept()
while doing > 0:
try:
data = conn.recv(buffer_size).decode(‘utf-8‘)
if not data :
break
print(‘recv:‘,data)
print(‘test:‘)
if data == ‘exit‘:
conn.close()
if data == ‘exitall‘:
doing = 0
break
else:
if data:
conn.send(data.upper().encode(‘utf-8‘))
except Exception as e:
time.sleep(1)
print(e)
except Exception as e:
time.sleep(1)
print(e)
server.close()
3、 IO 多路復用-select
用戶進程調用select,內核負責所有select添加的sokect的狀態,當任何一個socket中的數據準備好了,select就會返回,返回所有select添加的socket。這個時候用戶進程再調用read操作,將數據從kernel拷貝到用戶進程。IO多路復select在按找時間空間劃分cpu,就已經實現來單線程高並發。缺點也很明顯,水平出發消耗比較高,非活躍socket本因無須操作,select還依賴文件描述符,每臺服務器能打開的文件描述符資源也是有限的。
服務端代碼
import socket
import select
sk=socket.socket()
sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
sk.bind(("127.0.0.1",8008))
sk.listen(5)
inputs=[sk,]
while True:
r, w, e = select.select(inputs, [], [], 5)
for i in r:
if i is sk: # 看上述r描述
conn, add = i.accept() # 把r進來的socket接收到 不接收會一直存在內核
inputs.append(conn)
else:
data_byte = i.recv(1024)
print(str(data_byte, ‘utf-8‘))
inp = input(‘回答%s號客戶>>>‘ % inputs.index(i))
i.sendall(bytes(inp, ‘utf-8‘))
print(‘>>>>>>‘)
客戶端代碼
import socket
sk=socket.socket()
sk.connect(("127.0.0.1",8000))
while 1:
inp=input(">>").strip()
sk.send(inp.encode("utf8"))
data=sk.recv(1024)
print(data.decode("utf8"))
IO多路復用-poll-epoll
poll 本質跟select區別不大,只是取消來文件描述符的限制
epoll在linux內核2.6之後出現,同時支持水平觸發跟邊緣觸發,邊緣觸發意思就是返回文件描述符發生變化的socket,大大減少來服務器的開銷,內核復制數據到用戶空間使用mmap內存映射技術,省掉了文件描述符在系統調用時復制的開銷,另一個本質的改進在於epoll采用基於事件的就緒通知方式,一旦基於某個文件描述符就緒時,內核會采用類似callback的回調機制,迅速激活這個文件描述符。
事件通知方式實現方式
from concurrent.futures import ProcessPoolExecutor
import requests
def task(url):
response = requests.get(url)
return response
def done(future,*args,**kwargs):
response = future.result()
print(response.status_code,response.content)
pool = ProcessPoolExecutor(7)
url_list = [
‘https://www.taobao.com‘,
‘https://www.sina.com‘,
‘https://www.baidu.com‘,
]
for url in url_list:
print(url)
v = pool.submit(task,url)
v.add_done_callback(done)
print(‘all‘)
pool.shutdown(wait=True)
epoll的實現在python中特別簡單,selectors模塊的EpollSelector
服務端
import selectors
import socket
sel = selectors.EpollSelector()
def accept(sock, mask):
conn, addr = sock.accept() # Should be ready
print(‘accepted‘, conn, ‘from‘, addr)
conn.setblocking(False)
sel.register(conn, selectors.EVENT_READ, read)
def read(conn, mask):
data = conn.recv(1000) # Should be ready
if data:
print(‘echoing‘, repr(data), ‘to‘, conn)
conn.send(data) # Hope it won‘t block
else:
print(‘closing‘, conn)
sel.unregister(conn)
conn.close()
sock = socket.socket()
sock.bind((‘localhost‘, 1234))
sock.listen(100)
sock.setblocking(False)
sel.register(sock, selectors.EVENT_READ, accept)
while True:
events = sel.select()
for key, mask in events:
callback = key.data
callback(key.fileobj, mask)
客戶端
import socket
client = socket.socket()
client.connect((‘localhost‘, 9000))
while True:
cmd = input(‘>>> ‘).strip()
if len(cmd) == 0 : continue
client.send(cmd.encode(‘utf-8‘))
data = client.recv(1024)
print(data.decode())
client.close()
Linux web服務前言