数据安全传输平台项目笔记

课程地址

项目介绍与密码学知识

项目架构图

数据安全传输平台项目笔记_第1张图片
主要功能:对网络通信的数据进行加解密

基础组件:

  1. 数据序列化:protobuf
  2. socket通信:线程池,连接池
  3. 共享内存IPC
  4. Mysql数据库
  5. 数据加密:openssl(Secure Sockets Layer)

对称加密与非对称加密

加密三要素:

  1. 明文、密文
  2. 秘钥:一个定长的字符串
  3. 算法:加密算法和解密算法

对称加密:密钥比较短且只有一个,加密和解密使用的密钥是相同的,是由自己负责生成一个随机字符串。加密效率高,但加密强度低,密钥分发困难,不能在网络环境中直接传送。以AES为代表

非对称加密:密钥比较长,加密和解密使用的密钥不同,是由专门的算法生成的,分为公钥(小)和私钥(大)。如果使用公钥加密,必须使用私钥解密;如果使用私钥加密,必须使用公钥解密。公钥可以直接在网络环境中分发

为了兼顾效率和安全性,通常采用非对称加密加密对称加密的密钥,这样就可以安全地进行对称加密了,流程:

  • 客户端生成(读入)密钥对,将公钥封装到请求报文,并对其使用私钥进行签名,将签名同样封装到请求报文中
  • 服务端收到请求报文后并验明身份后生成一个随机字符串,对其使用公钥进行加密,封装到响应报文中发送给客户端
  • 客户端收到响应报文后使用私钥解密,这样两端就安全地得到了对称加密的密钥

常用加密算法

DES与3DES:

DES:已经被破解了,不安全。它的秘钥长度为8byte,对数据分段加密,每组8字节,得到的密文和明文长度是相同的

3DES:即3重DES,它是安全的,但是效率比较低

它的秘钥长度24字节,在算法内部会被平均分成3份,每份8字节,所以可以将其看成是3个秘钥,每个8字节

处理逻辑:

  • 加密:使用秘钥1进行加密
  • 解密:使用秘钥2进行解密,由于秘钥并不匹配,相当于又做了一次加密
  • 加密:使用秘钥3进行加密

三组秘钥都不同,加密了3次,此时3DES加密的等级是最高的。如果3次秘钥都相同,3DES就退化为了DES


AES:

它是一种最安全,效率最高的公开的对称加密算法

秘钥长度可以选择:16字节,24字节,32字节。秘钥越长加密的数据越安全,但是效率越低

它也是分组加密,每组长度 16 字节。每组的密文和明文的长度相同,都为16byte


哈希算法(单向散列函数)

  1. 不管原始数据有多长,得到的哈希结果的位数是一样长的
  2. 原始数据相差一点,得到的结果完全不一样
  3. 有很强的抗碰撞性
  4. 不可逆:不可能由结果反推原内容(这就决定了它不能是加密算法,因为不能解密)

应用场景:

  1. 数据校验:下载文件时会提供MD5值,便于校验文件是否被篡改过
  2. 登录验证:在数据库中不必保存密码明文,只需保存其对应的散列值即可
  3. 网盘的秒传功能(百度网盘)

哈希运算的结果又可被称为:哈希值,指纹,摘要

MD4/MD5:散列值长度16B,抗碰撞性已经被破解

SHA-1:散列值长度20B

SHA-2(224,256,384,512):每个子类名表明了其占用的比特位数


消息认证码HMAC

作用:验证通信时的数据有没有被篡改

HMAC的本质还是一个散列值

HMAC=hash(原始数据, 密钥)

缺点:

  1. 密钥分发困难
  2. 不能区分消息的所有者

数字签名(私钥加密公钥解密的过程)

目的:防止篡改数据,保证数据的完整性

作用:可以甄别文件的所有者

发送者数字签名的过程:

  1. 生成一个非对称密钥对,分发公钥
  2. 使用散列函数对原始数据进行hash运算,得到散列值摘要(散列值更小,相较于原始数据更容易加密)
  3. 对散列值摘要使用私钥进行加密,得到密文
  4. 将原始数据和密文一起发送给接收者

接收者校验签名的过程:

  1. 接收签名的一方分发的公钥
  2. 接收发送者发来的数据:原始数据和签名
  3. 对接收的原始数据进行同样的哈希运算,得到散列值1
  4. 使用公钥对密文进行解密,得到散列值2
  5. 比对这两个散列值,若相同,则证明安全

