python網路編程-socket套接字通訊循環-粘包問題-struct模組-02

  • 2019 年 10 月 7 日
  • 筆記

前置知識

不同電腦程式之間數據的傳輸

應用程式中的數據都是從程式所在電腦記憶體中讀取的。 記憶體中的數據是從硬碟讀取或者網路傳輸過來的

不同電腦程式數據傳輸需要經過七層協議物理連接介質才能到達目標程式

socket (套接字)

json.dump/dumps 只是把數據類型序列化成字元串 要想用來文件傳輸,還需要encode 給它編碼成二進位數據才能傳輸 不用pickle是因為要和其他語言交互(你給頁面就是js來處理,能不能支援是問題),而pickle只能是在python中用

程式設計師不需要七層一層一層地去操作硬體寫網路傳輸程式,直接使用python解釋器提供的socket 模組即可

大多數注意點都在程式碼後面的注釋里,要仔細看哦~

初略版的雙端(C/S)通訊

程式運行時先啟動服務端再啟動客戶端(程式碼設置一下可以一份程式碼跑好幾個(客戶端可能會啟好幾個))

server服務端

socket.scoket()不傳參數默認就是TCP協議

import socket    server = socket.socket()  # 有一個參數 type=SOCK_STREAM,即不傳參數,默認就是TCP協議  # socket.socket()  # socket模組中有個socket類,加() 實例化成一個對象(ctrl + 單擊 可以看到)  #   不要形成固有思想, 模組.名字() 就以為是模組里的方法,點進去,可能還是類(看他這個類的名字還是全小寫的...)    server.bind(('127.0.0.1', 8080))  # 127.0.0.1 本機迴環地址只能本機訪問,其他電腦訪問不了(識別到了就不用走七層協議這些了)  # address: Union[tuple, str, bytes]) ---> address 參數是一個元組,綁定ip 和 埠    server.listen(5)  # 半連接池      print("waitting....")  # waitting....  conn, addr = server.accept()  # 阻塞,等待客戶端連接,沒有收到資訊會停在這裡  print("hi")  # 在連通之前並沒有反應  # hi    # --------------------------------------  # send 與 recv 要對應  #   不要兩邊都 recv,不然就都等著接收了  # --------------------------------------  data = conn.recv(1024)  # 阻塞,等待客戶端發送數據,接收1024個位元組的數據  print(data)  # b'halo baby'    conn.send(b'ok')  # 發送數據(必須是二進位數據)    conn.close()  # 關閉連接  server.close()  # 關閉服務

client客戶端

import socket    client = socket.socket()    client.connect(('127.0.0.1', 8080))  # 去連接伺服器上的程式(伺服器的IP + port)    client.send(b'halo baby')    data = client.recv(1024)  print(data)    client.close()

點進去發現socket這個類有實現 __enter_ 、 __exit__方法,__exit__方法中有關閉連接的方法,故可以用with上下文來操作(暫不舉例了,面向對象這兩個函數的知識點提一嘴)

在重啟伺服器的時候可能會遇到的BUG(mac居多)

解決方法

# 加入一條socket配置,重用ip和埠  import socket  from socket import SOL_SOCKET, SO_REUSEADDR    server = socket.socket()    # -------------------------------------  # 加上他就可以防止重啟報錯了(注意位置)  # -------------------------------------  server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)    server.bind(('127.0.0.1', 8080))  # 把地址綁定到套接字  server.listen(5)  # 半連接池    conn, addr = server.accept()  # 接受客戶端鏈接  ret = conn.recv(1024)  # 接收客戶端資訊  print(ret)  # 列印客戶端資訊  conn.send(b'hi')  # 向客戶端發送資訊    conn.close()  # 關閉客戶端套接字  server.close()  # 關閉伺服器套接字(可選)

服務端需要具備的條件

  • 固定的ip和port 讓客戶端可以連接你(試想如果百度一天一個域名/ip?咋上百度))
  • 要能24小時不間斷提供服務 伺服器不在線的話,客戶端連啥?(雙重循環 server.accpet() 來連接建立連接)
  • 暫時不知道

半連接池,允許等待的最大個數

server.listen(5)指定5個等待席位

通訊循環

雙方都處於收的等待狀態

直接回車沒有發出數據,自身程式碼往下走進入了等待接收狀態, 而另一端也沒有收到消息,依然處於等待接收狀態圖,雙方就都處於等待接收的狀態了

