1. 程式人生 > 其它 >今日學習內容總結3.1

今日學習內容總結3.1

今日學習內容總結

      昨日我們學習了網路程式設計,對軟體開發結構有了一個明確的瞭解。並且知道了計算機的生產過程中都必須有的相同功能,OSI七層協議,也可以總結成五層。而今天的主要學習內容就是通過程式碼,實現客戶端與伺服器的資訊互動。

socket

socket套接字簡介

      什麼是套接字?套接字就是網路間進行通訊的方式的名稱,行內人一般都稱之為套接字通訊。比如說HTTP協議,需要具體的程式設計去實現,或者現在我們做前後端分離專案的時候需要遵循RESTful協議,那麼實現此協議的方法就是RESTful API。那麼傳輸層的兩種傳輸服務分別遵循了TCP、UDP協議,實現這兩種協議的方法就是套接字。

      套接字的表示方法:

  1.套接字Socket=(IP地址:埠號),套接字的表示方法是點分十進位制的lP地址後面寫上埠號,中間用冒號或逗號隔開。
  2.每一個傳輸層連線唯一地被通訊兩端的兩個端點(即兩個套接字)所確定。例如:如果IP地址192.168.0.1,而埠號是23,那麼得到套接字就是(192.168.0.1:23)

      套接字工作流程

      1.通過網際網路進行通訊,至少需要一對套接字,其中一個運行於客戶端,我們稱之為 Client Socket,另一個運行於伺服器端,我們稱之為 Server Socket。

      2.根據連線啟動的方式以及本地套接字要連線的目標,套接字之間的連線過程可以分為三個步驟:

  1.伺服器監聽:指伺服器端套接字並不定位具體的客戶端套接字,而是處於等待連線的狀態,實時監控網路狀態。
  2.客戶端請求:指由客戶端的套接字提出連線請求,要連線的目標是伺服器端的套接字。為此,客戶端的套接字必須首先描述它要連線的伺服器的套接字,指出伺服器端套接字的地址和埠號,然後就向伺服器端接字提出連線請求。
  3.連線確認:當伺服器端套接字監聽到或者說接收到客戶端套接字的連線請求,就會響應客戶端套接字的請求,建立一個新的執行緒,並把伺服器端套接字的描述傳送給客戶端。一旦客戶端確認了此描述,連線就建立好了。而伺服器端套接字繼續處於監聽狀態,接收其他客戶端套接字的連線請求。

      我們今天的學習內容是要編寫一個cs架構的程式,實現資料的互動。由於操作OSI七層是所有cs架構的程式都需要經歷的過程,所以有固定的模組。這個模組就是socket模組。

socket模組

      socket模組的基本用法:

服務端

  import socket

  server = socket.socket()  # 建立例項

  server.bind(('127.0.0.1', 8080))  # 繫結監聽
  """
  服務端應該具備的特徵
      固定的地址
      ...  
  127.0.0.1是計算機的本地迴環地址 只有當前計算機本身可以訪問
  """
  server.listen(5)  # 監聽
  """
  半連線池(暫且忽略 先直接寫 後面講)
  """
  sock, addr = server.accept()  # 獲取從客戶端發過來的資料  沒有資料就原地等待(程式阻塞)
  """
  listen和accept對應TCP三次握手服務端的兩個狀態
  """
  print(addr)  # 客戶端的地址
  data = sock.recv(1024)  # recv就是接收資料 用一個最大位元組數來作為引數,如果不確定,使用1024比較好
  print(data.decode('utf8'))  
  sock.send('你好啊'.encode('utf8'))  # send傳送資料,用字串作為引數
  """
  recv和send接收和傳送的都是bytes型別的資料
  """
  sock.close()  # 主動關閉連線
  server.close()  # 關機

      這段程式碼的意思是開啟一個socket服務,客戶端傳送過來訊息後。經過服務端的處理後。再返回給客戶端,然後斷開連線。接下來看客戶端的程式碼。

客戶端

  import socket

  client = socket.socket()  # 建立一個socket物件
  client.connect(('127.0.0.1', 8080))  # 根據服務端的地址連結

  client.send(b'hello sweet heart!!!')  # 給服務端傳送訊息
  data = client.recv(1024)  # 接收服務端回覆的訊息
  print(data.decode('utf8'))

  client.close()  # 關閉客戶端

      客戶端的程式碼的意思是,開啟連線,連線到指定埠,使用者輸入資料傳送到服務端,然後接受服務端返回的資料。最後再關閉這個連線。

      這樣,我們就簡單的實現了一個客戶端與服務端的首次互動了,雖然只能互動一次就結束。所以接下來的通訊迴圈就是實現服務端與客戶端的連續訊息傳送了。

通訊迴圈

迴圈通訊的實現

      上面兩個檔案最後都關閉了連線,我們怎麼保持訊息的連續傳送呢?僅僅是不做關閉就可以了嗎?答案是不行。我們怎麼實現一次連線,就可以持續傳送呢,我們可以在一次連線成功後做一個while true的迴圈,這樣我們就可以持續傳送訊息了。下面是對程式碼的進一步改寫。

