python网络编程-socket套接字通信循环-粘包问题-struct模块-02
- 2019 年 10 月 7 日
- 筆記
前置知识
不同计算机程序之间数据的传输

应用程序中的数据都是从程序所在计算机内存中读取的。 内存中的数据是从硬盘读取或者网络传输过来的
不同计算机程序数据传输需要经过七层协议物理连接介质才能到达目标程序
socket (套接字)
json.dump/dumps 只是把数据类型序列化成字符串 要想用来文件传输,还需要encode 给它编码成二进制数据才能传输 不用pickle是因为要和其他语言交互(你给页面就是js来处理,能不能支持是问题),而pickle只能是在python中用
程序员不需要七层一层一层地去操作硬件写网络传输程序,直接使用python解释器提供的socket 模块即可
大多数注意点都在代码后面的注释里,要仔细看哦~

初略版的双端(C/S)通信
程序运行时先启动服务端再启动客户端(代码设置一下可以一份代码跑好几个(客户端可能会启好几个))
server服务端
socket.scoket()不传参数默认就是TCP协议
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个字节的数据 print(data) # b'halo baby' conn.send(b'ok') # 发送数据(必须是二进制数据) conn.close() # 关闭连接 server.close() # 关闭服务
client客户端
import socket client = socket.socket() client.connect(('127.0.0.1', 8080)) # 去连接服务器上的程序(服务器的IP + port) client.send(b'halo baby') data = client.recv(1024) print(data) client.close()
点进去发现socket这个类有实现 __enter_ 、 __exit__
方法,__exit__
方法中有关闭连接的方法,故可以用with
上下文来操作(暂不举例了,面向对象这两个函数的知识点提一嘴)

在重启服务器的时候可能会遇到的BUG(mac居多)

解决方法
# 加入一条socket配置,重用ip和端口 import socket from socket import SOL_SOCKET, SO_REUSEADDR server = socket.socket() # ------------------------------------- # 加上他就可以防止重启报错了(注意位置) # ------------------------------------- server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) server.bind(('127.0.0.1', 8080)) # 把地址绑定到套接字 server.listen(5) # 半连接池 conn, addr = server.accept() # 接受客户端链接 ret = conn.recv(1024) # 接收客户端信息 print(ret) # 打印客户端信息 conn.send(b'hi') # 向客户端发送信息 conn.close() # 关闭客户端套接字 server.close() # 关闭服务器套接字(可选)
服务端需要具备的条件
- 固定的ip和port 让客户端可以连接你(试想如果百度一天一个域名/ip?咋上百度))
- 要能24小时不间断提供服务 服务器不在线的话,客户端连啥?(双重循环 server.accpet() 来连接建立连接)
- 暂时不知道
半连接池,允许等待的最大个数

server.listen(5)
指定5个等待席位
通信循环
双方都处于收的等待状态
直接回车没有发出数据,自身代码往下走进入了等待接收状态, 而另一端也没有收到消息,依然处于等待接收状态图,双方就都处于等待接收的状态了

linux、mac断开链接时不会报错,会一直返回空(b‘’)
穷,买不起mac…没图
解决方案
服务端
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('