linux、mac斷開鏈接時不會報錯,會一直返回空(b『』)

窮,買不起mac…沒圖

解決方案

服務端

import socket    server = socket.socket()  server.bind(('127.0.0.1', 8080))  # 本地迴環地址  server.listen(5)    conn, addr = server.accept()  # 阻塞    for i in range(1, 5):      try:          data = conn.recv(1024)  # 阻塞          print(data.decode('utf-8'))          msg = f"收到 {i} ga ga ga~"          # 發的時候要判斷非空,空的自己send出去處於接收狀態,對方依舊是接收狀態,那就都等待了          conn.send(msg.encode('utf-8'))  # ***** send 直接傳回車會導致兩遍都處於接收狀態      except ConnectionResetError:  # ***** 當服務端被強制關閉時彙報異常,這裡捕獲並做處理          # mac或者linux 會一直輸空,不會自動結束          break  conn.close()  server.close()

客戶端

import socket    client = socket.socket()  client.connect(('127.0.0.1', 8080))    hi = input(">>>:").strip()    for i in range(1, 5):      msg = f'-{i} hi 咯~'      client.send(msg.encode('utf-8'))      data = client.recv(1024)      if len(data) == 0:  # ***** mac或者linux 需要加,避免客戶端突然斷開,他不會報錯,會一直列印空          break      print(f"收到 {i} {data.decode('utf-8')}")    client.close()

實現服務端可以接收多個客戶端通訊(一個結束還可以接收下一個) — 利用好server.listen(5) 半連接池以及conn, addr = server.accept()把接收的程式碼用循環包起來

粘包問題

多次發送被並為一次

根據最上面的前置知識可以知道,數據是從記憶體中讀取過來的

產生問題的原因

黏包現象只發生在tcp協議中

1.從表面上看,黏包問題主要是因為發送方和接收方的快取機制、tcp協議面向流通訊的特點

2.實際上,主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少位元組的數據所造成的

粘包是接收長度沒對上導致的

控制recv接收的位元組數與之對應(你發多少位元組我收多少位元組)

在很多情況下並不知道數據的長度,服務端不能寫死

思路一如果在不知道數據有多長的情況下就會出現意外,那麼我們可以先傳一個固定長度的數據過去告訴他真實數據有多長,就可以對應著收了

struct模組

該模組可以把一個類型,如數字,轉成固定長度的bytes

這裡利用struct模組模組的struct.pack() struct.unpack()方法來實現打包(將真實數據長度變為固定長度的數字)解包(將該數字解壓出打包前真實數據的長度)

pack unpack模式參數對照表(standard size 轉換後的長度)

i 模式的範圍:-2147483648 <= number <= 2147483647

在傳真實數據之前還想要傳一些描述性資訊

如果在傳輸數據之前還想要傳一些描述性資訊,那麼就得在中間再加一步了(傳個電影,我告訴你電影名,大小,大致情節,演員等資訊,你再選擇要不要),前面的方法就不適用了

粘包問題解決思路

伺服器端

  • 先製作一個發送給客戶端的字典
  • 製作字典的報頭
  • 發送字典的報頭
  • 發送字典
  • 再發真實數據

客戶端

  • 先接收字典的報頭
  • 解析拿到字典的數據長度
  • 接收字典
  • 從字典中獲取真實數據的長度
  • 循環獲取真實數據

ps:為什麼要多加一個字典

  • pack打包的數據長度(的長度)有限,字典再打包會很小(長度值也會變很小)(120左右)
  • 可以攜帶更多的描述資訊

粘包問題解決最終版模板

伺服器端

import socket  import subprocess  import struct  import json      server = socket.socket()  server.bind(('127.0.0.1',8080))  server.listen(5)      while True:      conn, addr = server.accept()      while True:          try:              cmd = conn.recv(1024)              if len(cmd) == 0:break              cmd = cmd.decode('utf-8')              obj = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)              res = obj.stdout.read() + obj.stderr.read()              d = {'name':'jason','file_size':len(res),'info':'asdhjkshasdad'}              json_d = json.dumps(d)              # 1.先製作一個字典的報頭              header = struct.pack('i',len(json_d))              # 2.發送字典報頭              conn.send(header)              # 3.發送字典              conn.send(json_d.encode('utf-8'))              # 4.再發真實數據              conn.send(res)              # conn.send(obj.stdout.read())              # conn.send(obj.stderr.read())          except ConnectionResetError:              break      conn.close()

客戶端

import socket  import struct  import json    client = socket.socket()  client.connect(('127.0.0.1',8080))    while True:      msg = input('>>>:').encode('utf-8')      if len(msg) == 0:continue      client.send(msg)      # 1.先接受字典報頭      header_dict = client.recv(4)      # 2.解析報頭 獲取字典的長度      dict_size = struct.unpack('i',header_dict)[0]  # 解包的時候一定要加上索引0      # 3.接收字典數據      dict_bytes = client.recv(dict_size)      dict_json = json.loads(dict_bytes.decode('utf-8'))      # 4.從字典中獲取資訊      print(dict_json)      recv_size = 0      real_data = b''      while recv_size < dict_json.get('file_size'):  # real_size = 102400          data = client.recv(1024)          real_data += data          recv_size += len(data)      print(real_data.decode('gbk'))

案例-客戶端向服務端傳輸文件

需求

# 寫一個上傳電影功能    1.循環列印某一個文件夾下面的所有文件  2.用戶選取想要上傳的文件  3.將用戶選擇的文件上傳到服務端  4.服務端保存該文件

服務端(沒有處理斷開連接的報錯以及空輸入的報錯,linux、mac的兼容)

import os  import sys  import socket  import struct  import json      server = socket.socket()  server.bind(('192.168.13.34', 8080))  server.listen(5)    conn, addr = server.accept()      '''  伺服器端將文件都放在同一個目錄    '''  BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))  sys.path.append(BASE_DIR)  dir_path = os.path.join(BASE_DIR, 'datas', 're_movies')  if not os.path.exists(dir_path):      os.makedirs(dir_path)      import time  from functools import wraps      # 統計運行時間裝飾器  def count_time(func):      @wraps(func)      def inner(*args, **kwargs):          start_time = time.time()          res = func(*args, **kwargs)          end_time = time.time()          print(f"耗時{end_time - start_time}s")          return res      return inner      @count_time  def save_file(file_path, file_size):      with open(file_path, 'ab') as f:          # 一行一行地收文件,同時寫入文件          recv_size = 0          while recv_size < file_size:              data = conn.recv(1024)              # 存文件              # json.dump(data.decode('utf-8'), f)  # -------------可能報錯,不傳文件對象              f.write(data)              f.flush()              recv_size += len(data)        msg = f'已收到{file_name},{file_size/1024/1024}MB,over~'      print('33[33m', msg, '33[0m')      conn.send(msg.encode('utf-8'))      while True:      print("等待接收客戶端的資訊......")        # 1.接收報頭大小      dict_header_recv = conn.recv(4)        # 2.接收字典      dict_header_size = struct.unpack('i', dict_header_recv)[0]      recv_dict_str = conn.recv(dict_header_size).decode('utf-8')      recv_dict = json.loads(recv_dict_str)      print(recv_dict)        # 3.獲取字典中的數據長度以及文件名      file_name = recv_dict.get('file_name')      file_size = recv_dict.get('file_size')        # 4.循環獲取真實數據,並存起來      file_path = os.path.join(dir_path, file_name)      # with open(file_path, 'ab') as f:      #     # 一行一行地收文件,同時寫入文件      #     recv_size = 0      #     while recv_size < file_size:      #         data = conn.recv(1024)      #         # 存文件      #         # json.dump(data.decode('utf-8'), f)  # -------------可能報錯,不傳文件對象      #         f.write(data)      #         f.flush()      #         recv_size += len(data)      #      # msg = f'已收到{file_name},{file_size/1024/1024}MB,over~'      # print('33[33m', msg, '33[0m')      # conn.send(msg.encode('utf-8'))      save_file(file_path, file_size)    # conn.close()  # server.close()

客戶端

