设计思路
安全协议的目的就是传输数据 并且实现两个基础目标 安全性和可靠性 前者指数据不会被泄露 后者指数据不会被篡改
所以数据的传输过程 可以归类于四个阶段
“发” “传“ ”收“ ”验“
发送阶段 需要对数据进行加密
这里有两种思路 一种是使用对称加密 还有一种是选择非对称加密 从安全性上 个人更倾向使用后者
传输阶段 这里打算使用socket来负责两个设备之间的通讯 同时还需要考虑进行身份验证 防止非法用户传输
接受阶段 为了防止攻击者使用过量的数据来瘫痪通讯 也许需要加入过滤机制来接受数据 这一点考虑到实现的复杂 是否加入还有待考虑
验证阶段 需要对接受到的数据进行检测 以防止数据被篡改或者是传输不完整
传输阶段的一些尝试
对于socket的使用并不是非常熟练 打算先通过一些简单的交互来加深一下
本机的ubuntu虚拟机同时充当客户端和服务端 使用python语言编写
v0.1
服务端代码
import socket |
客户端代码
import socket |
身份验证的实现
目前思考的是采用哪种身份验证方案
一种是采用第三方验证 比如auth0 这种的虽然实现起来更加高级且安全
第二种就是只需要做到安全的密钥交换 然后采用静态的用户密码库存储(也可以更换成sql) 然后加密传输用户名密码即可
考虑到第一种方法工作量太大 而且实现起来复杂(其实就是不会得边学边写) 还是乖乖采用第二种方法吧
这里采用dh算法 来实现在不安全的通讯中传输共享密钥 获得到共享密钥后 使用aes加密算法来加密用户名和密码
暂时是通过静态的用户名和密码库来实现身份验证 同时这里还存在一个攻击漏洞 对于密码的检测是遍历密码库中的所有密码 有满足的即可 并没有检验是否对应该用户
v0.2
服务端代码
import socket |
客户端代码
import socket |
针对账号密码的问题 进行了一波优化
使用sqlite3模块和sql数据库配合使用 实现账号密码的校验
暂时只打算把注册账号的权限给admin用户 在权限校验这块还存在问题 使用的是username的检测 严谨一点的应该加入数据库的参数 这样方便后面更改用户的操作权限
这一版大致优化了一些交互 不过还是很简陋 预计后面要增加窗口化界面
同时目前最关键的问题是 dh算法的g参数 我还没去跑出来一个较大的数 还是使用的2
v0.3
服务端
import socket |
客户端
import socket |
传输阶段的数据加密
这一章节的目的是设计出来一个数据包格式
常规的应该是由这几个部分组成 包头+密文长度+密文+校验和
这就要设计到两个算法 消息加密算法以及消息摘要算法
加密还是老样子 使用dh算法得到的共享密钥来进行aes加密
摘要算法的目的是为了保证消息的完整性
这里就使用md5算法
目前打算将数据包格式定义成下图所示
消息类型负责优化原本的服务端和客户端 原本的服务端针对传输数据类型不同以及新增用户的功能 是采用了菜单形式 过于简单且多了许多额外的交互 这里选择将choice变量融合到数据中一起传输
然后还得完善上一章节预留的 传输文件 这里打算暂时只包括传输图像 音频 文本文件 更多的格式和文件类型由于没有经过测试 不知道能不能完整的传输过去
对于服务端来说 接受到文件后 还需要判断属于哪种类型的文件 方便保存
想到的是两种方案 一种是客户端发送文件的时候额外发送文件的类型 服务端接受后按照类型保存文件
第二种是 在服务端接受数据后利用magic库来判断文件类型
但是这两种方法 似乎都很容易被绕过 要是客户端将马伪造成正常格式的文件传过来就不好玩了
虽然但是 本次只是打算设计一个能用的安全协议 所以安全性这里就只是预警一下 不去研究如何改进了 这里采用第二种方法
在编写传输图像文件的时候 遇到了一个新的问题
按照上面的数据包 单个数据包最多传输0xffffbytes 但是一个图像不止这些字节 所以就需要分段传输 那么我们就需要在数据包格式中的消息类型 加入是否属于切片传输的位置 来供服务端判断
迭代了数据包格式 前面的判断位之所以给了4byte 考虑到服务端处理切片数据的时候 需要确定当前是否属于最后一个切片(常规的协议一般在数据包中增加当前切片对于整段数据的偏移 这里我打算做简单一点) 所以前2个byte用来存储一共有多少个切片 后两个byte用来存储当前是第几个切片
而检测是否属于切片数据包也很容易 只要这4个byte不为\x00即可
同时 这个版本还优化了之前代码中的一些逻辑漏洞 容易造成服务端的server运行中断
v0.4
服务端
import socket |
客户端
import socket |
代码迭代
针对一个简单协议所需要的功能已经大体实现了 接下来的任务就是优化服务端和客户端的代码
主要的目标大致如下
1.添加图形化界面 优化交互过程
2.优化代码逻辑 增加注释 提高代码美观度
3.完善程序功能
v0.5
该版本优化了代码逻辑 完善了注释
客户端
import socket |
服务端
import socket |
v0.6
这一版针对传输过慢的原因进行了排除 感觉是因为传之前过了一遍base64的原因 这里进行了删除
以前的代码实际上是没有考虑过多端连接的 所以这一版的代码主要针对多端连接进行优化
原本的代码 client1连接后 client2能够连接上服务器 但是交换公钥的时候会阻塞
这是因为公钥交换相关的代码没有考虑到多端的情况
来假设一种情况 客户机A连接上服务端后 进行密钥交换完成后 开始进入数据传输阶段
按照原本的程序设计 此时的服务端程序已经进入了while循环处理数据传输的请求
但是如果此时有客户机B想要连接服务端 服务端就不会去处理客户机B的密钥交换
为了解决这个问题 应该要使用线程池来处理多线程的情况
多线程的实现难点应该在于确保多个线程之间的共享数据同步 在设计程序之间并没有这方面的经验 所以打算先通过编写几个简单的程序来熟悉一下
多线程实现的学习
import threading |
通过定义一个thread对象 并且赋值target 即需要该线程执行的函数 同时给予参数
定义完成后thread对象完成初始化 通过start来运行线程
主进程和线程是同时运行的 如果想要让主进程阻塞 等待线程运行结束后再运行主线程
这时候可以使用join来让主进程等待线程执行完毕
import threading |
import threading |
join的原理是等待对应thread对象执行完毕 或者给予参数 等待参数时间
所以也可以用来规划两个进程的执行先后顺序
import threading |
上述代码应该是先执行thread1 但是由于thread2调用了join 此时应该会先等待thread2执行完毕再执行其他线程 这里print_name函数的sleep也是为了防止thread1执行过快导致还没主进程还没执行thread2.join()就结束了
接着来看下面的程序
import threading |
按照预期的效果 在输出”主进程即将结束”后 主进程应该就结束了 同时thread1执行全部循环至少需要5s 而主进程执行时间应该在2-5以内 但是事实是主进程要一直等到thread1执行完毕才能结束
这一点是由定义thread对象的时候 一个叫daemon的参数决定的
其默认值为False 即主进程等待thread1线程执行完毕后才能结束 如果改为True 主进程结束时 线程就会结束
import threading |
如果想要让线程1执行两个函数呢
即执行完函数A后再执行函数B 这一点实现起来也很简单 只需要在函数A的最后调用函数B即可
import threading |
但是这一操作的弊端也很明显 那就是假如此时我们需要创建两个线程
线程1需要先执行函数A再执行函数B
线程2需要先执行函数B再执行函数A
这时候就会产生冲突 这又得去编写一个中转函数来识别需求 然后做if分支了 显然十分麻烦
这时候就可以自定义thread类
import threading |
如上述代码 我们自定义了一个myself_thread类 并且继承了threading.Thread 这个类的run函数以及其他函数都可以由我们自定义 针对不同的需求来调用函数也变得十分简单
如果服务端采用这种方式来新增线程 考虑到如果遭受ddos攻击 那么处理的线程数就会过多
这时候应该考虑采用线程池的方法来处理
线程池在程序启动时就创建大量空闲线程 在程序需要线程来执行函数的时候 就会分配一个线程出来 当线程执行完毕后 又会回到线程池 因此使用线程池可以有效的控制程序中并发线程的数量
运行下面这个程序 创建一个含有10个空闲线程的线程池
from concurrent.futures import ThreadPoolExecutor |
输出的结果为
说明执行时间过短 在线程0执行完毕后 就作为空闲线程返回线程池 随后第二次需要执行print_name函数的时候 就又被分配出来了
为了印证猜想 在print_name函数中加入time.sleep(0.5)
这时就是由两个线程来处理了
还可以使用map方法来并发的调用线程 不过需要注意的是 (1,2,3)是赋予给print_name函数的参数 但是实际上print_name不需要参数 这一点暂时还没找到方法来优化 总感觉有点多余 但是不给予参数 map又无法指定调用多少个线程
from concurrent.futures import ThreadPoolExecutor |
最后的输出结果仍然是全部由线程0来负责 说明map方法就是简化了多个submit方法
多线程应用
目前的整改思路如下:
服务端:
主进程负责处理accpet 有客户机连接就调用一个线程来处理密钥交换和数据传输 然后这个线程就专门负责这台客户机 如果客户机中断连接 那就释放线程 回到线程池
客户端:
这块的整改还在考虑中 因为就目前来说 传输效率确实有点感人了 传一个10mb的音频都需要十来分钟 如果客户端改成 可以并发的传输数据 即同时传输音频和图像 或者是把一个音频拆成多个线程来传输 这样效率应该可以大大提高 但是感觉编写难度有点大啊 看着来吧
服务端
import socket |
客户端
import socket |
v0.7
程序到这一版已经设计的比较完善了 可能还存在一些逻辑漏洞可以造成服务端宕机等问题 这个就留到后续的安全性分析吧 这一版本主要还是想优化一下文件传输的问题
目前的问题在于 校验数据完整性没有进行处理 比如如果失败 应该记录下失败的数据组序号 然后返回给客户机
然后就是识别文件类别的那块代码 现在用的是python-magic库来识别是属于什么文件 然后创建对应的文件用来存储接收到的数据
不过不是很确定这个库他是不是只是对文件头进行识别 如果是的话感觉还是相当好伪造的 这里应该有一个比较大的安全隐患
同时原本的类型处理也比较少 只适配了txt png mp3这三个最常见的格式 需要增加如果识别文件类型失败 就进行临时存储 同时报错
客户端
import socket |
服务端
import socket |
v0.8
这一版本的目的在于增加日志功能 因为就目前来说 一旦连接的客户机多了 服务端那边的输出就会比较混乱
具体的实现应该也比较简单 在accpet连接后就创建一个文件以及字符串(作为记录该客户机操作记录的缓冲区)
然后在检测客户机中断连接后就把缓冲区的内容写入日志 并且加入时间 来方便朔源
或者直接在执行对应操作后就写入日志 不过从理论上来说应该是前者的方案更加节省资源
预计难点在于在哪里塞入判断来check什么时候写入日志 所以还是选择后者吧
同时还把原本写的和屎一样的传输数据类型转化优化了一下 变成了好闻的屎
服务端
import socket |
客户端
import socket |
v0.9
这个版本的目的是修修bug 尽量优化一下代码 准备在1.0版本实现图形化管理界面 然后就正式结束了这个项目
针对传输速率过慢的问题 大概有了头绪了 原因出在客户端输出当前组数据校验是否正确 这一功能是利用客户端和服务端高频send和recv的 所以会造成卡顿
取消掉了以后发送速度明显提高 但是接受速度跟不上了 这里想到的解决办法是专门给大文件收发开一个新线程 看看能不能实现 然后关于切片那一块可能也要优化一下了
11/9记
mlgb 我觉得用socket作为这个协议的基础框架根本就是个错误的选择 服务端运行在服务器上的时候 会因为各种奇奇怪怪的原因导致丢包 最后只能通过sleep来控制客户端发包的速度
服务端
import socket |
客户端
import socket |
v1.0
最终版本! 最大的更新就是给客户端增加了图形化界面 看起来更高级一点
而且为了服务图形化界面的逻辑 所以一些客户机和服务机的交互也更改了
tkinter部分控件学习
第一次尝试编写图形化界面 这里就选用tkinter库
from tkinter import * |
这样短短三行 即可生成一个根窗口 子窗口关闭后根窗口不会关闭 根窗口关闭后子窗口会关闭
可以对窗口进行一些简单的操作
from tkinter import * |
lable
from tkinter import * |
lable控件可以在窗口上显示文字或者图像
size可以控制控件在哪里显示 默认是top
padx和pady则是代表着和x轴或者y轴的偏移是多少
显示图片的话 最好再加上pillow库来配合 因为tk本身支持的图片格式有点少 只支持png
from tkinter import * |
entry
这个控件可以在窗口上显示一个输入框 配合lable控件就可以实现引导程序使用者输入变量
from tkinter import * |
不过这样需要两个控件的位置相匹配 用pack中的padx,pady,side处理一对还好 要是界面需要设计的点多 就会比较杂乱 这里可以使用grid控件来配合
grid
grid插件相当于把整个窗口界面转化成了一个二维坐标系
row参数用来规定行数 column用来规定列数
于是上面用pack需要计算padx和pady来实现的效果 可以简单的用这两个参数来实现
from tkinter import * |
button
上述控件和button控件组合起来 就可以实现一个简单的账号密码登录界面
from tkinter import * |
frame
大部分时候 主窗口的大小都需要提前设置 而非由控件大小来自动规划窗口大小 这个时候控件相对于主窗口就会较小 造成界面的不美观
下面这个示例 frame1容器收纳了三个控件 这三个控件的grid控制的是在frame1容器中的位置
而frame1的grid控制的则是在主窗口中的位置 这样可以更加方便的设置不同控件的相对位置
不过最好使用grid_propagate方法来固定容器的位置 不然随着后续容器中的控件位置发生变化 容器的位置也会发生变化
from tkinter import * |
text
这一个控件的作用和entry相似 只不过entry只能支持一行的输入
from tkinter import * |
上述代码中insert负责给文本框写入数据
这里的1.0代表的是第一行第0个字符开始写入
你也可以使用1.end来表示第一行的最后一个字符
scrollbar
这个控件主要是配合text使用 在最右边加一个滚动条 看起来美观一点 也方便用户更直观的知道当前文本框中的所处位置
from tkinter import * |
yscrollcommand用来绑定滚动条和text wrap参数用来规定是否自动换行 参数有WORD CHAR NONE三种 第一种是单词换行 第二种是字符换行 第三种是不自动换行
canvas
这个控件主要用于显示文本 图片等 主要是可以绑定scrollbar
from tkinter import * |
创建出一个canvas控件后 还需要创建一个内部窗口 用来显示文本或者图片
scrollbar绑定的是canvas 但是输出的文本还是依靠label
tkinter实际利用
基于tk来实现图形化界面 现在主要是卡在滚动条的实现 本来预期是打算设计成
上面部分用来显示操作界面 下面用来输出信息
然后下面的部分本来想着是加一个滚动条来查看输出信息过多的情况下以往的输出信息
但是tk的滚动条实现起来实在有点太麻烦了 而且tk的美观程度也很差 就是因为他的控件布局太难用了 所以写到这里就烂尾了 看看后面还有没有兴趣搞下去吧 暂时是打算转用pyside6了
import socket |
pyside6学习
简单窗口显示
from PySide6.QtWidgets import * |
上述程序可以创建出一个窗口后简单的输出文字 总体的逻辑是和tk差不多的
需要创建一个窗口对象 然后给窗口增加控件
qt designer
我觉得pyside6相较于tk最显著的优势就是 可以使用编辑工具来简化ui的设计
如图所示 设计一个简单的ui
保存ui文件 在程序中载入ui 然后加载到一个窗口上
import sys |
可以看到非常简单的就实现了这几个控件 比tk挪半天位置好用多了 tk是人用的吗???
函数绑定
解决了ui设计的难题后 接下来的重点就是放在函数的绑定上 其实也很简单 就是要注意自己在qt designer中定义的控件名称
然后需要把ui文件转化成py文件
pyside6-uic filename.ui -o filename.py |
随后在程序中直接导入即可
然后函数绑定的大体上是和tk一致的 具体到各个控件的方法有些不太一样 就有需求的自己查询 这里放一个我自己弄的账号密码登录程序
import sys |
写这么久博客第一次放gif图 看起来画质一般般 而且也捕获不到弹出的第二个窗口 哎下次改进吧
最终代码
v1.0版本最后采用了pyside6作为图形化界面框架 把客户端用图形化界面包了起来
同时为了配合客户端的优化 服务端也作了一些修改
服务端
import socket |
客户端
|
v1.1
这个版本主要是继续增加一些功能
1.原本的只是简单的实现客户端向服务端传输数据 并且ui的设计中上半部分显然也是预留给双端通讯(以服务端为媒介 客户机a与客户机b实时通讯)的 缺乏了这个功能总感觉有点遗憾
2.登录和注册界面的功能也不是特别完善 再加上每次运行程序都需要重新输入ip地址和账号密码感觉不是特别方便 所以考虑优化一下这些东西
3.作为一个合格的协议 目前还缺少的一点就是数字签名部分 需要提供不可抵赖性 证明消息是由对应客户机发送的 而非被劫持后的
第二点的实现比较简单 稍微更改一下login.ui和ip_connect.ui和对应的类就可以了
要实现第一点的话 就要对目前的ui界面进行大改动了 而且还要给服务端增加处理与好友私聊以及群聊的功能 客户端这边也要处理好发送和接收 感觉是一个比较大的工程
总结下为了实现第一点需要去补充的功能
1.客户端新增好友功能 以登录时的username为账号 添加好友(或者干脆不做这个 实现起来没啥必要)后可以进行通讯
第一点的实现问题在于 如果客户端B离线 那么服务端那边就需要缓存A传输给B的数据 然后在B上线后发送给B
如果两者都在线的情况 如何处理好数据的转发也是一种难题 毕竟现在的代码就因为高频的send和recv的时候会因为网络延迟导致丢包 偷懒没有去写处理丢包的逻辑 而是人为控制客户端发包的速率来解决这个问题 本来速率就比较慢了 如果这个时候还需要把数据从服务端发送给客户端B 这个时间就比较久了 没办法实现实时通讯
按理来说 解决这一个方法也比较简单 大概可以从两个方面来实现
(1)加一个检测丢包的机制 如果发现丢包就重传
(2)经过测试 如果是一个体型较小的文件传输程序 是可以做到快速传输的 不需要手动添加发包延迟也可以 所以如果在传输大数据前新分配一个线程专门负责 会不会改善网络延迟、丢包的影响
2.服务端需要和数据库进行交互来记录不同用户之间的好友列表 还要写添加好友的功能
3.ui界面需要进行更新 这里依然是偷懒的选择抄袭qq的聊天界面 打算再原本左侧的选项栏的右边增加选择在哪个频道(群聊or私聊)发送消息
如果把上面提到的全部解决 这会是一个庞大的工程量 接下来优先解决丢包重传的问题
然后还有一点最关键的就是 如果想要实现一个基本的通讯软件的功能 即文件文本同时传输 例如表情包加文字的情况 那还需要优化服务端 需要把对于每一个数据包的data_type都进行校验
优化数据包传输
这一点的优化原本是打算更改一下程序现有的逻辑
目前是 如果是无切片情况 那么就根据数据包中的data_byte来决定处理函数
如果是切片情况 先处理第一个切片的data_byte 然后选择处理函数
那么切片情况的处理就比较不理想了 如果攻击者伪造第一个包的data_type 就有可能导致服务端进入错误的处理函数 导致负责的进程陷入死机 所以理想情况应该是 服务端先接收所有的包 然后再根据每个包的data_type来分别处理
然后就是如果是单线程处理切片数据的话 会比较慢 往往传输完毕后过个几十秒才能把数据全部处理完
所以打算给处理数据的也加一个线程池 最多10个线程来同时处理
同时新增了重传机制 当服务端接收到了大小不满足一个切片长的数据包(除开最后一个数据包) 就会向客户端发送重传请求 客户端接收到后 就会从错误切片编号开始重新传输
客户端
import socket |
服务端
import socket |
添加个人界面ui以及好友系统
这一版本主要是添加一下个人界面的ui设计以及好友系统
好友系统的实现 目前是打算客户端这边显示好友列表之前 先向服务端发送请求 服务端根据客户端登录的user来查询存储于数据库中的好友列表并且返回
然后就是完善了一下程序运行环境的配置 使得用户使用起来更加方便
目前的进度是卡在了用户头像的设置 设想是客户端本地缓存 如果需要更换头像 则传输至服务端 这样在客户端之间互相通讯的时候 可以及时更新