基础知识:C++11 与 Boost Asio
一、概述
项目介绍
服务器开发中 Web 服务是一个基本的代码单元,将服务端的请求和响应部分的逻辑抽象出来形成框架,能够做到最高级别的框架级代码复用。本次项目将综合使用 C++11 及 Boost 中的 Asio 实现 HTTP 和 HTTPS 的服务器框架。
项目涉及的知识点
- C++基本知识
- 面向对象
- 模板
- 命名空间
- 常用 IO 库
- C++11 相关
- lambda expression
- std::shared_ptr
- std::make_shared
- std::unordered_map
- std::regex
- std::smatch
- std::regex_match
- std::function
- std::thread
- Boost Asio 相关
- boost::asio::io_service
- boost::asio::ip::tcp::socket
- boost::asio::ip::tcp::v4()
- boost::asio::ip::tcp::endpoint
- boost::asio::ip::tcp::acceptor
- boost::asio::streambuf
- boost::asio::async_read
- boost::asio::async_read_until
- boost::asio::async_write
- boost::asio::transfer_exactly
- boost::asio::ssl::stream
- boost::asio::ssl::stream_base::server
- boost::asio::ssl::context
- boost::asio::ssl::context::sslv23
- boost::asio::ssl::context::pem
- boost::system::error_code
二、编译环境介绍
在 g++ 4.9 之前,regex 库并不支持 ECMAScript 的正则语法,换句话说,在 g++4.9 之前,g++ 对 C++11 标准库的支持并不完善,为保证本次项目的顺利进行,请确保将 g++ 版本升级至 4.9 以上。
// 下面的这段代码可以测试你的编译器对正则表达式的支持情况
#include <iostream>
#include <regex>
int main()
{
std::regex r1("S");
printf("S works.\n");
std::regex r2(".");
printf(". works.\n");
std::regex r3(".+");
printf(".+ works.\n");
std::regex r4("[0-9]");
printf("[0-9] works.\n");
return 0;
}
如果你的运行结果遇到了下图所示的错误,说明你确实需要升级你的 g++ 了:
使用 g++ -v
可以查看到当前编译器版本:
如果你最后一行中的
gcc version
显示的是4.8.x
,那么你需要手动将编译器版本升级至4.9
以上,方法如下:# 安装 add-apt-repository 工具 sudo apt-get install software-properties-common # 增加源 sudo add-apt-repository ppa:ubuntu-toolchain-r/test # 更新源 sudo apt-get update # 更新安装 sudo apt-get upgrade # 安装 gcc/g++ 4.9 sudo apt-get install gcc-4.9 g++-4.9 # 更新链接 sudo updatedb sudo ldconfig sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.8 48 \ --slave /usr/bin/g++ g++ /usr/bin/g++-4.8 \ --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-4.8 \ --slave /usr/bin/gcc-nm gcc-nm /usr/bin/gcc-nm-4.8 \ --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-4.8 sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.9 49 \ --slave /usr/bin/g++ g++ /usr/bin/g++-4.9 \ --slave /usr/bin/gcc-ar gcc-ar /usr/bin/gcc-ar-4.9 \ --slave /usr/bin/gcc-nm gcc-nm /usr/bin/gcc-nm-4.9 \ --slave /usr/bin/gcc-ranlib gcc-ranlib /usr/bin/gcc-ranlib-4.9
此外,本次项目依赖了 Boost
和 OpenSSL
这两个库,不过好在实验楼的环境已经提供了这两个非常基本的库,你不需要再操心他们的安装了。
三、C++ 基础
面向对象和模板是 C++进阶知识的基础,这里不做过多介绍,本次项目我们将开发一个 Web 框架,我们在这里先回顾一下命名空间、和 sstream 字符串 IO 流的相关知识。如果对这部分比较熟悉,可以直接跳过本小节。
3.1 命名空间
在开发库时,库通常会有定义大量的全局名称,这时候当我们使用的库越来越多时,就不可避免的发生名称冲突的情况,这也就是我们常说的命名空间污染。
在命名空间诞生以前,通常使用的办法就是把一个函数、类、甚至变量名等名字取得足够长,在每一个名字的前面都增加相应的前缀,例如,当我们只想要定义一个 port 的变量时候:
// 原本的样子
int port;
// 实际的样子
int shiyanlou_web_server_port;
命名空间的定义非常简单,通过关键字 namespace
加上命名空间的名字,再使用花括号包裹需要的定义和声明即可完成相关的定义,例如:
namespace shiyanlou_web_server {
int port = 0;
}
这时,这个 port
就被限制在了命名空间 shiyanlou_web_server
当中,如果不通过命名空间的指定,就不会被访问到。
参考下面的例子:
//
// main.cpp
//
#include <iostream>
#include "web.hpp"
#include "web2.hpp"
int main() {
std::cout << "hello world!" << std::endl;
std::cout << "shiyanlou_web_server, port=" << shiyanlou_web_server::port << std::endl;
std::cout << "shiyanlou_web2_server, port=" << shiyanlou_web2_server::port << std::endl;
return 0;
}
//
// web.hpp
//
namespace shiyanlou_web_server{
int port = 0;
}
//
// web2.hpp
//
namespace shiyanlou_web2_server{
int port = 2;
}
最后的输出结果为:
hello world!
shiyanlou_web_server, port=0
shiyanlou_web2_server, port=2
3.2 常用 IO 库
我们常说的 C++ IO 库一般指 iostream
, fstream
, sstream
。
- iostream 包含了
istream
(从流读)/ostream
(从流写)/iostream
(读写流) - fstream 包含了
ifstream
(从文件读)/ofstream
(condition 文件写)/fstream
(读写文件) - sstream 包含了
istringstream
(从 string 读)/ostringstream
(向 string 写)/stringstream
(读写 string)
其实标准库还有宽字符版本,但我们这里不讨论,有兴趣的话可以参考参考链接。
iostream
和 fstream
是两个比较常用的IO 库,我们这里不再回顾,这里简单回顾一下 sstream
。
如果你熟悉 C 语言,就知道将 int 转换为 string 类型其实是一件很麻烦的事情,虽然标准库中提供了 itoa()
这种函数,但是依然需要对转换后的 C 风格字符串(char *)通过 std::string 的构造函数构造为 std::string。 如果使用流操作,那么这将变得异常的简单:
#include <string>
#include <sstream>
#include <iostream>
int main() {
// std::stringstream 支持读写
std::stringstream stream;
std::string result;
int number = 12345;
stream << number; // 将 number 输入到 stream
stream >> results; // 从 stream 读取到 result
std::cout < result << std::endl; // 将输出为字符串"12345"
}
如果希望让sstream 和 C 风格的字符串打交道,同样也可以:
#include <sstream>
#include <iostream>
int main()
{
std::stringstream stream;
char result[6];
stream << 12345;
stream >> result;
std::cout << result << std::endl;
}
需要注意的一点就是,在进行多次IO 操作时,如果希望结果彼此不影响,需要对 stream 对象进行一次 clear()
操作:
stream.clear()
四、C++11 相关
C++11 几乎重新定义了 C++ 的一切,C++11 的出现伴随着大量的有用的新特性和标准库,这些特性和标准使得 C++ 变得更加现代,甚至在编码范式上都与传统 C++ 有着本质上的差异,本节我们将回顾一下这些特性:
- lambda expression
- std::shared_ptr
- std::make_shared
- std::unordered_map
- std::regex
- std::smatch
- std::regex_match
- std::function
- std::thread
如果对这些特性比较熟悉,可以直接跳过本节。
4.1 lambda 表达式
Lambda 表达式是 C++11中最重要的新特性之一,而 Lambda 表达式,实际上就是提供了一个类似匿名函数的特性,而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。这样的场景其实有很多很多,所以匿名函数几乎是现代编程语言的标配。
Lambda 表达式的基本语法如下:
[捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 {
// 函数体
}
上面的语法规则除了 [捕获列表]
内的东西外,其他部分都很好理解,只是一般函数的函数名被略去,返回值使用了一个 ->
的形式进行。
所谓捕获列表,其实可以理解为参数的一种类型,lambda 表达式内部函数体在默认情况下是不能够使用函数体外部的变量的,这时候捕获列表可以起到传递外部数据的作用。根据传递的行为,捕获列表也分为以下几种:
1. 值捕获
与参数传值类似,值捕获的前期是变量可以拷贝,不同之处则在于,被捕获的变量在 lambda 表达式被创建时拷贝,而非调用时才拷贝:
void learn_lambda_func_1() {
int value_1 = 1;
auto copy_value_1 = [value_1] {
return value_1;
};
value_1 = 100;
auto stored_value_1 = copy_value_1();
// 这时, stored_value_1 == 1, 而 value_1 == 100.
// 因为 copy_value_1 在创建时就保存了一份 value_1 的拷贝
}
2. 引用捕获
与引用传参类似,引用捕获保存的是引用,值会发生变化。
void learn_lambda_func_2() {
int value_2 = 1;
auto copy_value_2 = [&value_2] {
return value_2;
};
value_2 = 100;
auto stored_value_2 = copy_value_2();
// 这时, stored_value_2 == 100, value_2 == 100.
// 因为 copy_value_2 保存的是引用
}
3. 隐式捕获
手动书写捕获列表有时候是非常复杂的,这种机械性的工作可以交给编译器来处理,这时候可以在捕获列表中写一个 &
或 =
向编译器声明采用 引用捕获或者值捕获.
总结一下,捕获提供了lambda 表达式对外部值进行使用的功能,捕获列表的最常用的四种形式可以是:
- [] 空捕获列表
- [name1, name2, ...] 捕获一系列变量
- [&] 引用捕获, 让编译器自行推导捕获列表
- [=] 值捕获, 让编译器执行推导应用列表
std::shared_ptr, std::make_shared
C++11 在内存管理上同样做了很多改进,std::make_shared 就是其中之一。它是和 std::shared_ptr
共同出现的,std::shared_ptr
是一种智能指针,它能够记录多少个 shared_ptr 共同指向一个对象(熟悉 Objective-C 的可能知道,这种特性叫做引用计数),能够消除显式的调用 delete
,当引用计数变为0的时候就会将对象自动删除。
但还不够,因为使用 std::shared_ptr
仍然需要使用 new来调用,这使得代码出现了某种程度上的不对称。因此就需要另一种手段(工厂模式)来解决这个问题。
std::make_shared
就能够用来消除显式的使用 new
,所以std::make_shared
会分配创建传入参数中的对象,并返回这个对象类型的std::shared_ptr
指针。例如:
#include <iostream>
#include <memory>
void foo(std::shared_ptr<int> i)
{
(*i)++;
}
int main()
{
// 构造了一个 std::shared_ptr
auto pointer = std::make_shared<int>(10);
foo(pointer);
std::cout << *pointer << std::endl;
}
4.2 无序容器 std::unordered_map
在传统的 C++中,我们已经熟知了 std::map 关联容器,std::map 容器在插入元素时,会根据 <
操作符比较元素大小并判断元素是否相同,并选择合适的位置插入到容器中。当对这个容器中的元素进行遍历时,输出结果会按照 <
操作符的顺序来逐个遍历。
而 C++11 终于推出了无序容器。 std::unordered_map
就是无序容器其中之一,这个容器会计算元素的 Hash 值,并根据 Hash 值来判断元素是否相同。由于无序容器没有定义元素之间的顺序,仅靠 Hash 值来判断元素是否已经存在于容器中,所以遍历 std::unordered_map
时,结果是无序的。
来看一个例子:
#include <iostream>
#include <string>
#include <unordered_map>
#include <map>
int main() {
// 两组结构按同样的顺序初始化
std::unordered_map<int, std::string> u = {
{1, "1"},
{3, "3"},
{2, "2"}
};
std::map<int, std::string> v = {
{1, "1"},
{3, "3"},
{2, "2"}
};
// 分别对两组结构进行遍历
std::cout << "std::unordered_map" << std::endl;
for( const auto & n : u)
std::cout << "Key:[" << n.first << "] Value:[" << n.second << "]\n";
std::cout << std::endl;
std::cout << "std::map" << std::endl;
for( const auto & n : v)
std::cout << "Key:[" << n.first << "] Value:[" << n.second << "]\n";
}
最终的输出结果为:
std::unordered_map
Key:[2] Value:[2]
Key:[3] Value:[3]
Key:[1] Value:[1]
std::map
Key:[1] Value:[1]
Key:[2] Value:[2]
Key:[3] Value:[3]
可以看到 std::map
的遍历结果是有序的,而 std::unordered_map
的遍历结果是无序的。
事实上,std::unordered_map
在单个元素访问时,总是能够获得更高的性能。
4.3 std::regex/std::regex_match/std::smatch
正则表达式是一个独立于 C++ 语言本身的另一个很大的话题,我们这里不详细讨论它的行为。
作为学习 std::regex
的一些介绍性内容,我们这里说明一下接下来会用到的一些正则表达式:
[a-z]+\.txt
: 在这个正则表达式中,[a-z]
表示匹配一个小写字母,+
可以使前面的表达式匹配多次,因此[a-z]+
能够匹配一个小写字母组成的字符串。在正则表达式中一个.
表示匹配任意字符,而\.
则表示匹配字符.
,最后的txt
表示严格匹配txt
则三个字母。因此这个正则表达式的所要匹配的内容就是由纯小写字母组成的文本文件。
std::regex_match
用于匹配字符串和正则表达式,有很