服務端

  import socket

  server = socket.socket()  # 建立例項

  server.bind(('127.0.0.1', 8080))  # 繫結監聽
  """
  服務端應該具備的特徵
      固定的地址
      ...  
  127.0.0.1是計算機的本地迴環地址 只有當前計算機本身可以訪問
  """
  server.listen(5)  # 監聽
  """
  半連線池(暫且忽略 先直接寫 後面講)
  """
  sock, addr = server.accept()  # 獲取從客戶端發過來的資料  沒有資料就原地等待(程式阻塞)
  """
  listen和accept對應TCP三次握手服務端的兩個狀態
  """
  print(addr)  # 客戶端的地址
  
  while True:
    data = sock.recv(1024)  # recv就是接收資料 用一個最大位元組數來作為引數,如果不確定,使用1024比較好
    print(data.decode('utf8'))  
    msg = input('請回復訊息>>>:').strip()
    sock.send(msg.encode('utf8'))  # send傳送資料,用字串作為引數
  """
  recv和send接收和傳送的都是bytes型別的資料
  """
  sock.close()  # 主動關閉連線
  server.close()  # 關機

客戶端

  import socket

  client = socket.socket()  # 建立一個socket物件
  client.connect(('127.0.0.1', 8080))  # 根據服務端的地址連結

  while True:
    msg = input('請輸入你需要傳送的訊息>>>:').strip()
    client.send(msg.encode('utf8'))  # 給服務端傳送訊息
    data = client.recv(1024)  # 接收服務端回覆的訊息
    print(data.decode('utf8'))

  client.close()  # 關閉客戶端

迴圈通訊的程式碼優化及連線迴圈

      優化問題:

  1.傳送訊息不能為空
  2.反覆重啟服務端可能會報錯
  3.連線迴圈:
      1.如果是windows 客戶端異常退出之後服務端會直接報錯
      2.如果是mac或linux 服務端會接收到一個空訊息

      優化方式:

  1.統計長度並判斷即可
  2.在bind前加  from socket import SOL_SOCKET,SO_REUSEADDR
               server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) 
  3.1 用異常處理解決
  3.2用len判斷

      目前我們的服務端只能實現一次服務一個人,不能做到同事服務多個 ,學了併發才可以實現。

半連線池

      當伺服器在響應了客戶端的第一次請求後會進入等待狀態,會等客戶端傳送的ack資訊,這時候這個連線就稱之為半連線。而半連線池其實就是一個容器,系統會自動將半連線放入這個容器中,可以避免半連線過多而保證資源耗光。所以我們的半連線池可以設定最大等待人數。節約資源,提高效率。

      寫法:listen(3) 。引數可以設定最大的半連線數,最大3個.

黏包問題

       TCP作為常用的網路傳輸協議,資料流解析是網路應用開發人員永遠繞不開的一個問題。TCP資料傳輸是以無邊界的資料流傳輸形式,所謂無邊界是指資料傳送端傳送的位元組數,在資料接收端接受時並不一定等於傳送的位元組數,可能會出現粘包情況。

TCP黏包情況:

      1. 傳送端傳送了數量比較大的資料,接收端讀取資料時候資料分批到達,造成一次傳送多次讀取;通常網路路由的快取大小有關係,一個數據段大小超過快取大小,那麼就要拆包傳送。

      2. 傳送端傳送了幾次資料,接收端一次性讀取了所有資料,造成多次傳送一次讀取;通常是網路流量優化,把多個小的資料段集滿達到一定的資料量,從而減少網路鏈路中的傳輸次數。

      問題產生的原因其實是因為recv括號內我們不知道即將要接收的資料到底多大,如果每次接收的資料我們都能夠精確的知道它的大小,那麼肯定不會出現黏包。

解決黏包問題

      因為困擾我們的核心問題是不知道即將要接收的資料多大,如果能夠精準的知道資料量多大,那麼黏包問題就自動解決了。所以我們解決的方向就是精確獲取資料的大小。

  # 可以使用struct模組
  import struct

  data1 = 'hello world!'
  print(len(data1))  # 12
  res1 = struct.pack('i', len(data1))  # 第一個引數是格式 寫i就可以了
  print(len(res1))  # 4
  ret1 = struct.unpack('i', res1)
  print(ret1)  # (12,)


  data2 = 'hello baby baby baby baby baby baby baby baby'
  print(len(data2))  # 45
  res2 = struct.pack('i', len(data2))
  print(len(res2))  # 4
  ret2 = struct.unpack('i', res2)
  print(ret2)  # (45,)

      pack可以將任意長度的數字打包成固定長度,unpack可以將固定長度的數字解包成打包之前資料真實的長度。解決思路:

    1.先將真實資料打包成固定長度的包
    2.將固定長度的包先發給對方
    3.對方接收到包之後再解包獲取真實資料長度
    4.接收真實資料長度