首頁 > 軟體

Python網路程式設計之socket與socketserver

2022-05-30 22:01:32

一、基於TCP協定的socket通訊端程式設計

1、通訊端工作流程

先從伺服器端說起。伺服器端先初始化Socket,然後與埠繫結(bind),對埠進行監聽(listen),呼叫accept阻塞,等待使用者端連線。在這時如果有個使用者端初始化一個Socket,然後連線伺服器(connect),如果連線成功,這時使用者端與伺服器端的連線就建立了。使用者端傳送資料請求,伺服器端接收請求並處理請求,然後把迴應資料傳送給使用者端,使用者端讀取資料,最後關閉連線,一次互動結束,使用以下Python程式碼實現:

import socket
# socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,預設值為 0
socket.socket(socket_family, socket_type, protocal=0)
# 獲取tcp/ip通訊端
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 獲取udp/ip通訊端
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

1、 伺服器端通訊端函數

  • s.bind():繫結(主機,埠號)到通訊端
  • s.listen():開始TCP監聽
  • s.accept():被動接受TCP客戶的連線,(阻塞式)等待連線的到來

2、 使用者端通訊端函數

  • s.connect():主動初始化TCP伺服器連線
  • s.connect_ex():connect()函數的擴充套件版本,出錯時返回出錯碼,而不是丟擲異常

3、 公共用途的通訊端函數

  • s.recv():接收TCP資料
  • s.send():傳送TCP資料(send在待傳送資料量大於己端快取區剩餘空間時,資料丟失,不會發完)
  • s.sendall():傳送完整的TCP資料(本質就是迴圈呼叫send,sendall在待傳送資料量大於己端快取區剩餘空間時,資料不丟失,迴圈呼叫send直到發完)
  • s.recvfrom():接收UDP資料
  • s.sendto():傳送UDP資料
  • s.getpeername():連線到當前通訊端的遠端的地址
  • s.getsockname():當前通訊端的地址
  • s.getsockopt():返回指定通訊端的引數
  • s.setsockopt():設定指定通訊端的引數
  • s.close():關閉通訊端

4、 面向鎖的通訊端方法

  • s.setblocking():設定通訊端的阻塞與非阻塞模式
  • s.settimeout():設定阻塞通訊端操作的超時時間
  • s.gettimeout():得到阻塞通訊端操作的超時時間

5、 面向檔案的通訊端的函數

  • s.fileno():通訊端的檔案描述符
  • s.makefile():建立一個與該通訊端相關的檔案

2、基於TCP協定的通訊端程式設計

可以通過netstat -an | findstr 8080檢視通訊端狀態

1、 伺服器端

import socket
# 1、買手機
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # tcp稱為流式協定,udp稱為資料包協定SOCK_DGRAM
# print(phone)
# 2、插入/繫結手機卡
# phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(('127.0.0.1', 8080))
# 3、開機
phone.listen(5) # 半連線池,限制的是請求數
# 4、等待電話連線
print('start....')
while True: # 連線迴圈
conn, client_addr = phone.accept() # (三次握手建立的雙向連線,(使用者端的ip,埠))
# print(conn)
print('已經有一個連線建立成功', client_addr)
# 5、通訊:收發訊息
while True: # 通訊迴圈
try:
print('伺服器端正在收資料...')
data = conn.recv(1024) # 最大接收的位元組數,沒有資料會在原地一直等待收,即傳送者傳送的資料量必須>0bytes
# print('===>')
if len(data) == 0: break # 在使用者端單方面斷開連線,伺服器端才會出現收空資料的情況
print('來自使用者端的資料', data)
conn.send(data.upper())
except ConnectionResetError:
break
# 6、掛掉電話連線
 conn.close()
# 7、關機
phone.close()
# start....
# 已經有一個連線建立成功 ('127.0.0.1', 4065)
# 伺服器端正在收資料...
# 來自使用者端的資料 b'xad'
# 伺服器端正在收資料...

2、 使用者端