加密库:OpenSSL

测试demo:

#include 
#include 
#include 
#include 

//#define _CRT_SECURE_NO_WARNINGS

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

如果安装了库但是动态链接器找不到:

查找库所在目录:

$ locate libcrypto
/usr/lib/x86_64-linux-gnu/libcrypto.so.3

将其路径写入/etc/ld.so.conf文件中,并使其生效:sudo ldconfig

编译时添加选项-lcrypto


他这里的git讲的很烂,不记笔记了

protobuf部署:

win:先用CMake生成VS工程,然后用VS编译,使用时添加PROTOBUF_USE_DLLS宏定义

linux:下载源码,然后三部曲:

git clone https://github.com/google/protobuf --branch 3.8.x		#使用稳定版
sudo apt-get install libtool		#安装编译工具
./autogen.sh

configure
make
sudo 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类图描述如下:

数据安全传输平台项目笔记_第2张图片

  • public: +
  • protected: #
  • private: -

Inheritance

使用一端带有空心三角的实线指向基类

由上面的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类图继承关系表示如下:

数据安全传输平台项目笔记_第3张图片

抽象类和抽象方法:包含纯虚函数的类称为抽象类,不能被实例化,子类必须重写父类的纯虚函数

比方说我想实现一个链表(Link),插入(insert)与删除(remove)动作我想让子类去实现,链表本身只实现统计链表中元素个数的动作(count),然后有一个子类单向链表(SingleLink)去实现父类没有实现的动作,C++代码为:

class Link {
 public:
  //纯虚函数
  virtual void insert() = 0;
  virtual void move() = 0;
  int count();
};

class SingleLink : public Link {
 public:
  void insert() { }
  void move() { }
};

UML图描述如下,注意抽象类的类名字体是倾斜的,纯虚函数的名称字体也是倾斜的
数据安全传输平台项目笔记_第4张图片


Association

一个类的对象作为另外一个类的成员变量

关联(Assocition) 关系是类与类之间最常见的一种关系,它是一种结构化的关系,表示一-类对象与另一类对象之间有联系,如汽车和轮胎、师傅和徒弟、班级和学生等。在UML类图中,用实线连接有关联关系的对象所对应的类,在C++中通常将一个类的对象作为另一个类的成员变量。关联关系分单向关联、双向关联、自关联

class Address {};

class Customer {
 private:
  Address addr;
};

关联关系用->来表示:

数据安全传输平台项目笔记_第5张图片

双向关联,相互包含:

class Product {
 private:
  Customer customer;
};

class Customer {
 private:
  Product product[64];
};

数据安全传输平台项目笔记_第6张图片

自关联:例如链表

class Node {
 private:
  Node* next;
};

数据安全传输平台项目笔记_第7张图片


Aggregation

聚合关系:表示总体与个体的关系,在聚合关系中,成员对象是总体的一部分,但是成员对象可以脱离整体对象而独立存在。例如一群海龟,一群羊

在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图中

数据安全传输平台项目笔记_第8张图片


Composition

组合关系:表示一种整体与部分的关系,但是在组合关系中整体对象可以控制成员对象的生命周期,一旦整体对象不存在,成员对象也不存在,整体对象和成员对象之间具有同生共死的关系。在UML中用带实心菱形的直线表示

class Mouse {};

class Nose {};

class Head {
 public:
  Head() {
    this->mouse = new Mouse();
    this->nose = new Nose();
  }
  void shake() {}
  
  ~Head() {
  	delete mouse;
  	delete nose;
  }

 private:
  Mouse* mouse;
  Nose* nose;
};

数据安全传输平台项目笔记_第9张图片

关于组合关系中成员对象的构造与析构:

#include 
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函数中就不会再析构了


Dependency

依赖关系是一种广泛存在的使用关系,在UML图中,用带箭头的虚线相连

  1. 一个类的对象作为另一个类的方法的参数或返回值
  2. 在一个类的方法中将另一个类的对象作为其对象的局部变量
  3. 在一个类的方法中调用另一个类的静态方法

依赖关系实例:

class Car {
 public:
  void move();
};

class Driver {
 public:
  void drive(Car c) { c.move(); }
};

数据安全传输平台项目笔记_第10张图片

下面是一个购物商城的UML类图实例:
数据安全传输平台项目笔记_第11张图片


