Python之路(第三十一篇) 网络编程:简单的tcp套接字通信、粘包现象
一、簡單的tcp套接字通信
套接字通信的一般流程
服務端
server = socket() #創建服務器套接字server.bind() #把地址綁定到套接字,網絡地址加端口server.listen() #監聽鏈接inf_loop: #服務器無限循環conn,addr = server.accept() #接受客戶端鏈接,建立鏈接connconn_loop: #通訊循環conn.recv()/conn.send() #通過建立的鏈接conn不斷的對話(接收與發送消息)conn.close() #關閉客戶端套接字鏈接connserver.close() #關閉服務器套接字(可選)
客戶端
client = socket() # 創建客戶套接字client.connect() # 嘗試連接服務器,用ip+portcomm_loop: # 通訊循環client.send()/client.recv() # 對話(發送/接收)消息client.close() # 關閉客戶套接字
套接字通信例子
socket通信流程與打電話流程類似,我們就以打電話為例實現簡單的tcp套接字通信
服務端
import socket?# 1.買手機phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 基于網絡通信的 基于tcp通信的套接字?# 2.綁定手機卡(IP地址) 運行這個軟件的電腦IP地址 ip和端口都應該寫到配置文件中phone.bind(('127.0.0.1',8080)) # 端口0-65535 0-1024 給操作系統,127.0.0.1是本機地址即本機之間互相通信?# 3.開機phone.listen(5) # 5 代表最大掛起的鏈接數?# 4.等電話鏈接print('服務器運行啦...')# res = phone.accept() #底層 就是 tcp 三次握手# print(res)conn,client_addr = phone.accept() # conn 電話線 拿到可以收發消息的管道 conn鏈接?while True: #通信循環,可以不斷的收發消息# 5.收發消息data = conn.recv(1024) # 1024個字節 1.單位:bytes 2.1024代表最大接收1024個bytesprint(data)?conn.send(data.upper())?# 6.掛電話conn.close()
?
客戶端
import socket?# 1.買手機 客戶端的phone 相當于服務端的 connphone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 基于網絡通信的 基于tcp通信的套接字?# 2.撥號 (服務端的ip 和服務端的 端口)phone.connect(('127.0.0.1',8080)) #phone 拿到可以發收消息的管道 鏈接對象phone,建立了與服務端的鏈接?while True:# 3.發收消息 bytes型msg = input("請輸入:")phone.send(msg.encode('utf-8'))data = phone.recv(1024)print(data)?# 4.關閉phone.close()
?
注意:這里的發消息收消息都不能為空,否則會出現錯誤。
這里只能接收一個鏈接,不能循環接收鏈接,即打一次電話不能再打了,只能重新開機(重新運行程序)再打,
所以這里要加上鏈接循環。
加上鏈接循環
服務端
import socket?phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)#這里是重用ip和端口,防止出現地址被占用的情況,即time_wait狀態phone.bind(('127.0.0.1',8080))phone.listen(5)while True: #連接循環 沒有并發 但可一個一個 接收客戶端的請求,一個鏈接結束,另外一個鏈接進來print('服務器開始運行啦...')conn,client_addr = phone.accept() # 現在沒并發 只能一個一個print(client_addr)?while True:try: # try...except 出異常適合windows 出異常這里指客戶端斷開,防止服務端直接終止data = conn.recv(1024)if not data:break #linux客戶端意外斷開,這里接收的就是空,防止接收為空的情況print('客戶端數據:',data)conn.send(data.upper())except ConnectionResetError:breakconn.close()phone.close()?# 針對客戶端意外斷開的兩種情況#使用try ...except 是防止客戶端意外斷開產生# ConnectionResetError: [WinError 10054] 遠程主機強迫關閉了一個現有的連接。# 錯誤,針對windows系統?# linux客戶端意外斷開,這里接收的就是空,防止接收為空的情況# 用if 判斷接收的消息是否為空
客戶端
import socketphone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)phone.connect(('127.0.0.1',8080))while True:msg = input('msg>>>:').strip() # ''if not msg:continue #防止輸入為空的情況phone.send(msg.encode('utf-8')) # b''data = phone.recv(1024)print(data.decode('utf-8')) #解碼?phone.close()
附:一個服務端,多個客戶端,將一個客戶端復制多個相同的文件,同時運行多個相同代碼的客戶端文件即可實現多個客戶端鏈接服務端,但是這種鏈接不是同時的,只能一個客戶端通信完,另外一個客戶端在連接池(backlog設置的內容)里等著,等一個鏈接結束才能開始通信。
?
這個是由于你的服務端仍然存在四次揮手的time_wait狀態在占用地址(如果不懂,請深入研究1.tcp三次握手,四次揮手 2.syn洪水攻擊 3.服務器高并發情況下會有大量的time_wait狀態的優化方法),即之前用的端口在系統中仍未清理
解決方法
方法1
#加入一條socket配置,重用ip和端口?phone=socket(AF_INET,SOCK_STREAM)phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加phone.bind(('127.0.0.1',8080))
方法2
?
在linux系統中發現系統存在大量TIME_WAIT狀態的連接,通過調整linux內核參數解決,vi /etc/sysctl.conf?編輯文件,加入以下內容:net.ipv4.tcp_syncookies = 1net.ipv4.tcp_tw_reuse = 1net.ipv4.tcp_tw_recycle = 1net.ipv4.tcp_fin_timeout = 30然后執行 /sbin/sysctl -p 讓參數生效。net.ipv4.tcp_syncookies = 1 表示開啟SYN Cookies。當出現SYN等待隊列溢出時,啟用cookies來處理,可防范少量SYN攻擊,默認為0,表示關閉;?net.ipv4.tcp_tw_reuse = 1 表示開啟重用。允許將TIME-WAIT sockets重新用于新的TCP連接,默認為0,表示關閉;?net.ipv4.tcp_tw_recycle = 1 表示開啟TCP連接中TIME-WAIT sockets的快速回收,默認為0,表示關閉。?net.ipv4.tcp_fin_timeout 修改系統默認的 TIMEOUT 時間
?
?
?
二、基于tcp實現遠程執行命令
模擬ssh遠程執行命令 ,執行命令即Windows的命令提示行里輸入命令,在linux的終端輸入命令
通過tcp模擬執行命令并獲得結果,這里需要用到subprocess模塊
如何執行系統命令: 并拿到執行結果? import os
? os.system # 只能拿到 運行結果 0 執行成功 非0 失敗
? 一般用:
? ? ? import subprocess
? ? ? obj = subprocess.Popen('dir d:',shell=True) # shell 啟了一個cmd
? ? ? 把命令結果丟到管道里面:
? ? ? ? ? subprocess.Popen('dir d:',shell=True,
? ? ? ? ? ? ? ? stdout=subprocess.PIPE)
print(obj.stdout.read().decode('gbk'))拿到命令的結果
print(obj.stderr.read().decode('gbk'))拿到產生的錯誤,Windows系統用'gbk'編碼,linux用'utf-8'編碼
#且只能從管道里讀一次結果
?
例子
服務端
import socketimport subprocess?ip_port = ("127.0.0.1",8000)buffer_size = 1024tcp_server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)tcp_server.bind(ip_port)tcp_server.listen(5)?while True:print("服務器開始運行啦")conn,addr = tcp_server.accept()# print("conn是",conn)while True:try:# 1、收到命令cmd = conn.recv(buffer_size)print("收到客戶端的命令",cmd.decode("utf-8"))# 2、執行命令,拿到結果p = subprocess.Popen(cmd.decode("utf-8"),stdout=subprocess.PIPE,stdin=subprocess.PIPE,stderr=subprocess.PIPE,shell=True)res_cmd_err = p.stderr.read()res_cmd_out = p.stdout.read() #這里產生的結果Windows的編碼是'gbk',linux是'utf-8'# print("res_cmd——out",res_cmd_out)if res_cmd_err: #出現錯誤res_cmd = res_cmd_errconn.send(res_cmd)else:if not res_cmd_out: #命令正常執行,但沒有返回值res_cmd = "命令執行成功!"conn.send(res_cmd.encode("gbk")) #3、將結果返回給客戶端,注意Windows和linux的編碼不同else:conn.send(res_cmd_out)except Exception as e:print(e)breakconn.close()
?
?
客戶端
import socket?ip_port = ("127.0.0.1",8000)buffer_size = 1024tcp_client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)tcp_client.connect(ip_port)while True:# 1、發命令cmd = input("請輸入命令:").strip()if not cmd:continueif cmd == "quit":breaktcp_client.send(cmd.encode("utf-8"))# 2、接收命令,但是這里接收的數據量可能大于buffersize,即一次接收不完,下次通信接收的是上次未接收完的數據,就會產生粘包現象res = tcp_client.recv(buffer_size)print(res.decode("gbk")) #注意Windows和linux的編碼不同tcp_client.close()
?
三、tcp粘包現象
須知:只有TCP有粘包現象,UDP永遠不會粘包。
socket收發消息的底層原理
收發消息流程
1、發送方的應用程序將字節要發送的消息復制到自己的緩存(內存),操作系統(os)通過調用網卡將緩存的消息發送到接收方的網卡
2、接收方網卡將消息存在自己操作系統的緩存中,接收方的應用程序從自己的緩存中取出消息
總結
1、程序的內存和os(操作系統)的內存兩個內存互相隔離,程序的內存是用戶態 的內存,操作系統的內存是內核態的內存
2、發送消息是將用戶態的內存復制給內核態的內存
3、發送方遵循tcp協議將消息通過網卡發送給接收方,接收方通知接收方的操作系統調用網卡接收數據,還要講內存態的消息復制到用戶態的內存
4、發送方消息復制給自己內核態的內存速度快時間短,接收方要通知OS收消息,還要復制消息,用時長
不管是recv還是send都不是直接接收對方的數據,而是操作自己的操作系統內存,不是一個send對應一個recv
基于tcp的套接字客戶端往服務端上傳文件,發送時文件內容是按照一段一段的字節流發送的,在接收方看了,根本不知道該文件的字節流從何處開始,在何處結束。
所謂粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的。
發送方引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,發送方往往要收集到足夠多的數據后才發送一個TCP段。若連續幾次需要send的數據都很少,通常TCP會根據優化算法(Nagle算法,將多次間隔較小且數據量小的數據,合并成一個大的數據塊,然后進行封包)把這些數據合成一個TCP段后一次發送出去,這樣接收方就收到了粘包數據。
?
兩種情況下會發生粘包
1、發送端需要等緩沖區滿才發送出去,造成粘包(發送數據時間間隔很短,數據了很小,會合到一起,產生粘包)
2、接收方不及時接收緩沖區的包,或者由于buffersize的限制,一次接收不完,造成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿上次遺留的數據,產生粘包)
?
例子
服務端
import socketimport timeserver = socket.socket(socket.AF_INET, socket.SOCK_STREAM)server.bind(('127.0.0.1', 9999))server.listen(5)?print('... 開始運行...')conn, addr = server.accept()?#data1 = conn.recv(1024)?data1 = conn.recv(1) # 當只取一個字符的時候,剩下的數據還在緩存池里面,下次接收時間很短的話,# 會繼續把上次沒接收完的一起取出來,就發生的粘包現象print('第一次', data1)?data2 = conn.recv(1024)print('第二次', data2)?conn.close()server.close()
客戶端
# 兩次send:數據量小,時間間隔很短,會發生粘包?import socketimport time?client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)client.connect(('127.0.0.1', 9999))?client.send('hello'.encode('utf-8'))?# time.sleep(1) #兩次send直接隔一段時間,不會發生粘包現象?client.send('world'.encode('utf-8'))?client.close()
四、解決粘包問題
粘包問題產生的根源是接收方不知道一次提取多少字節的數據,那么需要發送方在發送數據前告知接收方我這次要發送多少字節的數據即可。
解決方式的簡單版
先用struct 發送固定長度的消息,傳遞要發送消息的長度,然后按照這個長度接收消息
服務端
import socketimport subprocessimport struct?ip_port = ("127.0.0.1",9001)back_log = 5buffer_size = 1024?tcp_server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)tcp_server.bind(ip_port)tcp_server.listen(back_log)?while True:conn,addr = tcp_server.accept()print("服務器開始運行啦!")while True:try:cmd = conn.recv(buffer_size)if not cmd: breakp = subprocess.Popen(cmd.decode("utf-8"), stderr=subprocess.PIPE, stdin=subprocess.PIPE,stdout=subprocess.PIPE, shell=True)err = p.stderr.read()if err:res_cmd = errelse:res_cmd = p.stdout.read()if not res_cmd:res_cmd = "執行成功!".encode("gbk")print("命令已經執行!")?# 第一步:獲取結果消息的長度length = len(res_cmd)# 第二步:將結果消息的長度封裝為一個固定長度的報頭header = struct.pack("i", length)# 第三步:先向接收方發送報頭,使接收方知道真正接收的消息是多長,# 然后根據這個長度來重復循環接收消息conn.send(header)conn.send(res_cmd)except Exception as e:print(e)breakconn.close()
?
客戶端
?
import socketimport struct?ip_port = ("127.0.0.1", 9001)buffer_size = 1024?tcp_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)tcp_client.connect(ip_port)?while True:cmd = input("請輸入命令:")if not cmd: continueif cmd == "quit": breaktcp_client.send(cmd.encode("utf-8"))# 第一步:接收一個固定長度的報頭header = tcp_client.recv(4)# 第二步:解碼獲取報頭里隱藏的真實要接收消息的長度res_length = struct.unpack("i", header)[0]# 第三步:根據消息的長度來不斷的循環收取消息recv_data = b""recv_data_size = 0while recv_data_size < res_length:res_cmd = tcp_client.recv(buffer_size)recv_data = recv_data + res_cmdrecv_data_size = len(recv_data)print("收取的數據是", recv_data.decode("gbk"))?tcp_client.close()
解決方式終極版
通過自定義的報頭來傳遞除了消息長度外更多的消息,為傳遞的消息做一個字典。
服務端
?
import socketimport subprocessimport structimport json?ip_port = ("127.0.0.1", 9000)back_log = 5buffer_size = 1024tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)tcp_server.bind(ip_port)tcp_server.listen(back_log)?while True:print("服務器開始運行啦!")conn, address = tcp_server.accept()while True:try:cmd = conn.recv(buffer_size)if not cmd: continuep = subprocess.Popen(cmd.decode("utf-8"), stderr=subprocess.PIPE, stdin=subprocess.PIPE,stdout=subprocess.PIPE, shell=True)err = p.stderr.read()# print(err)if err:res_cmd = errelse:res_cmd = p.stdout.read()# print(res_cmd)# print(res_cmd)if not res_cmd:res_cmd = "已經執行啦!".encode("gbk")res_length = len(res_cmd)# 第一步:制作自定制的字典作為報頭,存儲多種信息header_dict = {"filename": "a.txt","md5": "7887414147774415","size": res_length}# 第二步:將字典序列化轉為json字符串,然后進行編碼轉成bytes,以便于直接網絡發送header_bytes = json.dumps(header_dict).encode("utf-8")# 第三步:獲得這個報頭的長度header_length = len(header_bytes)# 第四步:將報頭的長度打包成固定的長度,以便接收方先接收報頭send_header = struct.pack("i", header_length)# 第五步:先發送報頭的長度conn.send(send_header)# 第六步:發送報頭conn.send(header_bytes)# 第七步:發送真實的消息conn.send(res_cmd)except Exception as e:print(e)breakconn.close()
?
客戶端
?
import socket import struct import jsonip_port = ("127.0.0.1", 9000) buffer_size = 1024 tcp_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) tcp_client.connect(ip_port)while True:cmd = input("請輸入命令:")if not cmd: continueif cmd == "quit":breaktcp_client.send(cmd.encode("utf-8"))# 第一步:接收報頭的長度信息header_length = tcp_client.recv(4)# 第二步:獲取報頭的長度,解碼獲取報頭的長度header_size = struct.unpack("i", header_length)[0]# 第三步:根據報頭的長度信息接收報頭信息header_bytes = tcp_client.recv(header_size).decode("utf-8")# 第四步:根據接收的報頭信息反序列化獲得真實的報頭header_dict = json.loads(header_bytes)print("客戶端收到的報頭字典是",header_dict)# 第五步:根據報頭字典獲取真實消息的長度res_size = header_dict["size"]# 第六步:根據獲取的真實消息的長度不斷循環獲取真實消息data = b""data_size = 0while data_size < res_size:recv_data = tcp_client.recv(buffer_size)data = data + recv_datadata_size = len(data)print("接收的數據是", data.decode("gbk"))tcp_client.close()
轉載于:https://www.cnblogs.com/Nicholas0707/p/9817040.html
總結
以上是生活随笔為你收集整理的Python之路(第三十一篇) 网络编程:简单的tcp套接字通信、粘包现象的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CPU和微架构的概念
- 下一篇: Tower-web 0.3.1/2 发布