import socket
# 1、買手機
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# print(phone)
# 2、撥電話
phone.connect(('127.0.0.1', 8080)) # 指定伺服器端ip和埠
# 3、通訊:發收訊息
while True: # 通訊迴圈
msg = input('>>: ').strip() # msg=''
if len(msg) == 0: continue
phone.send(msg.encode('utf-8'))
# print('has send----->')
data = phone.recv(1024)
# print('has recv----->')
print(data)
# 4、關閉
phone.close()
# >>: 啊
# b'a'
# >>: 啊啊
# b'xb0xa1xb0xa1'
# >>:

3、地址佔用問題

這個是由於你的伺服器端仍然存在四次揮手的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 = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.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 時間

4、模擬ssh遠端執行命令

伺服器端通過subprocess執行該命令,然後返回命令的結果。

伺服器端:

from socket import *
import subprocess
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1', 8000))
server.listen(5)
print('start...')
while True:
conn, client_addr = server.accept()
while True:
print('from client:', client_addr)
cmd = conn.recv(1024)
if len(cmd) == 0: break
print('cmd:', cmd)
obj = subprocess.Popen(cmd.decode('utf8'), # 輸入的cmd命令
shell=True, # 通過shell執行
stderr=subprocess.PIPE, # 把錯誤輸出放入管道,以便列印
stdout=subprocess.PIPE) # 把正確輸出放入管道,以便列印

stdout = obj.stdout.read() # 列印正確輸出
stderr = obj.stderr.read() # 列印錯誤輸出

conn.send(stdout)
conn.send(stderr)
conn.close()
server.close()

使用者端

import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8000))
while True:
data = input('please enter your data')
client.send(data.encode('utf8'))
data = client.recv(1024)
print('from server:', data)
client.close()

輸入dir命令,由於伺服器端傳送位元組少於1024位元組,使用者端可以接受。

輸入tasklist命令,由於伺服器端傳送位元組多於1024位元組,使用者端只接受部分資料,並且當你再次輸入dir命令的時候,使用者端會接收dir命令的結果,但是會列印上一次的剩餘未傳送完的資料,這就是粘包問題。

5、粘包

1、傳送端需要等緩衝區滿才傳送出去,造成粘包

傳送資料時間間隔很短,資料量很小,會合到一起,產生粘包。

伺服器端

# _*_coding:utf-8_*_
from socket import *
ip_port = ('127.0.0.1', 8080)
TCP_socket_server = socket(AF_INET, SOCK_STREAM)
TCP_socket_server.bind(ip_port)
TCP_socket_server.listen(5)
conn, addr = TCP_socket_server.accept()

data1 = conn.recv(10)
data2 = conn.recv(10)
print('----->', data1.decode('utf-8'))
print('----->', data2.decode('utf-8'))
conn.close()

使用者端

# _*_coding:utf-8_*_
import socket
BUFSIZE = 1024
ip_port = ('127.0.0.1', 8080)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
res = s.connect_ex(ip_port)
s.send('hello'.encode('utf-8'))
s.send('world'.encode('utf-8'))

# 伺服器端一起收到b'helloworld'

2、接收方不及時接收緩衝區的包,造成多個包接收

使用者端傳送了一段資料,伺服器端只收了一小部分,伺服器端下次再收的時候還是從緩衝區拿上次遺留的資料,產生粘包。

伺服器端

# _*_coding:utf-8_*_
from socket import *
ip_port = ('127.0.0.1', 8080)
TCP_socket_server = socket(AF_INET, SOCK_STREAM)
TCP_socket_server.bind(ip_port)
TCP_socket_server.listen(5)
conn, addr = TCP_socket_server.accept()
data1 = conn.recv(2) # 一次沒有收完整
data2 = conn.recv(10) # 下次收的時候,會先取舊的資料,然後取新的
print('----->', data1.decode('utf-8'))
print('----->', data2.decode('utf-8'))
conn.close()

使用者端

# _*_coding:utf-8_*_
import socket
BUFSIZE = 1024
ip_port = ('127.0.0.1', 8080)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
res = s.connect_ex(ip_port)
s.send('hello feng'.encode('utf-8'))

6、解決粘包問題

1、先傳送的位元組流總大小(low版)

問題的根源在於,接收端不知道傳送端將要傳送的位元組流的長度,所以解決粘包的方法就是圍繞,如何讓傳送端在傳送資料前,把自己將要傳送的位元組流總大小讓接收端知曉,然後接收端來一個死迴圈接收完所有資料。

