前置知识:不同计算机程序之间的数据传输

应用程序中的数据都是从程序所在计算机内存中读取的。

内存中的数据是从硬盘读取或者网络传输过来的

不同计算机程序数据传输需要经过七层协议物理连接介质才能到达目标程序

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()

8.7 day28 网络编程 socket套接字 半连接池 通信循环 粘包问题 struct模块的更多相关文章

  1. python网络编程-socket套接字通信循环-粘包问题-struct模块-02

    前置知识 不同计算机程序之间数据的传输 应用程序中的数据都是从程序所在计算机内存中读取的. 内存中的数据是从硬盘读取或者网络传输过来的 不同计算机程序数据传输需要经过七层协议物理连接介质才能到达目标程 ...

  2. 19 网络编程--Socket 套接字方法

    1.Socket(也称套接字)介绍 socket这个东东干的事情,就是帮你把tcp/ip协议层的各种数据封装啦.数据发送.接收等通过代码已经给你封装好了 ,你只需要调用几行代码,就可以给别的机器发消息 ...

  3. 网络编程--Socket(套接字)

    网络编程 网络编程的目的就是指直接或间接地通过网络协议与其他计算机进行通讯.网络编程中 有两个主要的问题,一个是如何准确的定位网络上一台或多台主机,另一个就是找到主机后 如何可靠高效的进行数据传输.在 ...

  4. 19、网络编程 (Socket套接字编程)

    网络模型 *A:网络模型 TCP/IP协议中的四层分别是应用层.传输层.网络层和链路层,每层分别负责不同的通信功能,接下来针对这四层进行详细地讲解. 链路层:链路层是用于定义物理传输通道,通常是对某些 ...

  5. UNIX网络编程——原始套接字(dos攻击)

    原始套接字(SOCK_RAW).应用原始套接字,我们可以编写出由TCP和UDP套接字不能够实现的功能. 注意原始套接字只能够由有 root权限的人创建. 可以参考前面的博客<<UNIX网络 ...

  6. UNIX网络编程——原始套接字的魔力【续】

    如何从链路层直接发送数据帧 上一篇里面提到的是从链路层"收发"数据,该篇是从链路层发送数据帧. 上一节我们主要研究了如何从链路层直接接收数据帧,可以通过bind函数来将原始套接字绑 ...

  7. TCP/IP网络编程之套接字类型与协议设置

    套接字与协议 如果相隔很远的两人要进行通话,必须先决定对话方式.如果一方使用电话,另一方也必须使用电话,而不是书信.可以说,电话就是两人对话的协议.协议是对话中使用的通信规则,扩展到计算机领域可整理为 ...

  8. TCP/IP网络编程之网络编程和套接字

    网络编程和套接字 网络编程又称为套接字编程,就是编写一段程序,使得两台连网的计算机彼此之间可以交换数据.那么,这两台计算机用什么传输数据呢?首先,需要物理连接,将一台台独立的计算机通过物理线路连接在一 ...

  9. Linux网络编程——原始套接字实例:MAC 头部报文分析

    通过<Linux网络编程——原始套接字编程>得知,我们可以通过原始套接字以及 recvfrom( ) 可以获取链路层的数据包,那我们接收的链路层数据包到底长什么样的呢? 链路层封包格式 M ...

随机推荐

  1. 齐治运维堡垒机后台存在命令执行漏洞(CNVD-2019-17294)分析

    基本信息 引用:https://www.cnvd.org.cn/flaw/show/CNVD-2019-17294 补丁信息:该漏洞的修复补丁已于2019年6月25日发布.如果客户尚未修复该补丁,可联 ...

  2. STM32F0_HAL库驱动描述——HAL驱动程序概述

    HAL库文件结构: HAL驱动文件: 外设驱动API文件和头文件:包含了常见主要的通用API,其中ppp表示外设名称,如adc.usart.gpio.irda等: stm32f0xx_hal_ppp. ...

  3. 【并查集】连接格点-C++

    连接格点 描述 有一个M行N列的点阵,相邻两点可以相连.一条纵向的连线花费一个单位,一条横向的连线花费两个单位.某些点之间已经有连线了,试问至少还需要花费多少个单位才能使所有的点全部连通. 输入 第一 ...

  4. 微信小程序 setData 数组 渲染问题 删除之后的数组渲染不正确

    list: [ { id: , mode: , src: ' }, { id: , mode: , src: ' }, { id: , mode: , src: ' } ], onDelete(e) ...

  5. 记一次java.lang.NoClassDefFoundError异常

    前阵子做了个评论过滤敏感词的功能,本地测试没有任何问题,然后就部署到线上服务器,通知相关人员线上测试.大约过了十来天,那货和我说接口出问题了,当时一脸懵逼,用了十来天突然出问题了???好吧,出问题了咱 ...

  6. 基站搭建与IMSI捕获

     写在前面 : 实验目的是为了教学交流,坚决抵制违法行为. 一.实验目的 搭建基于OpenBTS的基站,手机接入该基站,进行短信.语音等测试. 二.所用仪器 USRP B210 1台,天线2根,PC机 ...

  7. Git学习(二):Git的初步使用

    一.Git的最小配置 1.使用如下命令创建Git的用户名和邮箱,如下所示: $git config --global user.name 'your_name' $git config --globa ...

  8. Java集合 HashSet的原理及常用方法

    目录 一. HashSet概述 二. HashSet构造 三. add方法 四. remove方法 五. 遍历 六. 合计合计 先看一下LinkedHashSet 在看一下TreeSet 七. 总结 ...

  9. Java编程基础阶段笔记 day06 二维数组

    二维数组 笔记Notes 二维数组 二维数组声明 二维数组静态初始化与二位初始化 二维数组元素赋值与获取 二维数组遍历 二维数组内存解析 打印杨辉三角 Arrays工具类 数组中常见的异常 二维数组 ...

  10. 机器学习经典分类算法 —— k-均值算法(附python实现代码及数据集)

    目录 工作原理 python实现 算法实战 对mnist数据集进行聚类 小结 附录 工作原理 聚类是一种无监督的学习,它将相似的对象归到同一个簇中.类似于全自动分类(自动的意思是连类别都是自动构建的) ...