TCP/UDP協議、理解三次握手四次揮手、Socket
一、什麼是socket?
中文名叫套接字,是對底層的 TCP IP UDP 等網路協議進行封裝,使得上層的應用程式開發者,不用直接接觸這對複雜,醜陋的協議。
在程式設計師的言論,他就是一個封裝好的模組,要完成網路通訊,只需要使用系統提供的socket模組就行,我們通過呼叫模組中已經實現的方法建立兩個程序之間的
連線和通訊。
瞭解socket層:
二、套接字的發展史
套接字起源於 20 世紀 70 年代加利福尼亞大學伯克利分校版本的 Unix,即人們所說的 BSD Unix。 因此,有時人們也把套接字稱為“伯克利套接字”或“BSD 套接字”。一開始,套接字被設計用在同 一臺主機上多個應用程式之間的通訊。這也被稱程序間通訊,或 IPC。套接字有兩種(或者稱為有兩個種族),分別是基於檔案型的和基於網路型的。
基於檔案型別的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆檔案,基於檔案的套接字呼叫的就是底層的檔案系統來取資料,兩個套接字程序執行在同一機器,可以通過訪問同一個檔案系統間接完成通訊
基於網路型別的套接字家族
套接字家族的名字:AF_INET
(還有AF_INET6被用於ipv6,還有一些其他的地址家族,不過,他們要麼是隻用於某個平臺,要麼就是已經被廢棄,或者是很少被使用,或者是根本沒有實現,所有地址家族中,AF_INET是使用最廣泛的一個,python支援很多種地址家族,但是由於我們只關心網路程式設計,所以大部分時候我麼只使用AF_INET
三、TCP和UDP協議
TCP(Transmission Control Protocol)可靠的、面向連線的協議(eg:打電話)、傳輸效率低全雙工通訊(傳送快取&接收快取)、面向位元組流。使用TCP的應用:Web瀏覽器;電子郵件、檔案傳輸程式
UDP(User Datagram Protocol)不可靠的、無連線的服務,傳輸效率高(傳送前時延小),一對一、一對多、多對一、多對多、面向報文,盡最大努力服務,無擁塞控制。使用UDP的應用:域名系統 (DNS);視訊流;IP語音(VoIP)
conclusion:
TCP和UDP是傳輸層最常見的協議,主要控制傳輸資料的方式。
TCP:
優點:通過三次握手來與伺服器建立連線,可以保證資料的完整性。
a機器給b機器傳送資料包 要求b機器必須立即返回一個確認包
a機器會等待一段時間,如果超時還沒有收到確認,則重發資料
缺點:傳輸效率低
使用場景: 文字聊天,支付寶轉賬等,
UDP:
傳輸方式:不需要建立連線,直接傳送
缺點: 不能保證資料的完整性
優點:傳輸效率比TCP高
使用場景:視訊通話,語音通話,UDP
python中的socket,在使用socket的時候使用者需要關心的是 ip地址,port埠, 傳輸協議TCP/UDP,你要傳送的資料data,在寫網路程式設計的時候,必然是有兩臺程式碼, 對應著客戶端和伺服器,使用socket來完成TCP通訊,應該先完成伺服器的程式碼編寫。
重點理解TCP協議中三次握手、四次握手的概念:
當應用程式希望通過 TCP 與另一個應用程式通訊時,它會發送一個通訊請求。這個請求必須被送到一個確切的地址。在雙方“握手”之後,TCP 將在兩個應用程式之間建立一個全雙工 (full-duplex) 的通訊。這個全雙工的通訊將佔用兩個計算機之間的通訊線路,直到它被一方或雙方關閉為止。
三次握手示意圖:
step1:第一次握手
建立連線時,客戶端傳送SYN包到伺服器,其中包含客戶端的初始序號seq=x,並進入SYN_SENT狀態,等待伺服器確認。(其中,SYN=1,ACK=0,表示這是一個TCP連線請求資料報文;序號seq=x,表明傳輸資料時的第一個資料位元組的序號是x)。
step2:第二次握手
伺服器收到請求後,必須確認客戶的資料包。同時自己也傳送一個SYN包,即SYN+ACK包,此時伺服器進入SYN_RECV狀態。(其中確認報文段中,標識位SYN=1,ACK=1,表示這是一個TCP連線響應資料報文,並含服務端的初始序號seq(伺服器)=y,以及伺服器對客戶端初始序號的確認號ack(伺服器)=seq(客戶端)+1=x+1)。
step3:第三次握手
客戶端收到伺服器的SYN+ACK包,向伺服器傳送一個序列號(seq=x+1),確認號為ack(客戶端)=y+1,此包傳送完畢,客戶端和伺服器進入ESTAB_LISHED(TCP連線成功)狀態,完成三次握手。
為什麼需要三次握手,兩次不可以嗎?或者四次、五次可以嗎?
我們來分析一種特殊情況,假設客戶端請求建立連線,發給伺服器SYN包等待伺服器確認,伺服器收到確認後,如果是兩次握手,假設伺服器給客戶端在第二次握手時傳送資料,資料從伺服器發出,伺服器認為連線已經建立,但在傳送資料的過程中資料丟失,客戶端認為連線沒有建立,會進行重傳。假設每次傳送的資料一直在丟失,客戶端一直SYN,伺服器就會產生多個無效連線,佔用資源,這個時候伺服器可能會掛掉。這個現象就是我們聽過的“SYN的洪水攻擊”。
conclusion:
第三次握手是為了防止:如果客戶端遲遲沒有收到伺服器返回確認報文,這時會放棄連線,重新啟動一條連線請求,但問題是:伺服器不知道客戶端沒有收到,所以他會收到兩個連線,浪費連線開銷。如果每次都是這樣,就會浪費多個連線開銷。
四次揮手示意圖:
step1:第一次揮手
首先,客戶端傳送一個FIN,用來關閉客戶端到伺服器的資料傳送,然後等待伺服器的確認。其中終止標誌位FIN=1,序列號seq=u。
step2:第二次揮手
伺服器收到這個FIN,它傳送一個ACK,確認ack為收到的序號加一。
step3:第三次揮手
關閉伺服器到客戶端的連線,傳送一個FIN給客戶端。
step4:第四次揮手
客戶端收到FIN後,併發回一個ACK報文確認,並將確認序號seq設定為收到序號加一。首先進行關閉的一方將執行主動關閉,而另一方執行被動關閉。
客戶端傳送FIN後,進入終止等待狀態,伺服器收到客戶端連線釋放報文段後,就立即給客戶端傳送確認,伺服器就進入CLOSE_WAIT狀態,此時TCP伺服器程序就通知高層應用程序,因而從客戶端到伺服器的連線就釋放了。此時是“半關閉狀態”,即客戶端不可以傳送給伺服器,伺服器可以傳送給客戶端。
此時,如果伺服器沒有資料報傳送給客戶端,其應用程式就通知TCP釋放連線,然後傳送給客戶端連線釋放資料報,並等待確認。客戶端傳送確認後,進入TIME_WAIT狀態,但是此時TCP連線還沒有釋放,然後經過等待計時器設定的2MSL後,才進入到CLOSE狀態。
為什麼需要2MSL時間?
首先,MSL即Maximum Segment Lifetime,就是最大報文生存時間,是任何報文在網路上的存在的最長時間,超過這個時間報文將被丟棄。《TCP/IP詳解》中是這樣描述的:MSL是任何報文段被丟棄前在網路內的最長時間。RFC 793中規定MSL為2分鐘,實際應用中常用的是30秒、1分鐘、2分鐘等。
TCP的TIME_WAIT需要等待2MSL,當TCP的一端發起主動關閉,三次揮手完成後傳送第四次揮手的ACK包後就進入這個狀態,等待2MSL時間主要目的是:防止最後一個ACK包對方沒有收到,那麼對方在超時後將重發第三次握手的FIN包,主動關閉端接到重發的FIN包後可以再發一個ACK應答包。在TIME_WAIT狀態時兩端的埠不能使用,要等到2MSL時間結束才可以繼續使用。當連線處於2MSL等待階段時任何遲到的報文段都將被丟棄。
為什麼是四次揮手,而不是三次或是五次、六次?
雙方關閉連線要經過雙方都同意。所以,首先是客服端給伺服器傳送FIN,要求關閉連線,伺服器收到後會傳送一個ACK進行確認。伺服器然後再發送一個FIN,客戶端傳送ACK確認,並進入TIME_WAIT狀態。等待2MSL後自動關閉。
conclusion:
1、為了保證客戶端傳送的最後一個ACK報文段能夠到達伺服器。即最後一個確認報文可能丟失,伺服器會超時重傳,然後伺服器傳送FIN請求關閉連線,客戶端傳送ACK確認。一個來回是兩個報文生命週期。
如果沒有等待時間,傳送完確認報文段就立即釋放連線的話,伺服器就無法重傳,因此也就收不到確認,就無法按步驟進入CLOSE狀態,即必須收到確認才能close。
2、防止已經失效的連線請求報文出現在連線中。經過2MSL,在這個連續持續的時間內,產生的所有報文段就可以都從網路消失。
四、socket初使用(用程式碼去實現)
基於TCP協議的socket
sever端
import socket # 1.建立一個代表伺服器的socket物件 s = socket.socket() # 2.繫結IP地址和埠號(一般8000以後) # 127.0.0.1 表示當前這個電腦的ip address = ("127.0.0.1",8080) s.bind(address) print("伺服器已啟動!") # 3.開始監聽這個埠 # 5表示 可以有5個處於半連線狀態的連線 指的不是最大連線數 s.listen(5) print("test") # 4.接受連線請求 # 該函式是阻塞的 會卡主程式的執行,必須等到有一個客戶端進來才會繼續執行 # 返回元組 第一個是代表客戶端的socket物件 第二客戶端的地址資訊 client,c_address = s.accept() print("有一個連線已建立!") print(c_address) # 5.讀寫資料 # 接受資料 res = client.recv(1024) print(res) # 6.關閉連線 # s.close()
client端
import socket # 1.建立客戶端的socket物件 c = socket.socket() # 2.指定伺服器的ip和port server_address = ("127.0.0.1",8080) # 3.建立連線 c.connect(server_address) # 4.讀寫資料 # 傳送資料到伺服器 c.send("hello 我是客戶端!".encode("utf-8")) # 5.關閉連線 c.close()
基於UDP協議的socket
server端
import socket # 1.建立socket物件 s = socket.socket(type=socket.SOCK_DGRAM) # 2.繫結埠和ip s.bind(("127.0.0.1",10000)) while True: # 3.接受資料 res = s.recv(1024) print(res) while True: msg = input(">>>:") # 需要獲取對方的ip和埠 #s.sendto(msg.encode("utf-8"), ("127.0.0.1", 10000)) # 關閉資源 s.close()
client端
import socket # 1.建立socket物件 c = socket.socket(type=socket.SOCK_DGRAM) while True: msg = input(">>>:") c.sendto(msg.encode("utf-8"),("127.0.0.1",10000)) c.close()
輸出結果
client端:
>>>>>:hello >>>>>:world >>>>>:
sever端:
b'hello' b'world'
練習:模擬一個qq多人聊天(由於udp無連線,所以可以同時多個客戶端去跟服務端通訊)
client1端:
#!/usr/bin/env python # -*- coding:utf-8 -*- # Author:Wwl import socket udp_client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) qq_name_dict = { 'Taylor': ('127.0.0.1', 8081), 'lana': ('127.0.0.1', 8081), 'Sia': ('127.0.0.1', 8081) } while True: qq_name = input('請選擇你要聊天的物件:').strip() while True: msg = input('請輸入資訊').strip() if msg == 'q': break udp_client.sendto(msg.encode('utf-8'), qq_name_dict[qq_name]) back_msg, addr = udp_client.recvfrom(1024) print('收到來自%s %s 的訊息%s' % (addr[0], addr[1], back_msg.decode('utf-8'))) udp_client.close()
client2端:
#!/usr/bin/env python # -*- coding:utf-8 -*- # Author:Wwl import socket udp_client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) qq_name_dict = { 'Taylor': ('127.0.0.1', 8081), 'lana': ('127.0.0.1', 8081), 'Sia': ('127.0.0.1', 8081) } while True: qq_name = input('請選擇你要聊天的物件:').strip() while True: msg = input('請輸入資訊').strip() if msg == 'q': break udp_client.sendto(msg.encode('utf-8'), qq_name_dict[qq_name]) back_msg, addr = udp_client.recvfrom(1024) print('收到來自%s %s 的訊息%s' % (addr[0], addr[1], back_msg.decode('utf-8'))) udp_client.close()同client1端
sever端:
#!/usr/bin/env python # -*- coding:utf-8 -*- # Author:Wwl import socket udp_server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) udp_server.bind(('127.0.0.1',8081)) while True: qq_msg,addr = udp_server.recvfrom(1024) print('來自[%s:%s]的一條訊息:%s'%(addr[0],addr[1],qq_msg.decode('utf-8'))) back_qq_msg = input('請輸入:').strip() udp_server.sendto(back_qq_msg.encode('utf-8'),addr)