Protobuf

序列化(Serialization):将对象的状态信息转换为可以存储或传输的形式的过程,与之相对应的过程称之为反序列化(Unserialization)

序列化和反序列化主要用于解决在跨平台和跨语言的情况下,模块之间的交互和调用,但其本质是为了解决数据传输问题

实现数据序列化,要有原始数据:

  • 复合类型:最常见的情况
  • 基础数据类型

然后通过某些方式,将其转化问另外一种形式的数据

最后,将得到的数据进行分发,分发到不同的平台,保证不同的平台能正确解析:

  • 网络传输
  • 磁盘拷贝

常见的数据序列化方式:

  1. XML
  2. JSON
  3. Protobuf

protobuf使用步骤:

  • 首先准备一个数据结构,可以为复合类型或基础类型
  • 然后创建一个新文件:file.proto
  • 然后将要序列化的数据写入到file.proto文件(有一定的语法格式)
  • 接着通过一个命令protoc file.proto --cpp_out=./file.proto文件生成一个C++类:对应一个头文件和一个源文件
  • 最后直接使用这个类,里面有读写数据的api

proto文件的语法格式:

syntax="proto3";

message struct_name{
	data_type data_name=data_index;	//start from 1
	...
}

protobuf中的数据类型:

数据安全传输平台项目笔记_第12张图片

例如对于这样一个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下使用步骤:

  1. 包含进头文件和源文件
  2. 工程属性配置头文件目录和库目录
  3. 工程属性的链接器输入添加lib库
  4. 预处理器添加宏定义PROTOBUF_USE_DLLS

测试代码:

#include 
#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;
}

在程序中使用:

//enum
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类
	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");

	//enum
	p.set_color(itcast::PINK);

	//其一个成员是itheima命名空间下的Person类
	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;
}

业务数据分析:

要发送的数据:

struct RequestMsg {
	int cmdType;
	string clientID;
	string serverID:
	string sign;
	string data;
};

对应的proto文件:

syntax="proto3";

message RequestMsg{
	int32 cmdType=1;
	bytes clientID=2;
	bytes serverID=3;
	bytes sign=4;
	bytes data=5;
}

响应的数据:

struct RespondMsg {
	bool rv;
	int seckeyID;
	string clientID;
	string serverID;
	string datga;
};

对应的proto文件:

syntax="proto3";

message ResponseMsg{
	int32 status=1;
	int32 seckeyID=2;
	bytes clientID=3;
	bytes serverID=4;
	bytes data=5;
}

二者对应的UML类图描述如下:

数据安全传输平台项目笔记_第13张图片


win下所有的动态库找不到的问题:将动态库所在目录放入系统环境变量中

库文件名后面带d的一般用于开发时的debug版本,不带d的一般用于release版本

编解码类图:

数据安全传输平台项目笔记_第14张图片

利用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 

enum Cmd {
	SecKeyAgree = 1,
	SecKeyCheck,
	SecKeyWriteOff
};

class Codec {
public:
	Codec();
	virtual ~Codec();
	//纯虚,基类不实现,交给子类实现
	virtual std::string encodeMsg() = 0;
	virtual void* decodeMsg() = 0;
};
#endif

需要注意的是利用多态技术时基类的析构函数需要实现为虚析构。目的是为了保证delete父类指针时,子类的析构函数也能被调用

源文件:

#include "Codec.h"

Codec::Codec() {
}

Codec::~Codec() {
}

编解码类的代码实现:

请求报文的编解码:

//RequestCodec.h
#ifndef REQUESTCODEC_H
#define REQUESTCODEC_H
#include 

#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"

//默认构造,手动调用initMessage()
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);
}

//序列化到output并返回
std::string RequestCodec::encodeMsg() {
	std::string output;
	this->m_msg.SerializeToString(&output);
	return output;
}

//反序列化,并将信息存储到RequestInfo并返回
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 

#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"

//默认构造,手动调用initMessage()
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);
}

//将m_msg中的信息序列化为string并返回
std::string ResponseCodec::encodeMsg() {
	std::string output;
	this->m_msg.SerializeToString(&output);
	return output;
}

//将m_encstr中的信息反序列化为ResponseInfo并返回其指针
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 

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,但是仍然违背类不改代码的原则

优点:去除了与具体产品的依赖,实现简单