為何low:程式的執行速度遠快於網路傳輸速度,所以在傳送一段位元組前,先用send去傳送該位元組流長度,這種方式會放大網路延遲帶來的效能損耗。

伺服器端:

import socket, subprocess
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8000))
server.listen(5)
while True:
conn, addr = server.accept()
print('start...')
while True:
cmd = conn.recv(1024)
print('cmd:', cmd)
obj = subprocess.Popen(cmd.decode('utf8'),
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
stdout = obj.stdout.read()
if stdout:
ret = stdout
else:
stderr = obj.stderr.read()
ret = stderr
ret_len = len(ret)
 conn.send(str(ret_len).encode('utf8'))
data = conn.recv(1024).decode('utf8')
if data == 'recv_ready':
conn.sendall(ret)
conn.close()
server.close()

使用者端:

import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8000))
while True:
msg = input('please enter your cmd you want>>>').strip()
if len(msg) == 0: continue
client.send(msg.encode('utf8'))
length = int(client.recv(1024))
client.send('recv_ready'.encode('utf8'))
send_size = 0
recv_size = 0
data = b''
while recv_size < length:
data = client.recv(1024)
recv_size += len(data)
print(data.decode('utf8'))

2、自定義固定長度報頭(struct模組)

struct模組解析

import struct
import json
# 'i'是格式
try:
obj = struct.pack('i', 1222222222223)
except Exception as e:
print(e)
obj = struct.pack('i', 1222)
print(obj, len(obj))
# 'i' format requires -2147483648 <= number <= 2147483647
# b'xc6x04x00x00' 4

res = struct.unpack('i', obj)
print(res[0])
# 1222

解決粘包問題的核心就是:為位元組流加上自定義固定長度報頭,報頭中包含位元組流長度,然後一次send到對端,對端在接收時,先從快取中取出定長的報頭,然後再取真實資料。

1、 使用struct模組建立報頭:

import json
import struct
header_dic = {
'filename': 'a.txt',
'total_size':111111111111111111111111111111111222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222223131232,
'hash': 'asdf123123x123213x'
}
header_json = json.dumps(header_dic)
header_bytes = header_json.encode('utf-8')
print(len(header_bytes))# 223
# 'i'是格式
obj = struct.pack('i', len(header_bytes))
print(obj, len(obj))
# b'xdfx00x00x00' 4

res = struct.unpack('i', obj)
print(res[0])
# 223

2、伺服器端:

from socket import *
import subprocess
import struct
import json
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1', 8000))
server.listen(5)
print('start...')
while True:
conn, client_addr = server.accept()
print(conn, client_addr)
while True:
cmd = conn.recv(1024)
obj = subprocess.Popen(cmd.decode('utf8'),
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
stderr = obj.stderr.read()
stdout = obj.stdout.read()
# 製作報頭
header_dict = {
'filename': 'a.txt',
'total_size': len(stdout) + len(stderr),
'hash': 'xasf123213123'
}
header_json = json.dumps(header_dict)
header_bytes = header_json.encode('utf8')
# 1. 先把報頭的長度len(header_bytes)打包成4個bytes,然後傳送
conn.send(struct.pack('i', len(header_bytes)))
# 2. 傳送報頭
 conn.send(header_bytes)
# 3. 傳送真實的資料
 conn.send(stdout)
conn.send(stderr)
conn.close()
server.close()

3、 使用者端:

from socket import *
import json
import struct
client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8000))
while True:
cmd = input('please enter your cmd you want>>>')
if len(cmd) == 0: continue
client.send(cmd.encode('utf8'))
# 1. 先收4個位元組,這4個位元組中包含報頭的長度
header_len = struct.unpack('i', client.recv(4))[0]
# 2. 再接收報頭
header_bytes = client.recv(header_len)
# 3. 從包頭中解析出想要的東西
header_json = header_bytes.decode('utf8')
header_dict = json.loads(header_json)
total_size = header_dict['total_size']
# 4. 再收真實的資料
recv_size = 0
res = b''
while recv_size < total_size:
data = client.recv(1024)
res += data
recv_size += len(data)
print(res.decode('utf8'))
client.close()

