socket在python下的使用
- 建立套接字物件
- 套接字物件方法
- socket緩衝區與阻塞
- 粘包(資料的無邊界性)
- 案例之模擬ssh命令
- 案例之檔案上傳
1.1建立套接字物件
Linux中的一切都是檔案,每個檔案都有一個整數型別的檔案描述符;socket也可以視為一個檔案物件,也有檔案描述符。
import socket
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# <socket.socket fd=496, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0>
print(sock)
1.AF為地址族(address family),也就是IP地址型別,常用的有AF_INET和AF_INET6,INET是“Internet”的簡寫,AF_INET表示IPV4地址,例如127.0.0.1;AF_INET6表示ipv6地址。127.0.0.1,他是一個特殊IP地址,表示本機地址,會經常用到。
2.type為資料傳輸方式/套接字型別,常用的有SOCK_STREAM(流格式套接字/面向連線的套接字)和SOCK_DGRAM(資料包套接字/無連線的套接字)。
3.protocol表示傳輸協議,常用的有IPPROTO_TCP和IPPTOTO_UDP,分別表示TCP傳輸協議和UDP傳輸協議。有了地址型別和資料傳輸方式,還不足以決定採用哪種協議嗎?為什麼還需要第三個引數呢?一般情況下有了af和type兩個引數地址就可以建立套接字了,作業系統會自動推演出協議型別,除非遇到這樣的情況:有兩種不同的協議支援同一種地址型別和資料傳輸型別。如果我們不指明使用哪種協議,作業系統是沒有辦法自動推演的。
如果使用sock_stream傳輸資料,那麼滿足這兩個條件的協議只有TCP,因此可以這樣來代用socket():
# IPPROTO_TCP表示TCP協議
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
這種套接字稱為 TCP 套接字。
如果使用 SOCK_DGRAM 傳輸方式,那麼滿足這兩個條件的協議只有 UDP,因此可以這樣來呼叫 socket() :
# IPPROTO_UDP表示UDP協議
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
這種套接字稱為 UDP 套接字。
上面兩種情況都只有一種協議滿足條件,可以將 protocol 的值設為 0,系統會自動推演出應該使用什麼協議,如下所示:
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) # 建立TCP套接字
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0) # 建立UDP套接字
4、sock = socket.socket()預設建立TCP套接字。
1.2套接字物件方法
服務端:bind方法
socket用來建立套接字物件,確定套接字的各種屬性,然後在伺服器端要用bind()方法將套接字與特定的IP地址和埠的資料才能交給套接字處理。類似的,客戶端也要用connect()方法建立連線。
import socket #呼叫socket模組
sock = socket.socket() #呼叫docket
sock.bind(("127.0.0.1",8899)) #建立地址和埠,使用預設tcp協議。
服務端:listen方法
透過listen()方法可以讓套接字進入被動監聽狀態,sock為需要進入監聽狀態的套接字,backlog為請求佇列的最大長度,所謂被動監聽,是指當沒有客戶端請求時,套接字處於“睡眠”狀態,只有當接收到客戶端請求時,套接字太會被“喚醒”來響應請求,當套接字正在處理客戶端請求時,如果有新的請求進來,套接字是沒辦法處理的,只能把它放進緩衝區,待當前請求處理完畢後,再衝緩衝區讀取出來處理。如果不斷有新的請求,他們就按照先後順序在緩衝區中排隊,知道緩衝區滿。這個緩衝區,就稱為請求佇列(request queue)
緩衝區的長度(能存放多少個客戶端請求)可以透過listen()方法和backlog引數指定,但究竟為多少並沒有什麼標準,可以根據你需求來定,併發量小的話可以是10或者20.
如果將backlog的值設定為SOMAXCONN,就由系統來決定請求佇列長度,這個值比較龐大,可能是幾百,或者更多。當請求佇列滿時,就不再接受新的請求。
注意:listen()只是讓套接字處於監聽狀態,並沒有接受請求,接收請求需要使用accept()函式
sock.listen(5)
服務端:accept方法
當套接字處於監聽狀態時,可以透過accept()函式來接收客戶端請求。accept()返回一個新的套接字來和客戶端通訊,addr儲存了客戶端的IP地址和埠號,而sock是伺服器端的套接字,大家注意區分。後面和客戶端通訊是,要使用這個新生成的套接字,而不是原來伺服器端的套接字。
conn,addr=sock.accept()
print("conn:",conn) # conn: <socket.socket fd=560, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8899), raddr=('127.0.0.1', 64915)>
print("addr:",addr) # addr: ('127.0.0.1', 64915)
客戶端:connect方法
connect()是客戶端程式用來連線伺服器的方法:
import socket
ip_port=("127.0.0.1",8899)
sk=socket.socket()
sk.connect(ip_port)
注意:只有經過connect連線成功後的套接字物件才能呼叫傳送和接收方法(send/recv)。所以服務端的sock物件不能send or recv。
收發資料方法:send和recv
s.recv()
接收TCP資料,資料以字串形式返回,bufsize指定要接收的最大資料量。flag提供有關訊息的其他訊息,通常可以忽略。
s.send()
傳送TCP資料,將string中的資料傳送到連線的套接字。返回值是要傳送的位元組數量,該數量可能小於string的位元組大小。
簡單聊天案例
服務端:
import socket
# 1.構建套接字物件
sock = socket.socket()
# 2.繫結:度無端的ip和埠
sock.bind(("127.0.0.1",8899))
# 3.確定最大監聽數
sock.listen(3)
# 4.等待連線
while 1:
print("等待伺服器連線。。。")
#獲取客戶端的套接字物件和地址
conn,addr = sock.accept()
print(conn,addr)
#conn:傳送訊息 send 方法 接收資料 recv方法
try:
while 1:
msg = conn.recv(1024) #
print(msg.decode())
if msg.decode() == "quit":
break
res = input("響應>>>")
conn.send(res.encode())
except ConnectionResetError:
print("客戶端斷開,連線下一個客戶端")
客戶端:
import socket
# 建立客戶端的套接字物件
sock = socket.socket()
# 連線伺服器
sock.connect(("127.0.0.1",8899))
while 1:
data = input(">>>")
sock.send(data.encode())
if data == "quit":
break
res = sock.recv(1024)
print("伺服器響應",res.decode())
圖解socket函式:
1.3、socket緩衝區與阻塞
1.socket緩衝區
每個socket被建立後,都會分配兩個緩衝區,輸入緩衝區和輸出緩衝區。write()/send()並不立即向網路中傳輸資料,而是現將資料寫入緩衝區中,再由TCP協議將資料從緩衝區傳送到目標及其。一旦將資料寫入到緩衝區,函式就可以成功返回,不管它們有麼有到達目標及其,也不管它們何時被髮送到網路,這些都是TCP協議負責的事情。
TCP協議獨立於write()/send()函式,資料有可能剛被寫入緩衝區就傳送到網路,也可能在緩衝區中不斷積壓,多次寫入的資料被一次性傳送到網路,這取決於當時網路情況、當前執行緒是否空閒等諸多因素,不由程式設計師控制。read()/recv()函式亦是如此,也從輸入緩衝區中讀取資料,而不是直接從網路中讀取。
這些I/O緩衝區特性可整理如下:
- I/O緩衝區在每個TCP套接字中單獨存在;
- I/O緩衝區在建立套接字時自動生成;
- 即使關閉套接字也會繼續傳送輸出緩衝區中遺留的資料;
- 關閉套接字將丟失輸入緩衝區中的資料。
輸入輸出緩衝區的預設大小一般都是8K!
2、阻塞模式
對於TCP套接字(預設情況下),當使用send()傳送資料時:
1.首先會檢查緩衝區,如果緩衝區的可用空間長度小於要傳送的資料,那麼send()會被阻塞(暫停執行),直到緩衝區中的資料被髮送到目標機器,騰出足夠的空間,才喚醒send()函式繼續寫入資料。
2.如果TCP協議正在向網路傳送資料,那麼輸出緩衝區會被鎖定,不允許寫入,send()也會被阻塞,直到資料傳送完畢緩衝區解鎖,send()才會被喚醒。
3.如果要寫入的資料大於緩衝區的最大長度,那麼將分批寫入。
4.直到所有資料被寫入緩衝區send()才能返回。
當使用recv()讀取資料時:
1.首先會檢查緩衝區,如果緩衝區中有資料,那麼就讀取,否則函式會被阻塞,直到網路上有資料到來。
2.如果要去讀的資料長度 小於緩衝區中的資料長度,那麼就不能一次性將緩衝區中所有的資料讀出,剩餘資料將不斷積壓,直到有recv()函式再次讀取。
3.直到讀取到資料後,recv()函式才會返回,否則就一直被阻塞
TCP套接字預設情況下是阻塞模式,也是常用的。當然也可以更改為非阻塞模式。
粘包(資料的無邊界性)
socket資料的接收和傳送是無關的,recv()不管資料傳送了多少次,都會盡可能多的接收資料,也就是說,recv()和send()的執行次數可能不同。
例如,send()重複執行三次,每次都會傳送字串“abc”,那麼目標機器上的read()/recv()可能分三次接收,每次都接收“abc”;也可能分兩次接收,第一次接收“abcab”,第二次接收“cabc”;也可能一次就接收到字串“abcabc”.
假設我們希望客戶端每次傳送一位學生的學號,讓服務端返回該學生的姓名、住址、成績等資訊,這時候可能就會出現問題,服務端不能區分學生的學號,例如第一次傳送1,第二次傳送3,伺服器可能當成13來處理,返回的資訊顯然是錯誤的。
這就是資料的“粘包”問題,客戶端傳送的多個資料包被當做一個資料包接收,也稱資料的無邊界性,recv()函式不知道資料包的開始或結束標誌(實際上也沒有任何開始或結束標誌),只把它們當做連續的資料流來處理。
服務端
import socket
import time
s = socket.socket()
s.bind(("127.0.0.1",8888)) #暴露的地址埠協議
s.listen(5) #最大連線數
client,addr = s.accept() #接收客戶端資訊
time.sleep(10) #程式睡眠10秒
data = client.recv(1024) #接收客戶端資料資訊,單次最大接收1024位元組
print(data)
client.send(data) #將客戶端到傳送過來的資料給返回去
客戶端
import socket
s = socket.socket() #呼叫socket
s.connect(("127.0.0.1",8888)) #連線服務端資訊
data = input(">>>")
s.send(data.encode()) #將輸入資訊轉換位元組碼傳送給服務端
s.send(data.encode()) #將輸入資訊轉換位元組碼傳送給服務端
s.send(data.encode()) #將輸入資訊轉換位元組碼傳送給服務端
res = s.recv(1024) #列印服務端返回資料
print(res)
上傳檔案
服務端
import socket
import time
import struct
import json
import os
#基礎配置 IP地址 埠 協議 最大監聽數
sock = socket.socket()
sock.bind(("127.0.0.1",8899))
sock.listen(5)
while True:
print("server is waiting....")
client_sock,addr = sock.accept()
print("客戶端%s建立連線"%str(addr))
while True:
try:
msg_len = client_sock.recv(4) #接收客戶端上傳的檔案,前4位元組
except Exception:
print("客戶端斷開")
break
msg_len = struct.unpack("i",msg_len)[0] #解析出客戶端上傳檔案的大小
info = client_sock.recv(msg_len) #接收客戶端上傳基本內容,msg_len:上傳檔案的大小
info_dict = json.loads(info) #將接收的資料反序列化
print(info_dict)
cmd = info_dict.get("cmd") #拿去客戶端列表中cmd的值,判斷客戶端是上傳/下載檔案,put上傳/get下載
if cmd == "put":
"""拿出客戶端傳輸過來的基本資訊"""
file_name = info_dict["file_name"] #獲取客戶端上傳的名字
# file_name = file_name
print(file_name)
file_size = info_dict["file_size"] #獲取客戶端上傳的檔案大小
"""儲存客戶端上傳檔案"""
with open(file_name,"wb") as f: #以客戶端上傳的名字在服務端建立檔案
receive_len = 0
while receive_len < file_size:
tmp = client_sock.recv(4095)
f.write(tmp)
receive_len += len(tmp)
"""驗證上傳檔案是否完整"""
if file_size == os.path.getsize(file_name):
print("檔案上傳完成!!!")
client_sock.send("上傳完成!".encode())
else:
print("檔案上傳不完整,請重新上傳!!!")
elif cmd == "get":
"""下載"""
FileName = info_dict["FileName"]
print(FileName)
"""下載檔案"""
try:
f = open(FileName,"rb")
except Exception:
KK = "檔案不存在或許可權錯誤!"
print(KK)
ll = json.dumps(KK).encode() # 內容json化和位元組化
megs = struct.pack("i", len(ll)) # 轉為4位元組,存放ll的大小
client_sock.send(megs) # 傳送struct存放的資料 (2121,)
client_sock.send(ll) # 傳送具體資料內容 (json後的檔案不存在或許可權錯誤)
else: #如果try中的語句沒有引發異常,則執行else中的語句
"""檔案大小,傳送給客戶端"""
megs = json.dumps(os.path.getsize(FileName)) #判斷髮送給客戶端檔案大小轉為位元組碼,並且json化
print(megs)
print(type(megs))
megs_len = struct.pack("i",int(megs)) #封裝成4位元組
print(megs_len)
client_sock.send(megs_len) #傳送struce封裝的資料 (21232,)
print("傳送成功")
"""傳送檔案內容給服務端,每次傳送給客戶端4095位元組,傳送完次跳出迴圈"""
while True:
date = f.read(4095)
if date == b"": # 表示傳輸完成,已經拿不到資料了
break
client_sock.send(date)
f.close()
客戶端
import socket
import struct
import os
import json
import time
ip_port = ("127.0.0.1",8899) #服務端地址
sk = socket.socket()
sk.connect(ip_port) #連線服務端
while 1: #使可以多次上傳
msg = input("輸入執行命令>>>") #格式 put meinv.jpg
cmd,params = msg.split(" ") #以空格拆分使用者輸入命令
if cmd == "put":
"""上傳檔案"""
try:
f = open(params,mode="rb") #以位元組方式開啟使用者上傳的檔案
except Exception:
print("上傳檔案不存在")
continue
"""獲取上傳基本資訊,名稱 大小"""
file_name = f.name #獲取上傳檔名稱,就是params
file_size = os.path.getsize(params) #獲取上傳檔案大小
"""將基本資訊打包,json化 傳輸給服務端"""
data = {} #將基本資訊存放列表
data["file_name"] = file_name
data["file_size"] = file_size
data["cmd"] = "put"
msg = json.dumps(data).encode() #將基本資訊json化
msg_len = struct.pack("i",len(msg)) #統計基本資訊的大小並將結果進行封裝成4位元組檔案
# print(data)
"""傳送檔案基本資訊到服務端"""
sk.send(msg_len) #將4位元組檔案大小傳送給服務端
sk.send(msg) #將序列化後的基本資訊傳輸給服務端
"""傳送檔案內容給服務端,每次傳送給客戶端4095位元組,傳送完次跳出迴圈"""
while True:
date = f.read(4095)
sk.send(date)
if date == b"": #表示傳輸完成,已經拿不到資料了
break
f.close() #關閉開啟的檔案
print(sk.recv(1024).decode()) #列印上傳檔案是否完整
elif cmd == "get":
"""下載檔案"""
"""打包基本資訊傳送給服務端,名稱 方式,以字典的形式"""
Data = {}
Data["cmd"] = "get"
Data["FileName"] = params
# print(Data)
"""將資料json化,封裝"""
msgs = json.dumps(Data).encode() #將資料Jason化,並轉為位元組碼
msgs_len = struct.pack("i",len(msgs)) #基本資訊位元組大小
"""傳送資料給服務端"""
sk.send(msgs_len) #傳送基本資訊長度到服務端
sk.send(msgs) #基本資料傳送給服務端
"""接受服務端資料"""
# time.sleep(5)
msg_len = sk.recv(4) # 接收服務端傳的檔案,前4位元組,內容:傳輸的檔案大小
msg_len = struct.unpack("i", msg_len)[0] # 解析出服務端傳檔案的大小
print(msg_len)
# info = sk.recv(msg_len) # 接收客戶端上傳基本內容,msg_len:上傳檔案的大小
# print(info)
# info = info.decode()
# print(info)
# info_dict = json.loads(info) # 將接收的資料反序列化,下載檔案大小資料
"""檔案內容下載"""
with open(params,"wb") as f: #以客戶端上傳的名字在服務端建立檔案
receive_len = 0
while receive_len < msg_len:
tmp = sk.recv(2048)
f.write(tmp)
receive_len += len(tmp)
案例-模擬ssh命令
服務端
import socket
import subprocess
import struct
socket = socket.socket()
socket.bind(("127.0.0.1", 8899)) # 建立服務端IP地址,埠,預設tcp協議
socket.listen(5) # 最大監聽數
while 1:
client,addr = socket.accept() #接收客戶端連線資訊
print("客戶端%s建立連線"%str(addr))
while 1:
try:
cmd = client.recv(1024)
cmd_res_bytes = subprocess.getoutput(cmd.decode()) #使用subprocess模組去自己記憶體中拿去客戶端輸入的命令資訊
except :
print("客戶端%s退出"%str(addr))
client.close() #斷開連線
break #跳出本次迴圈
print(cmd_res_bytes) #列印輸出客戶端傳送過來的命令資訊
cmd_res_bytes = cmd_res_bytes.encode()
print("len:",len(cmd_res_bytes)) #檢視輸出字串長度,便於驗證傳輸是否一致
data_len = len(cmd_res_bytes) #長度賦值變數
data_len_pack = struct.pack("i",data_len) #傳送輸出內容大小到客戶端,客戶端取固定長度,解決粘包問題
#打包資料長度傳送
client.send(data_len_pack) #傳送長度到客戶端
client.send(cmd_res_bytes) #傳送資料到客戶端
客戶端
import socket
import time
import struct
sk = socket.socket()
sk.connect(("127.0.0.1",8899)) #連線服務端
while 1:
data = input("輸入執行命名>>>") #輸入遠端連線的命令
sk.send(data.encode()) #傳送資料過去,位元組碼
#接收資料長度
time.sleep(2) #觸發粘包
data_len = sk.recv(4) #傳送過來的位元組大小,解決粘包
total_len = struct.unpack("i",data_len)[0] #解壓內容為元組,傳輸字串的大小值
ret = b"" #接收到的內容
receive_len = 0 #本地從0計算
while receive_len < total_len: #當本地接收到的位元組數不小於傳輸過的位元組數時,表示檔案傳輸完成 #
temp = sk.recv(1024) #單次拿去資料長度
receive_len += len(temp) #接收位元組大小做累加
ret += temp #接收內容做累加
print(ret.decode()) #列印傳輸過來的內容
print(len(ret)) #列印接收到的大小
print("total_len",total_len)