Python之路(第三十二篇) 网络编程:udp套接字、简单文件传输

时间:2023-03-08 15:35:44
Python之路(第三十二篇) 网络编程:udp套接字、简单文件传输

一、UDP套接字

服务端

  # udp是无链接的,先启动哪一端都不会报错
# udp没有链接,与tcp相比没有链接循环,只有通讯循环
server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #创建一个服务器的套接字
server.bind() #绑定服务器套接字
inf_loop: #服务器无限循环
cs = server.recvfrom()/server.sendto() # 对话(接收与发送)
server.close() # 关闭服务器套接字

  

客户端

  
  client = socket()   # 创建客户套接字
comm_loop: # 通讯循环
client.sendto()/client.recvfrom() # 对话(发送/接收)
client.close() # 关闭客户套接字

  

简单例子

服务端

  import socket

ip_port = ('127.0.0.1',8081)
server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
server.bind(ip_port)
while True:
print('udp服务端开始运行了')
data,addr = server.recvfrom(1024)
print(data.decode('utf-8'))
msg = input("请输入").strip()
server.sendto(msg.encode("utf-8"),addr)

  

客户端

  
  import socket

ip_port = ('127.0.0.1', 8081)
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
print('udp客户端开始运行了')
msg = input("请输入").strip()
server.sendto(msg.encode("utf-8"), ip_port)
data, addr = server.recvfrom(1024)
print(data.decode("utf-8"))

  

注意:udp 可以发空 数据报协议 说是发空,其实不是空 ,还有一个IP 端口的信息,发空时 带个端口信息,

tcp:不是一一对应的,udp:是一一对应的 数据报完整的

用upd做一个ntp时间服务器

服务端

  
  import socket
import time

ip_port = ("127.0.0.1",8080)
buffer_size = 1024
ntp_server = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
ntp_server.bind(ip_port)

while True:
data,addr = ntp_server.recvfrom(buffer_size)
print("收到客户端的命令是",data.decode("utf-8"))
if not data:
fmt = "%Y-%m-%d %X"
else:
fmt = data.decode("utf-8")
time_now = time.strftime(fmt,time.localtime())
ntp_server.sendto(time_now.encode("utf-8"),addr)

  

客户端

  
  import socket

ip_port = ("127.0.0.1",8080)
buffer_size = 1024
ntp_client = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

while True:
msg = input(">>>")
ntp_client.sendto(msg.encode("utf-8"),ip_port)
recv_msg,addr = ntp_client.recvfrom(buffer_size)
print(recv_msg.decode("utf-8"))

  

基于udp简单实现QQ聊天

服务端

  from socket import *
udp_server= socket(AF_INET,SOCK_DGRAM)
udp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
udp_server.bind(('127.0.0.1',8080))
print('start running...')

while True:
qq_msg,addr = udp_server.recvfrom(1024)
print('来自[%s:%s]的一条消息:\033[44m%s\033[0m'%(addr[0],addr[1],qq_msg.decode('utf-8')))
back_msg = input('回复消息:>>').strip()
udp_server.sendto(back_msg.encode('utf-8'),addr)
udp_server.close()

  

客户端

  
  from socket import *
udp_client = socket(AF_INET,SOCK_DGRAM)
qq_name_dic = {
'pony':('127.0.0.1',8080),
'jack':('127.0.0.1',8080),
'charles':('127.0.0.1',8080),
'nick':('127.0.0.1',8080)
}
while True:
print("QQ名单列表:")
for i in qq_name_dic.keys():
print(i)
qq_name = input('请输入聊天对象:>>').strip()
if qq_name not in qq_name_dic: continue
while True:
msg = input('请输入消息,回车发送:').strip()
if msg=='quit':break
if not msg or not qq_name or qq_name not in qq_name_dic:continue
udp_client.sendto(msg.encode('utf-8'),qq_name_dic[qq_name])
back_msg,addr = udp_client.recvfrom(1024)
print('来自[%s:%s]的一条消息:\033[41m%s\033[0m'%(addr[0],addr[1],back_msg.decode('utf-8')))
udp_client.close()

  

二、tcp与udp对比

tcp基于链接通信,数据流式协议

  • 基于链接,则需要listen(backlog),指定连接池的大小

  • 基于链接,必须先运行的服务端,然后客户端发起链接请求

  • 对于mac/linux系统:如果客户端断开了链接,那服务端的链接recv将会阻塞,通讯循环收到的是一直空(解决方法是:服务端在收消息后加上if判断,空消息就break掉通信循环)

  • 对于windows系统:如果一端断开了链接,那另外一端的链接也跟着出错,(解决方法是:服务端通信循环内加异常处理,捕捉到异常后就break掉通讯循环)

  • 相对于upd传输速度慢

  • 流式协议 会粘包 不可以发空 send recv 不是 一 一对应

  • tcp适用于:

    • 数据一定要可靠

    • 远程执行命令

    • 下载文件

udp无链接,数据报式协议

  • 无链接,因而无需listen(backlog),更加没有什么链接池

  • 无链接,udp的sendto不用管是否有一个正在运行的服务端可以一直发消息,只不过数据可能会丢失

  • recvfrom收的数据小于sendto发送的数据时,在mac和linux系统上数据直接丢失,在windows系统上发送的比接收的大直接报错

  • 只有sendto发送数据没有recvfrom收数据,数据丢失

  • 数据报协议 不会粘包 可以发空 sendto recvfrom 一 一 对应 数据报协议 数据不安全 有可能发送数据 > 1024 或者网络网络异常 数据没了

  • udp适用于

    • QQ

    • 查询操作 eg: ntp时间服务器 dns服务器(查域名,转ip) 能保证查询效率高,数据虽然不可靠,传输过程中可能会发生数据丢失

三、基于socket实现文件网络传输

简单版本

服务端

  
  import socket
import os
import hashlib
import json
import struct

ip_port = ("127.0.0.1",9001)
back_log = 5
buffer_size = 1024
base_path = os.path.dirname(os.path.abspath(__file__))
share_path =os.path.join(base_path,"share")

ftp_server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
ftp_server.bind(ip_port)
ftp_server.listen(back_log)

def creat_md5(file):
md5_value = hashlib.md5()
with open(file,"rb") as f:
while True:
data = f.read(1024)
if not data:
break
md5_value.update(data)
return md5_value.hexdigest()


while True:
print("FTP服务器开始运行啦!")
conn,address = ftp_server.accept()
while True:
try:
# 第一步:收命令
res = conn.recv(8096) #"get a.txt"
if not res: continue
# 第二步:解析命令, 提取相应的命令参数
cmds = res.decode("utf-8").split()
file_name = cmds[1]
if cmds[0] == "get":

file_path = os.path.join(share_path,file_name)
file_md5 = creat_md5(file_path)
file_size = os.path.getsize(file_path)
#第三步:以读的方式打开文件,读取文件内容 发送给客户端,
# 1、先自制报头,传递文件的相关信息
header_dic = {
"filename":file_name,
"filemd5":file_md5,
"filesize":file_size
}
header_json = json.dumps(header_dic).encode("utf-8")
header_length = len(header_json)
header_struct = struct.pack("i",header_length)
# 2、发送报头的长度
conn.send(header_struct)
# 3、发送报头,传递文件的各种信息
conn.send(header_json)
# 4、打开文件,读取内容,一行一行的发送读取的内容给客户端
with open(file_path,"rb") as f:
for line in f:
conn.send(line)

except Exception as e:
print(e)
break
conn.close()

  

客户端

  
  import socket
import os
import struct
import json
import time

ip_port = ("127.0.0.1", 9001)
buffer_size = 1024
base_path = os.path.dirname(os.path.abspath(__file__))
download_path = os.path.join(base_path,"download")

ftp_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ftp_client.connect(ip_port)

while True:
# 第一步:写命令,发送命令给服务端
cmd = input("请输入命令: ")
if not cmd: continue
if cmd == "quit": break
ftp_client.send(cmd.encode("utf-8"))
# 第二步:收取自制报头的长度
header_struct = ftp_client.recv(4)
header_length = struct.unpack("i", header_struct)[0]
print("报头长度",header_length)
# 第三步:收取自制报头的信息
header_json = ftp_client.recv(header_length).decode("utf-8")
header_dic = json.loads(header_json)
print("报头字典",header_dic)
# 第四步:根据报头信息拼出文件的各种信息
file_name = header_dic["filename"]
file_md5 = header_dic["filemd5"]
file_size = header_dic["filesize"]
file_download_path = os.path.join(download_path,file_name)
# 第五步:以写的方式打开一个新文件,接收服务端发来的文件内容写入客户的新文件
with open(file_download_path,"wb") as f:
data_size = 0
start_time = time.perf_counter()
while data_size < file_size:
line = ftp_client.recv(buffer_size)
f.write(line)
data_size = data_size + len(line)
# print("已经写入数据",data_size)
download_percent = int((data_size/file_size)*100)
# print("百分比",download_percent)
a = "*" * download_percent
# print(a)
b = "." * (100 - download_percent)
# print(b)
c = (data_size/file_size)*100
during_time = time.perf_counter() - start_time
print("\r{:3.0f}%[{}-{}]共计用时:{:.3f}s".format(c,a,b,during_time),end="")
# sys.stdout.flush()
print("\n" + "执行结束")
ftp_client.close()

  

基于类写的文件传输

服务端

  
  import socket
import os
import struct
import pickle


class TCPServer:
address_family = socket.AF_INET
socket_type = socket.SOCK_STREAM
listen_count = 5
max_recv_bytes = 8192
coding = 'utf-8'
allow_reuse_address = False
# 下载的文件存放路径
down_filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'share')
# 上传的文件存放路径
upload_filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'upload')

def __init__(self,server_address,bind_and_listen=True):
self.server_address = server_address
self.socket = socket.socket(self.address_family,self.socket_type)

if bind_and_listen:
try:
self.server_bind()
self.server_listen()
except Exception:
self.server_close()

def server_bind(self):
if self.allow_reuse_address: #重用ip和端口
self.socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
self.socket.bind(self.server_address)

def server_listen(self):
self.socket.listen(self.listen_count)

def server_close(self):
self.socket.close()

def server_accept(self):
return self.socket.accept()

def conn_close(self,conn):
conn.close()

def run(self):
print('starting...')
while True:
self.conn,self.client_addr = self.server_accept()
print(self.client_addr)
while True:
try:
res = self.conn.recv(self.max_recv_bytes)
if not res:continue
cmds = res.decode(self.coding).split()
if hasattr(self,cmds[0]):
func = getattr(self,cmds[0])
func(cmds)
except Exception:
break
self.conn_close(self.conn)

def get(self,cmds):
""" 下载
1.找到下载的文件
2.发送 header_size
3.发送 header_bytes file_size
4.读文件 rb 发送 send(line)
5.若文件不存在,发送0 client提示:文件不存在
:param cmds: 下载的文件 eg:['get','a.txt']
:return:
"""
filename = cmds[1]
file_path = os.path.join(self.down_filepath, filename)
if os.path.isfile(file_path):
header = {
'filename': filename,
'md5': 'xxxxxx',
'file_size': os.path.getsize(file_path)
}
header_bytes = pickle.dumps(header) #直接用pickle转为bytes,不用json+encode转为bytes
self.conn.send(struct.pack('i', len(header_bytes)))
self.conn.send(header_bytes)
with open(file_path, 'rb') as f:
for line in f:
self.conn.send(line)
else:
self.conn.send(struct.pack('i', 0))

def put(self,cmds):
""" 上传
1.接收4个bytes 得到文件的 header_size
2.根据 header_size 得到 header_bytes header_dic
3.根据 header_dic 得到 file_size
3.以写的形式 打开文件 f.write()
:param cmds: 下载的文件 eg:['put','a.txt']
:return:
"""
obj = self.conn.recv(4)
header_size = struct.unpack('i', obj)[0]
header_bytes = self.conn.recv(header_size)
header_dic = pickle.loads(header_bytes)
print(header_dic)
file_size = header_dic['file_size']
filename = header_dic['filename']

with open('%s/%s' % (self.upload_filepath, filename), 'wb') as f:
recv_size = 0
while recv_size < file_size:
res = self.conn.recv(self.max_recv_bytes)
f.write(res)
recv_size += len(res)


tcp_server = TCPServer(('127.0.0.1',8080))
tcp_server.run()
tcp_server.server_close()

  

客户端

import socket
import struct
import pickle
import os class FTPClient:
address_family = socket.AF_INET
socket_type = socket.SOCK_STREAM
# 下载的文件存放路径
down_filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'download')
# 上传的文件存放路径
upload_filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'share')
coding = 'utf-8'
max_recv_bytes = 8192 def __init__(self, server_address, connect=True):
self.server_address = server_address
self.socket = socket.socket(self.address_family, self.socket_type)
if connect:
try:
self.client_connect()
except Exception:
self.client_close() def client_connect(self):
self.socket.connect(self.server_address) def client_close(self):
self.socket.close() def run(self):
while True:
# get a.txt 下载 put a.txt 上传
msg = input(">>>:").strip()
if not msg: continue
self.socket.send(msg.encode(self.coding))
cmds = msg.split()
if hasattr(self,cmds[0]):
func = getattr(self,cmds[0])
func(cmds) def get(self, cmds):
""" 下载
1.得到 header_size
2.得到 header_types header_dic
3.得到 file_size file_name
4.以写的形式 打开文件
:param cmds: 下载的内容 eg: cmds = ['get','a.txt']
:return:
"""
obj = self.socket.recv(4)
header_size = struct.unpack('i', obj)[0]
if header_size == 0:
print('文件不存在')
else:
header_types = self.socket.recv(header_size)
header_dic = pickle.loads(header_types)
print(header_dic)
file_size = header_dic['file_size']
filename = header_dic['filename'] with open('%s/%s' % (self.down_filepath, filename), 'wb') as f:
recv_size = 0
while recv_size < file_size:
res = self.socket.recv(self.max_recv_bytes)
f.write(res)
recv_size += len(res)
print('总大小:%s 已下载:%s' % (file_size, recv_size))
else:
print('下载成功!') def put(self, cmds):
""" 上传
1.查看上传的文件是否存在
2.上传文件 header_size
3.上传文件 header_bytes
4.以读的形式 打开文件 send(line)
:param cmds: 上传的内容 eg: cmds = ['put','a.txt']
:return:
"""
filename = cmds[1]
file_path = os.path.join(self.upload_filepath, filename)
if os.path.isfile(file_path):
file_size = os.path.getsize(file_path)
header = {
'filename': os.path.basename(filename),
'md5': 'xxxxxx',
'file_size': file_size
}
header_bytes = pickle.dumps(header)
self.socket.send(struct.pack('i', len(header_bytes)))
self.socket.send(header_bytes) with open(file_path, 'rb') as f:
send_bytes = b''
for line in f:
self.socket.send(line)
send_bytes += line
print('总大小:%s 已上传:%s' % (file_size, len(send_bytes)))
else:
print('上传成功!')
else:
print('文件不存在') ftp_client = FTPClient(('127.0.0.1',8080))
ftp_client.run()
ftp_client.client_close()