单例模式可能是大家最为熟知的一种设计模式,本身没什么好谈的。但是在 C++ 中,由单例模式可以引出一系列的问题,可能会比较有意思,这里探讨一下。
常见的简单实现
1.使用 static 实现
class S1 {
public:
static S1& GetInstance() {
static S1 s;
return s;
}
void fun() {}
private:
S1(){cout<<"S1 inited"<<endl;}
S1(const S1&) = delete;
S1& operator=(const S1&) = delete;
};
class S2{
public:
static S2& GetInstance(){
return s;
}
void fun(){}
private:
S2(){cout<<"S2 inited"<<endl;}
//...
private:
static S2 s;
};
S2 S2::s;
其中 S2 是要避免的。因为 1. 类的静态成员变量的初始化时间一般早于 main 函数 2. 如果静态成员的初始化里调用了其它类,可能出现未定义的错误。
2.使用指针判断是否初始化
class S3{
public:
static S3& GetInstance(){
if(s == 0)
s = new S3();
return *s;
}
private:
S3(){}
static S3 *s;
};
这两种方式在单线程程序中使用都是可以的,但如果在多线程中,就会出现问题。
Magic Static
对于 S1 要注意, C++ 局部静态变量的初始化可能不是线程安全的 , 这就是 Magic Static ,是指 返回一个静态局部变量的引用 的用法,在某些情况下,如S1 可能会被编译器这样解析:
继续阅读
TCP 协议错综复杂, 很容易出现错误,错误码非常之多。这里探究一些在编程中比较常见的错误码。tcp 错误码 在不同的操作系统中的值不同。这里取用 Linux 与 Windows 两种,如 connection_refused 在windows 中的值为10054, 在 Linux 中为54 .
eof :
eof 标志着流的结束。当对端关闭连接(调用了 shutdown 或 close) 后,处于 CLOSE_WAIT 状态。 本端会收到 FIN, ACK 。此时如果本端再试图 read ,则会读到 eof
connection_refused
61 : Connection refused
10061 : No connection could be made because the target machine actively refused it . 由于目标计算机积极拒绝,无法连接
在客户端试图与服务端建立连接的时候发生。一般是服务端没有处于监听状态。 客户端发送发 SYN , 但是收到了 RST, ACK
connection_reset
54 : Connection reset by peer
10054 : An existing connection was forcibly closed by the remote host . 远程主机强迫关闭了一个现有的连接。
对端对处于连接状态的socket 进行了异常断开, 如进程中断等。此时如果本端进行读操作,可能会得到此错误。 继续阅读
最近一段时间在解决一个网络方面的 BUG,发现自己对 TCP 的状态了解得不够,于是复习了一遍 TCP 的各种状态与状态间的转换,并做了一个整理,以加深自己的理解。
网络上的两台设备想要一起工作,就必须使用相同的网络协议。像 TCP 这种复杂的协议,我们很难简洁地描述其各种确切的操作。所以我们试图使用有限状态机来解释这个复杂的协议。
一. 有限状态机
有限状态机 (FSM : Finite State Machine) 又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。FSM 的四个基本概念如下:
状态 state: 描述机器在特定时间上处于的 环境(circumstance)或状况(status)
转换 Transition: 从一种状态到另一种状态的行为(act)
事件 Event: 导致状态发生变化的事情
动作 Action: 机器从一种状态转换之前对事件所做的响应
FSM通过解释协议可以处于的所有不同状态、可以在每个状态中发生的事件、针对事件采取的操作以及结果发生的转换来描述协议。协议通常在第一次运行时以特定的开始状态启动。尔后,它遵循一系列步骤使其进入常规操作状态,并根据特定类型的输入或其他情况移动到其他状态。状态机之所以称为有限状态机,是因为只有有限数量的状态。
二. TCP 的操作与状态
对于 TCP 来说,可以使用 FSM 来描述一个连接的生命周期 : 一个 TCP 设备和另一个 TCP 设备之间的每个连接,都从一个空状态(null state) 开始,经过一系列的状态变化,直到建立了(established)连接。然后它将保持这种状态,直到遇到某种事件,它将进行一系列状态直到回到关闭状态。
我们使用三个缩写词来表示状态间转换的三种类型消息,它们对应于 TCP 头的标志。(关于 TCP 头的内容可以参见我之前的博客 《TCP 协议概述》) :
继续阅读
前一章介绍了 cmake 的基本语法 以及如何构建一个最简单的工程。这里接着聊一聊使用 cmake 构建工程的常用操作:添加库
一、由源代码添加库
这一节中我们将向工程中添加一个库项目。
假设我们需要开发一个 mathlib 库, 并在其他项目中调用,可以像下面这样操作:
1. 在工程路径下建立子文件夹 mathlib
2. 在 ./mathlib 中添加项目源文件 mymath.h , mymath.cxx,并添加 CMakeLists.txt:
继续阅读
组织结构
cmake 文件包括 CMakeLists.txt 和以 .cmake 为后缀的文件。
程序源文件最外层的 CMakeLists.txt 文件是 cmake 的入口文件,这个文件可以定义了整个工程的构建规范。它也可以使用 add_subdirectory() 命令来包含一个子文件夹,每个使用该命令添加的子文件夹中也需要有一个 CMakeLists.txt 文件。
变量
变量的包装与展开
在 cmake 中, 变量都是以字符串的形式存在的。使用 set( ) 包装变量, 使用 ${} 展开变量。${} 可以嵌套使用。 使用未包装的变量会导致空展开:
set(var_name_1 var1)
message("var_name is ${var_name_1}")
set(var_name_2 var2)
set(${var_name_2} var) #same as set(var2 var)
set(${${var_name_2}}_x foo) #same as set(var_x foo)
message("var2 is ${var2}")
message("var_name_2 is ${var_name_2}")
message("var_x is ${var_x}")
message("empty var is ${empty_var}")
执行结果如下:
var_name is var1
var2 is var
var_name_2 is var2
var_x is foo
empty var is
列表
继续阅读
什么是 CMake