使用流程:

  1. 创建一个新的类
  2. 在该类中添加一个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,会使用两层多态

特点:基本完美,完全遵循“不改代码”的原则

使用流程:

  1. 创建一个工厂类的基类,指定一个工厂方法,将其设置为虚函数
  2. 将每一个要创建的子类对象对应一个子工厂类
  3. 在每个子工厂类中实现父类的虚函数

伪码描述:

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

工厂模式的使用:

//创建RequestFactory对象,由父类指向,这里实现了多态
BaseFactory* bf = new RequestFactory;
//bf生产出一个RequestCodec实例,并由其父类Codec指向,这里也实现了多态
Codec* c = bf->createObj();
//使用c
c->encodeMsg();

使用工厂模式后整体的UML描述:

数据安全传输平台项目笔记_第15张图片


客户端服务器通信

多线程与多进程的选择:

  • 优先选择多线程
  • 当需要启动磁盘上另一个可执行文件文件时需要使用多进程

还可以采用IO多路转接,采用单线程方式处理多客户端连接,但是其效率不高,因为所有客户端的请求都是顺序处理的

效率最高的方式:多线程+IO多路转接

epoll的伪码描述:

void acceptConn(void* arg){
	int fd=accept();
	//将fd添加到epoll树上
	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){
				//accept();
				pthread_create(&tid, NULL, acceptConn, &epfd);
			}else{
				//read();	write();
				//这里不能直接传curfd的地址
				//而且考虑到断开连接,需要将epfd也一并传入
				pthread_create(&tid, NULL, connClient, &curfd);
			}
		}
	}
}

线程池:

  • 多个线程的集合,可以回收用完的线程
  • 不需要频繁的创建和销毁线程,避免了系统开销

需要一个管理者线程和N个工作者线程。用一个任务队列存储任务

使用条件变量 cond_x_broadcast/signalcond_x_wait 进行控制

线程池中的线程个数:

  • 如果业务逻辑是计算密集型,需要大量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;
}

类图描述:

数据安全传输平台项目笔记_第16张图片


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

//滑动窗口满时write也会阻塞
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);

//关于timeval
struct timeval {
	time_t      tv_sec;         /* seconds */
    suseconds_t tv_usec;        /* microseconds */
};

//调用实例
struct timeval tval{10,0};
fd_set rdset;
FD_ZERO(&rdset);
FD_SET(listenFd, &rdset);
int ret=select(listenFd+1, &rdset, NULL, NULL, &tval);
if(ret==0){
	//timeout
}else if(ret==1){
	//有新连接
	int ra=accept(listenFd, &clientAddr, &clientAddrLen);
}else{
	//返回了-1,发生了错误
}

具体代码实现:

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;
    }
  }
  // ret>0,这时调用accept必然不会阻塞
  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, &rdset, NULL, NULL, &tval);
if(ret==0){
	//timeout
}else if(ret==1){
	//有新连接
	read()/recv();	//有对方发过来的新数据到达
}else{
	//返回了-1,发生了错误
}

具体代码实现:

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){
	//timeout
}else if(ret==1){
	//有新连接
	write()/send();	//有对方发过来的新数据到达
}else{
	//返回了-1,发生了错误
}

具体代码实现:

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()非阻塞:

数据安全传输平台项目笔记_第17张图片

客户端调用connect()函数连接服务器,具有阻塞特性

函数返回0表示连接成功,返回-1表示连接失败

该函数默认有一个超时处理,经过一段时间连接失败后会返回,但是这个超时时间过长,需要我们手动处理

  1. 设置connect函数操作的文件描述符为非阻塞
  2. 调用connect
  3. 使用select检测:需要用getsockopt进行判断
  4. 设置connect函数操作的文件描述符为阻塞:状态还原

判断socket状态:

数据安全传输平台项目笔记_第18张图片

