项目介绍与密码学知识
项目整体架构图:
主要功能:对网络通信的数据进行加解密
基础组件:
- 数据序列化:protobuf
- socket通信:线程池,连接池
- 共享内存IPC
- 数据库oracle:使用OCI接口
- 数据加密:openssl(Secure Sockets Layer)
加密三要素:
- 明文、密文
- 秘钥:一个定长的字符串
- 算法:加密算法和解密算法
常用加密方式:
-
对称加密 密钥比较短且只有一个,加密和解密使用的密钥是相同的,是由自己负责生成一个随机字符串。加密效率高,但加密强度低,密钥分发困难,不能在网络环境中直接传送。以AES为代表 -
非对称加密 密钥比较长,加密和解密使用的密钥不同,是由专门的算法生成的,分为公钥(小)和私钥(大) 如果使用公钥加密,必须使用私钥解密 如果使用私钥加密,必须使用公钥解密 公钥可以直接分发
为了兼顾效率和安全性,通常采用非对称加密加密对称加密的密钥,这样就可以安全地进行对称加密了,流程:
- 客户端生成(读入)密钥对,将公钥封装到请求报文并对其使用私钥进行签名同样封装到请求报文中
- 服务端收到请求报文后并验明身份后生成一个随机字符串,对使用公钥进行加密,封装到响应报文中发送给客户端
- 客户端收到响应报文后使用私钥解密,这样两端就安全地得到了对称加密的密钥
常用对称加密算法:
DES与3DES: AES: 秘钥交换过程:
哈希算法(单向散列函数)
- 不管原始数据有多长,得到的哈希结果的位数是一样长的
- 原始数据相差一点,得到的结果完全不一样
- 有很强的抗碰撞性(扛暴破)
- 不可逆:不可能由结果反推原内容(这就决定了它不能是加密算法)
应用场景:
- 数据校验:下载文件时会提供MD5值,便于校验文件是否被篡改过
- 登录验证:在系统中不必保存密码明文,只需保存其对应的散列值即可
- 网盘的秒传功能(百度网盘)
哈希运算的结果称为:哈希值,指纹,摘要
MD4/MD5:散列值长度16B ,抗碰撞性已经被破解
SHA-1:散列值长度20B
SHA-2(224,256,384,512):每个子类名表明了其占用的比特位数
消息认证码HMAC
作用:验证通信时的数据有没有被篡改
HMAC的本质还是一个散列值
HMAC=hash(原始数据, 密钥)
缺点:
- 密钥分发困难
- 不能区分消息的所有者
数字签名(私钥加密公钥解密的过程)
目的:防止篡改数据,保证数据的完整性
作用:可以甄别文件的所有者
发送者数字签名的过程:
- 生成一个非对称密钥对,分发公钥
- 使用散列函数对原始数据进行hash运算,得到散列值(散列值更小,相较于原始数据更容易加密)
- 对散列值使用私钥进行加密,得到密文
- 将原始数据和密文一起发送给接收者
接收者校验签名的过程:
- 接收签名的一方分发的公钥
- 接收发送者发来的数据:原始数据和签名
- 对接收的原始数据进行同样的0哈希运算,得到散列值1
- 使用公钥对密文进行解密,得到散列值2
- 比对这两个散列值,若相同,则证明安全
OpenSSL介绍:
SSL是Secure Sockets Layer(安全套接层协议)的缩写,可以在Internet上提供秘密性传输。Netscape公司在推出第一个Web浏览器的同时,提出了SSL协议标准。其目标是保证两个应用间通信的保密性和可靠性,可在服务器端和用户端同时实现支持。已经成为Internet上保密通讯的工业标准。 SSL能使用户/服务器应用之间的通信不被攻击者窃听,并且始终对服务器进行认证,还可选择对用户进行认证。SSL协议要求建立在可靠的传输层协议(TCP)之上。SSL协议的优势在于它是与应用层协议独立无关的,高层的应用层协议(例如:HTTP,FTP,TELNET等)能透明地建立于SSL协议之上。SSL协议在应用层协议通信之前就已经完成加密算法、通信密钥的协商及服务器认证工作。在此之后应用层协议所传送的数据都会被加密,从而保证通信的私密性。
vs配置openssl:
设置工程属性:包含目录,库目录,链接器的输入
测试demo:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/md5.h>
void getMD5(const char* src, char* result) {
MD5_CTX ctx;
MD5_Init(&ctx);
MD5_Update(&ctx, src, strlen(src));
unsigned char md5[16] = { 0 };
MD5_Final(md5, &ctx);
for (int i = 0; i < 16; ++i) {
sprintf(&result[i * 2], "%02x", md5[i]);
}
}
int main(int argc, char* argv[]) {
char result[33] = { 0 };
getMD5("hello, md5", result);
printf("md5 value:%s\n", result);
system("pause");
return 0;
}
linux安装并测试openssl
安装完成后查看库版本:openssl version -a
如果安装了库但是找不到:
查找库所在目录:find / -name libcrypto.so
将其路径写入/etc/ld.so.conf 文件中,并使其生效:sudo ldconfig
编译时添加选项-lcrypto
git
分布式版本控制系统
git的核心优点:支持分支,在每个分支上可以进行版本号迭代开发
git的相关概念:
工作区:用户自己创建的目录,用于存放源代码
版本库:管理提交的代码,创建方式:win下直接右键,使用tortoisegGit创建版本库
暂存区:暂时保存,不参与版本管理
流程:
- 在工作区新建文件
- 将新文件添加到本地仓库,新文件被保存在了暂存区
- 将暂存区的数据提交到版本库
在linux上安装git并连接gitee:
安装:sudo apt-get install git
配置用户名和邮箱:
git config --global user.name "daniel187" git config --global user.name "syc198@qq.com"
初始化一个仓库:git init
生成密钥对:ssh-keygen -t rsa -C "syc198@qq.com"
将公钥的内容复制到gitee网站的输入框,其位置在~/.ssh/id_rsa.pub
添加内容并本地提交,然后提交到远程仓库:
git add README.md
git commit -m "first commit"
git remote add origin https://gitee.com/daniel187/demo1.git
git push -u origin "master"
win上tortoiseGit的基本使用:
将本地新文件添加到暂存区:鼠标右键
将暂存区的文件提交到本地版本库:鼠标右键
对已修改的文件进行还原:鼠标右键
查看提交的历史版本信息:在工作区鼠标右键-显示日志
双击文件可查看内容,左边是旧版本,右边是新版本
可以导出某一个版本:直接在当前版本上鼠标右键
比较差异:鼠标右键
删除文件:直接del,然后提交
删除并提交后,若还想恢复,可以从历史节点中恢复
设置文件忽略目录:选中文件,右键忽略,并提交
本地仓库同步到远程:右键git同步
生成密钥对,在gitbash下:ssh-keygen -t rsa
同样将公钥送到gitee上
base64:用64个字符表示原始数据:a~z, A~Z, +, /
可以分支,亦可以反过来合并
创建新分支:右键
分支的切换:右键,切入检出
分支的合并:要合并到哪个分支,应先切换到哪个分支,然后右键合并
如果两个分支有同名的文件,会出现问题:相同的行有不同的数据,git无法判断,需要手动解决
如何将一个本地仓库推送到一个非空的远程仓库:
将远程仓库拉取到本地:将远程仓库的内容复制到本地的一个新的分支,不会合并
获取:直接将远程仓库中的内容和本地仓库的内容进行合并
常用的git命令:https://www.php.cn/tool/git/469391.html
protobuf部署
win:先用CMake生成VS工程,然后用VS编译,使用时添加PROTOBUF_USE_DLLS 宏定义 linux:下载源码,然后
configure
make
make install
UML类图
UML=Unified Modeling Language
对于一个Person类:
class Person {
public:
string get_name();
void set_name(string name);
protected:
void play_basketball();
void pass();
private:
string m_name="Jack";
};
其UML类图描述如下:
- public: +
- protected: #
- private: -
继承关系:使用一端带有空心三角的实线指向基类
由上面的Person派生出Student和Teacher两个子类:
class Student : public Person {
public:
void study();
private:
string stu_no;
};
class Teacher : public Person {
public:
void teach();
private:
string teacher_no;
};
UML类图继承关系表示如下:
抽象继承关系: 包含纯虚函数的类称为抽象类,不能被实例化,子类必须重写父类的纯虚函数
class Link {
public:
virtual void insert() = 0;
virtual void move() = 0;
int count();
};
class SingleLink : public Link {
public:
void insert() { return; }
void move() { return; }
};
UML图描述如下,注意抽象类的类名字体是倾斜的,纯虚函数的名称字体也是倾斜的
关联关系:一个类的对象作为另外一个类的成员变量
一个类的对象作为另一个类的成员变量
class Address {};
class Customer {
private:
Address addr;
};
关联关系用-> 来表示:
双向关联,相互包含:
class Product {
private:
Customer customer;
};
class Customer {
private:
Product product[64];
};
自关联:例如链表
class Node {
private:
Node* next;
};
聚合关系:
表示整体与部分的关系,在聚合关系中,成员对象是整体的一部分,但是成员对象可以脱离整体对象而独立存在。
在UML中,聚合关系用带空心菱形的直线表示,如汽车与引擎,轮胎,车灯:
class Wheel {};
class Light {};
class Engine {};
class Car {
public:
Car(Wheel w, Light l, Engine e) {
this->wheel = w;
this->light = l;
this->engine = e;
}
void dirve() {}
private:
Wheel wheel;
Light light;
Engine engine;
};
注意构造函数不必写入UML图中
组合关系:
组合关系也表示的是一种整体与部分的关系,但是在组合关系中整体对象可以控制成员对象的生命周期,一旦整体对象不存在,成员对象也不存在,整体对象和成员对象之间具有同生共死的关系。在UML中用带实心菱形的直线表示。
class Mouse {};
class Nose {};
class Head {
public:
Head() {
this->mouse = new Mouse;
this->nose = new Nose;
}
void shake() {}
private:
Mouse* mouse;
Nose* nose;
};
关于组合关系中成员对象的构造与析构:
#include <iostream>
using namespace std;
class Mouse {
public:
Mouse() { cout << "A mouse has been constructed" << endl; }
~Mouse() { cout << "A mouse has been deconstructed" << endl; }
};
class Nose {
public:
Nose() { cout << "A nose has been constructed" << endl; }
~Nose() { cout << "A nose has been deconstructed" << endl; }
};
class Head {
public:
Head() {
this->mouse = new Mouse;
this->nose = new Nose;
}
~Head() {
delete this->mouse;
delete this->nose;
}
void shake() {}
private:
Mouse* mouse;
Nose* nose;
};
void foo() {
cout << "h1 in the stack,in the function foo" << endl;
Head h1;
}
int main() {
foo();
cout << "\nh1 in the stack" << endl;
Head h1;
cout << "\nh2 in the heap" << endl;
Head* h2 = new Head;
delete h2;
system("pause");
return 0;
}
程序运行结果:
h1 in the stack,in the function foo
A mouse has been constructed
A nose has been constructed
A mouse has been deconstructed
A nose has been deconstructed
h1 in the stack
A mouse has been constructed
A nose has been constructed
h2 in the heap
A mouse has been constructed
A nose has been constructed
A mouse has been deconstructed
A nose has been deconstructed
注意,在组合关系中构造函数中new出来的成员对象必须在析构函数中手动delete掉,C++编译器不会在析构Head的实例时同时析构其成员对象
另外值得注意的是,在在函数调用栈上创建的局部变量退出函数时会自动析构(RAII的基础),但在main函数中就不会再析构了
依赖关系:一种广泛存在的使用关系,在UML图中,用带箭头的虚线相连
- 一个类的对象作为另一个类的方法的参数或返回值
- 在一个类的方法中将另一个类的对象作为其对象的局部变量
- 在一个类的方法中调用另一个类的静态方法
依赖关系实例:
class Car {
public:
void move();
};
class Driver {
public:
void drive(Car c) { c.move(); }
};
Protobuf序列化
序列化的整体过程:
常见的数据序列化方式:
- XML
- JSON
- Protobuf
protobuf使用步骤:
proto文件的语法格式:
syntax="proto3";
message struct_name{
data_type data_name=data_index;
...
}
protobuf中的数据类型:
例如对于这样一个C++类:
struct Person{
int id;
string name;
string sex;
int age;
}
其protobuf数据文件为:
syntax="proto3";
message Person{
int32 id=1;
string name=2;
string sex=3;
int32 age=4;
}
生成cpp类:protoc Person.proto --cpp_out=./
api命名特点,name 是成员变量的名字:
读:name() 写:set_name()
vs下使用步骤:
- 包含进头文件和源文件
- 工程属性配置头文件目录和库目录
- 工程属性的链接器输入添加lib库
- 预处理器添加宏定义
PROTOBUF_USE_DLLS
测试代码:
#include <iostream>
#include "Person.pb.h"
using namespace std;
int main() {
Person p;
p.set_age(20);
p.set_id(1001);
p.set_name("daniel");
p.set_sex("man");
string output;
output=p.SerializeAsString();
cout << "output:" << output << endl;
Person p2;
p2.ParseFromString(output);
cout << "p2.age:" << p2.age() << ",p2.id:" << p2.id() << ",p2.name:" << p2.name() << ",p2.sex:" << p2.sex() << endl;
system("pause");
return 0;
}
数组的使用:使用repeated 关键字修饰
syntax="proto3";
message Person{
int32 id=1;
repeated string name=2;
string sex=3;
int32 age=4;
}
相应的,在程序中要指明数组下标:
int main() {
Person p;
p.set_age(20);
p.set_id(1001);
p.add_name();
p.set_name(0,"daniel");
p.add_name();
p.set_name(1,"anny");
p.add_name();
p.set_name(2,"alan");
p.set_sex("man");
string output;
output=p.SerializeAsString();
cout << "output:" << output << endl;
Person p2;
p2.ParseFromString(output);
cout << "p2.age:" << p2.age() << ",p2.id:" << p2.id() << ",p2.name:" << p2.name(0)<<","<<p2.name(1)<<"," <<p2.name(2) << ",p2.sex:" << p2.sex() << endl;
system("pause");
return 0;
}
protobuf中使用枚举:语法同C语言,但是第一个枚举值必须为0
syntax="proto3";
enum Color{
RED=0;
GREEN=1;
BALCK=2;
PINK=3;
}
message Person{
int32 id=1;
repeated string name=2;
string sex=3;
int32 age=4;
Color color=5;
}
在程序中使用:
p.set_color(PINK);
protobuf中的类可以具有关联关系,类和类之间可以嵌套使用,使用import 导入
例如定义Info类并在Person中使用:
syntax="proto3";
message Info{
bytes address=1;
int32 no=2;
}
syntax="proto3";
import "Info.proto";
enum Color{
RED=0;
GREEN=1;
BALCK=2;
PINK=3;
}
message Person{
int32 id=1;
repeated string name=2;
string sex=3;
int32 age=4;
Color color=5;
Info info=6;
}
为了操纵Info对象,程序中还需#include "Info.pb.h"
在程序中的使用方法:
首先使用mutable_info() 将info对象的指针取出,方便操纵info
接着调用info的set方法,对其进行初始化
需要取出info中的相关信息时,应先创建一个临时的Info对象i,然后调用i的get方法取出各值
Info* info=p.mutable_info();
info->set_address("China");
info->set_no(23);
string output;
output=p.SerializeAsString();
cout << "output:" << output << endl;
Person p2;
p2.ParseFromString(output);
Info i = p2.info();
cout << "p2.age:" << p2.age() << ",p2.id:" << p2.id()
<< ",p2.name:" << p2.name(0)<<","<<p2.name(1)<<"," <<p2.name(2)
<< ",p2.sex:" << p2.sex() <<",p2.color:"<< p2.color()
<<"p2.info.address:"<<i.address()<<",p2.info.no:"<<i.no() << endl;
protobuf中添加命名空间:使用package 关键字
syntax="proto3";
package itheima;
message Person{
bytes address=1;
int32 no=2;
}
syntax="proto3";
import "Info.proto";
package itcast;
enum Color{
RED=0;
GREEN=1;
BALCK=2;
PINK=3;
}
message Person{
int32 id=1;
repeated string name=2;
string sex=3;
int32 age=4;
Color color=5;
itheima.Person person=6;
}
相应地在程序中使用对应的命名空间:
int main() {
itcast::Person p;
p.set_age(20);
p.set_id(1001);
p.add_name();
p.set_name(0,"daniel");
p.add_name();
p.set_name(1,"anny");
p.add_name();
p.set_name(2,"alan");
p.set_sex("man");
p.set_color(itcast::PINK);
itheima::Person* ps = p.mutable_person();
ps->set_address("China");
ps->set_no(23);
string output;
output=p.SerializeAsString();
cout << "output:" << output << endl;
itcast::Person p2;
p2.ParseFromString(output);
itheima::Person p3 = p2.person();
cout << "p2.age:" << p2.age() << ",p2.id:" << p2.id()
<< ",p2.name:" << p2.name(0)<<","<<p2.name(1)<<"," <<p2.name(2)
<< ",p2.sex:" << p2.sex() <<",p2.color:"<< p2.color()
<<",p2.person.address:"<<p3.address()<<",p2.person.no:"<<p3.no() << endl;
system("pause");
return 0;
}
业务数据分析:
发送的数据: 对应的proto文件:
syntax="proto3";
message RequestMsg{
int32 cmdType=1;
bytes clientID=2;
bytes serverID=3;
bytes sign=4;
bytes data=5;
}
响应的数据:
对应的proto文件:
syntax="proto3";
message ResponseMsg{
int32 status=1;
int32 seckeyID=2;
bytes clientID=3;
bytes serverID=4;
bytes data=5;
}
二者对应的UML类图描述如下:
win下所有的动态库找不到的问题:将动态库所在目录放入系统环境变量中
库文件名后面带d 的一般用于开发时的debug版本,不带d 的一般用于release版本
编解码类图:
利用protoc生成类文件:
syntax="proto3";
message ResponseMsg{
int32 status=1;
int32 seckeyID=2;
bytes clientID=3;
bytes serverID=4;
bytes data=5;
}
message RequestMsg{
int32 cmdType=1;
bytes clientID=2;
bytes serverID=3;
bytes sign=4;
bytes data=5;
}
Codec基类的代码框架实现:
头文件:
#ifndef CODEC_H
#define CODEC_H
#include <string>
enum Cmd {
SecKeyAgree = 1,
SecKeyCheck,
SecKeyWriteOff
};
class Codec {
public:
Codec();
virtual ~Codec();
virtual std::string encodeMsg() = 0;
virtual void* decodeMsg() = 0;
};
#endif
需要注意的是利用多态技术时基类的析构函数需要实现为虚函数
源文件:
#include "Codec.h"
Codec::Codec() {
}
Codec::~Codec() {
}
下面深入理解一下C++中的虚函数:
https://www.bilibili.com/video/av498280539
首先需要注意的是,即使是一个空类,其实例化的对象至少占用1B的内存空间:
class A {
};
int main(int argc, char* argv[]) {
A a;
cout << "sizeof(a):" << sizeof(a) << endl;
system("pause");
return 0;
}
程序输出结果为1
接下来向类中加入两个普通的成员函数,再观察其sizeof值:
class A {
public:
void func1() {}
void func2() {}
};
int main(int argc, char* argv[]) {
A a;
cout << "sizeof(a):" << sizeof(a) << endl;
system("pause");
return 0;
}
程序输出结果还是为1
说明A的普通成员函数,并不会占用类对象的内存空间
然后向A中加入一个虚函数,再观察a的sizeof值:
class A {
public:
void func1() {}
void func2() {}
virtual void vfunc() {}
};
int main(int argc, char* argv[]) {
A a;
cout << "sizeof(a):" << sizeof(a) << endl;
system("pause");
return 0;
}
程序输出结果为8 ,64位机器一个指针的大小
原因在于编译器向a中插入了一个虚函数表指针vptr :
在vs的调试窗口可以观察到:
当类A中至少包含一个虚函数,编译器会为类A产生一个虚函数表vtbl ,这个虚函数表会一直伴随着类A,包括其装入内存
虚函数表指针被赋值的时机:执行A的构造函数时,让对象的虚函数指针指向类A的vtbl
类实例在内存中的布局;
class A {
public:
void func1() {}
void func2() {}
virtual void vfunc() {}
virtual void vfunc2() {}
virtual ~A() {}
private:
int m_a;
int m_b;
};
int main(int argc, char* argv[]) {
A a;
cout << "sizeof(a):" << sizeof(a) << endl;
system("pause");
return 0;
}
程序输出结果为16,注意普通成员函数不会占用类实例的内存空间,普通成员变量会占用类实例的内存空间:
在vs中也能观察到: 虚函数的工作原理以及多态性的体现(多态一定要有虚函数,没有虚函数多态无从谈起):
用父类指针指向子类对象
class Base {
public:
virtual void myvfunc() {}
};
int main(int argc, char* argv[]) {
Base* b1 = new Base();
b1->myvfunc();
Base b2;
b2.myvfunc();
Base* b3 = &b2;
b3->myvfunc();
system("pause");
return 0;
}
虚函数的表现形式:
- 程序中既存在父类也存在子类,父类中必须含有虚函数,子类中也必须重写父类中的虚函数
- 父类指针指向子类对象,或者父类引用绑定子类对象
- 当通过父类的指针或引用,调用子类中重写的虚函数时,就能看出多态性的表现了
下面是示例代码:
class Base {
public:
virtual void myvfunc() {
cout << "I'm Base" << endl;
}
};
class Derive : public Base {
public:
virtual void myvfunc() {
cout << "I'm Derive" << endl;
}
};
int main(int argc, char* argv[]) {
Derive d1;
Base* b1 = &d1;
b1->myvfunc();
Base* b2 = new Derive();
b2->myvfunc();
Derive d2;
Base& b3 = d2;
b3.myvfunc();
system("pause");
return 0;
}
可以料想上面的调用全部都是多态调用,程序的输出结果也说明了这一点:
I'm Derive
I'm Derive
I'm Derive
考虑下面的继承关系和重写:
class Base {
public:
virtual void f() {}
virtual void g() {}
virtual void h() {}
};
class Derive :public Base {
public:
virtual void g() {}
};
int main(int argc, char* argv[]) {
Base b;
Derive d;
system("pause");
return 0;
}
子类中f()和h()继承自父类,与父类指向相同的位置,但是g()重写后指向子类自己的g(),子类和父类的虚函数表的布局如下:
vs中的调试也说明了这一点:
编解码类的实现,根据下面的UML图:
代码实现如下:
RequestCodec.h
#ifndef REQUESTCODEC_H
#define REQUESTCODEC_H
#include <string>
#include "Codec.h"
#include "Message.pb.h"
struct RequestInfo {
int cmdType;
std::string clientID;
std::string serverID;
std::string sign;
std::string data;
};
class RequestCodec final : public Codec {
public:
RequestCodec();
RequestCodec(const std::string& encstr);
RequestCodec(const RequestInfo* info);
void initMessage(const std::string& encstr);
void initMessage(const RequestInfo* info);
virtual std::string encodeMsg() override;
virtual void* decodeMsg() override;
~RequestCodec();
private:
std::string m_encStr;
RequestMsg m_msg;
};
#endif
RequestCodec.cpp
#include "RequestCodec.h"
RequestCodec::RequestCodec() {
}
RequestCodec::RequestCodec(const std::string& encstr) {
initMessage(encstr);
}
RequestCodec::RequestCodec(const RequestInfo* info) {
initMessage(info);
}
void RequestCodec::initMessage(const std::string& encstr) {
this->m_encStr = encstr;
}
void RequestCodec::initMessage(const RequestInfo* info) {
this->m_msg.set_cmdtype(info->cmdType);
this->m_msg.set_clientid(info->clientID);
this->m_msg.set_serverid(info->serverID);
this->m_msg.set_sign(info->sign);
this->m_msg.set_data(info->data);
}
std::string RequestCodec::encodeMsg() {
std::string output;
this->m_msg.SerializeToString(&output);
return output;
}
void* RequestCodec::decodeMsg() {
this->m_msg.ParseFromString(this->m_encStr);
RequestInfo* reqinfo = new RequestInfo;
reqinfo->cmdType = m_msg.cmdtype();
reqinfo->clientID = m_msg.clientid();
reqinfo->serverID = m_msg.serverid();
reqinfo->sign = m_msg.sign();
reqinfo->data = m_msg.data();
return reqinfo;
}
RequestCodec::~RequestCodec() {
}
ResponseCodec.h
#ifndef RESPONSECODEC_H
#define RESPONSECODEC_H
#include <string>
#include "Codec.h"
#include "Message.pb.h"
struct ResponseInfo {
int status;
int seckeyid;
std::string clientID;
std::string serverID;
std::string data;
};
class ResponseCodec final : public Codec {
public:
ResponseCodec();
ResponseCodec(const std::string& encstr);
ResponseCodec(const ResponseInfo* info);
void initMessage(const std::string& encstr);
void initMessage(const ResponseInfo* info);
virtual std::string encodeMsg() override;
virtual void* decodeMsg() override;
~ResponseCodec();
private:
std::string m_encstr;
ResponseMsg m_msg;
};
#endif
ResponseCodec.cpp
#include "ResponseCodec.h"
ResponseCodec::ResponseCodec() {
}
ResponseCodec::ResponseCodec(const std::string& encstr) {
initMessage(encstr);
}
ResponseCodec::ResponseCodec(const ResponseInfo* info) {
initMessage(info);
}
void ResponseCodec::initMessage(const std::string& encstr) {
this->m_encstr = encstr;
}
void ResponseCodec::initMessage(const ResponseInfo* info) {
this->m_msg.set_status(info->status);
this->m_msg.set_seckeyid(info->seckeyid);
this->m_msg.set_clientid(info->clientID);
this->m_msg.set_serverid(info->serverID);
this->m_msg.set_data(info->data);
}
std::string ResponseCodec::encodeMsg() {
std::string output;
this->m_msg.SerializeToString(&output);
return output;
}
void* ResponseCodec::decodeMsg() {
ResponseInfo* respinfo = new ResponseInfo;
this->m_msg.ParseFromString(this->m_encstr);
respinfo->status = m_msg.status();
respinfo->seckeyid = m_msg.seckeyid();
respinfo->clientID = m_msg.clientid();
respinfo->serverID = m_msg.serverid();
respinfo->data = m_msg.data();
return respinfo;
}
ResponseCodec::~ResponseCodec() {
}
二者的父类Codec:
Codec.h
#ifndef CODEC_H
#define CODEC_H
#include <string>
enum Cmd {
SecKeyAgree = 1,
SecKeyCheck,
SecKeyWriteOff
};
class Codec {
public:
Codec();
virtual ~Codec();
virtual std::string encodeMsg() = 0;
virtual void* decodeMsg() = 0;
};
#endif
Codec.cpp
#include "Codec.h"
Codec::Codec() {
}
Codec::~Codec() {
}
工厂模式
尽可能地在添加新功能时不用修改源代码,必然会用到多态
简单工厂模式:只需要一个工厂类
工厂:使用一个单独的类来做创建实例的过程
简单工厂:把对象的创建放到一个工厂类中,通过参数来创建不同的对象
缺点:每添加一个对象,都需要对简单工厂进行修改(虽然不是删代码,仅仅是添加一个switch-case,但是仍然违背类不改代码的原则)
优点:去除了与具体产品的依赖,实现简单
使用流程:
- 创建一个新的类
- 在该类中添加一个public的成员函数,用于生产对象(实现了多态的子类对象),并返回对象的地址,这称之为工厂函数
伪码描述:
class RequestCodec : public Codec;
class ResponseCodec : public Codec;
class Factory {
public:
Factory();
~Factory();
Codec* createObj(int flag) {
Codec* c = NULL;
switch (flag) {
case 1:
c = new RequestCodec();
break;
case 2:
c = new ResponseCodec();
break;
}
return c;
}
};
当需要判断的条件较多时,使用switch效率更高,但是注意flag应连续性较强
简单工厂类的使用:
Factory* f = new Factory();
Codec* c=f->createObj(1);
c->encodeMsg();
工厂模式:每种产品都由一个工厂来创建,一个工厂保存一个new,会使用两层多态
特点:基本完美,完全遵循“不改代码”的原则
使用流程:
- 创建一个工厂类的基类,指定一个工厂方法,将其设置为虚函数
- 将每一个要创建的子类对象对应一个子工厂类
- 在每个子工厂类中实现父类的虚函数
伪码描述:
class RequestCodec : public Codec;
class ResponseCodec : public Codec;
class TestCodec : public Codec;
class BaseFactory {
public:
BaseFactory();
~BaseFactory();
virtual Codec* createObj() = 0;
};
class RequestFactory : public BaseFactory {
public:
RequestFactory();
~RequestFactory();
Codec* createObj() {
return new RequestCodec();
}
};
class ResponseFactory : public BaseFactory {
public:
ResponseFactory();
~ResponseFactory();
Codec* createObj() {
return new ResponseCodec();
}
};
class TestFactory : public BaseFactory {
public:
TestFactory();
~TestFactory();
Codec* createObj() {
return new TestCodec();
}
};
工厂模式的使用:
BaseFactory* bf = new RequestFactory;
Codec* c = bf->createObj();
c->encodeMsg();
使用工厂模式后整体的UML描述:
客户端服务器通信
多线程与多进程的选择:
- 优先选择多线程
- 当需要启动磁盘上另一个可执行文件文件时需要使用多进程
还可以采用IO多路转接,采用单线程方式处理多客户端连接,但是其效率不高,因为所有客户端的请求都是顺序处理的
效率最高的方式:多线程+IO多路转接
epoll的伪码描述:
void acceptConn(void* arg){
int fd=accept();
epoll_ctl(*arg);
}
void connClient(void* arg){
read();
write();
epoll_ctl(epfd,epoll_ctl_del, fd, NULL);
}
int main(){
int lfd=socket();
bind();
listen();
int epfd=epoll_create(x);
epoll_ctl(epfd, epoll_ctl_add, ev);
struct epoll_event avsp1024];
while(1){
int num=epoll_wait(epfd, evs, 1024, NULL);
for(int i=0;i<num;++i){
int curfd=evs[i].fata.fd;
if(curfd==lfd){
pthread_create(&tid, NULL, acceptConn, &epfd);
}else{
pthread_create(&tid, NULL, connClient, &curfd);
}
}
}
}
线程池:
- 多个线程的集合,可以回收用完的线程
- 不需要频繁的创建和销毁线程,避免了系统开销
线程池中的线程个数:
- 如果业务逻辑是计算密集型,需要大量CPU时间的,则线程个数与CPU核心数保持一致时效率最高
- 如果业务逻辑是IO密集型,则线程个数=2倍CPU核心数
客户端提升效率的优化:连接池
- 在进行业务通信之前,先将需要的连接都创建出来,放到一个容器中
- 当前要通信的时候,从容器中取出一个连接(fd),与服务器进行通信
- 通信完成后,将这个连接放回容器中
伪码描述:
class ConnectionPopl{
public:
ConnectionPool(int N){
for(int i=0;i<N;++i){
int fd=socket();
connect();
this->m_connections.push(fd);
}
}
int getConnection(){
if(this->m_connections.size()>0){
int fd=this->m_connections.head();
this->m_connections.pop();
return fd;
}
return -1;
}
int putConnection(int fd){
this->m_connections.push(fd);
}
~ConnectionPool(){
}
private:
queue<int> m_connections;
};
套接字通信的客户端类封装
class TCPClient{
public:
TCPClient();
~TCPClient();
int connectHost(string ip, unsigned short port, int connectTime);
int disConnect();
int sendMsg(string msg,int timeout=1000);
string receiveMsg(int timeout=1000);
private:
int m_connfd;
};
服务端类封装,服务端不负责通信,只负责监听,如果通信使用客户端类
将上面的TCPClient改为TCPSocket,专门用于通信,则大大简化了服务器类的设计:
class TCPSocket{
public:
TCPSocket(){
this->m_connfd=socket(AF_INET, SOCK_STREAM, 0);
}
TCPSocket(int fd){
this->m_connfd=fd;
}
~TCPSocket();
int connectHost(string ip, unsigned short port, int connectTime){
connect(m_connfd, &serverAddress, &len);
}
int disConnect();
int sendMsg(string msg,int timeout=1000){
send(m_connfd, data, datalen, 0);
}
string receiveMsg(int timeout=1000){
recv(m_connfd, buf, size, 0);
return string(buf);
}
private:
int m_connfd;
};
class TCPServer{
public:
TCPServer();
~TCPServer();
TPCSocket* acceptConn(int timeout){
int fd=accept(m_listenFd, &address, &len);
TCPSocket* t=new TCPSocket(fd);
return t;
}
int setListen(unsigned short port);
private:
int m_listenFd;
};
通信流程伪码描述
服务端:
void* callback(void* arg){
TCPSocket* s=(TCPSocket*)arg;
s->recvMsg();
s->sendMsg();
s->disconnect();
delete s;
return NULL;
}
int main(){
TCPServer* server=new TCPServer();
while(1){
TCPSocket* sck=server->acceptConn();
ptherad_create(&tid, NULL, callback, sck);
}
delete server;
return 0;
}
客户端:
int main(){
TCPSocket* tcp=new TCPSocket();
TCPSocket->connectHost(ip, port, timeout);
tcp->sendMsg();
tcp->recvMsg();
tcp->disConnect();
delete tcp;
return 0;
}
类图描述:
socket通信中会阻塞的函数:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
ssize_t read(int fd, void *buf, size_t count);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t write(int fd, const void *buf, size_t count);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
处理思路:设置一个timeout,当时间到后强制切换线程处理其他任务
使用IO多路转接函数,委托内核检测fds的状态,包括读,写和异常,这些函数的最后一个参数都是设置超时时长,在阻塞时间内,如果由fd状态发生变化,函数直接返回
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval* timeout);
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
accpet超时处理:检测listenFd 的读缓冲区状态即可,这里使用select() 多路IO转接
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
};
struct timeval tval{10,0};
fd_set rdset;
FD_ZERO(&rdset);
FD_SET(listenFd, &rdset);
int ret=select(listenFd+1, &rset, NULL, NULL, &tval);
if(ret==0){
}else if(ret==1){
int ra=accept(listenFd, &clientAddr, &clientAddrLen);
}else{
}
具体代码实现:
TCPSocket* TCPServer::acceptConnect(int waitSeconds) {
int ret = 0;
if (waitSeconds > 0) {
fd_set acceptFdSet;
FD_ZERO(&acceptFdSet);
FD_SET(this->m_listenFd, &acceptFdSet);
struct timeval tval = {waitSeconds, 0};
do {
ret = select(m_listenFd + 1, &acceptFdSet, NULL, NULL, &tval);
} while (ret < 0 && errno == EINTR);
if (ret <= 0) {
return NULL;
}
}
struct sockaddr_in clientAddr;
socklen_t clientAddrLen = sizeof(clientAddr);
int connectFd = accept(m_listenFd, (sockaddr*)&clientAddr, &clientAddrLen);
if (connectFd == -1) {
return NULL;
}
return new TCPSocket(connectFd);
}
read超时处理:同样使用select() 检测fd 的读缓冲区状态:
伪码描述:
struct timeval tval{10,0};
fd_set rdset;
FD_ZERO(&rdset);
FD_SET(fd, &rdset);
int ret=select(connFd+1, &rset, NULL, NULL, &tval);
if(ret==0){
}else if(ret==1){
read()/recv();
}else{
}
具体代码实现:
std::string TCPSocket::recvMsg(int timeout) {
int ret = readTimeout(timeout);
if (ret != 0) {
if (ret == -1 && errno == ETIMEDOUT) {
printf("readTimeout(timeout) error:TimeoutError\n");
return std::string();
} else {
printf("readTimeout(timeout) error:%d\n", errno);
return std::string();
}
}
int netDataLen = 0;
ret = readn(&netDataLen, 4);
if (ret == -1) {
printf("func readn() error:%d\n", ret);
} else if (ret < 4) {
printf("func readn() error, peer closed%d\n", ret);
}
int dataLen = ntohl(netDataLen);
char *buf = (char *)malloc(dataLen);
ret = readn(buf, dataLen);
if (ret == -1) {
printf("func readn() error:%d\n", ret);
} else if (ret < dataLen) {
printf("func readn() error, peer closed%d\n", ret);
}
free(buf);
return std::string(buf, dataLen);
}
write超时处理:本地的写缓冲满了之后,write() 会阻塞,则只需检测write() 函数的写缓冲即可
伪码描述:
struct timeval tval{10,0};
fd_set wrset;
FD_ZERO(&wrset);
FD_SET(fd, &wrset);
int ret=select(connFd+1, &wrset, NULL, NULL, &tval);
if(ret==0){
}else if(ret==1){
write()/send();
}else{
}
具体代码实现:
int TCPSocket::sendMsg(std::string str, int timeout) {
int ret = writeTimeout(timeout);
if (ret == 0) {
int dataLen = str.size() + 4;
char *netData = (char *)malloc(dataLen);
if (netData == NULL) {
ret = MallocError;
printf("func sendMsg malloc error\n");
return ret;
}
int netLen = htonl(str.size());
memcpy(netData, &netLen, 4);
memcpy(netData + 4, str.data(), str.size());
int writed = this->writen(netData, dataLen);
if (writed < dataLen) {
if (netData != NULL) {
free(netData);
}
}
} else {
if (ret == -1 && errno == ETIMEDOUT) {
ret = TimeoutError;
printf("func sendMsg TimeoutError:%d\n", ret);
}
}
return ret;
}
conncet超时处理思路
conncet() 非阻塞:
客户端调用connect() 函数连接服务器,具有阻塞特性
函数返回0表示连接成功,返回-1表示连接失败
该函数默认有一个超时处理,经过一段时间连接失败后会返回,但是这个超时时间过长,需要我们手动处理:
- 设置connect函数操作的文件描述符为非阻塞
- 调用connect
- 使用select检测:需要用getsockopt进行判断
- 设置connect函数操作的文件描述符为阻塞:状态还原
判断socket状态:
代码实现:
int TCPSocket::connectHost(std::string IP, unsigned short port, int timeout) {
int ret = 0;
if (port < 0 || port > 65535 || timeout < 0) {
ret = ParamError;
return ret;
}
this->m_connFd = socket(AF_INET, SOCK_STREAM, 0);
if (m_connFd < 0) {
ret = errno;
printf("func socket() error:%d\n", ret);
return ret;
}
struct sockaddr_in serverAddr;
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(port);
serverAddr.sin_addr.s_addr = inet_addr(IP.data());
ret = this->connectTimeout(&serverAddr, timeout);
if (ret < 0) {
if (ret == -1 && errno == ETIMEDOUT) {
ret = TimeoutError;
} else {
printf("func connectTimeout() error:ret=%d,errno=%d\n", ret, errno);
ret = errno;
}
}
return ret;
}
int TCPSocket::connectTimeout(struct sockaddr_in *addr, uint waitSeconds) {
setNonBlock(this->m_connFd);
int ret = connect(this->m_connFd, (const sockaddr *)addr, sizeof(*addr));
if (ret < 0 && errno == EINPROGRESS) {
struct timeval tval = {waitSeconds, 0};
fd_set wrset;
FD_ZERO(&wrset);
FD_SET(this->m_connFd, &wrset);
do {
ret = select(this->m_connFd + 1, NULL, &wrset, NULL, &tval);
} while (ret < 0 && errno == EINTR);
}
if (ret == 0) {
ret = -1;
errno = ETIMEDOUT;
} else if (ret == 1) {
int opt = 0;
socklen_t optLen = sizeof(opt);
int gr = getsockopt(this->m_connFd, SOL_SOCKET, SO_ERROR, &opt, &optLen);
if (gr == -1) {
ret = -2;
}
if (opt == 0) {
ret = 0;
} else {
errno = opt;
ret = -3;
}
} else {
ret = -4;
}
setBlock(this->m_connFd);
return ret;
}
TCP通信中粘包问题
考虑到网络延迟和内核缓冲区的特性,可能由于接收方接收数据的频率低,导致其一次性接收到多条发送的数据
解决方案:
- 发送数据时,强制flush缓冲区
- 发送数据时对每个数据包添加包头,存储这条数据的metadata,例如这条数据属于谁,有多大,这样就能根据metadata对TCP的数据流进行合理拆分
- 添加结束标记
项目中对粘包的处理:
int TCPSocket::sendMsg(std::string str, int timeout) {
int ret = writeTimeout(timeout);
if (ret == 0) {
int dataLen = str.size() + 4;
char *netData = (char *)malloc(dataLen);
if (netData == NULL) {
printf("func sendMsg() malloc error\n");
return MallocError;
}
int netLen = htonl(str.size());
memcpy(netData, &netLen, 4);
memcpy(netData + 4, str.data(), str.size());
int writed = this->writen(netData, dataLen);
if (writed < dataLen) {
if (netData != NULL) {
free(netData);
}
return -1;
} else {
free(netData);
}
return 0;
} else {
if (ret == -1 && errno == ETIMEDOUT) {
printf("func sendMsg TimeoutError:%d\n", ret);
return TimeoutError;
}
}
}
std::string TCPSocket::recvMsg(int timeout) {
int ret = readTimeout(timeout);
if (ret != 0) {
if (ret == -1 && errno == ETIMEDOUT) {
printf("readTimeout(timeout) error:TimeoutError\n");
return std::string();
} else {
printf("readTimeout(timeout) error:%d\n", errno);
return std::string();
}
}
int netDataLen = 0;
ret = readn(&netDataLen, 4);
if (ret == -1) {
printf("func readn() error:%d\n", ret);
return std::string();
} else if (ret < 4) {
printf("func readn() error, peer closed%d\n", ret);
return std::string();
}
int dataLen = ntohl(netDataLen);
char *buf = (char *)malloc(dataLen + 1);
if (buf == NULL) {
printf("func recvMsg malloc error:%d\n", MallocError);
return std::string();
}
memset(buf, 0, dataLen + 1);
ret = readn(buf, dataLen);
if (ret == -1) {
printf("func readn() error:%d\n", ret);
free(buf);
return std::string();
} else if (ret < dataLen) {
printf("func readn() error, peer closed%d\n", ret);
free(buf);
return std::string();
}
std::string str(buf);
free(buf);
return str;
}
解决安装了动态库却找不到:
- 修改配置文件:
sudo vim /etc/ld.so.conf - 写入动态库的绝对路径
- 使配置文件生效:
sudo ldconfig -v
共享内存
使用流程:
使用流程:
头文件:
#include <sys/ipc.h>
#include <sys/shm.h>
函数原型:
int shmget(key_t key, size_t size, int shmflg);
功能:创建一块共享内存,若其已经存在则打开该共享内存
参数:
- key:通过key记录共享内存在内核中的位置,必须>0
- size:创建时指定大小,如果是打开一块已经存在的共享内存则size=0
- shmflg:创建共享内存的时候使用,类似于open函数的flag:
IPC_CREAT :创建共享内存,需要指定权限:IPC_CREAT|0664 IPC_CREAT|IPC_EXCL :检测共享内存是否存在,若存在返回-1,若不存在返回0
返回值:
On success, a valid shared memory identifier is returned. On error, -1 is returned, and errno is set to indicate the error.
应用:
int shmID=shmget(100, 4096, IPC_CREAT|0664);
int shmID2=shmget(100, 0, 0);
将当前进程和共享内存关联到一起:
函数原型:
void* shmat(int shmid, const void *shmaddr, int shmflg);
参数:
- shmid:
shmget() 的返回值,相当于句柄 - shmaddr:指定共享内存在内核中的位置,一般传NULL,由内核指定
- shmflg:关联成功后对共享内存的操作权限:
SHM_RDONLY 表示只读,0 表示读写
返回值:
On success, shmat() returns the address of the attached shared memory segment; on error, (void *) -1 is returned, and errno is set toindicate the cause of the error.
应用:
void* ptr=shmat(shmID, NULL, 0);
将当前进程和共享内存分离:
函数原型:
int shmdt(const void* shmaddr);
参数:共享内存首地址
返回值:成功返回0,失败返回-1
共享内存操作函数:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
- shmid:操作句柄
- cms:操作类型:
- IPC_STAT:获取共享内存的状态
- IPC_SET:设置共享内存状态
- IPC_RMID:标记共享内存要被销毁
- buf:为第二个参数服务,取决于第二个参数指定的操作类型,删除共享内存时一般传NULL
返回值:成功返回0,失败返回-1
应用实例:删除共享内存:
int ret=shmctl(shmid, IPC_RMID, NULL);
使用共享内存进行进程间通信实例:
写者:
#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main() {
int shmid = shmget(100, 4096, IPC_CREAT | 0664);
void* ptr = shmat(shmid, NULL, 0);
const char* str = "hello,world\n";
memcpy(ptr, str, strlen(str));
printf("blocking...\n");
getchar();
shmdt(ptr);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
读者:
#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main() {
int shmid = shmget(100, 0, 0);
void* ptr = shmat(shmid, NULL, 0);
printf("%s", (char*)ptr);
printf("blocking...\n");
getchar();
shmdt(ptr);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
思考问题:
ipcs查看进程的通信情况:
ipcs -m :查看使用共享内存通信的进程情况
ipcs -s :查看使用信号量通信的进程情况
ipcs -a :查看所有通信的进程情况
使用ipcs -m 查看共享内存状态时,若key==0,说明该共享内存已经被删除,但是还有进程与之关联
使用命令删除共享内存:ipcrm -M shmkey 或ipcrm -m shmid
ftok函数:
key_t ftok(const char *pathname, int proj_id);
调用实例:
key_t ftok("/home/",100);
int shmid = shmget(key_t, 4096, IPC_CREAT | 0664);
mmap和shm的区别:
共享内存类的封装:
代码实现如下:
class BaseShm {
public:
BaseShm(int key);
BaseShm(std::string path);
BaseShm(int key, int size);
BaseShm(std::string path, int size);
~BaseShm();
void* mapShm();
int unmapShm();
int delShm();
private:
int m_shmid;
protected:
void* m_ptr;
};
BaseShm::BaseShm(int key) { this->m_shmid = shmget(key, 0, 0); }
BaseShm::BaseShm(std::string path) {
key_t key = ftok(path.c_str(), 100);
this->m_shmid = shmget(key, 0, 0);
}
BaseShm::BaseShm(int key, int size) {
this->m_shmid = shmget(key, size, IPC_CREAT | 0664);
}
BaseShm::BaseShm(std::string path, int size) {
key_t key = ftok(path.c_str(), 100);
this->m_shmid = shmget(key, size, IPC_CREAT | 0664);
}
BaseShm::~BaseShm() {
unmapShm();
delShm();
}
void* BaseShm::mapShm() {
this->m_ptr = shmat(this->m_shmid, NULL, 0);
return this->m_ptr;
}
int BaseShm::unmapShm() { return shmdt(this->m_ptr); }
int BaseShm::delShm() { return shmctl(this->m_shmid, IPC_RMID, NULL); }
C++语法问题:当父类没有默认构造函数时,子类需显式的调用父类的某一个构造函数,例如以下形式:
SecureKeyShm::SecureKeyShm(int key) : BaseShm(key) {}
SecureKeyShm::SecureKeyShm(std::string name) : BaseShm(name) {}
如果父类的构造函数有默认参数,则可以不必显式调用
openssl加密
常见的哈希算法:
MD5的散列值长度:16B
sha1的散列值长度:20B
MD5的api:
#define MD5_DIGEST_LENGTH 16
int MD5_Init(MD5_CTX *c);
int MD5_Update(MD5_CTX *c, const void *data, size_t len);
int MD5_Final(unsigned char *md, MD5_CTX *c);
unsigned char *MD5(const unsigned char *d, size_t n, unsigned char *md);
api说明:
SH1的api:
# define SHA_DIGEST_LENGTH 20
int SHA1_Init(SHA_CTX *c);
int SHA1_Update(SHA_CTX *c, const void *data, size_t len);
int SHA1_Final(unsigned char *md, SHA_CTX *c);
unsigned char *SHA1(const unsigned char *d, size_t n, unsigned char *md);
SH256的api:
int SHA224_Init(SHA256_CTX *c);
int SHA224_Update(SHA256_CTX *c, const void *data, size_t len);
int SHA224_Final(unsigned char *md, SHA256_CTX *c);
unsigned char *SHA224(const unsigned char *d, size_t n, unsigned char *md);
int SHA256_Init(SHA256_CTX *c);
int SHA256_Update(SHA256_CTX *c, const void *data, size_t len);
int SHA256_Final(unsigned char *md, SHA256_CTX *c);
unsigned char *SHA256(const unsigned char *d, size_t n, unsigned char *md);
用法都同MD5,其余的也是如此
散列值长度:
# define SHA224_DIGEST_LENGTH 28
# define SHA256_DIGEST_LENGTH 32
# define SHA384_DIGEST_LENGTH 48
# define SHA512_DIGEST_LENGTH 64
sha1测试:
void SHA_test() {
SHA_CTX ctx;
SHA1_Init(&ctx);
SHA1_Update(&ctx, "hello", strlen("hello"));
SHA1_Update(&ctx, ", world", strlen(", world"));
unsigned char* md = new unsigned char[SHA_DIGEST_LENGTH];
SHA1_Final(md, &ctx);
char* res = new char[SHA_DIGEST_LENGTH * 2 + 1];
for (int i = 0; i < SHA_DIGEST_LENGTH; ++i) {
sprintf(res + i * 2, "%02x", md[i]);
}
std::cout << "sha1:" << res << std::endl;
delete res;
delete md;
return;
}
哈希类的封装:
代码实现:
enum HashType { H_MD5 = 0, H_SHA1, H_SHA224, H_SHA256, H_SHA384, H_SHA512 };
class Hash {
public:
Hash(HashType hashtype);
~Hash();
void addData(const std::string& str);
std::string result();
std::string str2hash(const std::string& str);
private:
HashType m_hashtype;
void* m_ctx;
unsigned char* m_digest;
char* m_str;
};
Hash::Hash(HashType hashtype) {
this->m_hashtype = hashtype;
switch (hashtype) {
case H_MD5:
m_digest = new unsigned char[MD5_DIGEST_LENGTH];
m_str = new char[MD5_DIGEST_LENGTH * 2 + 1];
m_ctx = new MD5_CTX;
MD5_Init(static_cast<MD5_CTX*>(m_ctx));
break;
case H_SHA1:
m_digest = new unsigned char[SHA_DIGEST_LENGTH];
m_str = new char[SHA_DIGEST_LENGTH * 2 + 1];
m_ctx = new SHA_CTX;
SHA1_Init(static_cast<SHA_CTX*>(m_ctx));
break;
case H_SHA224:
m_digest = new unsigned char[SHA224_DIGEST_LENGTH];
m_str = new char[SHA224_DIGEST_LENGTH * 2 + 1];
m_ctx = new SHA256_CTX;
SHA224_Init(static_cast<SHA256_CTX*>(m_ctx));
break;
case H_SHA256:
m_digest = new unsigned char[SHA256_DIGEST_LENGTH];
m_str = new char[SHA256_DIGEST_LENGTH * 2 + 1];
m_ctx = new SHA256_CTX;
SHA256_Init(static_cast<SHA256_CTX*>(m_ctx));
break;
case H_SHA384:
m_digest = new unsigned char[SHA384_DIGEST_LENGTH];
m_str = new char[SHA384_DIGEST_LENGTH * 2 + 1];
m_ctx = new SHA512_CTX;
SHA384_Init(static_cast<SHA512_CTX*>(m_ctx));
break;
case H_SHA512:
m_digest = new unsigned char[SHA512_DIGEST_LENGTH];
m_str = new char[SHA512_DIGEST_LENGTH * 2 + 1];
m_ctx = new SHA512_CTX;
SHA512_Init(static_cast<SHA512_CTX*>(m_ctx));
break;
default:
m_ctx = nullptr;
break;
}
}
Hash::~Hash() {
if (m_ctx != nullptr) {
switch (m_hashtype) {
case H_MD5:
delete static_cast<SHA_CTX*>(m_ctx);
break;
case H_SHA1:
delete static_cast<SHA_CTX*>(m_ctx);
break;
case H_SHA224:
delete static_cast<SHA256_CTX*>(m_ctx);
break;
case H_SHA256:
delete static_cast<SHA256_CTX*>(m_ctx);
break;
case H_SHA384:
delete static_cast<SHA512_CTX*>(m_ctx);
break;
case H_SHA512:
delete static_cast<SHA512_CTX*>(m_ctx);
break;
default:
break;
}
}
delete m_digest;
delete m_str;
}
void Hash::addData(const std::string& m_str) {
switch (m_hashtype) {
case H_MD5:
MD5_Update(static_cast<MD5_CTX*>(m_ctx), m_str.c_str(), m_str.size());
break;
case H_SHA1:
SHA1_Update(static_cast<SHA_CTX*>(m_ctx), m_str.c_str(), m_str.size());
break;
case H_SHA224:
SHA224_Update(static_cast<SHA256_CTX*>(m_ctx), m_str.c_str(),
m_str.size());
break;
case H_SHA256:
SHA256_Update(static_cast<SHA256_CTX*>(m_ctx), m_str.c_str(),
m_str.size());
break;
case H_SHA384:
SHA384_Update(static_cast<SHA512_CTX*>(m_ctx), m_str.c_str(),
m_str.size());
break;
case H_SHA512:
SHA512_Update(static_cast<SHA512_CTX*>(m_ctx), m_str.c_str(),
m_str.size());
break;
default:
break;
}
}
std::string Hash::result() {
std::string ret;
switch (m_hashtype) {
case H_MD5: {
MD5_Final(m_digest, static_cast<MD5_CTX*>(m_ctx));
for (int i = 0; i < MD5_DIGEST_LENGTH; ++i) {
sprintf(m_str + i * 2, "%02x", m_digest[i]);
}
ret = std::string(m_str);
} break;
case H_SHA1: {
SHA1_Final(m_digest, static_cast<SHA_CTX*>(m_ctx));
for (int i = 0; i < SHA_DIGEST_LENGTH; ++i) {
sprintf(m_str + i * 2, "%02x", m_digest[i]);
}
ret = std::string(m_str);
} break;
case H_SHA224: {
SHA224_Final(m_digest, static_cast<SHA256_CTX*>(m_ctx));
for (int i = 0; i < SHA224_DIGEST_LENGTH; ++i) {
sprintf(m_str + i * 2, "%02x", m_digest[i]);
}
ret = std::string(m_str);
} break;
case H_SHA256: {
SHA256_Final(m_digest, static_cast<SHA256_CTX*>(m_ctx));
for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
sprintf(m_str + i * 2, "%02x", m_digest[i]);
}
ret = std::string(m_str);
} break;
case H_SHA384: {
SHA512_Final(m_digest, static_cast<SHA512_CTX*>(m_ctx));
for (int i = 0; i < SHA384_DIGEST_LENGTH; ++i) {
sprintf(m_str + i * 2, "%02x", m_digest[i]);
}
ret = std::string(m_str);
} break;
case H_SHA512: {
SHA512_Final(m_digest, static_cast<SHA512_CTX*>(m_ctx));
for (int i = 0; i < SHA512_DIGEST_LENGTH; ++i) {
sprintf(m_str + i * 2, "%02x", m_digest[i]);
}
ret = std::string(m_str);
} break;
default:
break;
}
return ret;
}
std::string Hash::str2hash(const std::string& m_str) {
this->addData(m_str);
return this->result();
}
非对称加密的特点和应用场景:
生成RSA密钥对:
RSA *RSA_new(void);
BIGNUM *BN_new(void);
int BN_set_word(BIGNUM *a, BN_ULONG w);
int RSA_generate_key_ex(RSA *rsa, int bits, BIGNUM *e, BN_GENCB *cb);
生成密钥对实例(在内存中):
void generateRSAKsy() {
RSA* rsa = RSA_new();
BIGNUM* bn = BN_new();
BN_set_word(bn, 12345);
RSA_generate_key_ex(rsa, 1024, bn, NULL);
return;
}
将密钥对写入磁盘:
代码实现:
void generateRSAKsy() {
RSA* rsa = RSA_new();
BIGNUM* bn = BN_new();
BN_set_word(bn, 12345);
RSA_generate_key_ex(rsa, 1024, bn, NULL);
FILE* fp = fopen("public.pem", "w");
PEM_write_RSAPublicKey(fp, rsa);
fclose(fp);
fp = fopen("private.pem", "w");
PEM_write_RSAPrivateKey(fp, rsa, NULL, NULL, 0, NULL, NULL);
fclose(fp);
return;
}
vs下可能会报错误:
使用bio方式将密钥对写入磁盘,用法与上面基本相同:
BIO* bio = BIO_new_file("public_bio.pem", "w");
PEM_write_bio_RSAPublicKey(bio, rsa);
BIO_free(bio);
bio = BIO_new_file("private_bio.pem", "w");
PEM_write_bio_RSAPrivateKey(bio, rsa, NULL, NULL, 0, NULL, NULL);
BIO_free(bio);
从内存RSA对象中取出公钥或私钥:
RSA加解密函数:
int RSA_public_encrypt(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding);
int RSA_private_encrypt(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding);
int RSA_public_decrypt(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding);
int RSA_private_decrypt(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding);
参数说明:
返回值是密文长度;
公钥加密代码示例:
从磁盘中读出公钥或私钥:
如果返回值为空,则说明读取失败
代码实现:
string encryptPublicKey() {
string msg = "hello, world";
RSA* publicKey = RSA_new();
FILE* fp = fopen("public.pem", "r");
PEM_read_RSAPublicKey(fp, &publicKey, NULL, NULL);
fclose(fp);
int keyLen = RSA_size(publicKey);
unsigned char* buf = new unsigned char[keyLen];
int strLen = RSA_public_encrypt(msg.size(), (const unsigned char*)msg.c_str(),
buf, publicKey, RSA_PKCS1_PADDING);
string ret = string((char*)buf, strLen);
delete buf;
return ret;
}
string decryptPrivateKey(const string& msg) {
RSA* privateKey = RSA_new();
FILE* fp = fopen("private.pem", "r");
PEM_read_RSAPrivateKey(fp, &privateKey, NULL, NULL);
fclose(fp);
unsigned char* to = new unsigned char[128];
int strLen = RSA_private_decrypt(msg.size(), (unsigned char*)msg.data(), to,
privateKey, RSA_PKCS1_PADDING);
string ret = string((char*)to, strLen);
delete to;
return ret;
}
RSA签名和校验签名:
签名函数:
int RSA_sign(int type, const unsigned char *m, unsigned int m_length, unsigned char *sigret, unsigned int *siglen, RSA *rsa);
校验签名函数:
int RSA_verify(int type, const unsigned char *m, unsigned int m_length, const unsigned char *sigbuf, unsigned int siglen, RSA *rsa);
返回值:成功返回1,失败返回不等于1
签名和验证签名代码实现:
void RSASignVerify_test() {
string s = "hello, world";
FILE* fp = fopen("private.pem", "r");
RSA* privateKey = RSA_new();
PEM_read_RSAPrivateKey(fp, &privateKey, NULL, NULL);
fclose(fp);
int len = RSA_size(privateKey);
unsigned char* sign = new unsigned char[len];
unsigned int outLen;
RSA_sign(NID_sha1, (uchar*)s.data(), s.size(), sign, &outLen, privateKey);
fp = fopen("public.pem", "r");
RSA* publicKey = RSA_new();
PEM_read_RSAPublicKey(fp, &publicKey, NULL, NULL);
fclose(fp);
len = RSA_size(publicKey);
int ret =
RSA_verify(NID_sha1, (uchar*)s.data(), s.size(), sign, outLen, publicKey);
cout << "ret:" << ret << endl;
sign[2] = (sign[2] & 0x80) ? (sign[2] & 0x7f) : (sign[2] | 0x80);
ret =
RSA_verify(NID_sha1, (uchar*)s.data(), s.size(), sign, outLen, publicKey);
cout << "ret:" << ret << endl;
}
RSA类的封装:
AES对称加密:
CBC(cipher block chaining)密文分组链接模式:
- 需要一个初始化向量,可以是一个随机字符串,其长度与分组长度相同
- 加密和解密的时候都需要这个初始化向量,并且保持相同
#include <openssl/aes.h>
# define AES_BLOCK_SIZE 16
int AES_set_encrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key);
int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key);
加解密函数:
# define AES_ENCRYPT 1
# define AES_DECRYPT 0
void AES_cbc_encrypt(const unsigned char *in, unsigned char *out, size_t length, const AES_KEY *key, unsigned char *ivec, const int enc);
AES加解密代码实现:
void AES_test() {
const char *msg = "hello, world";
int msgLen = strlen((char *)msg) + 1;
int length = 0;
if (msgLen % 16 != 0) {
length = (msgLen / 16 + 1) * 16;
} else {
length = msgLen;
}
char *ciphertext = new char[length];
const char *userKey = "0123456776543210";
unsigned char ivec[AES_BLOCK_SIZE];
memset(ivec, 9, sizeof(ivec));
AES_KEY enkey;
AES_set_encrypt_key((uchar *)userKey, 128, &enkey);
AES_cbc_encrypt((uchar *)msg, (uchar *)ciphertext, length, &enkey, ivec, AES_ENCRYPT);
AES_KEY dekey;
AES_set_decrypt_key((uchar *)userKey, 128, &dekey);
unsigned char *data = new unsigned char[length];
memset(ivec, 9, sizeof(ivec));
AES_cbc_encrypt((uchar *)ciphertext, data, msgLen, &dekey, ivec, AES_DECRYPT);
printf("data:%s\n", data);
delete[] ciphertext;
delete[] data;
}
openssl内存释放:
RSA* RSA_new(void);
void RSA_free(RSA* rsa);
BIGNUM* BN_new(void);
void BN_free(BIGNUM* bignum);
jsoncpp的使用
json组织数据的两种方式:数组和对象,可以嵌套使用
数组:也是用[] 表示,但是其中的数据类型可以各异,如array a=[int, double, float, char, string, char*, json数组, json对象]
对象:使用{} 表示,用键值对表示key: value ,key必须是字符串,value任意
实例:
{
"name":"Daniel",
"age":22,
"sex":man,
"married":false,
"family":["father", "mother", "sister"],
"asset":{
"car":"BMW",
"house":"BeiJing"
}
}
写json文件的注意事项:最外层的节点是数组或者对象,根节点只能有一个
VS下生成jsoncpp库:https://blog.csdn.net/qq_43469158/article/details/112172292
linux安装jsoncpp:
sudo apt-get install libjsoncpp-dev
#头文件目录
/usr/include/jsoncpp/json
#动态库目录
/usr/lib/x86_64-linux-gnu
库名称:libjsoncpp.so ,链接选项:-ljsoncpp
jsoncpp中的Value类:
Value(ValueType type = nullValue);
Value(Int value);
Value(UInt value);
Value(Int64 value);
Value(UInt64 value);
Value(double value);
Value(const char* value);
Value(const char* begin, const char* end);
判断存储的对象类型:
bool isNull() const;
bool isBool() const;
bool isInt() const;
bool isInt64() const;
bool isUInt() const;
bool isUInt64() const;
bool isIntegral() const;
bool isDouble() const;
bool isNumeric() const;
bool isString() const;
bool isArray() const;
bool isObject() const;
将Value对象转换为实际的数据类型:
Int asInt() const;
UInt asUInt() const;
Int64 asInt64() const;
UInt64 asUInt64() const;
LargestInt asLargestInt() const;
LargestUInt asLargestUInt() const;
float asFloat() const;
double asDouble() const;
bool asBool() const;
将Value对象转换为字符串(有换行,适合于查看):
String toStyledString() const;
Reader类:使用parse() 将json字符串解析为Value对象:
bool parse(const std::string& document, Value& root, bool collectComments = true);
bool parse(const char* beginDoc, const char* endDoc, Value& root, bool collectComments = true);
bool parse(IStream& is, Value& root, bool collectComments = true);
FasterWriter类:
std::string write(const Value& root) JSONCPP_OVERRIDE;
组织json数据写磁盘代码实例:
void json_write_test() {
Value root;
root.append(12);
root.append(3.14);
root.append("hello, world");
root.append(true);
Value sub;
sub.append(1);
sub.append(2);
sub.append(false);
root.append(sub);
Value obj;
obj["name"] = "daniel";
obj["age"] = 22;
obj["sex"] = "man";
root.append(obj);
FastWriter w;
string json_str = w.write(root);
cout << json_str << endl;
ofstream ofs("test.json");
ofs << json_str;
ofs.close();
}
读json数据代码实例:
void json_read_test() {
ifstream ifs("test.json");
Value root;
Reader r;
r.parse(ifs, root);
if (root.isArray()) {
for (unsigned int i = 0; i < root.size(); ++i) {
if (root[i].isInt()) {
cout << root[i].asInt() << endl;
} else if (root[i].isDouble()) {
cout << root[i].asDouble() << endl;
} else if (root[i].isBool()) {
cout << root[i].asBool() << endl;
} else if (root[i].isString()) {
cout << root[i].asString() << endl;
} else if (root[i].isArray()) {
cout << "oh,it's fucking array" << endl;
} else if (root[i].isObject()) {
cout << root[i]["age"].asString() << endl;
cout << root[i]["name"].asString() << endl;
cout << root[i]["sex"].asString() << endl;
}
}
}
ifs.close();
}
密钥协商客户端需求分析
客户端发起请求:
客户端需要提供和用户交互的功能
客户端与服务器通信需要携带数据:
- 通信的业务数据
- 鉴别客户端身份的数据:客户端的ID
- 和客户端通信的服务器ID
- 实现约定好一个标记
cmdType ,服务器根据这个标记判断客户端的请求类型 - 给对方提供签名,判断数据块是否被修改过
将以上内容封装为结构体:
struct RequestInfo {
int cmdType;
std::string clientID;
std::string serverID;
std::string sign;
std::string data;
};
密钥协商客户端操作流程:
密钥协商服务器业务数据分析:
struct ResponseInfo {
int status;
int seckeyid;
std::string clientID;
std::string serverID;
std::string data;
};
服务器只需被动接受客户端请求,不需要和用户进行交互,守护进程即可,脱离终端
接受客户端请求并处理,给客户端回复数据:
密钥协商服务器业务流程:
使用VS编写linux项目需要设置的属性:
在项目属性的链接器输入中添加库依赖项,加入库的名字,也即gcc中-l的参数,如pthread
虽然不需要,但也可以添加头文件目录,方便代码提示
语言选择-std=gnu++11
密钥协商流程
客户端main函数处理逻辑:
int main(int argc, char* argv[]) {
while (1) {
int select = input();
switch (select) {
case 1:
break;
case 2:
break;
case 3:
break;
case 4:
break;
case 0:
break;
default:
break;
}
}
printf("\nbye\n");
return 0;
}
int input() {
int select = -1;
printf("\n***************");
printf("\n***1.密钥协商***");
printf("\n***2.密钥校验***");
printf("\n***3.密钥注销***");
printf("\n***4.密钥查看***");
printf("\n***0.退出系统***");
scanf("%d", &select);
while (getchar() != '\n')
;
return select;
}
客户端操作的封装:
enum Cmd {
SecKeyAgree = 1,
SecKeyCheck,
SecKeyWriteOff
};
class ClientOP {
public:
explicit ClientOP(const std::string& configFilePath);
~ClientOP();
bool secKeyAgree();
void secKeyCheck();
void secKeyWriteOff();
private:
std::string m_clientID;
std::string m_clientIP;
std::string m_clientPort;
std::string m_serverID;
std::string m_serverIP;
std::string m_serverPort;
};
智能指针的使用:std::shared_ptr<TCPSocket> tcp(new TCPSocket());
客户端密钥协商的实现:
bool ClientOP::secKeyAgree(bool generateKey, const std::string& publicKeyPath = "", const std::string& privateKeyPath = "") {
RequestInfo reqInfo;
reqInfo.cmdType = SecKeyAgree;
reqInfo.clientID = this->m_clientID;
reqInfo.serverID = this->m_serverID;
reqInfo.data = "";
reqInfo.sign = "";
RSACrypto* rsa;
if (generateKey) {
rsa = new RSACrypto();
std::ifstream ifs("public.pem");
std::stringstream strs;
strs << ifs.rdbuf();
std::string pubKey = strs.str();
reqInfo.data = pubKey;
reqInfo.sign = rsa->sign(pubKey);
ifs.close();
} else {
rsa = new RSACrypto(publicKeyPath, privateKeyPath);
std::ifstream ifs(publicKeyPath);
std::stringstream strs;
strs << ifs.rdbuf();
std::string pubKey = strs.str();
reqInfo.data = pubKey;
reqInfo.sign = rsa->sign(pubKey);
ifs.close();
}
CodecFactory* reqcFactory = new RequestFactory(&reqInfo);
Codec* reqc = reqcFactory->createCodec();
std::string encstr = reqc->encodeMsg();
delete reqcFactory;
delete reqc;
std::shared_ptr<TCPSocket> tcp(new TCPSocket());
int ret = tcp->connectHost(this->m_serverIP, std::stoi(m_serverPort));
std::string response;
if (ret != 0) {
return false;
} else {
ret = tcp->sendMsg(encstr);
if (ret != 0) {
return false;
}
response = tcp->recvMsg();
}
CodecFactory* respcFactory = new ResponseFactory(response);
Codec* respc = respcFactory->createCodec();
ResponseInfo* respinfo;
respinfo = (ResponseInfo*)respc->decodeMsg();
delete respcFactory;
delete respc;
if (respinfo->status != 0) {
return false;
} else {
this->m_AESKey = rsa->decryptByPrivateKey(respinfo->data);
return true;
}
delete respinfo;
delete rsa;
}
在C++中使用pthrea_create() ,回调函数必须是如下三种类型:
如下面的例子:
void ServerOP::start() {
std::shared_ptr<TCPServer> s(new TCPServer);
while (1) {
std::shared_ptr<TCPSocket> tcp(s->acceptConnect());
if (tcp == nullptr) {
continue;
}
pthread_t tid;
pthread_create(&tid, NULL, working, NULL);
}
}
working() 函数按照上述三种写法如下:
使用静态函数:
class ServerOP {
public:
explicit ServerOP(const std::string& configFilePath);
~ServerOP();
void start();
static void* working(void* arg);
private:
std::string m_serverID;
std::string m_serverIP;
std::string m_serverPort;
};
友元函数实现,并不建议使用友元函数,因为它破坏了类的封装性:
class ServerOP {
public:
explicit ServerOP(const std::string& configFilePath);
~ServerOP();
void start();
friend void* working(void* arg);
private:
std::string m_serverID;
std::string m_serverIP;
std::string m_serverPort;
};
void* working(void* arg) {
}
普通全局函数实现,略
服务端密钥协商的实现:
void ServerOP::start() {
m_server->setListen(stoi(this->m_serverPort));
while (1) {
TCPSocket* tcp = m_server->acceptConnect();
if (tcp == nullptr) {
continue;
}
pthread_t tid;
pthread_rwlock_wrlock(&m_tid2tcp_lock);
pthread_create(&tid, NULL, working, (void*)this);
this->m_tid2tcp.insert(std::make_pair(tid, tcp));
pthread_rwlock_unlock(&m_tid2tcp_lock);
}
}
void* ServerOP::working(void* arg) {
ServerOP* self = (ServerOP*)arg;
pthread_rwlock_rdlock(&(self->m_tid2tcp_lock));
TCPSocket* tcp = self->m_tid2tcp[pthread_self()];
pthread_rwlock_unlock(&(self->m_tid2tcp_lock));
std::string msg = tcp->recvMsg();
CodecFactory* reqFactory = new RequestFactory(msg);
Codec* reqc = reqFactory->createCodec();
RequestInfo* reqinfo = (RequestInfo*)reqc->decodeMsg();
delete reqFactory;
delete reqc;
switch (reqinfo->cmdType) {
case SecKeyAgree:
self->secKeyAgree(reqinfo);
break;
case SecKeyCheck:
break;
default:
break;
}
delete reqinfo;
return nullptr;
}
void ServerOP::secKeyAgree(RequestInfo* reqinfo) {
std::ofstream ofs1("public.pem");
ofs1 << reqinfo->data;
ofs1.close();
ResponseInfo respinfo;
RSACrypto rsa("public.pem", "");
if (!rsa.verify(reqinfo->data, reqinfo->sign)) {
respinfo.status = -1;
} else {
std::string key = getRandKeyString(Len16);
std::string cipherKey = rsa.encryptByPublicKey(key);
respinfo.clientID = reqinfo->clientID;
respinfo.data = cipherKey;
respinfo.seckeyid = 0;
respinfo.serverID = m_serverID;
respinfo.status = 0;
}
CodecFactory* respFactory = new ResponseFactory(&respinfo);
Codec* respc = respFactory->createCodec();
std::string msg = respc->encodeMsg();
delete respFactory;
delete respc;
int ret = this->m_tid2tcp[pthread_self()]->sendMsg(msg);
if (ret == 0) {
this->m_tid2tcp[pthread_self()]->~TCPSocket();
this->m_tid2tcp.erase(pthread_self());
} else {
}
}
生成随机密钥:
std::string ServerOP::getRandKeyString(KeyLen keylen) {
srand(time(NULL));
std::string ret(keylen, '\0');
int flag = 0;
const char* specialChara = "~!@#$%^&*(){}|:<>?";
for (int i = 0; i < keylen; ++i) {
flag = rand() % 4;
switch (flag) {
case 0:
ret[i] = 'a' + rand() % 26;
break;
case 1:
ret[i] = 'A' + rand() % 26;
break;
case 2:
ret[i] = '0' + rand() % 10;
break;
case 3:
ret[i] = specialChara[rand() % strlen(specialChara)];
break;
default:
break;
}
}
return ret;
}
openssl库排查错误的方法:
#include <openssl/err.h>
std::cout << "error:" << std::hex << ERR_get_error() << std::endl;
openssl errstr HEXNUM
注意在RSACrypto 加密工具类中提供的签名和校验签名函数中要注意签名数据不能过长,为了解决这个问题,应该将msg先生成哈希值,再对哈希值进行签名和校验签名:
std::string RSACrypto::sign(const std::string& msg, int hashType) {
int len = RSA_size(m_privateKey);
unsigned char* sign = new unsigned char[len];
unsigned int signLen = 0;
Hash h;
std::string digest = h.str2hash(msg);
int RSA_signRet = RSA_sign(hashType, (uchar*)digest.data(), digest.size(), sign, &signLen, m_privateKey);
if (RSA_signRet != 1) {
std::cout << "\nERR_get_error:" << std::hex << ERR_get_error() << std::endl
<< "RSA_signRet:" << RSA_signRet << std::endl
<< "signLen:" << signLen << std::endl;
}
std::string ret = std::string((char*)sign, (int)signLen);
delete[] sign;
return ret;
}
bool RSACrypto::verify(const std::string& msg, const std::string& signature, int hashType) {
Hash h;
std::string digest = h.str2hash(msg);
int ret = RSA_verify(hashType, (uchar*)digest.data(), digest.size(), (uchar*)signature.data(), signature.size(), m_publicKey);
if (ret == 1) {
return true;
} else {
return false;
}
}
base64
解决传输过程中出现的\0 问题:使用base64编码
alphabet:
base64应用场景:
base64算法:
编码之后数据变长了,每3个字节为一组,编码后每组增加一个字节
编码之后的数据的= 代表填充数据0
openssl中BIO链的工作模式:
bio对应的api:
bio链类似于流水线的概念,将不同功能的节点串联起来后就能完成一连串操作
使用openssl的bio进行base64编解码操作:
编码:
string bio_base64_encode_demo(const string &s) {
const char *str = s.data();
BIO *b64 = BIO_new(BIO_f_base64());
BIO *mem = BIO_new(BIO_s_mem());
b64 = BIO_push(b64, mem);
BIO_write(b64, str, strlen(str));
BIO_flush(b64);
BUF_MEM *ptr;
BIO_get_mem_ptr(b64, &ptr);
char *buf = new char[ptr->length];
memcpy(buf, ptr->data, ptr->length);
string ret(buf);
BIO_free_all(b64);
delete[] buf;
return ret;
}
解码:
string bio_base64_decode_demo(const string &s) {
BIO *b64 = BIO_new(BIO_f_base64());
BIO *mem = BIO_new_mem_buf(s.data(), s.length());
mem = BIO_push(b64, mem);
char *buf = new char[s.length()];
BIO_read(mem, buf, s.length());
string ret(buf);
BIO_free_all(b64);
delete[] buf;
return ret;
}
日志类与单例模式
单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
- 私有化它的构造函数,以防止外界创建单例类的对象;
- 使用类的私有静态指针变量指向类的唯一实例;
- 使用一个公有的静态方法获取该实例。
class Logger final {
public:
enum Type {
CONSOLE,
FILE
};
enum Level {
DEBUG,
INFO,
WARRING,
ERROR,
CRITICAL
};
static Logger* getInstanse();
void Log(std::string text, std::string file, int line, Level level = INFO);
void setEnableLevel(Level level);
void setDevice(Type device);
~Logger();
private:
Logger();
Logger(const Logger& logger);
Type m_device;
std::ofstream m_writer;
static Logger m_log;
Level m_level;
Type m_device;
};
采取了饿汉单例模式:将构造函数声明为私有,并将Logger的一个类实例作为全局的成员变量保存,提供一个static方法获取该实例
连接池代码实现:
class ConnectionPool final {
public:
ConnectionPool(std::string IP, unsigned short port, int capacity);
TCPSocket* getConnection();
void putConnection(TCPSocket* tcp, bool isValid = true);
bool isEmpty();
~ConnectionPool();
private:
void createConnection();
std::string m_serverIP;
unsigned short m_port;
int m_capacity;
int m_nodeNum;
std::queue<TCPSocket*> m_queue;
pthread_mutex_t m_lock;
};
ConnectionPool::ConnectionPool(std::string IP, unsigned short port, int capacity)
: m_serverIP(IP),
m_port(port),
m_capacity(capacity),
m_nodeNum(capacity) {
pthread_mutex_init(&m_lock, NULL);
createConnection();
}
TCPSocket* ConnectionPool::getConnection() {
if (m_queue.empty()) {
std::cout << "getConnection() error:queue is empty" << std::endl;
return NULL;
}
pthread_mutex_lock(&m_lock);
TCPSocket* tcp = m_queue.front();
m_queue.pop();
pthread_mutex_unlock(&m_lock);
std::cout << "get a connection,current queue size:" << m_queue.size() << std::endl;
return tcp;
}
void ConnectionPool::putConnection(TCPSocket* tcp, bool isValid = true) {
if (isValid) {
pthread_mutex_lock(&m_lock);
m_queue.push(tcp);
pthread_mutex_unlock(&m_lock);
std::cout << "put a valid connection,current queue size:" << m_queue.size() << std::endl;
} else {
tcp->disconnect();
delete tcp;
pthread_mutex_lock(&m_lock);
m_nodeNum = m_queue.size() + 1;
pthread_mutex_unlock(&m_lock);
createConnection();
}
}
bool ConnectionPool::isEmpty() {
return m_queue.empty();
}
ConnectionPool::~ConnectionPool() {
pthread_mutex_destroy(&m_lock);
while (m_queue.size() > 0) {
TCPSocket* tcp = m_queue.front();
m_queue.pop();
delete tcp;
}
}
void ConnectionPool::createConnection() {void ConnectionPool::createConnection() {
std::cout << "current queue size:" << m_queue.size() << " nodeNum:" << m_nodeNum << std::endl;
if (m_queue.size() >= m_nodeNum) {
return;
}
TCPSocket* tcp = new TCPSocket();
int ret = tcp->connectHost(m_serverIP, m_port);
if (ret == 0) {
m_queue.push(tcp);
} else {
delete tcp;
std::cout << "createConnection() connectHost() failed,ret:" << ret << std::endl;
}
createConnection();
}
C++小知识:inline 成员函数必须在声明同时实现,不可分离
gdb小知识:全部执行完当前循环:until
time函数:1970.1.1到现在经过的秒数
因为服务器会连接多个不同的客户端,所以其需要多个不同的密钥;而客户端只需要一个密钥即可
为了支持高并发和集群,服务器的共享内存可以换为redis
共享内存
存储共享内存的数据结构定义:
struct NodeShmInfo {
NodeShmInfo()
: status(0), seckeyID(0) {
memset(clientID, 0, 12 + 12 + 128);
}
int status;
int seckeyID;
char clientID[12];
char serverID[12];
char seckey[128];
};
共享内存初始化与读写函数:
void SecureKeyShm::shmInit() {
if (this->m_ptr) {
memset(this->m_ptr, 0, m_maxNode * sizeof(NodeShmInfo));
}
}
int SecureKeyShm::shmWrite(NodeShmInfo* pnsmi) {
NodeShmInfo* nodeArr = static_cast<NodeShmInfo*>(mapShm());
if (nodeArr == nullptr || pnsmi == nullptr) {
return -1;
}
for (int i = 0; i < m_maxNode; ++i) {
if (strcmp(pnsmi->clientID, nodeArr[i].clientID) == 0 && strcmp(pnsmi->serverID, nodeArr[i].serverID) == 0) {
memcpy(&nodeArr[i], pnsmi, sizeof(NodeShmInfo));
unmapShm();
return 0;
}
if (nodeArr[i].status == 0) {
memcpy(&nodeArr[i], pnsmi, sizeof(NodeShmInfo));
unmapShm();
return 0;
}
}
return -2;
}
NodeShmInfo SecureKeyShm::shmRead(const std::string& clientID, const std::string& serverID) {
NodeShmInfo* nodeArr = static_cast<NodeShmInfo*>(mapShm());
if (nodeArr == nullptr) {
return NodeShmInfo();
}
const char* s1 = clientID.data();
const char* s2 = serverID.data();
for (int i = 0; i < m_maxNode; ++i) {
if (nodeArr[i].status == 1 && strcmp(s1, nodeArr[i].clientID) == 0 && strcmp(s2, nodeArr[i].serverID) == 0) {
NodeShmInfo ret(nodeArr[i]);
unmapShm();
return ret;
}
}
return NodeShmInfo();
}
occi
Oracle C++ Call Interface (OCCI)
su - root ,- 代表环境变量一并切换
查看防火墙状态:systemctl status firewalld
启动防火墙:systemctl start firewalld
关闭防火墙:systemctl stop firewalld
永久启动防火墙:systemctl enable firewalld
永久关闭防火墙:systemctl disable firewalld
启动oracle数据库
sqlplus / as sysdba
startup
shutdown immediate
lsnrctl start
occi使用步骤:
初始化环境:
Environment *env = Environment::createEnvironment();
Environment::terminateEnvironment(env);
连接:
virtual Connection * createConnection(
const OCCI_STD_NAMESPACE::string &userName,
const OCCI_STD_NAMESPACE::string &password,
const OCCI_STD_NAMESPACE::string &connectString = "") = 0;
连接串:IP:Port/orcl
数据表(mysql):
create database if not exists secmng character set utf8;
use secmng;
create table secnode(
id VARCHAR(7),
node_name VARCHAR(128) not null,
node_desc VARCHAR(512),
create_time datetime,
auth_code int,
node_state int
);
alter table secnode add constraint PK_SECNODE primary key(id);
insert into secnode values('hz_s001','Online banking center1','HangZhou','2022-02-03 12:32:09',187,0);
insert into secnode values('hz_c001','HangZhou user1','HangZhou','2022-08-03 14:32:48',173,0);
insert into secnode values('hz_s002','Online banking center2','HangZhou','2022-09-03 12:32:09',188,0);
insert into secnode values('hz_c002','HangZhou user2','HangZhou','2022-02-23 14:32:48',174,0);
insert into secnode values('hz_s003','Online banking center3','HangZhou','2022-02-03 12:32:09',189,0);
insert into secnode values('hz_c003','HangZhou user3','HangZhou','2022-02-13 14:32:48',175,0);
insert into secnode values('gz_s001','GuangDong sub center','GuangDong','2012-04-09 12:32:34',198,0);
commit;
create table seckeyinfo(
clientid VARCHAR(7),
serverid VARCHAR(7),
keyid int,
create_time datetime,
key_state int,
seckey VARCHAR(512)
);
alter table seckeyinfo add constraint PK_SECKEYINFO primary key(keyid);
alter table seckeyinfo add constraint FK_SECKEYINFO_CLIENTID foreign key(clientid) references secnode(id);
alter table seckeyinfo add constraint FK_SECKEYINFO_SERVERID foreign key(serverid) references secnode(id);
commit;
create table keysn(
ikeysn int primary key
);
insert into keysn values(1);
commit;
为了保证数据安全,可以创建多个二级用户,拥有不同的权限,例如创建只能查询的用户
增加操作mysql数据库类:
#ifndef MYSQLOP_H
#define MYSQLOP_H
#include <mysql/mysql.h>
#include <iostream>
#include "Logger.h"
#include "SecureKeyShm.h"
class MySQLOP final {
public:
MySQLOP(std::string host, std::string username, std::string pwd, std::string db);
MySQLOP(const MySQLOP& m) = delete;
MySQLOP operator=(const MySQLOP& m) = delete;
~MySQLOP();
int getKeyID();
bool updateKeyID(int keyID);
bool writeSecKey(const NodeShmInfo* pnode);
std::string getCurrTime();
private:
MYSQL* m_mysql;
};
MySQLOP::MySQLOP(std::string host, std::string username, std::string pwd, std::string db) {
m_mysql = mysql_init(NULL);
if (m_mysql == nullptr) {
}
m_mysql = mysql_real_connect(m_mysql, host.data(), username.data(), pwd.data(), db.data(), 0, NULL, 0);
if (m_mysql == nullptr) {
std::cout << "connect to mysql error" << std::endl;
}
mysql_query(m_mysql, "set autocommit=1");
std::cout << "connected to mysql" << std::endl;
}
MySQLOP::~MySQLOP() {
if (m_mysql) {
mysql_close(m_mysql);
}
}
int MySQLOP::getKeyID() {
const char* sql = "select ikeysn from keysn";
mysql_query(m_mysql, sql);
MYSQL_RES* res = mysql_store_result(m_mysql);
MYSQL_ROW rows = mysql_fetch_row(res);
if (rows[0] != nullptr) {
return atoi(rows[0]);
} else {
return -1;
}
}
bool MySQLOP::updateKeyID(int keyID) {
std::string sql = "update keysn set ikeysn=";
sql += std::to_string(keyID);
return 0 == mysql_query(m_mysql, sql.data());
}
bool MySQLOP::writeSecKey(const NodeShmInfo* pnode) {
char query_str[1024] = {0};
memset(query_str, 0, sizeof(query_str));
sprintf(query_str, "insert into seckeyinfo values('%s','%s',%d,date_format('%s','%%Y%%m%%d%%H%%i%%s'),%d,'%s')", pnode->clientID, pnode->serverID, pnode->seckeyID, getCurrTime().data(), pnode->status, pnode->seckey);
return 0 == mysql_query(m_mysql, query_str);
}
std::string MySQLOP::getCurrTime() {
time_t t = time(NULL);
char ch[64] = {0};
char result[100] = {0};
strftime(ch, sizeof(ch) - 1, "%Y-%m-%d %H:%M:%S", localtime(&t));
sprintf(result, "%s", ch);
return std::string(result);
}
#endif
配置管理终端:使用qt实现对secnode的crud,略过
外联接口
向用户提供加解密的api而非密钥
配置文件:
提供动态库或静态库:
AES对称加密类:
#ifndef AESCRYPTO_H
#define AESCRYPTO_H
#include <openssl/aes.h>
#include <string.h>
#include <string>
#include "base64.h"
class AESCrypto final {
public:
AESCrypto(std::string key);
~AESCrypto();
std::string AES_CBC_Entrypt(std::string text);
std::string AES_CBC_Decrypt(std::string ciphertext);
private:
std::string m_key;
AES_KEY m_encKey;
AES_KEY m_decKey;
};
AESCrypto::AESCrypto(std::string key) {
size_t len = key.size();
if (len == 16 || len == 24 || len == 32) {
const unsigned char* aeskey = (const unsigned char*)key.data();
AES_set_encrypt_key(aeskey, len * 8, &m_encKey);
AES_set_decrypt_key(aeskey, len * 8, &m_decKey);
m_key = key;
}
}
AESCrypto::~AESCrypto() {
}
std::string AESCrypto::AES_CBC_Entrypt(std::string text) {
int len = 0;
if (text.size() % 16 != 0) {
len = (text.size() / 16 + 1) * 16;
} else {
len = text.size();
}
unsigned char* cipherdata = new unsigned char[len];
unsigned char ivec[AES_BLOCK_SIZE];
memset(ivec, 9, sizeof(ivec));
AES_cbc_encrypt((unsigned char*)text.data(), cipherdata, len, &m_encKey, ivec, AES_ENCRYPT);
char* out = new char[BASE64_ENCODE_OUT_SIZE(len)];
base64_encode(cipherdata, len, out);
std::string ret(out);
delete[] out;
delete[] cipherdata;
return ret;
}
std::string AESCrypto::AES_CBC_Decrypt(std::string ciphertext) {
unsigned char* cipherdata = new unsigned char[BASE64_DECODE_OUT_SIZE(ciphertext.size())];
int len = base64_decode(ciphertext.data(), ciphertext.size(), cipherdata);
unsigned char* out = new unsigned char[len];
unsigned char ivec[AES_BLOCK_SIZE];
memset(ivec, 9, sizeof(ivec));
AES_cbc_encrypt(cipherdata, out, len, &m_decKey, ivec, AES_DECRYPT);
std::string ret((char*)out);
delete[] out;
delete[] cipherdata;
return ret;
}
#endif
外联接口类:
#ifndef CRYPTOINTERFACE_H
#define CRYPTOINTERFACE_H
#include <jsoncpp/json/json.h>
#include <fstream>
#include <string>
#include "AESCrypto.h"
#include "BaseShm.h"
#include "SecureKeyShm.h"
class CryptoInterface {
public:
CryptoInterface(std::string configure);
~CryptoInterface();
std::string encryptoData(std::string text);
std::string decryptoData(std::string ciphertext);
private:
std::string m_key;
};
CryptoInterface::CryptoInterface(std::string configure) {
std::ifstream ifs(configure);
Json::Value root;
Json::Reader r;
r.parse(ifs, root);
std::string key = root["shmKey"].asString();
std::string serverID = root["serverID"].asString();
std::string clientID = root["clientID"].asString();
int maxNode = root["maxNode"].asInt();
SecureKeyShm shm(key, maxNode);
NodeShmInfo node = shm.shmRead(clientID, serverID);
m_key = node.seckey;
ifs.close();
}
CryptoInterface::~CryptoInterface() {
}
std::string CryptoInterface::encryptoData(std::string text) {
AESCrypto aes(m_key);
return aes.AES_CBC_Entrypt(text);
}
std::string CryptoInterface::decryptoData(std::string ciphertext) {
AESCrypto aes(m_key);
return aes.AES_CBC_Decrypt(ciphertext);
}
#endif
将外联接口封装为库:
g++ -c *.cpp -fPIC
g++ -shared *.o -o libname.so
密钥校验和密钥注销
密钥校验:客户端将本地的密钥求哈希,将散列值发送给服务器比对即可
密钥注销:客户端将本地密钥状态进行修改,并通知服务器该ID的密钥废弃,服务器端通过该ID将共享内存中的密钥标记为不可用,并更新数据库
密钥查看:客户端委托服务器查数据库
|