当我们在构建工程时,在不同的平台上使用不同的工具,如在Windows上使用 Visual Studio 工程, 在 Mac 上使用 Xcode 工程。
这些工程只限于特定的平台。当我们需要跨平台构建工程时,就需要使用 CMake 了,它是一个开源的 跨平台系统构建工具 。同一个 CMake 工程可以在不同的平台上转换为与平台适应的工具,极大的方便了跨平台工程构建。
第一个 CMake
新建一个空目录,在其中新建一个文本文件,命名为 CMakeLists.txt,并输入以下内容:
message("Hello,This is my first cmake project")
好了,你的第一个 cmake 工程就已经建好了。接下来,新建一个文件夹 build, 并在该文件夹下运行命令:
cmake ..
你可能会得到以下输出:
-- Building for: Visual Studio 15 2017
-- The C compiler identification is MSVC 19.15.26732.1
-- The CXX compiler identification is MSVC 19.15.26732.1
-- Check for working C compiler: ....../cl.exe
-- Check for working C compiler: ....../cl.exe -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: ....../cl.exe
-- Check for working CXX compiler: ....../cl.exe -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
Hello,This is my first cmake project
-- Configuring done
-- Generating done
-- Build files have been written to: ....../build
再来看这个路径下,cmake 生成了一堆文件:

