天天看點

網絡程式設計(part9)--socket套接字程式設計之TCP套接字

鄙人學習筆記

文章目錄

  • ​​套接字介紹​​
  • ​​定義​​
  • ​​套接字分類(針對TCP和UDP的分類)​​
  • ​​TCP套接字程式設計​​
  • ​​服務端流程​​
  • ​​代碼實作​​
  • ​​舉個例子​​
  • ​​用戶端流程​​
  • ​​代碼實作​​
  • ​​舉個例子​​
  • ​​TCP套接字資料傳輸特點​​
  • ​​做個練習​​
  • ​​網絡收發緩沖區​​
  • ​​舉個例子​​
  • ​​TCP粘包​​

套接字介紹

定義

套接字是實作網絡程式設計進行資料傳輸的一種技術手段

套接字分類(針對TCP和UDP的分類)

①流式套接字(SOCK_STREAM): 以位元組流方式(就像是管道中的水流一樣)傳輸資料,實作TCP網絡傳輸方案。

②資料報套接字(SOCK_DGRAM):以資料報形式(就像是用瓶子打包好的水一樣)傳輸資料,實作UDP網絡傳輸方案。

TCP套接字程式設計

服務端流程

先來看一個流程圖:

網絡程式設計(part9)--socket套接字程式設計之TCP套接字

代碼實作

  1. 建立套接字
sockfd=socket.socket(socket_family=AF_INET,socket_type=SOCK_STREAM,proto=0)      
功能:建立套接字對象
參數:  
socket_family  網絡位址類型:AF_INET表示ipv4
socket_type  套接字類型:SOCK_STREAM 流式;SOCK_DGRAM 資料報
proto(在應用層網絡程式設計中用不到) 選擇子協定:通常為0  
傳回值: 套接字對象      

備注:底層/系統層套接字也針對某種協定,這些協定,有的時候會産生一些分支,也就是子協定。那麼為啥在應用層網絡程式設計中用不到proto這個參數呢?因為TCP協定和UDP協定沒有子協定,是以參數設為0,即不選擇任何子協定。

  1. 綁定位址
sockfd.bind(addr)      
功能: 綁定本機網絡位址(如果不寫本機網絡位址,則會報錯)
參數: 二進制元組 (ip,port)  比如('0.0.0.0',8888)      

備注:addr為一個元組,這個元組有兩個元素一個是網絡位址(IP),一個是端口号(port)。

  1. 設定監聽
sockfd.listen(n)      
功能 : 将套接字設定為監聽套接字,确定監聽隊列大小
參數 : 監聽隊列大小      

備注1:并不是所有的套接字都具備監聽的功能,即被用戶端連接配接的功能。通過調用listen,我們的套接字對象才能被用戶端連接配接。

備注2:一個服務端套接字對象可以同時連接配接多個用戶端,與用戶端進行連接配接需要進行3次握手,且連接配接的過程需要一個一個來進行。比如說,同時有5個用戶端發起了連接配接請求,這時我們就會形成一個”先來後到”的隊列,對這5個用戶端的連接配接請求,一個一個進行處理。比如說,我們設定監聽隊列大小為5,表示隊列最多容納5個用戶端等待處理。但有10個用戶端同時發起了連接配接請求,這時,服務端會先處理第1個發起請求的那個用戶端,前2~6個用戶端則在等待隊列中,最晚發起請求的4個用戶端,則會被拒絕。

  1. 等待處理用戶端連接配接請求
connfd,addr = sockfd.accept()      
功能: 阻塞等待處理用戶端請求(等待用戶端連接配接,啥時候有用戶端連接配接,才結束阻塞)
傳回值:
   connfd : 用戶端連接配接套接字.
   用戶端連接配接後,新産生的專門與用戶端進行通信的套接字對象。
   則每當有一個用戶端連接配接時,都會有一個新的專用于連接配接的套接字對象産生。
   就像對每個用戶端提供一個專屬管家一樣,提升使用者端體驗。
   我們如果找到一個連接配接套接字,就可以知道它對應的用戶端是誰
   addr :連接配接的用戶端位址      