代码实现:

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

  //这时调用connect()就不会阻塞等待连接建立成功了
  int ret = connect(this->m_connFd, (const sockaddr *)addr, sizeof(*addr));
  if (ret < 0 && errno == EINPROGRESS) {  //正在连接中
    //设置select监听
    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) {
    //在timeout内连接成功,获取文件描述符状态检测是否连接成功
    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通信中粘包问题

客户端每隔1s给服务器发送一条数据,每条数据长度 100字节,服务器每隔2s接收一次数据

  • 服务器接收一个数据得到200字节:粘包

怎么造成的:

  • 发送的时候,内核进行了优化,数据到达一定量发送一次
  • 网络环境不好,有延时
  • 接收方接收数据频率低,一次性读到了多条客户端发送的数据

考虑到网络延迟和内核缓冲区的特性,可能由于接收方接收数据的频率低,导致其一次性接收到多条发送的数据

解决方案:

  • 发送数据时,强制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 {     // writeTimeout()超时
    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;
}

解决安装了动态库却找不到:

  1. 修改配置文件:sudo vim /etc/ld.so.conf
  2. 写入动态库的绝对路径
  3. 使配置文件生效:sudo ldconfig -v

共享内存

使用流程:

  1. 向内核申请一块指定大小的共享内存
  2. 如果有2个进程,需要通信,可以使用这块共享内存来完成
  3. 进程A和进程B分别和共享内存相关联
  4. 两个进程可以通过这个首地址对共享内存进行读/写操作
  5. 如果这个进程不再使用这块共享内存,需要和共享内存断开关联。进程的退出对共享内存是没有任何影响的
  6. 当不再使用共享内存的时候,需要将共享内存销毁

shmget

#include 
#include 
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

返回值:成功返回一个共享内存id;失败返回-1并设置errno

应用:

//创建一块共享内存
int shmID=shmget(100, 4096, IPC_CREAT|0664);
//打开一块共享内存
int shmID2=shmget(100, 0, 0);

shmat

功能:将当前进程和共享内存关联到一起

函数原型:

void* shmat(int shmid, const void *shmaddr, int shmflg);

参数:

  • shmid:shmget()的返回值,相当于句柄
  • shmaddr:指定共享内存在内核中的位置,一般传NULL,由内核指定
  • shmflg:关联成功后对共享内存的操作权限:SHM_RDONLY表示只读,0表示读写

返回值:成功返回被关联的共享内存地址;失败返回(void *)-1并设置errno

应用:

void* ptr=shmat(shmID, NULL, 0);

shmdt

功能:将当前进程和共享内存分离

函数原型:

int shmdt(const void* shmaddr);

参数:共享内存首地址

返回值:成功返回0;失败返回-1

shmctl

共享内存操作函数:

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 
#include 
#include 
#include 

int main() {
  //申请一块共享内存
  int shmid = shmget(100, 4096, IPC_CREAT | 0664);

  //将shmid指向的共享内存与当前进程相关联
  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 
#include 
#include 
#include 

int main() {
  //打开一块共享内存
  int shmid = shmget(100, 0, 0);

  //将shmid指向的共享内存与当前进程相关联
  void* ptr = shmat(shmid, NULL, 0);
  printf("%s", (char*)ptr);

  printf("blocking...\n");
  getchar();

  //解除关联
  shmdt(ptr);
  //标记删除
  shmctl(shmid, IPC_RMID, NULL);
  return 0;
}

思考问题:

操作系统如何知道一块共享内存被多少进程关联?

  • 共享内存维护了一个结构体 struct shmid_ds ,其中有一个成员 shm_nattch 记录了关联的进程个数

是不是可以对共享内存进行多次调用 shmctl 删除?

  • 可以多次调用 shmctl,因为 shmctl 函数是标记删除共享内存,而不是直接删除。只有当关联这块共享内存的进程个数==0的时候,才真正地被删除了

ipcs查看进程的通信情况:

  • ipcs -m:查看使用共享内存通信的进程情况
  • ipcs -s:查看使用信号量通信的进程情况
  • ipcs -a:查看所有通信的进程情况

使用ipcs -m查看共享内存状态时,若key==0,说明该共享内存已经被删除,但是还有进程与之关联

使用命令删除共享内存:ipcrm -M shmkeyipcrm -m shmid


ftok

功能:convert a pathname and a project identifier to a System V IPC key

key_t ftok(const char *pathname, int proj_id);

参数:

  • pathname:一个存在的绝对路径,或路径中对应的文件名
  • proj_id:目前只使用了该变量的1字节,取值范围0 - 255

调用实例:

key_t = ftok("/home/",100);
int shmid = shmget(key_t, 4096, IPC_CREAT | 0664);

mmap和shm的区别:

数据安全传输平台项目笔记_第19张图片


共享内存类的封装:

代码实现如下:

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;
};

/*
根据key打开一块shm
*/
BaseShm::BaseShm(int key) { this->m_shmid = shmget(key, 0, 0); }

/*
根据path打开一块shm
*/
BaseShm::BaseShm(std::string path) {
  key_t key = ftok(path.c_str(), 100);
  this->m_shmid = shmget(key, 0, 0);
}

/*
根据key和size创建一块shm
*/
BaseShm::BaseShm(int key, int size) {
  this->m_shmid = shmget(key, size, IPC_CREAT | 0664);
}

/*
根据path和size创建一块shm
*/
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();
}

