網絡編程
低級別的網絡服務python
高級別的網絡服務web
socket又稱「套接字」,應用程序經過「套接字」向網絡發出請求或者應答網絡請求,使主機間或者一臺計算機上的進程間能夠通信。 數據庫
tcp
傳輸控制協議(Transfer Control Protocol)
tcp優劣勢:編程
一、穩定
二、相對於udp而言,要慢一些(幾乎能夠忽略不計)
三、web服務器都是使用的tcp
udp優劣勢:
一、不穩定(幾乎能夠忽略不計,可是總存在隱患)
二、比tcp要快一些
tcp的原理就相似生活中的「打電話」,先要雙方接通,才能通話
udp的原理就相似生活中的「寫信」,信寄出去,就無論了,能不能到達也是不能保證的
TCP三次握手
當須要使用tcp協議的時候(如http,其底層就是tcp協議)
第一次握手
客戶端先發一個「syn」的數據包給服務器。數據包中包括參數:序列號(SEQUENCE NUM),假設爲J。上述爲0.後端
第二次握手
服務器收到「syn」的數據包後,給客戶端發一個「syn+ack」的數據包。數據包中包括參數,,分別是:序列號(SEQUENCE NUM,假設爲K)確認號(ACK NUM,假設爲J+1),上述分別是「0」和「1」。
客戶端將以前發送的數據包中的J值加1,再將收到的服務器發送過來的數據包中的「J+1」進行比較,若是同樣,說明第二次鏈接成功。
第三次握手
客戶端給服務端發一個「ack」的數據包。瀏覽器
序列號(SEQUENCE NUM,假設爲J+1):上述爲1
確認號(ACK NUM,假設爲K+1):上述爲1
服務器將以前發送的數據包中的K值加1,而後將收到的客戶端發送過來的數據包中的「K+1」進行比較,若是同樣,說明第三次鏈接成功
如此,三次握手創建成功。
HTTP請求過程
一、客戶端向服務器發送「syn」請求包,創建第一次握手
二、服務器向客戶端發送「syn+ack」數據包,創建第二次握手
三、客戶端想服務器發送「ack」的數據包,創建第三次握手;緊隨第三次握手的數據包後面,客戶端緊接着向服務器發送「http」的數據包
四、服務器向客戶端發送以前的「http」的確認包,並緊隨着發送「http」的響應數據包,
五、客戶端接收到「http」包後,再向服務器發送「ack」的確認包,告訴服務器數據包收到了
tcp協議中,不論是客戶端仍是服務器,只要收到了數據,就必定會發送一個ack確認包給發送方。這也就致使了tcp比udp穩定的緣由。
TCP四次揮手
TCP的十種狀態
注服務器
當一端收到一個FIN後,內核讓read返回0來通知應用層另外一端已經終止了向本段的數據傳送
發送FIN一般是應用層對socket進行關閉的結果
TTL
Time To Live,IP包被路由器丟棄以前容許經過的最大網段數量。
雖然TTL從字面上翻譯,是能夠存活的時間,但實際上TTL是IP數據包在計算機網絡中能夠轉發的最大跳數。TTL字段由IP數據包的發送者設置,在IP數據包從源到目的的整個轉發路徑上,每通過一個路由器,路由器都會修改這個TTL字段值,具體的作法是把該TTL的值減1,而後再將IP包轉發出去。若是在IP包到達目的IP以前,TTL減小爲0,路由器將會丟棄收到的TTL=0的IP包並向IP包的發送者發送 ICMP time exceeded消息。
TTL的主要做用是避免IP包在網絡中的無限循環和收發,節省了網絡資源,並能使IP包的發送者能收到告警消息。
TTL 是由發送主機設置的,以防止數據包不斷在IP互聯網絡上永不終止地循環。轉發IP數據包時,要求路由器至少將 TTL 減少 1。
2MSL
MSL:Maximum Segment Lifetime,報文最大生存時間,一個數據包在網絡上傳輸所用的最大的時間,稱爲msl,通常爲1~2分鐘。
TCP的最後一次揮手,怎麼能保證服務器端必定會收到呢?網絡
若是在一個msl時間內,服務端沒有收到「最後一次揮手」,那麼服務端會再次發一個「FIN」數據包給客戶端,這一段時間最長又是一個msl,總的加起來就是2msl,在此期間,若是客戶端接收到了「FIN」數據包,那麼會再發一次「ACK」給服務器;相反若是在2msl時間後,尚未收到服務器的「FIN」數據包的話,說明「最後一次揮手」成功。
注:
一、在此等待的2msl期間內,會佔用端口,端口不會被釋放。
二、主動關閉的一段並不是必定是客戶端,也能夠是服務端。因此一旦是服務端主動關閉,因爲服務端是綁定了端口的,程序就沒法立馬運行了,由於在2msl期間內,端口仍是被佔用的。固然,客戶端無所謂,反正是動態分配端口。
長鏈接和短連接
TCP在真正的讀寫操做以前,server與client之間必須創建一個鏈接,
當讀寫操做完成後,雙方再也不須要這個鏈接時它們能夠釋放這個鏈接,
鏈接的創建經過三次握手,釋放則須要四次握手,
因此說每一個鏈接的創建都是須要資源消耗和時間消耗的。
TCP通訊的整個過程
1. TCP短鏈接
模擬一種TCP短鏈接的狀況:
l client 向 server 發起鏈接請求
l server 接到請求,雙方創建鏈接
l client 向 server 發送消息
l server 迴應 client
l 一次讀寫完成,此時雙方任何一個均可以發起 close 操做
在第 步驟5中,通常都是 client 先發起 close 操做。固然也不排除有特殊的狀況。
從上面的描述看,短鏈接通常只會在 client/server 間傳遞一次讀寫操做!
2. TCP長鏈接
再模擬一種長鏈接的狀況:
l client 向 server 發起鏈接
l server 接到請求,雙方創建鏈接
l client 向 server 發送消息
l server 迴應 client
l 一次讀寫完成,鏈接不關閉
l 後續讀寫操做...
l 長時間操做以後client發起關閉請求
3. TCP長/短鏈接操做過程
3.1 短鏈接的操做步驟是:
創建鏈接——數據傳輸——關閉鏈接...創建鏈接——數據傳輸——關閉鏈接
3.2 長鏈接的操做步驟是:
創建鏈接——數據傳輸...(保持鏈接)...數據傳輸——關閉鏈接
4. TCP長/短鏈接的優勢和缺點
l 長鏈接能夠省去較多的TCP創建和關閉的操做,減小浪費,節約時間。對於頻繁請求資源的客戶來講,較適用長鏈接。
l client與server之間的鏈接若是一直不關閉的話,會存在一個問題:
隨着客戶端鏈接愈來愈多,server遲早有扛不住的時候,這時候server端須要採起一些策略,
如關閉一些長時間沒有讀寫事件發生的鏈接,這樣能夠避免一些惡意鏈接致使server端服務受損(如LOL中的掛機,一段時間後就會斷開鏈接);
若是條件再容許就能夠以客戶端機器爲顆粒度,限制每一個客戶端的最大長鏈接數,
這樣能夠徹底避免某個蛋疼的客戶端連累後端服務。
l 短鏈接對於服務器來講管理較爲簡單,存在的鏈接都是有用的鏈接,不須要額外的控制手段。
l 但若是客戶請求頻繁,將在TCP的創建和關閉操做上浪費時間和帶寬。
5. TCP長/短鏈接的應用場景
長鏈接多用於操做頻繁,點對點的通信,並且鏈接數不能太多狀況。
每一個TCP鏈接都須要三次握手,這須要時間,若是每一個操做都是先鏈接,
再操做的話那麼處理速度會下降不少,因此每一個操做完後都不斷開,
再次處理時直接發送數據包就OK了,不用創建TCP鏈接。
例如:數據庫的鏈接用長鏈接,若是用短鏈接頻繁的通訊會形成socket錯誤,
並且頻繁的socket 建立也是對資源的浪費。
而像WEB網站的http服務通常都用短連接,由於長鏈接對於服務端來講會耗費必定的資源,
而像WEB網站這麼頻繁的成千上萬甚至上億客戶端的鏈接用短鏈接會更省一些資源,
若是用長鏈接,並且同時有成千上萬的用戶,若是每一個用戶都佔用一個鏈接的話,
那可想而知吧。因此併發量大,但每一個用戶無需頻繁操做狀況下需用短連好。
python代碼實現
服務端
流程
一、socket建立一個套接字
二、bind綁定ip和port
三、listen 使套接字變爲能夠被動連接(默認建立的套接字是主動去連接別人的)
四、accept 等待客戶端的連接(accept和客戶端的connect是一對一的關係,服務器的accept只響應客戶端的connect)
五、send/recv 發送和接收數據(recv和send是一對多的關係,服務器的recv響應客戶端的send,也響應客戶端socket的close;反之亦然)。有一個好玩的事情,當某一端send空數據的時候,另外一端recv並無響應,而當close的時候,recv倒是能響應的,不過數據爲空,猜想是send不能發送空數據
代碼
# coding:utf-8
import socket
import config
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)-15s %(levelname)s %(filename)s %(lineno)d %(message)s",)
def main():
# 建立套接字
# family:套接字家族,AF_UNIX或者AF_INET(默認)
# type:套接字類型,面向鏈接的仍是面向非鏈接的,SOCK_STREAM(默認)或者SOCK_DGRAM
# protocol:通常不填默認爲0
sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 重用ip和port,防止報錯
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 綁定ip和port
sk.bind((config.host, config.port))
# 使套接字變爲被動鏈接,最多可接收給定參數的客戶端的鏈接(默認套接字是主動去鏈接別人的)
sk.listen(5)
nsk, addr = sk.accept()
logging.info("client connected! socket={}, addr={}".format(nsk, addr))
data = nsk.recv(1024)
if len(data) == 0:
# 客戶端關閉了鏈接
nsk.close()
else:
nsk.send("thank you".encode("utf-8"))
data2 = nsk.recv(1024)
print(data2)
if __name__ == '__main__':
main()
客戶端
代碼實現的客戶端
# coding:utf-8
import socket
import config
def main():
# 建立socket
sk = socket.socket()
print("client connected! socket={}".format(sk))
# 鏈接服務器
sk.connect((config.host, config.port))
# 發送數據到服務器
sk.send(b"")
data = sk.recv(1024)
if len(data) == 0:
# 服務器端主動斷開鏈接
sk.close()
else:
print(data)
sk.close()
if __name__ == '__main__':
main()
瀏覽器客戶端
使用postman模擬瀏覽器請求(get/post/put/delete均可以),修改服務器代碼以下:併發
def main():
# 建立套接字
sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 重用ip和port,防止報錯
sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 綁定ip和port
sk.bind((config.host, config.port))
# 使套接字變爲被動鏈接,最多可接收給定參數的客戶端的鏈接(默認套接字是主動去鏈接別人的)
sk.listen(5)
nsk, addr = sk.accept()
logging.info("client connected! socket={}, addr={}".format(nsk, addr))
data = nsk.recv(1024)
if len(data) == 0:
# 客戶端關閉了鏈接
nsk.close()
else:
nsk.send("thank you".encode("utf-8"))
data2 = nsk.recv(1024)
print(data2)
調試發現,一次瀏覽器的請求,其實作了四個操做,分別是:socket
- 建立套接字:sk = socket.socket()
- 鏈接服務器:sk.connect((ip, port))
- 發送消息:socket.send(請求頭)。請求頭如:b'POST / HTTP/1.1\r\nUser-Agent: PostmanRuntime/7.17.1\r\nAccept: */*\r\nCache-Control: no-cache\r\nPostman-Token: 016ca998-9f45-4ba5-949b-07a51ea0f3e9\r\nHost: 127.0.0.1:5002\r\nAccept-Encoding: gzip, deflate\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n'
- 關閉鏈接:sk.close()。經過調試發現data2的數據爲空字符串,說明客戶端關閉了鏈接