阻塞函數:當程式運作到這個函數時,則程式暫停執行。程式在等待某種條件,當條件滿足後才繼續執行,如:input()、sleep()。阻塞函數在IO操作中很常見。

  1. 消息收發
data = connfd.recv(buffersize)      
功能 : 接受用戶端消息
參數 :每次最多接收消息的大小(最多一次接受多少位元組的消息)
傳回值: 接收到的内容      
n = connfd.send(data)      
功能 : 發送消息
參數 :要發送的内容-bytes格式(位元組串格式)
傳回值: 發送的位元組數      

備注1:所有和網絡相關的消息傳送都得是bytes格式(位元組串格式).發送和接收都得是位元組串。

備注2:我們發送/接收的是位元組串,但平時寫/讀的是字元串,是以我們需要用encode()/dencode()進行轉換。

  1. 關閉套接字
sockfd.close()      
功能:關閉套接字      
舉個例子

服務端代碼:

import socket

#建立流式套接字
sockfd = socket.socket(socket.AF_INET, \
                       socket.SOCK_STREAM)
#綁定位址
sockfd.bind(('127.0.0.1', 8888))

#設定監聽
sockfd.listen(5)

#等待處理用戶端連結
print("Waiting for connect....")
connfd, addr = sockfd.accept()

#收發消息
data = connfd.recv(1024)
print("接收到的消息:", data.decode())

n = connfd.send(b'Receive your message')
print("發送了 %d 個位元組資料" % n)

#關閉套接字
connfd.close()
sockfd.close()      

我們運作一下,得到以下結果:

網絡程式設計(part9)--socket套接字程式設計之TCP套接字

結果說明,程式阻塞在accept,等待用戶端連接配接。是以這時,我們就要找一個用戶端與之連接配接,下一節,我們就學一下用戶端流程。

用戶端流程

先看看流程圖:

網絡程式設計(part9)--socket套接字程式設計之TCP套接字

代碼實作

  1. 建立套接字(和用戶端代碼相同)

    注意:隻有相同類型的套接字才能進行通信

  2. 請求連接配接
sockfd.connect(server_addr)      
功能:連接配接伺服器
參數:元組  伺服器位址      
  1. 收發消息(和用戶端代碼相同)

    注意: 為了防止兩端都阻塞,故recv和send要配合使用。比如,服務端是先send後recv,那麼用戶端則需要先recv後send。否則,若兩端同時recv,則兩端都會阻塞。

  2. 關閉套接字(和用戶端代碼相同)
舉個例子

用戶端代碼:

網絡程式設計(part9)--socket套接字程式設計之TCP套接字

服務端代碼:

網絡程式設計(part9)--socket套接字程式設計之TCP套接字

我們想要運作,但是若先運作用戶端,則會報錯。是以我們要先啟動服務端

①運作服務端

檢視服務端的控制台Console 2/A輸出的結果:

網絡程式設計(part9)--socket套接字程式設計之TCP套接字

②運作用戶端

我們先看一下服務端的控制台Console 2/A輸出的結果:

網絡程式設計(part9)--socket套接字程式設計之TCP套接字

我們再看一下用戶端的控制台Console 3/A輸出的結果:

網絡程式設計(part9)--socket套接字程式設計之TCP套接字

消息收發成功了,很好~

TCP套接字資料傳輸特點

①TCP連接配接中,當一端退出,另一端如果阻塞在recv,此時recv會立即傳回一個空字串。

②TCP連接配接中,如果一端已經不存在,仍然試圖通過send發送資訊,則會産生BrokenPipeError

③一個監聽套接字可以同時連接配接多個用戶端,也能夠重複被連接配接。

做個練習

要求1:一個用戶端退出了,伺服器不會退出,而是連接配接下一個用戶端

要求2:用戶端可以不停的循環發送消息

服務端代碼:

#-*- coding: utf-8 -*-

import socket