我们成功的使用 cmake 在 Windows 上构建了一个 vs 工程。 当然,如果你的操作系统是 MacOS, 或者 Linux, 结果后有所不同。
其实在 CMakeLists.txt 所以在路径下也能运行 cmake 命令。不过cmake没有提供专门的工具来清理生成的文件,为了方便管理,我们将其生成在 build 目录下。
这个 cmake 工程的作用仅仅是输出一了条消息,并没有什么意义。下面我们来让它更有意义一点:
继续阅读
在之前一篇博客 ECC(Elliptic Curves Cryptography) 椭圆曲线加密原理 简单地阐述了 ECC 加解密的原理。这本篇博客中接着来聊一聊如何使用 OPENSSL 来进行 ECC 加解密。
首先需要明确一点的是:ECC 本身并没有定义一套加解密的方法,它主要作用于密钥交换(ECDHE),与签名认证(ECDSA). 不过后来中国工程师设计定义了一套加密方法,并于近年得到了世界的认可,这就是中国商用(国家标准) SM2 椭圆曲线公钥密码算法 。
一. 基本概念
其实在 openssl 中,椭圆曲线分两种形式,一种是之前讲到的质数域上的椭圆曲线 ,将其称为 $F_p$ 其方程为:
$$y^2 \ \mod \ p = x^3 + ax + b \ \mod \ p$$
另一种是二进制域,称为 $F_{2^m}$, 其方程为:
$$y^2 + xy = x^3 + ax^2 + b ,\ (b != 0)$$
在这里只讨论 $F_p$ 相关的内容。
椭圆曲线上的点
椭圆曲线上的点使用 EC_POINT 来表示, 它定义在 ec_locl.h :
struct ec_point_st {
// ...
BIGNUM *X;
BIGNUM *Y;
BIGNUM *Z;
// ...
};
继续阅读
在我之前的博客中聊到了 DH 密钥交换的原理 . 这里来讨论如何使用 OPENSSL 进行 DH 密钥交换。
一 原理验证
OPENSSL 提供了一系列API, 可以在方便地进行大数计算。在这里我们先对 DH 密钥交换的原理 中讲到的原理进行代码级别的验证。对原理不感兴趣的可以跳过这一小节。
由于在 DH 密钥交换过程中,大部分的操作过程都是一样的,可以定义一个基类来抽象:
class DHBase {
public:
DHBase() : m_pri_key(NULL) {}
virtual ~DHBase() {
if (m_pri_key)
BN_free(m_pri_key);
};
const BIGNUM *GetSharedNum() {
if (!m_pri_key)
GenRandomData();
BN_CTX *ctx = BN_CTX_new();
const BIGNUM *g = DH_get0_g(m_dh);
const BIGNUM *p = DH_get0_p(m_dh);
BIGNUM *r = BN_new();
// r = g ^ m_pri_key % p
int rst = BN_mod_exp(r, g, m_pri_key, p, ctx);
assert(rst == 1);
BN_CTX_free(ctx);
return r;
}
BIGNUM *GenKey(const BIGNUM *shared_num) {
if (!m_pri_key)
GenRandomData();
BIGNUM *key = BN_new();
BN_CTX *ctx = BN_CTX_new();
// key = shared_num ^ m_pri_key % p
int rst = BN_mod_exp(key, shared_num, m_pri_key, DH_get0_p(m_dh), ctx);
assert(rst == 1);
BN_CTX_free(ctx);
std::cout << m_name << ": received shared num: " << GetNumString(shared_num) << std::endl;
std::cout << m_name << ": generated KEY: " << GetNumString(key) << std::endl;
return key;
}
std::string GetName() {
return m_name;
}
static std::string GetNumString(const BIGNUM *num) {
return BN_bn2hex(num);
}
private:
void GenRandomData() {
assert(m_dh);
m_pri_key = BN_new();
int rst = BN_rand_range(m_pri_key, DH_get0_p(m_dh));
assert(rst == 1);
while (BN_is_zero(m_pri_key)) {
BN_rand_range(m_pri_key, DH_get0_p(m_dh));
}
std::cout << m_name << ": generated the private num" << std::endl;
}
protected:
DH *m_dh;
std::string m_name;
private:
BIGNUM *m_pri_key;
};
m_pri_key 是双方生成的私密随机数。
GetSharedNum() 返回双方交换的中间变量 G^A mod P
GenKey() 即完成了密钥交换,返回交换成功后的密钥。
继续阅读