1. 程式人生 > >Linux web服務前言

Linux web服務前言

linux web

HTTP(hypertext transport protocol),即超文本傳輸協議。這個協議詳細規定了瀏覽器和萬維網服務器之間互相通信的規則。
特點:

  1. HTTP叫超文本傳輸協議,基於請求/響應模式的!
  2. 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請求特點:

  1. 沒有請求體
  2. 數據必須在1K之內!
  3. 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請求特點

  1. 數據不會出現在地址欄中
  2. 數據的大小沒有上限
  3. 有請求體
  4. 請求體中如果存在中文,會使用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服務前言