/*
功能:将当前进程关联到创建/打开的shm
返回值:shm的首地址
*/
void* BaseShm::mapShm() {
  this->m_ptr = shmat(this->m_shmid, NULL, 0);
  return this->m_ptr;
}

/*
功能:将当前进程解除关联到创建/打开的shm
返回值:成功返回0,失败返回-1
*/
int BaseShm::unmapShm() { return shmdt(this->m_ptr); }

/*
功能:标记删除创建/打开的shm
返回值:成功返回0,失败返回-1
*/
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(20B), sha1(20B), sha224, sha256, sha384, sha512

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说明:

数据安全传输平台项目笔记_第20张图片

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, world", strlen("hello, world"));
  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;
}

非对称加密的特点和应用场景:

数据安全传输平台项目笔记_第21张图片
生成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);

数据安全传输平台项目笔记_第22张图片
生成密钥对实例(在内存中):

void generateRSAKsy() {
  RSA* rsa = RSA_new();
  BIGNUM* bn = BN_new();
  BN_set_word(bn, 12345);
  RSA_generate_key_ex(rsa, 1024, bn, NULL);
  return;
}

将密钥对写入磁盘:

数据安全传输平台项目笔记_第23张图片

代码实现:

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;
}

使用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对象中取出公钥或私钥:

数据安全传输平台项目笔记_第24张图片

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

参数说明:

  • flen:要加密/解密的数据长度,0 < flen <= 秘钥长度 - 11
  • from:传入参数,要加密/解密的数据
  • to:传出参数,存储密文/明文
  • padding:数据填充。RSA_PKCS1_PADDING会填充11字节

返回值是密文长度;

从磁盘中读出公钥或私钥:

数据安全传输平台项目笔记_第25张图片
如果返回值为空,则说明读取失败

代码实现:

string encryptPublicKey() {
  string msg = "hello, world";
  //这里必须要RSA_new()
  RSA* publicKey = RSA_new();
  FILE* fp = fopen("public.pem", "r");
  PEM_read_RSAPublicKey(fp, &publicKey, NULL, NULL);
  fclose(fp);

  // buf的大小指定为和密钥长度一致即可128B
  // 密文的长度和密钥的长度相同
  //可以通过RSA_size()计算密钥长度
  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);

数据安全传输平台项目笔记_第26张图片
校验签名函数:

int RSA_verify(int type, const unsigned char *m, unsigned int m_length, const unsigned char *sigbuf, unsigned int siglen, RSA *rsa);

数据安全传输平台项目笔记_第27张图片
返回值:成功返回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);
  //开辟签名的缓冲区sing
  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] = (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类的封装:
数据安全传输平台项目笔记_第28张图片

AES对称加密:

数据安全传输平台项目笔记_第29张图片

CBC(cipher block chaining)密文分组链接模式:

数据安全传输平台项目笔记_第30张图片

  • 需要一个初始化向量,可以是一个随机字符串,其长度与分组长度相同
  • 加密和解密的时候都需要这个初始化向量,并且保持相同
#include 
# 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);

数据安全传输平台项目笔记_第31张图片

加解密函数:

//加解密使用同一个函数
# 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);

数据安全传输平台项目笔记_第32张图片

AES加解密代码实现:

void AES_test() {
    //待加密的数据
    const char *msg = "hello, world";
    int msgLen = strlen((char *)msg) + 1;
    //长度必须是16的整数倍
    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);

    // cbc加密
    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];
    // ivec可能会被更改,再次使用需要重新memset
    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); ///< Copy til first 0. (NULL causes to seg-fault.)
Value(const char* begin, const char* end); ///< Copy all, incl zeroes.

判断存储的对象类型:

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);
//is是文件流对象,使用这个流对象打开一个磁盘文件
bool parse(IStream& is, Value& root, bool collectComments = true);

FasterWriter类:

std::string write(const Value& root) JSONCPP_OVERRIDE;

