前置知识:不同计算机程序之间的数据传输
应用程序中的数据都是从程序所在计算机内存中读取的。
内存中的数据是从硬盘读取或者网络传输过来的
不同计算机程序数据传输需要经过七层协议物理连接介质才能到达目标程序
套接字主要有两个版本,一个是基于文件类型的套接字家族(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)服务端
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()把接收的代码用循环包起来
根据最上面的前置知识可以知道,数据是从内存中读取过来的
发现要发送的三条消息粘在了一起
1.从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点
2.实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
控制recv接收的字节数与之对应(你发多少字节我收多少字节)
思路一如果在不知道数据有多长的情况下就会出现意外,那么我们可以先传一个固定长度的数据过去告诉他真实数据有多长,就可以对应着收了
该模块可以把一个类型,如数字,转成固定长度的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()转载于:https://www.cnblogs.com/PowerTips/p/11318545.html
相关资源:JAVA上百实例源码以及开源项目