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