1. 程式人生 > >TCP/UDP協議、理解三次握手四次揮手、Socket

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)