#建立流式套接字
sockfd = socket.socket(socket.AF_INET, \
                       socket.SOCK_STREAM)

#綁定位址
sockfd.bind(('127.0.0.1', 8888))

#設定監聽
sockfd.listen(5)

#等待處理用戶端連結
while True:
    print("Waiting for connect....")
    try:
        connfd, addr = sockfd.accept()
        print("Connect from:", addr)
    except KeyboardInterrupt:
        print("退出服務")
        break

    # 收發消息
    while True:
        data = connfd.recv(1024)
        # 得到空則退出循環
        if not data:
            break
        print("接收到的消息:", data.decode())
        n = connfd.send(b'Receive your message')
        print("發送了 %d 個位元組資料" % n)
    connfd.close()

#關閉套接字
sockfd.close()      

用戶端代碼:

#-*- coding: utf-8 -*-

from socket import *

#建立tcp套接字
sockfd = socket()

#發起連接配接
server_addr = ('127.0.0.1',8888)
sockfd.connect(server_addr)

#收發消息
while True:
    data = input("消息:")
    if not data:
        break
    sockfd.send(data.encode())
    data = sockfd.recv(1024)
    print("From server:",data.decode())

#關閉
sockfd.close()      

先運作服務端,再運作用戶端并發送消息。

用戶端結果:

網絡程式設計(part9)--socket套接字程式設計之TCP套接字

服務端結果:

網絡程式設計(part9)--socket套接字程式設計之TCP套接字

網絡收發緩沖區

①網絡緩沖區有效的協調了消息的收發速度、緩解收發壓力。

②send和recv實際是向緩沖區發送接收消息,當緩沖區不為空recv就不會阻塞。

網絡緩沖區完成消息傳遞示意圖:

網絡程式設計(part9)--socket套接字程式設計之TCP套接字
舉個例子

還記得我們上面那個練習麼?其中一段代碼是:

網絡程式設計(part9)--socket套接字程式設計之TCP套接字

它表示,每次最多可接收消息大小為1024個位元組.

我們來更改一下最大可接受的消息位元組數為5:

網絡程式設計(part9)--socket套接字程式設計之TCP套接字

用用戶端發送消息:

網絡程式設計(part9)--socket套接字程式設計之TCP套接字

服務端結果:

網絡程式設計(part9)--socket套接字程式設計之TCP套接字

用戶端結果:

網絡程式設計(part9)--socket套接字程式設計之TCP套接字

則說明,用戶端1次發送,服務端分3次接收了,同時傳回給服務端3句’ Receive your message’也就是說,服務端的内層循環(如下圖所示)循環了3次

網絡程式設計(part9)--socket套接字程式設計之TCP套接字

備注1:是以我們一開始說【函數recv()是阻塞函數,當發送方不發消息,就會阻塞】。但其實更準确的來說,并不是發送方不發消息就會阻塞,接收方其實是先從緩沖區去拿消息,當緩沖區為空時,會阻塞,當緩沖區一直有消息時,則會一直擷取消息,一直不阻塞。

注意! 運作上面的代碼時,用戶端有時候也會出現以下這種情況:

網絡程式設計(part9)--socket套接字程式設計之TCP套接字

TCP粘包

  • 原因

    TCP以位元組流方式傳輸,沒有消息邊界。多次發送的消息被一次接收,此時就會形成粘包。

  • 影響(分情況,比如:傳一部電影/發送使用者名消息)

    ①如果發送的内容中,每個資訊都有獨立的含義(比如:發送幾個使用者姓名),需要接收端獨立解析,此時,這時粘包會有影響。

    ②如果發送的是一個位元組流檔案,是一個連在一起的整體(比如:發送一部電影),接收端無需單獨解析,而是将所有接收到内容最終合成一個整體,這時粘包沒啥影響。

  • 處理方法

    ①人為的添加消息邊界,比如:每次發送一個姓名時,在姓名後加一個特殊符号。

    ②控制發送速度(因為粘包的産生是因為收發速度不協調),比如用sleep()函數調節。