C++ 开发 Web 服务框架

本文介绍了如何使用C++11和Boost Asio开发Web服务框架,涵盖了C++基础知识、C++11特性、Boost Asio相关知识,以及HTTP和HTTPS服务器的实现。文章详细讨论了Lambda表达式、无序容器、正则表达式、线程等C++11特性,并解释了如何使用Boost Asio创建HTTP和HTTPS连接。在设计和实现方面,文章提出使用基类处理通用逻辑,通过虚函数实现特定协议的连接处理,展示了如何解析请求、应答请求,并提供了HTTP服务器的实现代码。
摘要由CSDN通过智能技术生成

基础知识: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++ 了:

1-2-1

使用 g++ -v 可以查看到当前编译器版本:

1-2-2

如果你最后一行中的 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 库一般指 iostreamfstreamsstream

  • 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 用于匹配字符串和正则表达式,有很

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值