数据安全传输平台项目笔记_第33张图片

组织json数据写磁盘代码实例:

void json_write_test() {
    Value root;
    // Json::Value &Json::Value::append(const Json::Value &value),这里进行了隐式类型转换
    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);

    //带格式的字符串
    // string json_str = root.toStyledString();

    //不带格式的字符串
    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();
}

密钥协商客户端需求分析

数据安全传输平台项目笔记_第34张图片

客户端发起请求:

  • 密钥协商
  • 密钥校验
  • 密钥注销

客户端需要提供和用户交互的功能

客户端与服务器通信需要携带数据:

  • 通信的业务数据
  • 鉴别客户端身份的数据:客户端的ID
  • 和客户端通信的服务器ID
  • 实现约定好一个标记cmdType,服务器根据这个标记判断客户端的请求类型
  • 给对方提供签名,判断数据块是否被修改过

将以上内容封装为结构体:

struct RequestInfo {
    int cmdType;
    std::string clientID;
    std::string serverID;
    std::string sign;	//对data的签名
    std::string data;	//业务数据,根据cmdType的不同而不同
};

密钥协商客户端操作流程:

数据安全传输平台项目笔记_第35张图片

密钥协商服务器业务数据分析:

struct ResponseInfo {
    int status;		//客户端请求的处理状态
    int seckeyid;	//只在密钥协商生成新密钥时才有用
    std::string clientID;
    std::string serverID;
    std::string data;	//实际的业务数据
};

服务器只需被动接受客户端请求,不需要和用户进行交互,守护进程即可,脱离终端

接受客户端请求并处理,给客户端回复数据:

  • 请求的处理状态
  • 针对业务逻辑处理得到的业务数据

密钥协商服务器业务流程:

数据安全传输平台项目笔记_第36张图片


使用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 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 {
		//使用私钥对数据进行解密得到AES密钥
		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;
	//从map中获取通信套接字的指针
	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();

	//生产一个request解码类并解码得到reqinfo
	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;
		//TODO需要读数据库
		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 {  //发送失败
		//TODO错误处理
	}
}

生成随机密钥:

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 
std::cout << "error:" << std::hex << ERR_get_error() << std::endl;
//shell中:
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,再对digest签名
	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);
	//校验签名:先hash,再对digest校验签名
	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:

数据安全传输平台项目笔记_第37张图片

base64应用场景:
在这里插入图片描述

base64算法:

数据安全传输平台项目笔记_第38张图片
编码之后数据变长了,每3个字节为一组,编码后每组增加一个字节

编码之后的数据的=代表填充数据0

openssl中BIO链的工作模式:

数据安全传输平台项目笔记_第39张图片

bio对应的api:

数据安全传输平台项目笔记_第40张图片

bio链类似于流水线的概念,将不同功能的节点串联起来后就能完成一连串操作

数据安全传输平台项目笔记_第41张图片

使用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;
}

日志类与单例模式

单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

  1. 私有化它的构造函数,以防止外界创建单例类的对象;
  2. 使用类的私有静态指针变量指向类的唯一实例;
  3. 使用一个公有的静态方法获取该实例。
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使用步骤:

数据安全传输平台项目笔记_第42张图片

初始化环境:

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 

#include 

#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) {
		// TODO
	}
	m_mysql = mysql_real_connect(m_mysql, host.data(), username.data(), pwd.data(), db.data(), 0, NULL, 0);
	if (m_mysql == nullptr) {
		// TODO
		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));
	// insert into seckeyinfo values('hz_c001','hz_s001',187,date_format('2008-08-08 22:23:01', '%Y%m%d%H%i%s'),1,'randomstringtest');
	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);
	// TODO错误处理
	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而非密钥

数据安全传输平台项目笔记_第43张图片

配置文件:

数据安全传输平台项目笔记_第44张图片

提供动态库或静态库:

数据安全传输平台项目笔记_第45张图片

AES对称加密类:

#ifndef AESCRYPTO_H
#define AESCRYPTO_H
#include 
#include 

#include 

#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((uchar *)msg, (uchar *)ciphertext, length, &enkey, ivec, AES_ENCRYPT);
	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 

#include 
#include 

#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将共享内存中的密钥标记为不可用,并更新数据库

密钥查看:客户端委托服务器查数据库

你可能感兴趣的:(其他,c++,安全,https,uml,linux)