import json  import os  import struct  import socket    # 連接服務端  client = socket.socket()  client.connect(('192.168.13.34', 8080))      while True:      '''後續想做成可以更換目錄的,所以放到這裡面了'''      # 操作目標文件夾      BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))      dir_path = os.path.join(BASE_DIR, 'movies')      # dir_path = r'一個絕對路徑'      file_name_list = os.listdir(dir_path)        # 讓用戶選擇      print("您的文件夾下現有如下文件:")      for index, file_name in enumerate(file_name_list, 1):          # 可以在前面給文件名做一個分割,把後綴名去掉          print(f"t{index}. {file_name}")        choice = input("請選擇您想要上傳電影的編號>>>:").strip()      if choice in ['q', 'exit']:          print("感謝您的使用~")          break      elif choice.isdigit() and int(choice) - 1 in range(len(file_name_list)):          # 正確選好文件          file_name = file_name_list[int(choice) - 1]          file_path = os.path.join(dir_path, file_name)      else:          print("請輸入正確的編號!")          continue        # 準備開始上傳文件      file_size = os.path.getsize(file_path)        # 1.製作報頭字典      file_dict = {          'file_name': file_name,          'file_size': file_size      }        # 2.打包報頭字典      file_dict_str = json.dumps(file_dict)      file_dict_header_size = struct.pack('i', len(file_dict_str))        # 3.發送報頭大小      client.send(file_dict_header_size)        # 4.發送報頭字典      # file_dict_str = json.dumps(file_dict)      client.send(file_dict_str.encode('utf-8'))        # 5.一行一行地把文件發過去      with open(file_path, 'rb') as f:          # 一行一行地傳過去,避免大文件(一行還是不頂用,壓縮過的數據基本都在一行)          # 轉為每次發 1024 Bytes 數據          _file_size = file_size          while _file_size > 0:              if file_size > 1024:                  data = f.read(1024)                  _file_size -= 1024              else:                  data = f.read(_file_size)                  _file_size -= _file_size              client.send(data)              print(f"發送了 {len(data)} Bytes 數據~~~")        print('33[33m', f"文件{file_name},{file_size/1024/1024}MB已發送完畢~", '33[0m')      msg = client.recv(1024)      if msg:          print(f"伺服器端回復:", msg.decode('utf-8'))    client.close()

小提示: 上面的連接地址('192.168.13.34', 8080)可以換成你小夥伴的地址哦~ 客戶端和服務端的要記得一致

在命令行中(windows + r , 輸入 cmd 回車)輸入ipconfig可以查看本機的ip地址

伺服器端運行截圖(本地區域網速度主要受限於硬碟讀寫速度)

客戶端資源佔用截圖

伺服器端資源佔用截圖

另一份案例參考

服務端

import socket  import json  import struct    server = socket.socket()  server.bind(('127.0.0.1', 8080))  server.listen(5)    while True:      conn, addr = server.accept()      while True:          try:              header_len = conn.recv(4)              # 解析字典報頭              header_len = struct.unpack('i', header_len)[0]              # 再接收字典數據              header_dic = conn.recv(header_len)              real_dic = json.loads(header_dic.decode('utf-8'))              # 獲取數據長度              total_size = real_dic.get('file_size')              # 循環接收並寫入文件              recv_size = 0              with open(real_dic.get('file_name'), 'wb') as f:                  while recv_size < total_size:                      data = conn.recv(1024)                      f.write(data)                      recv_size += len(data)                  print('上傳成功')          except ConnectionResetError as e:              print(e)              break      conn.close()  # server.close()

客戶端

import socket  import json  import os  import struct    client = socket.socket()  client.connect(('127.0.0.1', 8080))    while True:      # 獲取電影列表 循環展示      MOVIE_DIR = r'D:python影片day25影片'      movie_list = os.listdir(MOVIE_DIR)      # print(movie_list)      for i, movie in enumerate(movie_list, 1):          print(i, movie)      # 用戶選擇      choice = input('please choice movie to upload>>>:')      # 判斷是否是數字      if choice.isdigit():          # 將字元串數字轉為int          choice = int(choice) - 1          # 判斷用戶選擇在不在列表範圍內          if choice in range(0, len(movie_list)):              # 獲取到用戶想上傳的文件路徑              path = movie_list[choice]              # 拼接文件的絕對路徑              file_path = os.path.join(MOVIE_DIR, path)              # 獲取文件大小              file_size = os.path.getsize(file_path)              # 定義一個字典              res_d = {                  'file_name': '性感荷官在線發牌.mp4',                  'file_size': file_size,                  'msg': '注意身體,多喝營養快線'              }              # 序列化字典              json_d = json.dumps(res_d)              json_bytes = json_d.encode('utf-8')                # 1.先製作字典格式的報頭              header = struct.pack('i', len(json_bytes))              # 2.發送字典的報頭              client.send(header)              # 3.再發字典              client.send(json_bytes)              # 4.再發文件數據(打開文件循環發送)              with open(file_path, 'rb') as f:                  for line in f:                      client.send(line)          else:              print('not in range')      else:          print('must be a number')    # client.close()