1. 程式人生 > >Python之網路程式設計(二)

Python之網路程式設計(二)

  tcp三次握手和四次揮手

      我們知道網路層,可以實現兩個主機之間的通訊。但是這並不具體,因為,真正進行通訊的實體是在主機中的程序,是一個主機中的一個程序與另外一個主機中的一個程序在交換資料。IP協議雖然能把資料報文送到目的主機,但是並沒有交付給主機的具體應用程序。而端到端的通訊才應該是應用程序之間的通訊。

      UDP,在傳送資料前不需要先建立連線,遠地的主機在收到UDP報文後也不需要給出任何確認。雖然UDP不提供可靠交付,但是正是因為這樣,省去和很多的開銷,使得它的速度比較快,比如一些對實時性要求較高的服務,就常常使用的是UDP。對應的應用層的協議主要有 DNS,TFTP,DHCP,SNMP,NFS 等。

      TCP,提供面向連線的服務,在傳送資料之前必須先建立連線,資料傳送完成後要釋放連線。因此TCP是一種可靠的的運輸服務,但是正因為這樣,不可避免的增加了許多的開銷,比如確認,流量控制等。對應的應用層的協議主要有 SMTP,TELNET,HTTP,FTP 等。

       三次握手:

      1.TCP伺服器程序先建立傳輸控制塊TCB,時刻準備接受客戶程序的連線請求,此時伺服器就進入了 LISTEN(監聽)狀態;

      2.TCP客戶程序也是先建立傳輸控制塊TCB,然後向伺服器發出連線請求報文,這是報文首部中的同部位SYN=1,同時選擇一個初始序列號 seq=x ,此時,TCP客戶端程序進入了 SYN-SENT(同步已傳送狀態)狀態。TCP規定,SYN報文段(SYN=1的報文段)不能攜帶資料,但需要消耗掉一個序號。

      3.TCP伺服器收到請求報文後,如果同意連線,則發出確認報文。確認報文中應該 ACK=1,SYN=1,確認號是ack=x+1,同時也要為自己初始化一個序列號 seq=y,此時,TCP伺服器程序進入了SYN-RCVD(同步收到)狀態。這個報文也不能攜帶資料,但是同樣要消耗一個序號。

      4.TCP客戶程序收到確認後,還要向伺服器給出確認。確認報文的ACK=1,ack=y+1,自己的序列號seq=x+1,此時,TCP連線建立,客戶端進入ESTABLISHED(已建立連線)狀態。TCP規定,ACK報文段可以攜帶資料,但是如果不攜帶資料則不消耗序號。

      5.當伺服器收到客戶端的確認後也進入ESTABLISHED狀態,此後雙方就可以開始通訊了。 

      四次揮手:

      資料傳輸完畢後,雙方都可釋放連線。最開始的時候,客戶端和伺服器都是處於ESTABLISHED狀態,然後客戶端主動關閉,伺服器被動關閉。服務端也可以主動關閉,一個流程。

      1.客戶端程序發出連線釋放報文,並且停止傳送資料。釋放資料報文首部,FIN=1,其序列號為seq=u(等於前面已經傳送過來的資料的最後一個位元組的序號加1),此時,客戶端進入FIN-WAIT-1(終止等待1)狀態。 TCP規定,FIN報文段即使不攜帶資料,也要消耗一個序號。

      2.伺服器收到連線釋放報文,發出確認報文,ACK=1,ack=u+1,並且帶上自己的序列號seq=v,此時,服務端就進入了CLOSE-WAIT(關閉等待)狀態。TCP伺服器通知高層的應用程序,客戶端向伺服器的方向就釋放了,這時候處於半關閉狀態,即客戶端已經沒有資料要傳送了,但是伺服器若傳送資料,客戶端依然要接受。這個狀態還要持續一段時間,也就是整個CLOSE-WAIT狀態持續的時間。

      3.客戶端收到伺服器的確認請求後,此時,客戶端就進入FIN-WAIT-2(終止等待2)狀態,等待伺服器傳送連線釋放報文(在這之前還需要接受伺服器傳送的最後的資料)。

      4.伺服器將最後的資料傳送完畢後,就向客戶端傳送連線釋放報文,FIN=1,ack=u+1,由於在半關閉狀態,伺服器很可能又傳送了一些資料,假定此時的序列號為seq=w,此時,伺服器就進入了LAST-ACK(最後確認)狀態,等待客戶端的確認。

      5.客戶端收到伺服器的連線釋放報文後,必須發出確認,ACK=1,ack=w+1,而自己的序列號是seq=u+1,此時,客戶端就進入了TIME-WAIT(時間等待)狀態。注意此時TCP連線還沒有釋放,必須經過2∗MSL(最長報文段壽命)的時間後,當客戶端撤銷相應的TCB後,才進入CLOSED狀態。

      6.伺服器只要收到了客戶端發出的確認,立即進入CLOSED狀態。同樣,撤銷TCB後,就結束了這次的TCP連線。可以看到,伺服器結束TCP連線的時間要比客戶端早一些。

