Python網絡編程之socket與socketserver

一、基於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'\xb0\xa1\xb0\xa1'
# >>:

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'\xc6\x04\x00\x00' 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'\xdf\x00\x00\x00' 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 網頁訪問 80 httplib, urllib, xmlrpclib
NNTP 閱讀和張貼新聞文章,俗稱為"帖子" 119 nntplib
FTP 文件傳輸 20 ftplib, urllib
SMTP 發送郵件 25 smtplib
POP3 接收郵件 110 poplib
IMAP4 獲取郵件 143 imaplib
Telnet 命令行 23 telnetlib
Gopher 信息查找 70 gopherlib, urllib

到此這篇關於Python網絡編程之socket與socketserver的文章就介紹到這瞭。希望對大傢的學習有所幫助,也希望大傢多多支持WalkonNet。

推薦閱讀: