网络编程 socket套接字 半连接池 通信循环 粘包问题 struct模块 Mac报错
前置知识:不同计算机程序之间的数据传输
应用程序中的数据都是从程序所在计算机内存中读取的。
内存中的数据是从硬盘读取或者网络传输过来的
不同计算机程序数据传输需要经过七层协议物理连接介质才能到达目标程序
sockt 套接字
json.dump/dumps 只是把数据类型序列化成字符串
要想用来文件传输,还需要encode 给它编码成二进制数据才能传输
不用pickle是因为要和其他语言交互(你给页面就是js来处理,能不能支持是问题),而pickle只能是在python中用
套接字主要有两个版本,一个是基于文件类型的套接字家族(AF_UNIX),一个是基于网络类型的套接字家族(AF_INET)。这里介绍的是后者。
使用了socket以后,你就只需要专注于应用层的设计编写。涉及到与其他层交互,只需要调用socket套接字就行。
程序员不需要七层一层一层地去操作硬件写网络传输程序,直接使用python解释器提供的socket 模块即可
服务端
import socket
server = socket.socket() # 买手机
# 有一个参数 type=SOCK_STREAM,即不传参数,默认就是TCP协议
# socket.socket() # socket模块中有个socket类,加() 实例化成一个对象(ctrl + 单击 可以看到)
# 不要形成固有思想, 模块.名字() 就以为是模块里的方法,点进去,可能还是类(看他这个类的名字还是全小写的...)
server.bind(('127.0.0.1', 8080)) # 127.0.0.1 本机回环地址只能本机访问,其他计算机访问不了(识别到了就不用走七层协议这些了)
# address: Union[tuple, str, bytes]) ---> address 参数是一个元组,绑定ip 和 端口
server.listen(5) # 开机 # 半连接池
print("waitting....")
# waitting....
conn, addr = server.accept() # 接听电话 等着别人给你打电话 阻塞
# 阻塞,等待客户端连接,没有收到信息会停在这里
print("hi") # 在连通之前并没有反应
# hi
# --------------------------------------
# send 与 recv 要对应
# 不要两边都 recv,不然就都等着接收了
# --------------------------------------
data = conn.recv(1024) # 听别人说话 接收1024个字节数据 阻塞
# 阻塞,等待客户端发送数据,接收1024个字节的数据
print(data)
# b'halo baby'
conn.send(b'ok') # 给别人回话
# 发送数据(必须是二进制数据)
conn.close() # 挂电话
# 关闭连接
server.close() # 关机
# 关闭服务
客户端
import socket
client = socket.socket() # 拿电话
client.connect(('127.0.0.1',8080)) # 拨号 写的是对方的ip和port
# 去连接服务器上的程序(服务器的IP + port)
client.send(b'hello stranger') # 对别人说话
data = client.recv(1024) # 听别人说话
print(data)
client.close() # 挂电话
先运行 服务端 ,然后运行 客户端
服务端:b'hello stranger'
客户端:b'hello back'
127.0.0.1
是本机回还地址,只能自己识别自己,其他人无法访问
TCP协议相当于打电话
send与recv对应
不要出现两边都是相同的情况
recv是跟`内存`要数据,send发数据也是发到`内存`
至于数据的来源,你无需考虑
点进去socket这个类发现有实现 __enter_ 、 __exit__
方法,__exit__
方法中有关闭连接的方法,故可以用with
上下文来操作(暂不举例了,面向对象这两个函数的知识点提一嘴)
注意:
如果遇到以下问题(尤其是Mac用户)
需要在以下位置加上这两句
from socket import SOL_SOCKET,SO_REUSEADDR
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
服务端需要具备的条件
-
固定的ip和port
让客户端可以连接你(试想如果百度一天一个域名/ip?咋上百度))
-
要能24小时不间断提供服务
服务器不在线的话,客户端连啥?(双重循环 server.accpet() 来连接建立连接)
-
暂时不知道
半连接池,允许等待的最大个数
server.listen(5)
指定5个等待席位
通信循环
双方都处于等待接收的状态
直接回车没有发出数据,自身代码往下走进入了等待接收状态, 而另一端也没有收到消息,依然处于等待接收状态图,双方就都处于等待接收的状态了
服务端
import socket
server = socket.socket() # 生成一个对象
server.bind(('127.0.0.1',8080)) # 绑定ip和port
server.listen(5) # 半连接池
while True:
conn, addr = server.accept() # 等到别人来 conn就类似于是双向通道
print(addr) # ('127.0.0.1', 51323) 客户端的地址
while True:
try:
data = conn.recv(1024)
print(data) # b'' 针对mac与linux 客户端异常退出之后 服务端不会报错 只会一直收b''
if len(data) == 0:break
conn.send(data.upper())
except ConnectionResetError as e:
print(e)
break
conn.close()
客户端
import socket
client = socket.socket()
client.connect(('127.0.0.1',8080))
while True:
msg = input('>>>:').encode('utf-8')
if len(msg) == 0:continue
client.send(msg)
data = client.recv(1024)
print(data)
Linux,Mac断开链接的时候不会报错,会一直返回空(b'')
解决方案
服务端
import socket
server = socket.socket()
server.bind(('127.0.0.1', 8080)) # 本地回环地址
server.listen(5)
conn, addr = server.accept() # 阻塞
for i in range(1, 5):
try:
data = conn.recv(1024) # 阻塞
print(data.decode('utf-8'))
msg = f"收到 {i} ga ga ga~"
# 发的时候要判断非空,空的自己send出去处于接收状态,对方依旧是接收状态,那就都等待了
conn.send(msg.encode('utf-8')) # ***** send 直接传回车会导致两遍都处于接收状态
except ConnectionResetError: # ***** 当服务端被强制关闭时汇报异常,这里捕获并做处理
# mac或者linux 会一直输空,不会自动结束
break
conn.close()
server.close()
客户端
import socket
client = socket.socket()
client.connect(('127.0.0.1', 8080))
hi = input(">>>:").strip()
for i in range(1, 5):
msg = f'-{i} hi 咯~'
client.send(msg.encode('utf-8'))
data = client.recv(1024)
if len(data) == 0: # ** mac或者linux 需要加,避免客户端突然断开,他不会报错,会一直打印空
break
print(f"收到 {i} {data.decode('utf-8')}")
client.close()
实现服务端可以接收多个客户端通讯(一个结束还可以接收下一个) --- 利用好server.listen(5) 半连接池
以及conn, addr = server.accept()
把接收的代码用循环包起来
粘包问题
多次发送被并为一次
根据最上面的前置知识可以知道,数据是从内存中读取过来的
发现要发送的三条消息粘在了一起
产生问题的原因
粘包现象只发生在tcp协议中
1.从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点
2.实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
粘包是接收长度没对上导致的
控制recv接收的字节数与之对应(你发多少字节我收多少字节)
在很多情况下并不知道数据的长度,服务端不能写死
思路一如果在不知道数据有多长的情况下就会出现意外,那么我们可以先传一个固定长度的数据过去告诉他真实数据有多长,就可以对应着收了
struct模块
该模块可以把一个类型,如数字,转成固定长度的bytes
这里利用struct模块模块的struct.pack() struct.unpack()
方法来实现打包(将真实数据长度变为固定长度的数字)解包(将该数字解压出打包前真实数据的长度)
pack unpack
模式参数对照表(standard size 转换后的长度)
i 模式的范围:-2147483648 <= number <= 2147483647
在传真实数据之前还想要传一些描述性信息
如果在传输数据之前还想要传一些描述性信息,那么就得在中间再加一步了(传个电影,我告诉你电影名,大小,大致情节,演员等信息,你再选择要不要),前面的方法就不适用了
粘包问题解决思路
服务器端
- 先制作一个发送给客户端的字典
- 制作字典的报头
- 发送字典的报头
- 发送字典
- 再发真实数据
客户端
- 先接收字典的报头
- 解析拿到字典的数据长度
- 接收字典
- 从字典中获取真实数据的长度
- 循环获取真实数据
ps:为什么要多加一个字典
- pack打包的数据长度(的长度)有限,字典再打包会很小(长度值也会变很小)(120左右)
- 可以携带更多的描述信息
粘包问题解决最终版模块
服务器端
import socket
import subprocess
import struct
import json
server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen(5)
while True:
conn, addr = server.accept()
while True:
try:
cmd = conn.recv(1024)
if len(cmd) == 0:break
cmd = cmd.decode('utf-8')
obj = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
res = obj.stdout.read() + obj.stderr.read()
d = {'name':'jason','file_size':len(res),'info':'asdhjkshasdad'}
json_d = json.dumps(d)
# 1.先制作一个字典的报头
header = struct.pack('i',len(json_d))
# 2.发送字典报头
conn.send(header)
# 3.发送字典
conn.send(json_d.encode('utf-8'))
# 4.再发真实数据
conn.send(res)
# conn.send(obj.stdout.read())
# conn.send(obj.stderr.read())
except ConnectionResetError:
break
conn.close()
客户端
import socket
import struct
import json
client = socket.socket()
client.connect(('127.0.0.1',8080))
while True:
msg = input('>>>:').encode('utf-8')
if len(msg) == 0:continue
client.send(msg)
# 1.先接受字典报头
header_dict = client.recv(4)
# 2.解析报头 获取字典的长度
dict_size = struct.unpack('i',header_dict)[0] # 解包的时候一定要加上索引0
# 3.接收字典数据
dict_bytes = client.recv(dict_size)
dict_json = json.loads(dict_bytes.decode('utf-8'))
# 4.从字典中获取信息
print(dict_json)
recv_size = 0
real_data = b''
while recv_size < dict_json.get('file_size'): # real_size = 102400
data = client.recv(1024)
real_data += data
recv_size += len(data)
print(real_data.decode('gbk'))
案例-客户端向服务端传输文件
需求
# 写一个上传电影功能
1.循环打印某一个文件夹下面的所有文件
2.用户选取想要上传的文件
3.将用户选择的文件上传到服务端
4.服务端保存该文件
服务端(没有处理断开连接的报错以及空输入的报错,linux、mac的兼容)
import os
import sys
import socket
import struct
import json
server = socket.socket()
server.bind(('192.168.13.34', 8080))
server.listen(5)
conn, addr = server.accept()
'''
服务器端将文件都放在同一个目录
'''
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(BASE_DIR)
dir_path = os.path.join(BASE_DIR, 'datas', 're_movies')
if not os.path.exists(dir_path):
os.makedirs(dir_path)
import time
from functools import wraps
# 统计运行时间装饰器
def count_time(func):
@wraps(func)
def inner(*args, **kwargs):
start_time = time.time()
res = func(*args, **kwargs)
end_time = time.time()
print(f"耗时{end_time - start_time}s")
return res
return inner
@count_time
def save_file(file_path, file_size):
with open(file_path, 'ab') as f:
# 一行一行地收文件,同时写入文件
recv_size = 0
while recv_size < file_size:
data = conn.recv(1024)
# 存文件
# json.dump(data.decode('utf-8'), f) # -------------可能报错,不传文件对象
f.write(data)
f.flush()
recv_size += len(data)
msg = f'已收到{file_name},{file_size/1024/1024}MB,over~'
print('\033[33m', msg, '\033[0m')
conn.send(msg.encode('utf-8'))
while True:
print("等待接收客户端的信息......")
# 1.接收报头大小
dict_header_recv = conn.recv(4)
# 2.接收字典
dict_header_size = struct.unpack('i', dict_header_recv)[0]
recv_dict_str = conn.recv(dict_header_size).decode('utf-8')
recv_dict = json.loads(recv_dict_str)
print(recv_dict)
# 3.获取字典中的数据长度以及文件名
file_name = recv_dict.get('file_name')
file_size = recv_dict.get('file_size')
# 4.循环获取真实数据,并存起来
file_path = os.path.join(dir_path, file_name)
# with open(file_path, 'ab') as f:
# # 一行一行地收文件,同时写入文件
# recv_size = 0
# while recv_size < file_size:
# data = conn.recv(1024)
# # 存文件
# # json.dump(data.decode('utf-8'), f) # -------------可能报错,不传文件对象
# f.write(data)
# f.flush()
# recv_size += len(data)
#
# msg = f'已收到{file_name},{file_size/1024/1024}MB,over~'
# print('\033[33m', msg, '\033[0m')
# conn.send(msg.encode('utf-8'))
save_file(file_path, file_size)
# conn.close()
# server.close()
客户端
import json
import os
import struct
import socket
# 连接服务端
client = socket.socket()
client.connect(('192.168.13.34', 8080))
while True:
'''后续想做成可以更换目录的,所以放到这里面了'''
# 操作目标文件夹
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
dir_path = os.path.join(BASE_DIR, 'movies')
# dir_path = r'一个绝对路径'
file_name_list = os.listdir(dir_path)
# 让用户选择
print("您的文件夹下现有如下文件:")
for index, file_name in enumerate(file_name_list, 1):
# 可以在前面给文件名做一个分割,把后缀名去掉
print(f"\t{index}. {file_name}")
choice = input("请选择您想要上传电影的编号>>>:").strip()
if choice in ['q', 'exit']:
print("感谢您的使用~")
break
elif choice.isdigit() and int(choice) - 1 in range(len(file_name_list)):
# 正确选好文件
file_name = file_name_list[int(choice) - 1]
file_path = os.path.join(dir_path, file_name)
else:
print("请输入正确的编号!")
continue
# 准备开始上传文件
file_size = os.path.getsize(file_path)
# 1.制作报头字典
file_dict = {
'file_name': file_name,
'file_size': file_size
}
# 2.打包报头字典
file_dict_str = json.dumps(file_dict)
file_dict_header_size = struct.pack('i', len(file_dict_str))
# 3.发送报头大小
client.send(file_dict_header_size)
# 4.发送报头字典
# file_dict_str = json.dumps(file_dict)
client.send(file_dict_str.encode('utf-8'))
# 5.一行一行地把文件发过去
with open(file_path, 'rb') as f:
# 一行一行地传过去,避免大文件(一行还是不顶用,压缩过的数据基本都在一行)
# 转为每次发 1024 Bytes 数据
_file_size = file_size
while _file_size > 0:
if file_size > 1024:
data = f.read(1024)
_file_size -= 1024
else:
data = f.read(_file_size)
_file_size -= _file_size
client.send(data)
print(f"发送了 {len(data)} Bytes 数据~~~")
print('\033[33m', f"文件{file_name},{file_size/1024/1024}MB已发送完毕~", '\033[0m')
msg = client.recv(1024)
if msg:
print(f"服务器端回复:", msg.decode('utf-8'))
client.close()
另一份案例参考
服务端
import socket
import json
import struct
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
conn, addr = server.accept()
while True:
try:
header_len = conn.recv(4)
# 解析字典报头
header_len = struct.unpack('i', header_len)[0]
# 再接收字典数据
header_dic = conn.recv(header_len)
real_dic = json.loads(header_dic.decode('utf-8'))
# 获取数据长度
total_size = real_dic.get('file_size')
# 循环接收并写入文件
recv_size = 0
with open(real_dic.get('file_name'), 'wb') as f:
while recv_size < total_size:
data = conn.recv(1024)
f.write(data)
recv_size += len(data)
print('上传成功')
except ConnectionResetError as e:
print(e)
break
conn.close()
# server.close()
客户端
import socket
import json
import os
import struct
client = socket.socket()
client.connect(('127.0.0.1', 8080))
while True:
# 获取电影列表 循环展示
MOVIE_DIR = r'D:\python视频\day25\视频'
movie_list = os.listdir(MOVIE_DIR)
# print(movie_list)
for i, movie in enumerate(movie_list, 1):
print(i, movie)
# 用户选择
choice = input('please choice movie to upload>>>:')
# 判断是否是数字
if choice.isdigit():
# 将字符串数字转为int
choice = int(choice) - 1
# 判断用户选择在不在列表范围内
if choice in range(0, len(movie_list)):
# 获取到用户想上传的文件路径
path = movie_list[choice]
# 拼接文件的绝对路径
file_path = os.path.join(MOVIE_DIR, path)
# 获取文件大小
file_size = os.path.getsize(file_path)
# 定义一个字典
res_d = {
'file_name': '性感荷官在线发牌.mp4',
'file_size': file_size,
'msg': '注意身体,多喝营养快线'
}
# 序列化字典
json_d = json.dumps(res_d)
json_bytes = json_d.encode('utf-8')
# 1.先制作字典格式的报头
header = struct.pack('i', len(json_bytes))
# 2.发送字典的报头
client.send(header)
# 3.再发字典
client.send(json_bytes)
# 4.再发文件数据(打开文件循环发送)
with open(file_path, 'rb') as f:
for line in f:
client.send(line)
else:
print('not in range')
else:
print('must be a number')
# client.close()