二、基於UDP協定的socket通訊端程式設計

  • UDP是無連結的,先啟動哪一端都不會報錯,並且可以同時多個使用者端去跟伺服器端通訊

  • UDP協定是資料包協定,發空的時候也會自帶報頭,因此使用者端輸入空,伺服器端也能收到。

  • UPD協定一般不用於傳輸巨量資料。

  • UPD通訊端無粘包問題,但是不能替代TCP通訊端,因為UPD協定有一個缺陷:如果資料傳送的途中,資料丟失,則資料就丟失了,而TCP協定則不會有這種缺陷,因此一般UPD通訊端使用者無關緊要的資料傳送,例如qq聊天。

UDP通訊端簡單範例

1、伺服器端

import socket
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 資料包協定-》UDP
server.bind(('127.0.0.1', 8080))
while True:
data, client_addr = server.recvfrom(1024)
print('===>', data, client_addr)
server.sendto(data.upper(), client_addr)
server.close()

2、使用者端

import socket
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 資料包協定-》UDP
while True:
msg = input('>>: ').strip() # msg=''
client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))
data, server_addr = client.recvfrom(1024)
print(data)
client.close()

三、基於socketserver實現並行的socket程式設計

1、基於TCP協定

基於tcp的通訊端,關鍵就是兩個迴圈,一個連結迴圈,一個通訊迴圈

socketserver模組中分兩大類:server類(解決連結問題)和request類(解決通訊問題)。

1、 server類

2、 request類

基於tcp的socketserver我們自己定義的類中的。

  • self.server即通訊端物件

  • self.request即一個連結
  • self.client_address即使用者端地址

3、 伺服器端

import socketserver
class MyHandler(socketserver.BaseRequestHandler):
def handle(self):
# 通訊迴圈
while True:
# print(self.client_address)
# print(self.request) #self.request=conn
try:
data = self.request.recv(1024)
if len(data) == 0: break
self.request.send(data.upper())
except ConnectionResetError:
break
if __name__ == '__main__':
s = socketserver.ThreadingTCPServer(('127.0.0.1', 8080), MyHandler, bind_and_activate=True)
s.serve_forever() # 代表連線迴圈
# 迴圈建立連線,每建立一個連線就會啟動一個執行緒(服務員)+呼叫Myhanlder類產生一個物件,呼叫該物件下的handle方法,專門與剛剛建立好的連線做通訊迴圈

4、 使用者端

import socket
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('127.0.0.1', 8080)) # 指定伺服器端ip和埠
while True:
# msg=input('>>: ').strip() #msg=''
msg = 'client33333' # msg=''
if len(msg) == 0: continue
phone.send(msg.encode('utf-8'))
data = phone.recv(1024)
print(data)
phone.close()

2、基於UDP協定

基於udp的socketserver我們自己定義的類中的

  • self.request是一個元組(第一個元素是使用者端發來的資料,第二部分是伺服器端的udp通訊端物件),如(b'adsf', )
  • self.client_address即使用者端地址

1、 伺服器端

import socketserver
class MyHandler(socketserver.BaseRequestHandler):
def handle(self):
# 通訊迴圈
print(self.client_address)
print(self.request)
data = self.request[0]
print('客戶訊息', data)
self.request[1].sendto(data.upper(), self.client_address)
if __name__ == '__main__':
s = socketserver.ThreadingUDPServer(('127.0.0.1', 8080), MyHandler)
s.serve_forever()

2、 使用者端

import socket
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 資料包協定-》udp
while True:
# msg=input('>>: ').strip() #msg=''
msg = 'client1111'
client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))
data, server_addr = client.recvfrom(1024)
print(data)
client.close()

四、Python Internet 模組

以下列出了 Python 網路程式設計的一些重要模組:

協定功能用處埠號Python 模組
HTTP網頁存取80httplib, urllib, xmlrpclib
NNTP閱讀和張貼新聞文章,俗稱為"貼文"119nntplib
FTP檔案傳輸20ftplib, urllib
SMTP傳送郵件25smtplib
POP3接收郵件110poplib
IMAP4獲取郵件143imaplib
Telnet命令列23telnetlib
Gopher資訊查詢70gopherlib, urllib

到此這篇關於Python網路程式設計之socket與socketserver的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com