基於TCP和UDP兩個協議下socket的通訊流程

   1.TCP和UDP對比

    TCP(Transmission Control Protocol)可靠的、面向連線的協議(eg:打電話)、傳輸效率低全雙工通訊(傳送快取&接收快取)、面向位元組流。使用TCP的應用:Web瀏覽器;檔案傳輸程式。

    UDP(User Datagram Protocol)不可靠的、無連線的服務,傳輸效率高(傳送前時延小),一對一、一對多、多對一、多對多、面向報文(資料包),盡最大努力服務,無擁塞控制。使用UDP的應用:域名系統 (DNS);視訊流;IP語音(VoIP)。

    直接看圖對比其中差異

上程式碼感受一下,需要建立兩個檔案,檔名稱隨便起,為了方便看,我的兩個檔名稱為tcp_server.py(服務端)和tcp_client.py(客戶端),將下面的server端的程式碼拷貝到tcp_server.py檔案中,將下面client端的程式碼拷貝到tcp_client.py的檔案中,然後先執行tcp_server.py檔案中的程式碼,再執行tcp_client.py檔案中的程式碼,然後在pycharm下面的輸出視窗看一下效果。

    server端程式碼示例(如果比喻成打電話)

import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8898))  #把地址繫結到套接字
sk.listen()          #監聽連結
conn,addr = sk.accept() #接受客戶端連結
ret = conn.recv(1024)  #接收客戶端資訊
print(ret)       #列印客戶端資訊
conn.send(b'hi')        #向客戶端傳送資訊
conn.close()       #關閉客戶端套接字
sk.close()        #關閉伺服器套接字(可選)

    client端程式碼示例

import socket
sk = socket.socket()           # 建立客戶套接字
sk.connect(('127.0.0.1',8898))    # 嘗試連線伺服器
sk.send(b'hello!')
ret = sk.recv(1024)         # 對話(傳送/接收)
print(ret)
sk.close()            # 關閉客戶套接字

 

    socket繫結IP和埠時可能出現下面的問題:

         解決辦法:

#加入一條socket配置,重用ip和埠
import socket
from socket import SOL_SOCKET,SO_REUSEADDR
sk = socket.socket()
sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #在bind前加,允許地址重用
sk.bind(('127.0.0.1',8898))  #把地址繫結到套接字
sk.listen()          #監聽連結
conn,addr = sk.accept() #接受客戶端連結
ret = conn.recv(1024)   #接收客戶端資訊
print(ret)              #列印客戶端資訊
conn.send(b'hi')        #向客戶端傳送資訊
conn.close()       #關閉客戶端套接字
sk.close()        #關閉伺服器套接字(可選)

    但是如果你加上了上面的程式碼之後還是出現這個問題:OSError: [WinError 10013] 以一種訪問許可權不允許的方式做了一個訪問套接字的嘗試。那麼只能換埠了,因為你的電腦不支援埠重用。

    記住一點,用socket進行通訊,必須是一收一發對應好。

  提一下:網路相關或者需要和電腦上其他程式通訊的程式才需要開一個埠。

  

  在看UDP協議下的socket之前,我們還需要加一些內容來講:看程式碼

    server端

import socket
from socket import SOL_SOCKET,SO_REUSEADDR
sk = socket.socket()
# sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
sk.bind(('127.0.0.1',8090))
sk.listen()
conn,addr = sk.accept()  #在這阻塞,等待客戶端過來連線
while True:
    ret = conn.recv(1024)  #接收訊息  在這還是要阻塞,等待收訊息
    ret = ret.decode('utf-8')  #位元組型別轉換為字串中文
    print(ret)
    if ret == 'bye':        #如果接到的訊息為bye,退出
        break
    msg = input('服務端>>')  #服務端發訊息
    conn.send(msg.encode('utf-8'))
    if msg == 'bye':
        break
 
