- C++ 开发 Web 服务框架
- 1 小时入门增强现实技术
- C++ 实现高性能内存池
- GDB 简明教程
- C++ 实现太阳系行星系统
- C++11/14 高速上手教程
- C 语言实现 Linux Shell 命令解释器
- C++ 打造 Markdown 解析器
- C 语言实现文件类型统计程序
- C 语言实现 Linux touch 命令
- C 语言入门教程
- C 语言实现多线程排序
- 多线程生产者消费者模型仿真停车场
- C++实现运动目标的追踪
- C 语言实现 Linux 网络嗅探器
- 100 行 C++ 代码实现线程池
- C 语言实现聊天室软件
- C 语言实现 Linux who 命令
- C 语言实现 Linux cp 命令
- C++实现第一人称射击游戏
- C++ 实现银行排队服务模拟
- 数据结构(新版)
- 软件工程(C 编码实践篇)
- C 语言制作简单计算器
- C 语言版 flappy_bird
- C 语言编写万年历
- C 语言版扫雷游戏
- C 语言实现一个支持 PHP 的简易 WEB 服务器
- C 语言制作 2048
- C 语言模拟 ATM 自动取款机系统
- Linux 系统编程
- C 语言利用 epoll 实现高并发聊天室
- C 语言快速实现五子棋
- C 语言实现 ping 程序
- 简单词法分析器(C++语言)
第 3 节 C++ 开发 Web 服务框架 - HTTPS 的原理及其 Web 框架的设计与实现
一、概述
项目介绍
服务器开发中 Web 服务是一个基本的代码单元,将服务端的请求和响应部分的逻辑抽象出来形成框架,能够做到最高级别的框架级代码复用。本次项目将综合使用 C++11 及 Boost 中的 Asio 实现 HTTP 和 HTTPS 的服务器框架。
项目涉及的知识点
- C++基本知识
- 面向对象
- 模板
- 命名空间
- 常用 IO 库
- C++11 相关
- lambda expression
- std::sharedptr
- std::makeshared
- std::unorderedmap
- std::regex
- std::smatch
- std::regexmatch
- std::function
- std::thread
- Boost Asio 相关
- boost::asio::ioservice
- 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::asyncread
- boost::asio::asyncreaduntil
- boost::asio::asyncwrite
- boost::asio::transferexactly
- boost::asio::ssl::stream
- boost::asio::ssl::streambase::server
- boost::asio::ssl::context
- boost::asio::ssl::context::sslv23
- boost::asio::ssl::context::pem
- boost::system::errorcode
编译环境提示
本次实验中的代码使用了 C++11 标准库中的正则表达式库,在 g++ 4.9 之前, regex 库并不支持 ECMAScript 的正则语法,因此需要将 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
以上,方法如下: > > > bash > # 安装 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 >
二、实现 HTTPS 框架
在上一节实验中,我们已经实现了 HTTP 的框架,利用这个框架,我们便能更加方便的进行框架级的复用。Boost Asio 包含了一个类及类模板用于对基本的 SSL 进行支持,这个类使我们实现 HTTPS 服务器成为可能。从实现上来看,我们只需要对已经存在的流再进行一层加密封装,比如加密 TCP Socket。这个过程异常的简单,我们只需稍加利用即可实现整个 HTTPS 的框架,如下:
//
// server_https.hpp
// web_server
// created by changkun at shiyanlou.com
//
#ifndef SERVER_HTTPS_HPP
#define SERVER_HTTPS_HPP
#include "server_http.hpp"
#include <boost/asio/ssl.hpp>
namespace ShiyanlouWeb {
// 定义 HTTPS 类型
typedef boost::asio::ssl::stream<boost::asio::ip::tcp::socket> HTTPS;
// 定义 HTTPS 服务,模板类型为 HTTPS
template<>
class Server<HTTPS> : public ServerBase<HTTPS> {
public:
// 一个 HTTPS 的服务器比 HTTP 服务器多增加了两个参数,一个是证书文件,另一个是私钥文件
Server(unsigned short port, size_t num_threads,
const std::string& cert_file, const std::string& private_key_file) :
ServerBase<HTTPS>::ServerBase(port, num_threads),
context(boost::asio::ssl::context::sslv23) {
// 使用证书文件
context.use_certificate_chain_file(cert_file);
// 使私钥文件,相比之下需要多传入一个参数来指明文件的格式
context.use_private_key_file(private_key_file, boost::asio::ssl::context::pem);
}
private:
// 和 HTTP 服务器相比,需要多定义一个 ssl context 对象
boost::asio::ssl::context context;
// HTTPS 服务器和 HTTP 服务器相比
// 其区别在于对 socket 对象的构造方式有所不同
// HTTPS 会在 socket 这一步中对 IO 流进行加密
// 因此实现的 accept() 方法需要对 socket 用 ssl context 初始化
void accept() {
// 为当前连接创建一个新的 socket
// Shared_ptr 用于传递临时对象给匿名函数
// socket 类型会被推导为: std::shared_ptr<HTTPS>
auto socket = std::make_shared<HTTPS>(m_io_service, context);
acceptor.async_accept(
(*socket).lowest_layer(),
[this, socket](const boost::system::error_code& ec) {
// 立即启动并接受一个新连接
accept();
// 处理错误
if(!ec) {
(*socket).async_handshake(boost::asio::ssl::stream_base::server,
[this, socket](const boost::system::error_code& ec) {
if(!ec) process_request_and_respond(socket);
});
}
});
}
};
}
#endif /* SERVER_HTTPS_HPP */
在上面整个过程中,我们仅仅只是重新实现了 accept()
方法,将启用一个 HTTPS 服务器需要的两个文件传递给了 Boost Asio,就完成了整个服务器框架。
三、开发 HTTPS 服务器
我们已经在上一节实验中写过了 handler.hpp
,这个文件中实现了服务器资源的访问和处理逻辑。而这部分逻辑,本质上是独立于服务器类型而存在的,因此我们根本不需要在进行任何开发,只需将 main_http.cpp
中的服务器类型修改就能获得一个完整的 HTTPS 服务器:
//
// main_https.cpp
// web_server
// created by changkun at shiyanlou.com
//
#include "server_https.hpp"
#include "handler.hpp"
using namespace ShiyanlouWeb;
int main() {
//HTTPS 服务运行在 12345 端口,并启用四个线程
Server<HTTPS> server(12345, 4, "server.crt", "server.key");
start_server<Server<HTTPS>>(server);
return 0;
}
在这个服务器上,我们额外传入了 HTTPS 服务器需要的证书和秘钥文件。
为了编译我们的 HTTPS 服务器,在 Makefile 中增加对 HTTPS 服务器的编译选项:
#
# Makefile
# web_server
#
# created by changkun at shiyanlou.com
#
CXX = g++
EXEC_HTTP = server_http
EXEC_HTTPS = server_https
SOURCE_HTTP = main_http.cpp
SOURCE_HTTPS = main_https.cpp
OBJECTS_HTTP = main_http.o
OBJECTS_HTTPS = main_https.o
LDFLAGS_COMMON = -std=c++11 -O3 -pthread -lboost_system
LDFLAGS_HTTP =
LDFLAGS_HTTPS = -lssl -lcrypto
LPATH_COMMON = -I/usr/include/boost
LPATH_HTTP =
LPATH_HTTPS = -I/usr/include/openssl
LLIB_COMMON = -L/usr/lib
all:
make http
make https
http:
$(CXX) $(SOURCE_HTTP) $(LDFLAGS_COMMON) $(LDFLAGS_HTTP) $(LPATH_COMMON) $(LPATH_HTTP) $(LLIB_COMMON) $(LLIB_HTTP) -o $(EXEC_HTTP)
https:
$(CXX) $(SOURCE_HTTPS) $(LDFLAGS_COMMON) $(LDFLAGS_HTTPS) $(LPATH_COMMON) $(LPATH_HTTPS) $(LLIB_COMMON) $(LLIB_HTTPS) -o $(EXEC_HTTPS)
clean:
rm -f $(EXEC_HTTP) $(EXEC_HTTPS) *.o
这时,我们的整个目录树为:
src
├── Makefile
├── handler.hpp
├── main_http.cpp
├── main_https.cpp
├── server_base.hpp
├── server_http.hpp
├── server_https.hpp
└── www
├── index.html
└── test.html
现在,我们可以:
- 使用
make
一次性编译 http 和 https 服务器; - 使用
make http
单独编译 http 服务器; - 使用
make https
单独编译 https 服务器。
完成了编译后还不够,我们还需要对创建 HTTPS 服务器所需的证书。
四、创建证书文件
第一步:生成私钥
openssl
工具包提供了一个生成 RSA 私钥和 CSR(Certificate Signing Request) 文件的工具。这使得我们可以将其用于生成自签名的证书,从而用于供给 HTTPS 服务器使用。
首先,就是要生成 RSA 私钥。我们生成一个 1024 位的 RSA 秘钥,并使用三重 DES 加密方式,并按照 PEM 格式存储(在库中我们指定了私钥的格式是 boost::asio::ssl::context::pem
):
openssl genrsa -des3 -out server.key 1024
如图所示,在产生 server.key
时,我们还被要求设置密码,这个密码保护了当别人尝试访问这个私钥时,需要提供密码(作为演示,不妨设置成 123456
):
完成后,可以看到产生了 server.key
这个文件。
第二步:生成 CSR
私钥生成后,就可以据此生成一个 CSR 文件了:
openssl req -new -key server.key -out server.csr
在生成 CSR 文件的过程中,会被要求输入刚才我们设置的保护密码,同时还需要输入一些相关的信息,例如这个证书会被用在哪个域名下。最后会要求设置一个 challenge passwrod,通常不去设置这个密码。如图:
第三步:从秘钥中移除密码
如果证书有密码,那么每次使用证书时都讲需要输入一次密码,这不是很方便,况且,秘钥证书位于我们服务器上,不太容易被泄露,因此我们可以将秘钥中的密码移除,首先我们先保存一份秘钥的备份:
cp server.key server.key.old
openssl rsa -in server.key.old -out server.key
第四步:生成自签名证书
最后,生成一个自签名的证书,并设置证书的过期时间为一年:
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
至此,我们便完成了所有的步骤,现在我们可以运行服务器:
./server_https
然后在浏览器中访问现在这个运行在 12345
端口的 https 服务器了,输入: https://localhost:12345
这时,我们会看到浏览器正在告诉我们这个链接不安全。
这是由于我们的证书是自签名的产生的原因。一般情况下,自签名的 SSL 证书可以随意的签发,没有第三方监督的审核,并不能收到浏览器的信任。这就非常容易造成伪造证书的中间人攻击,从而导致劫持 SSL 加密流量。
我们刚才在创建证书的时候,指定了这个证书会被用于 shiyanlou.com
这个域名,而实际上我们在访问时,访问的 URL 是 localhost
,这时浏览器识别到这个不同,也就阻止了这次连接。
为了测试,我们可以将本次连接添加新人列表中,增加一个安全例外:
这样我们就能看到使用 HTTPS 访问到的资源内容了:
可惜的是,我们依然不能做到像『正经』厂商一样,让那一把小锁变成绿色:
原因就如同之前我们所提到的那样,SSL 证书受到第三方监管,浏览器信任的证书一般来自国外的几个指明 SSL 证书签发商,而这种证书的签发往往需要向签发商支付一定的费用,虽然也有诸如 StartSSL
这样的提供免费 SSL 证书的签发商,但由于我们没有域名进行测试,这里就不再赘述了。
总结
经过本次项目,我们走过了很多艰难的历程。首先,我们基于 C++11 和 Boost Asio 的诸多特性,开发了一个 HTTP 服务器的 Web 框架,为了测试我们的框架,我们编写了自己的 HTTP 服务器。
我们的设计非常巧妙,在完成 HTTP 服务器 Web 框架和相关测试代码后,进一步扩展为 HTTPS 时,只使用了极少的代码量便完成了整个框架的开发。
我们的开发的框架一共包含三个文件:
- server_base.hpp
- server_http.hpp
- server_https.hpp
而我们基于这个框架,开发了简易的 http 和 https 的 web 服务器,但我们依然复用了服务器实际逻辑的代码,写在了:
- handler.hpp
之中。此外,我们基于这套框架实现的 http 和 https 服务器在本质上,只有一行代码的不同:
// http server:
Server<HTTP> server(12345, 4);
// https server:
Server<HTTPS> server(12345, 4, "server.crt", "server.key");
作为参考,这里附上本次项目中全部的代码,你可以使用 wget
来获取:
http://labfile.oss.aliyuncs.com/courses/568/web_server.zip
值得一提的是,里面没有包含 SSL 证书,你需要自己手动创建它们。
参考资料
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论