conn.close()
sk.close()

    client端

import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8090)) #連線服務端
 
while True:
    msg = input('客戶端>>>')  #input阻塞,等待輸入內容
    sk.send(msg.encode('utf-8'))
    if msg == 'bye':
        break
    ret = sk.recv(1024)
    ret = ret.decode('utf-8')
    print(ret)
    if ret == 'bye':
        break
sk.close()

你會發現,第一個連線的客戶端可以和服務端收發訊息,但是第二個連線的客戶端發訊息服務端是收不到的

    原因解釋:       tcp屬於長連線,長連線就是一直佔用著這個連結,這個連線的埠被佔用了,第二個客戶端過來連線的時候,他是可以連線的,但是處於一個佔線的狀態,就只能等著去跟服務端建立連線,除非一個客戶端斷開了(優雅的斷開可以,如果是強制斷開就會報錯,因為服務端的程式還在第一個迴圈裡面),然後就可以進行和服務端的通訊了。什麼是優雅的斷開呢?看程式碼。     server端程式碼:
import socket
from socket import SOL_SOCKET,SO_REUSEADDR
sk = socket.socket()
# sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #允許地址重用,這個東西都說能解決問題,我非常不建議大家這麼做,容易出問題
sk.bind(('127.0.0.1',8090))
sk.listen()
# 第二步演示,再加一層while迴圈
while True:    #下面的程式碼全部縮排進去,也就是迴圈建立連線,但是不管怎麼聊,只能和一個聊,也就是另外一個優雅的斷了之後才能和另外一個聊
                #它不能同時和好多人聊,還是長連線的原因,一直佔用著這個埠的連線,udp是可以的,然後我們學習udp
    conn,addr = sk.accept()  #在這阻塞,等待客戶端過來連線
    while True:
        ret = conn.recv(1024)  #接收訊息  在這還是要阻塞,等待收訊息
        ret = ret.decode('utf-8')  #位元組型別轉換為字串中文
        print(ret)
        if ret == 'bye':        #如果接到的訊息為bye,退出
            break
        msg = input('服務端>>')  #服務端發訊息
        conn.send(msg.encode('utf-8'))
        if msg == 'bye':
            break
    conn.close()

    client端程式碼

import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8090)) #連線服務端
 
while True:
    msg = input('客戶端>>>')  #input阻塞,等待輸入內容
    sk.send(msg.encode('utf-8'))
    if msg == 'bye':
        break
    ret = sk.recv(1024)
    ret = ret.decode('utf-8')
    print(ret)
    if ret == 'bye':
        break
# sk.close()

    強制斷開連線之後的報錯資訊:

總結一下UDP下的socket通訊流程

      先從伺服器端說起。伺服器端先初始化Socket,然後與埠繫結(bind),recvform接收訊息,這個訊息有兩項,訊息內容和對方客戶端的地址,然後回覆訊息時也要帶著你收到的這個客戶端的地址,傳送回去,最後關閉連線,一次互動結束

      上程式碼感受一下,需要建立兩個檔案,檔名稱隨便起,為了方便看,我的兩個檔名稱為udp_server.py(服務端)和udp_client.py(客戶端),將下面的server端的程式碼拷貝到udp_server.py檔案中,將下面cliet端的程式碼拷貝到udp_client.py的檔案中,然後先執行udp_server.py檔案中的程式碼,再執行udp_client.py檔案中的程式碼,然後在pycharm下面的輸出視窗看一下效果。

    server端程式碼示例

import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM)   #建立一個伺服器的套接字
udp_sk.bind(('127.0.0.1',9000))        #繫結伺服器套接字
msg,addr = udp_sk.recvfrom(1024)
print(msg)
udp_sk.sendto(b'hi',addr)                 # 對話(接收與傳送)
udp_sk.close()                         # 關閉伺服器套接字

    client端程式碼示例

import socket
ip_port=('127.0.0.1',9000)
udp_sk=socket.socket(type=socket.SOCK_DGRAM)
udp_sk.sendto(b'hello',ip_port)
back_msg,addr=udp_sk.recvfrom(1024)
print(back_msg.decode('utf-8'),addr)