Boost

Boost.Asio C++ 网络编程(全)

原文:annas-archive.org/md5/8b9e46aef0499a9bda54207bb9fe14f9

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

大约 20 年前,网络应用程序的开发并不容易。但由于 Boost.Asio 的出现,它为我们提供了网络编程功能以及异步操作功能,以便编写网络应用程序,我们现在可以轻松地开发它们。由于网络上传输可能需要很长时间,这意味着确认和错误可能无法像发送或接收数据的功能执行得那么快,因此异步操作功能在网络应用程序编程中确实是必需的。在本书中,您将学习网络基础知识,以及如何使用 Boost.Asio 库开发网络应用程序。

本书涵盖内容

第一章 简化 C++网络编程,解释了 C++编译器的准备工作,该编译器将用于编译本书中的所有源代码。此外,它还会告诉我们如何编译单个源代码并链接到多个源代码。

第二章 理解网络概念,涵盖了 OSI 和 TCP/IP 网络参考模型。它还提供了各种 TCP/IP 工具,我们经常会使用这些工具来检测我们的网络连接是否发生错误。

第三章 介绍 Boost C++库,解释了如何设置编译器以编译包含 Boost 库的代码,以及如何构建我们必须单独编译的库的二进制文件。

第四章 开始使用 Boost.Asio,讨论了并发和非并发编程。它还讨论了 I/O 服务,该服务用于访问操作系统的资源,并在我们的程序和执行 I/O 请求的操作系统之间建立通信。

第五章 深入了解 Boost.Asio 库,指导我们如何序列化 I/O 服务的工作,以确保工作顺序完全符合我们设计的顺序。它还涵盖了如何处理错误和异常以及在网络编程中创建时间延迟。

第六章 创建客户端-服务器应用程序,讨论了开发能够从客户端发送和接收数据流量的服务器,以及如何创建客户端程序以接收数据流量。

第七章 调试代码和解决错误,涵盖了跟踪可能由意外结果产生的错误的调试过程,例如在程序执行中间崩溃。阅读完本章后,您将能够通过调试代码解决各种错误。

本书所需内容

要阅读本书并成功编译所有源代码,您需要一台运行 Microsoft Windows 8.1(或更高版本)的个人电脑,并包含以下软件:

  • Windows 的 MinGW-w64,版本 4.9.2

  • Notepad++的最新版本

  • Boost C++库,版本 1.58.0

本书适合对象

本书适用于具有网络编程基础知识,但不了解如何使用 Boost.Asio 进行网络编程的 C++网络程序员。

约定

在本书中,您将找到一些区分不同类型信息的文本样式。以下是这些样式的一些示例及其含义的解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名显示如下:"等待片刻,直到mingw-w64-install.exe文件完全下载。"

代码块设置如下:

/* rangen.cpp */
#include <cstdlib>
#include <iostream>
#include <ctime>
int main(void) {

当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:

int guessNumber;
std::cout << "Select number among 0 to 10: ";
std::cin >> guessNumber;

任何命令行输入或输出都以以下形式书写:

rundll32.exe sysdm.cpl,EditEnvironmentVariables

新术语重要词汇以粗体显示。您在屏幕上看到的词语,例如菜单或对话框中的词语,会以这样的形式出现在文本中:"您将看到一个欢迎对话框。只需按下下一步按钮,即可进入设置设置对话框。"

注意

警告或重要提示以这样的框出现。

提示

技巧和窍门会显示在这样的形式下。

第一章:简化 C++中的网络编程

我们可以从网络上选择几个 C++编译器。为了让您更容易地跟随本书中的所有代码,我选择了一个可以使编程过程更简单的编译器——绝对是最简单的一个。在本章中,您将发现以下主题:

  • 设置 MinGW 编译器

  • 在 C++中编译

  • GCC C++中的故障排除

设置 MinGW 编译器和文本编辑器

这是最难的部分——我们必须在其他编译器中选择一个。尽管我意识到每个编译器都有其优势和劣势,但我想让你更容易地浏览本章中的所有代码。因此,我建议您应用与我们相同的环境,包括我们使用的编译器。

我将使用GCC,GNU 编译器集合,因为它被广泛使用的开源。由于我的环境包括 Microsoft Windows 作为操作系统,我将使用Windows 的 Minimalistic GCCMinGW)作为我的 C++编译器。对于那些没有听说过 GCC 的人,它是一个可以在 Linux 操作系统中找到的 C/C++编译器,也包含在 Linux 发行版中。MinGW 是 GCC 在 Windows 环境中的一个移植。因此,本书中的整个代码和示例都适用于任何其他 GCC 版本。

安装 MinGW-w64

为了您的方便,由于我们使用 64 位 Windows 操作系统,我们选择了 MinGW-w64,因为它可以用于 Windows 32 位和 64 位架构。要安装它,只需打开您的互联网浏览器,导航到sourceforge.net/projects/mingw-w64/,转到下载页面,然后点击下载按钮。等待片刻,直到mingw-w64-install.exe文件完全下载。请参考以下屏幕截图以找到下载按钮:

安装 MinGW-w64

现在,执行安装程序文件。您将会看到一个欢迎对话框。只需按下一步按钮,进入设置设置对话框。在此对话框中,选择最新的 GCC 版本(在撰写本文时,是4.9.2),其余选项选择如下:

安装 MinGW-w64

点击下一步按钮继续并进入安装位置选项。在这里,您可以更改默认安装位置。我将更改安装位置为C:\MinGW-w64,以便使我们的下一个设置更容易,但如果您愿意,也可以保留此默认位置。

安装 MinGW-w64

点击下一步按钮,进入下一步,并等待片刻,直到文件下载和安装过程完成。

设置路径环境

现在您已经在计算机上安装了 C++编译器,但只能从其安装目录访问它。为了从系统中的任何目录访问编译器,您必须通过执行以下步骤设置PATH 环境

  1. 通过按Windows + R键以管理员身份运行命令提示符。在文本框中键入cmd,而不是按Enter键,按Ctrl + Shift + Enter以以管理员模式运行命令提示符。然后将出现用户账户控制对话框。选择以确认您打算以管理员模式运行命令提示符。如果您正确执行此操作,您将获得一个标有管理员:命令提示符的标题栏。如果您没有获得它,您可能没有管理员权限。在这种情况下,您必须联系计算机的管理员。

  2. 在管理员模式下的命令提示符中键入以下命令:

rundll32.exe sysdm.cpl,EditEnvironmentVariables

  1. 按下Enter键,命令提示符将立即运行环境变量窗口。然后,转到系统变量,选择名为Path的变量,单击编辑按钮打开编辑系统变量对话框,然后在最后的变量值参数中添加以下字符串:
;C:\MinGW-w64\mingw64\bin

(否则,如果您使用默认位置,安装向导中给出的安装目录路径将需要进行调整)

  1. 单击编辑系统变量对话框中的确定按钮,然后在环境变量对话框中再次单击确定按钮以保存这些更改。

是时候尝试我们的环境变量设置了。在任何活动目录中打开一个新的命令提示符窗口,可以是管理员模式或非管理员模式,但不能是C:\MinGW-w64,然后输入以下命令:

g++ --version

如果您看到输出通知您以下信息,那么您已经配置了正确的设置:

g++ (x86_64-posix-seh-rev2, Built by MinGW-W64 project) 4.9.2

如果显示的是不同的版本号,您的计算机上可能有另一个 GCC 编译器。为了解决这个问题,您可以修改环境变量并删除与其他 GCC 编译器相关的所有路径环境设置,例如C:\StrawberryPerl\c\bin

然而,如果您确信已经正确地按照所有步骤操作,但仍然收到错误消息,如下面的片段所示,您可能需要重新启动计算机以设置新的系统设置:

'g++' is not recognized as an internal or external command, operable program or batch file.

选择和安装文本编辑器

Microsoft Windows 已经配备了Notepad,一个简单的文本编辑器,用于创建纯文本文件。您可以使用 Notepad 创建一个 C++文件,其中文件必须只包含纯文本格式。当您想要编辑代码时,您也可以转向重量级的集成开发环境IDE),但我更喜欢一个简单、轻量级和可扩展的编程纯文本编辑器,因此我选择使用文本编辑器而不是 IDE。由于在编写代码时我需要语法高亮以使其更易于阅读和理解,我选择了Notepad++作为我们的文本编辑器。您可以选择您喜欢的文本编辑器,只要将输出文件保存为纯文本即可。以下是 Notepad++中语法高亮的示例:

选择和安装文本编辑器

如果您决定像我一样使用 Notepad++,您可以访问notepad-plus-plus.org/获取最新版本的 Notepad++。在主页上找到下载菜单,选择当前版本链接。在那里,您将找到下载安装程序文件的链接。使用Notepad++安装程序文件而不是包文件,按照安装向导上的所有说明来设置它在您的计算机上的安装方式。

选择和安装文本编辑器

使用 GCC C++编译器

现在我们的开发准备好了,我们可以编写我们的第一个 C++程序。为了保持清洁,创建一个CPP文件夹在 C 盘(C:\CPP)中存储我们的示例代码。您可以在您的系统上具有相同的目录位置,以便更方便地按照所有步骤进行。否则,如果您决定使用不同的目录位置,您将需要进行一点修改。

编译 C++程序

我们不会为我们的第一个示例代码创建 Hello World!程序。在我看来,这很无聊,而且到目前为止,您应该已经知道如何编写 Hello World!程序了。我们将创建一个简单的随机数生成器。您可以使用这个程序和朋友一起玩。他们必须猜测程序将显示哪个数字。如果答案不正确,您可以用记号划掉他/她的脸,并继续玩下去,直到您无法再认出您朋友的脸为止。以下是创建此生成器的代码:

/* rangen.cpp */
#include <cstdlib>
#include <iostream>
#include <ctime>
int main(void) {
  int guessNumber;
  std::cout << "Select number among 0 to 10:";
  std::cin >> guessNumber;
  if(guessNumber < 0 || guessNumber > 10) {
    return 1;
  }
  std::srand(std::time(0));
  int randomNumber = (std::rand() % (10 + 1));
  if(guessNumber == randomNumber) {
    std::cout << "Congratulation, " <<guessNumber<<" is your lucky number.\n";
  }
  else {
    std::cout << "Sorry, I'm thinking about number \n" << randomNumber;
  }
  return 0;
}

在文本编辑器中输入代码,并将其保存为文件名为rangen.cpp的文件,保存在C:\CPP位置。然后,打开命令提示符,并通过在命令提示符中输入以下命令将活动目录指向C:\CPP位置:

cd C:\CPP

接下来,在控制台中输入以下命令来编译代码:

g++ -Wall rangen.cpp -o rangen

上述命令使用可执行文件rangen.exe编译rangen.cpp文件,其中包含一堆机器代码(exe扩展名会自动添加以指示该文件是 Microsoft Windows 中的可执行文件)。使用-o选项指定机器代码的输出文件。如果使用此选项,必须同时指定输出文件的名称;否则,编译器将报告缺少文件名的错误。如果省略-o选项和输出文件的文件名,输出将写入默认文件a.exe

提示

当前目录中具有与已编译源文件相同名称的可执行文件将被覆盖。

我建议您使用-Wall选项并养成习惯,因为此选项将打开所有最常用的编译器警告。如果禁用此选项,GCC 将不会给出任何警告。因为我们的随机数生成器代码是完全有效的,所以在编译时 GCC 不会给出任何警告。这就是为什么我们依赖于编译器警告来确保我们的代码是有效的并且编译干净的原因。

要运行程序,在控制台中输入rangen,并将C:\CPP位置作为活动目录,将显示欢迎词:在 0 到 10 之间选择数字。按照指示选择010之间的数字。然后,按下Enter,程序将输出一个数字。将其与你自己的数字进行比较。如果两个数字相同,你将受到祝贺。然而,如果你选择的数字与代码生成的数字不同,你将得到相同的通知。程序的输出将如下截图所示:

编译 C++程序

很遗憾,我在三次尝试中从未猜对正确的数字。事实上,即使每次生成数字时都使用新的种子,也很难猜到rand()函数生成了哪个数字。为了减少混乱,我将会解析rangen.cpp代码,如下所示:

int guessNumber;
std::cout << "Select number among 0 to 10: ";
std::cin >> guessNumber;

我保留了一个名为guessNumber的变量来存储用户输入的整数,并使用std::cin命令从控制台获取输入的数字。

if(guessNumber < 0 || guessNumber > 10) {
 return 1;
}

如果用户给出超出范围的数字,通知操作系统程序中发生了错误——我发送了错误 1,但实际上,你可以发送任何数字——并让它处理错误。

std::srand(std::time(0));
int randomNumber = (std::rand() % (10 + 1);

std::srand函数用于初始化种子,为了在每次调用std::rand()函数时生成不同的随机数,我们使用ctime头文件中的std::time(0)函数。为了生成一系列随机数,我们使用模数方法,如果调用std::rand() % n这样的函数,将生成一个从 0 到(n-1)的随机数。如果要包括数字n,只需将n1相加。

if(guessNumber == randomNumber) {
 std::cout << "Congratulation ,"<< guessNumber<<" is your lucky number.\n";
}
else {
 std::cout << "Sorry, I'm thinking about number " << randomNumber << "\n";
}

这是有趣的部分,程序将用户猜测的数字与生成的随机数字进行比较。无论发生什么,用户都将通过程序得到结果的通知。让我们看看以下代码:

return 0;

返回0告诉操作系统程序已正常终止,无需担心。让我们看看以下代码:

#include <cstdlib>
#include <iostream>
#include <ctime>

不要忘记在上述代码中包含前三个头文件,因为它们包含了我们在此程序中使用的函数,例如time()函数在<ctime>头文件中定义,srand()函数和rand()函数在<cstdlib>头文件中定义,cout()cin()函数在<iostream>头文件中定义。

如果您发现很难猜出程序生成的数字,那是因为我们使用当前时间作为随机生成器种子,这样做的结果是每次调用程序时生成的数字都会不同。以下是我在大约六到七次尝试后成功猜出生成的随机数的屏幕截图(对于所有程序调用,我们都猜错了数字,除了最后一次尝试):

编译 C++程序

编译多个源文件

有时,当代码存在错误或 bug 时,我们必须修改我们的代码。如果我们只制作一个包含所有代码行的单个文件,当我们想要修改源代码时,我们会感到困惑,或者我们很难理解程序的流程。为了解决这个问题,我们可以将代码拆分成多个文件,每个文件只包含两到三个函数,这样就容易理解和维护了。

我们已经能够生成随机数,现在,让我们来看一下密码生成器程序。我们将使用它来尝试编译多个源文件。我将创建三个文件来演示如何编译多个源文件,它们是pwgen_fn.hpwgen_fn.cpppassgen.cpp。我们将从pwgen_fn.h文件开始,其代码如下:

/* pwgen_fn.h */
#include <string>
#include <cstdlib>
#include <ctime>
class PasswordGenerator {
  public:
    std::string Generate(int);
};

前面的代码用于声明类名。在本例中,类名为PasswordGenerator,在这种情况下,它将生成密码,而实现存储在.cpp文件中。以下是pwgen_fn.cpp文件的清单,其中包含Generate()函数的实现:

/* pwgen_fn.cpp */
#include "pwgen_fn.h"
std::string PasswordGenerator::Generate(int passwordLength) {
  int randomNumber;
  std::string password;
  std::srand(std::time(0));
  for(int i=0; i < passwordLength; i++) {
    randomNumber = std::rand() % 94 + 33;
    password += (char) randomNumber;
  }
  return password;
}

主入口文件passgen.cpp包含使用PasswordGenerator类的程序:

/* passgen.cpp */
#include <iostream>
#include "pwgen_fn.h"
int main(void) {
  int passLen;
  std::cout << "Define password length: ";
  std::cin >> passLen;
  PasswordGenerator pg;
  std::string password = pg.Generate(passLen);
  std::cout << "Your password: "<< password << "\n";
  return 0;
}

从前面的三个源文件中,我们将生成一个单独的可执行文件。为此,请转到命令提示符并在其中输入以下命令:

g++ -Wall passgen.cpp pwgen_fn.cpp -o passgen

我没有收到任何警告或错误,所以你也不应该收到。前面的命令编译了passgen.cpppwgen_fn.cpp文件,然后将它们链接到一个名为passgen.exe的单个可执行文件中。pwgen_fn.h文件,因为它是与源文件同名的头文件,所以在命令中不需要声明相同的名称。

如果您在控制台窗口中键入passgen命令运行程序,您将每次运行程序时都会得到不同的密码。

编译多个源文件

现在,是时候我们来剖析前面的源代码了。我们将从pwgen_fn.h文件开始,该文件仅包含函数声明,如下所示:

std::string Generate(int);

从声明中可以看出,Generate()函数将具有int类型的参数,并将返回std::string函数。由于参数将自动与源文件匹配,因此在头文件中我们不定义参数的名称。

打开pwgen_fn.cpp文件,看以下语句:

std::string PasswordGenerator::Generate(int passwordLength)

在这里,我们可以指定参数名称,即passwordLength。在这种情况下,只要它们位于不同的类中,我们可以拥有两个或更多具有相同名称的函数。让我们看一下以下代码:

int randomNumber;
std::string password;

我保留了名为randomNumber的变量来存储由rand()函数生成的随机数,以及password参数来存储从随机数转换的 ASCII。让我们看一下以下代码:

std::srand(std::time(0));

种子随机srand()函数与我们在先前的代码中使用的相同,用于生成随机种子。我们使用它是为了在每次调用rand()函数时产生不同的数字。让我们看一下以下代码:

for(int i=0; i < passwordLength; i++) {
 randomNumber = std::rand() % 94 + 33;
 password += (char) randomNumber;
}
return password;

for迭代取决于用户定义的passwordLength参数。通过随机数生成器语句std::rand() % 94 + 33,我们可以生成表示 ASCII 可打印字符的数字,其代码范围从 33 到 126。有关 ASCII 代码表的更详细信息,您可以访问en.wikipedia.org/wiki/ASCII。让我们看一下以下代码:

#include "pwgen_fn.h"

#include头文件的单行将调用pwgen_fn.h文件中包含的所有头文件,因此我们不需要在此源文件中声明包含的头文件。

#include <string>
#include <cstdlib>
#include <ctime>

现在,我们转到我们的主要入口代码,存储在passgen.cpp文件中:

int passLen;
std::cout << "Define password length: ";
std::cin >> passLen;

首先,用户决定要拥有多长的密码,并且程序将其存储在passLen变量中:

PasswordGenerator pg;
std::string password = pg.Generate(passLen);
std::cout << "Your password: "<< password << "\n";

然后,程序实例化PasswordGenerator类并调用Generate()函数来生成用户之前定义的长度的密码。

如果您再次查看passgen.cpp文件,您会发现#include <iostream>(带有尖括号)和#include "pwgen_fn.h"(带有引号)两种形式的包含语句之间存在差异。通过在#include头语句中使用尖括号,编译器将查找系统头文件目录,但默认情况下不会查找当前目录。通过在#include头语句中使用引号,编译器将在查找系统头文件目录之前在当前目录中搜索头文件。

分别编译和链接程序

我们可以将一个大型程序分解为一组源文件并分别编译它们。假设我们有许多小文件,我们只想编辑其中一个文件中的一行,如果我们编译所有文件,而我们只需要修改一个文件,那将是非常耗时的。

通过使用-c选项,我们可以编译单独的源代码以生成具有.o扩展名的目标文件。在第一阶段,文件被编译而不创建可执行文件。然后,在第二阶段,目标文件由一个名为链接器的单独程序链接在一起。链接器将所有目标文件组合在一起,创建一个单一的可执行文件。使用之前的passgen.cpppwgen_fn.cpppwgen_fn.h源文件,我们将尝试创建两个目标文件,然后将它们链接在一起以生成一个单一的可执行文件。使用以下两个命令来执行相同的操作:

g++ -Wall -c passgen.cpp pwgen_fn.cpp
g++ -Wall passgen.o pwgen_fn.o -o passgen

第一个命令使用-c选项将创建两个具有与源文件名相同但具有不同扩展名的目标文件。第二个命令将将它们链接在一起,并生成具有在-o选项之后指定的名称的输出可执行文件,即passgen.exe文件。

如果您需要编辑passgen.cpp文件而不触及其他两个文件,您只需要编译passgen.cpp文件,如下所示:

g++ -Wall -c passgen.cpp

然后,您需要像前面的第二个命令一样运行链接命令。

检测 C++程序中的警告

正如我们之前讨论的,编译器警告是确保代码有效性的重要辅助工具。现在,我们将尝试从我们创建的代码中找到错误。这是一个包含未初始化变量的 C++代码,这将给我们一个不可预测的结果:

/* warning.cpp */
#include <iostream>
#include <string>
int main (void) {
  std::string name;
  int age;
  std::cout << "Hi " << name << ", your age is " << age << "\n";
}

然后,我们将运行以下命令来编译前面的warning.cpp代码:

g++ -Wall -c warning.cpp

有时,我们无法检测到这个错误,因为一开始并不明显。但是,通过启用-Wall选项,我们可以防止错误,因为如果我们使用警告选项编译前面的代码,编译器将产生警告消息,如下面的代码所示:

warning.cpp: In function 'int main()':
warning.cpp:7:52: warning: 'age' may be used uninitialized in this function [-Wmaybe-uninitialized]
std::cout << "Hi " << name << ", your age is " << age << "\n";]

警告消息说age变量在warning.cpp文件的第 7 行,第 52 列未初始化。GCC 生成的消息始终具有file:line-number:column-number:error-type:message的形式。错误类型区分了阻止成功编译的错误消息和指示可能问题的警告消息(但不会阻止程序编译)。

显然,开发程序而不检查编译器警告是非常危险的。如果有任何未正确使用的函数,它们可能会导致程序崩溃或产生不正确的结果。打开编译器警告选项后,-Wall选项会捕获 C++编程中发生的许多常见错误。

在 GCC C++编译器中了解其他重要选项

GCC 在 4.9.2 版本中支持ISO C++ 1998C++ 2003C++ 2011标准。在 GCC 中选择此标准是使用以下选项之一:-ansi-std=c++98-std=c++03–std=c++11。让我们看看以下代码,并将其命名为hash.cpp

/* hash.cpp */
#include <iostream>
#include <functional>
#include <string>
int main(void) {
  std::string plainText = "";
  std::cout << "Input string and hit Enter if ready: ";
  std::cin >> plainText;
  std::hash<std::string> hashFunc;
  size_t hashText = hashFunc(plainText);
  std::cout << "Hashing: " << hashText << "\n";
  return 0;
}

如果编译并运行程序,它将为每个纯文本用户输入给出一个哈希数。然而,编译上述代码有点棘手。我们必须定义要使用的 ISO 标准。让我们看看以下五个编译命令,并在命令提示符窗口中逐个尝试它们:

g++ -Wall hash.cpp -o hash
g++ -Wall -ansi hash.cpp -o hash
g++ -Wall -std=c++98 hash.cpp -o hash
g++ -Wall -std=c++03 hash.cpp -o hash
g++ -Wall -std=c++11 hash.cpp -o hash

当我们运行前面的四个编译命令时,应该会得到以下错误消息:

hash.cpp: In function 'int main()':
hash.cpp:10:2: error: 'hash' is not a member of 'std'
 std::hash<std::string> hashFunc;
hash.cpp:10:23: error: expected primary-expression before '>' token
 std::hash<std::string> hashFunc;
hash.cpp:10:25: error: 'hashFunc' was not declared in this scope
 std::hash<std::string> hashFunc;

它说std类中没有hash。实际上,自 C++ 2011 以来,头文件<string>中已经定义了哈希。为了解决这个问题,我们可以运行上述最后一个编译命令,如果不再抛出错误,那么我们可以在控制台窗口中输入hash来运行程序。

在 GCC C++编译器中了解其他重要选项

如您在前面的屏幕截图中所见,我调用了程序两次,并将Packtpackt作为输入。尽管我只改变了一个字符,但整个哈希值发生了巨大变化。这就是为什么哈希用于检测数据或文件的任何更改,以确保数据没有被更改。

有关 GCC 中可用的 ISO C++11 功能的更多信息,请访问gcc.gnu.org/projects/cxx0x.html。要获得标准所需的所有诊断,还应指定-pedantic选项(或-pedantic-errors选项,如果您希望将警告作为错误处理)。

注意

-ansi选项本身不会导致非 ISO 程序被毫无根据地拒绝。为此,还需要-ansi选项以及-pedantic选项或-pedantic-errors选项。

GCC C++编译器中的故障排除

GCC 提供了几个帮助和诊断选项,以帮助解决编译过程中的问题。您可以使用的选项来简化故障排除过程在接下来的部分中进行了解。

命令行选项的帮助

使用help选项获取 GCC 命令行选项的摘要。命令如下:

g++ --help

要显示 GCC 及其关联程序(如 GNU 链接器和 GNU 汇编器)的完整选项列表,请使用前面的help选项和详细(-v)选项:

g++ -v --help

由上述命令生成的选项的完整列表非常长-您可能希望使用more命令查看它,或将输出重定向到文件以供参考,如下所示:

g++ -v --help 2>&1 | more

版本号

您可以使用version选项找到已安装的 GCC 版本号,如下所示:

g++ --version

在我的系统中,如果运行上述命令,将会得到如下输出:

g++ (x86_64-posix-seh-rev2, Built by MinGW-W64 project) 4.9.2

这取决于您在安装过程中调整的设置。

版本号在调查编译问题时非常重要,因为较旧版本的 GCC 可能缺少程序使用的某些功能。版本号采用major-version.minor-versionmajor-version.minor-version.micro-version的形式,其中额外的第三个“micro”版本号(如前述命令中所示)用于发布系列中随后的错误修复版本。

详细编译

-v选项还可以用于显示关于用于编译和链接程序的确切命令序列的详细信息。以下是一个示例,展示了hello.cpp程序的详细编译过程:

g++ -v -Wall rangen.cpp

之后,在控制台中会得到类似以下内容:

Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=C:/mingw-w64/bin/../libexec/gcc/x86_64-w64-mingw32/4.9.2/lto-wrapper.exe
Target: x86_64-w64-mingw32
Configured with: ../../../src/gcc-4.9.2/configure –
...Thread model: posix
gcc version 4.9.2 (x86_64-posix-seh-rev2, Built by MinGW-W64 project)
...

使用-v选项生成的输出在编译过程中出现问题时非常有用。它显示用于搜索头文件和库的完整目录路径,预定义的预处理器符号,以及用于链接的目标文件和库。

总结

我们成功准备了 C++编译器,并且您学会了如何使用编译器编译您创建的源代码文件。在编译源代码时,请不要忘记每次都使用-Wall(警告所有)选项,因为避免警告和细微错误非常重要。此外,使用-ansi-pedantic选项也很重要,这样您的源代码就能够在任何编译器中编译,因为它将检查 ANSI 标准并拒绝非 ISO 程序。

现在,我们可以进入下一章学习网络概念,以便您能够理解网络架构,从而简化您的网络应用程序编程过程。

第二章:理解网络概念

在我们开始编写网络应用程序之前,最好先了解一下网络是如何工作的。在本章中,我们将探讨网络概念及其内容。本章将涵盖的主题如下:

  • 区分 OSI 模型和 TCP/IP 模型

  • 探索 IPv4 和 IPv6 中的 IP 地址

  • 使用各种工具排除 TCP/IP 问题

网络系统简介

网络架构是由层和协议构成的。架构中的每个都有自己的作用,其主要目的是向更高层提供某种服务,并与相邻的层进行通信。然而,协议是一组规则和约定,被所有通信方使用以标准化通信过程。例如,当设备中的n层与另一个设备中的n层进行通信时,为了进行通信,它们必须使用相同的协议。

如今有两种流行的网络架构:开放系统互连OSI)和TCP/IP参考模型。我们将深入了解每个参考模型及其优缺点,以便决定在我们的网络应用程序中应该使用哪种模型。

OSI 参考模型

OSI 模型用于连接到开放系统-这些系统是开放的,并与其他系统通信。通过使用这个模型,我们不再依赖于操作系统,因此可以与任何计算机上的任何操作系统进行通信。这个模型包含七个层,每个层都有特定的功能,并定义了数据在不同层上的处理方式。包含在这个模型中的七个层分别是物理层数据链路层网络层传输层会话层表示层应用层

物理层

这是 OSI 模型中的第一层,包含了网络的物理规范的定义,包括物理介质(电缆和连接器)和基本设备(中继器和集线器)。该层负责将输入的原始比特传输数据流转换为零,并将位于通信通道上的数据。然后将数据放置到物理介质上。它关注数据传输的完整性,并确保从一个设备发送的比特与另一个设备接收到的数据完全相同。

数据链路层

数据链路层的主要作用是提供原始数据传输的链路。在数据传输之前,它将数据分成数据帧,并连续传输数据帧。如果服务是可靠的,接收方将为每个已发送的帧发送一个确认帧

这一层包括两个子层:逻辑链路控制LLC)和媒体访问控制MAC)。LLC 子层负责传输错误检查和帧传输,而 MAC 子层定义了如何从物理介质中检索数据或将数据存储在物理介质中。

我们还可以在这一层找到 MAC 地址,也称为物理地址。MAC 地址用于识别连接到网络的每个设备,因为每个设备的 MAC 地址都是唯一的。通过命令提示符,我们可以通过在控制台窗口中输入以下命令来获取地址:

ipconfig /all

我们将得到控制台输出,如下所示,忽略除Windows IP Configuration无线局域网适配器 Wi-Fi之外的所有其他信息。我们可以在物理地址部分找到 MAC 地址,对于我的环境来说是80-19-34-CB-BF-FB。由于 MAC 地址对每个设备都是唯一的,您将得到不同的结果:

Windows IP Configuration

 Host Name . . . . . . . . . . . . : HOST1
 Primary Dns Suffix  . . . . . . . :
 Node Type . . . . . . . . . . . . : Hybrid
 IP Routing Enabled. . . . . . . . : No
 WINS Proxy Enabled. . . . . . . . : No

Wireless LAN adapter Wi-Fi:
 Connection-specific DNS Suffix  . :
 Description . . . . . . . . . . . : Intel(R) Wireless-N 7260
 Physical Address. . . . . . . . . : 80-19-34-CB-BF-FB
 DHCP Enabled. . . . . . . . . . . : Yes
 Autoconfiguration Enabled . . . . : Yes
 Link-local IPv6 Address . . . . . : fe80::f14e:d5e6:aa0a:5855%3 (Preferred)
 IPv4 Address. . . . . . . . . . . : 192.168.1.4(Preferred)
 Subnet Mask . . . . . . . . . . . : 255.255.255.0
 Default Gateway . . . . . . . . . : 192.168.1.254
 DHCP Server . . . . . . . . . . . : 192.168.1.254
 DHCPv6 IAID . . . . . . . . . . . : 58726708
 DHCPv6 Client DUID. . . . . . . . : 00-01-00-01-1C-89-E6-3E-68-F7- 28-1E-61-66
 DNS Servers . . . . . . . . . . . : 192.168.1.254
 NetBIOS over Tcpip. . . . . . . . : Enabled

MAC 地址包含十二个十六进制字符,其中两个数字成对出现。前六位数字代表组织唯一标识符,剩下的数字代表制造商序列号。如果你真的很好奇想知道这个数字的含义,你可以去www.macvendorlookup.com并在文本框中填写我们的 MAC 地址以了解更多信息。在我的系统中,我得到了英特尔公司作为供应商公司名称,这与我安装的网络卡品牌相同。

网络层

网络层负责定义从源到目的地设备的数据包的最佳路由方式。它将使用Internet 协议IP)作为路由协议生成路由表,并使用 IP 地址确保数据到达所需目的地的路由。如今有两个版本的 IP:IPv4IPv6。在 IPv4 中,我们使用 32 位地址来寻址协议,在 IPv6 中使用 128 位地址。您将在下一个主题中了解更多关于 Internet 协议、IPv4 和 IPv6 的信息。

传输层

传输层负责将数据从源传输到目的地。它将数据分割成较小的部分,或在这种情况下称为,然后将所有段连接起来,将数据恢复到目的地的初始形式。

在这一层中有两种主要的协议:传输控制协议TCP)和用户数据报协议UDP)。TCP 通过建立会话来提供数据传输。在建立会话之前,数据不会被传输。TCP 也被称为面向连接的协议,这意味着在传输数据之前必须建立会话。UDP 是一种尽最大努力传输数据的方法,但不提供保证的传输,因为它不建立会话。因此,UDP 也被称为无连接的协议。关于 TCP 和 UDP 的深入解释可以在下一个主题中找到。

传输层

会话层负责建立、维护和终止会话。我们可以将会话类比为网络上两个设备之间的连接。例如,如果我们想要从一台计算机向另一台计算机发送文件,这一层将在发送文件之前首先建立连接。然后,这一层将确保连接仍然保持到文件完全发送。最后,如果不再需要,这一层将终止连接。我们谈论的连接就是会话。

这一层还确保来自不同应用程序的数据不会互相交换。例如,如果我们同时运行互联网浏览器、聊天应用程序和下载管理器,这一层将负责为每个应用程序建立会话,并确保它们与其他应用程序保持分离。

这一层使用了三种通信方法:单工半双工全双工方法。在单工方法中,数据只能由一方传输,因此另一方无法传输任何数据。由于我们需要可以相互交互的应用程序,这种方法已经不再常用。在半双工方法中,任何数据都可以传输到所有涉及的设备,但只有一个设备可以在某个时间传输数据,完成发送过程后,其他设备也可以发送和传输数据。全双工方法可以同时向所有设备传输数据。为了发送和接收数据,这种方法使用不同的路径。

表示层

表示层的作用是确定已发送的数据,将数据转换为适当的格式,然后呈现出来。例如,我们通过网络发送一个 MP3 文件,文件被分成几个段。然后,使用段上的头信息,这一层将通过翻译段来构建文件。

此外,这一层负责数据压缩和解压缩,因为所有在互联网上传输的数据都经过压缩以节省带宽。这一层还负责数据加密和解密,以确保两个设备之间的通信安全。

应用层

应用层处理用户使用的计算机应用程序。只有连接到网络的应用程序才会连接到这一层。这一层包含用户需要的几个协议,如下所示:

  • 域名系统DNS):这个协议是用来找到 IP 地址的主机名的。有了这个系统,我们不再需要记住每个 IP 地址,只需要记住主机名。我们可以更容易地记住主机名中的单词,而不是 IP 地址中的一堆数字。

  • 超文本传输协议HTTP):这个协议用于在网页上在互联网上传输数据。我们还有 HTTPS 格式,用于发送加密数据以解决安全问题。

  • 文件传输协议FTP):这个协议用于从 FTP 服务器传输文件或到 FTP 服务器传输文件。

  • 简单文件传输协议TFTP):这个协议类似于 FTP,用于发送较小的文件。

  • 动态主机配置协议DHCP):这个协议是用于动态分配 TCP/IP 配置的方法。

  • 邮局协议POP3):这个协议是用于从 POP3 服务器获取电子邮件的电子邮件协议。服务器通常由互联网服务提供商ISP)托管。

  • 简单邮件传输协议SMTP):这个协议与 POP3 相反,用于发送电子邮件。

  • 互联网消息访问协议IMAP):这个协议用于接收电子邮件。使用这个协议,用户可以将他们的电子邮件消息保存在本地计算机上的文件夹中。

  • 简单网络管理协议SNMP):这个协议用于管理网络设备(路由器和交换机)并在问题变得重大之前检测并报告问题。

  • 服务器消息块SMB):这个协议是主要用于文件和打印机共享的 Microsoft 网络上的 FTP。

这一层还决定了是否有足够的网络资源可供网络访问。例如,如果您想使用互联网浏览器上网,应用层会决定是否可以使用 HTTP 访问互联网。

让我们看下面的图,看看 OSI 层中包含了哪些协议:

应用层

我们可以将所有七层分为两个部分层:上层下层。上层负责与用户交互,对低级细节不太关心,而下层负责在网络上传输数据,如格式化和编码。

每一层传输的数据格式都不同。物理层有比特,数据链路层有帧,依此类推。

TCP/IP 参考模型

TCP/IP 模型是在 OSI 模型之前创建的。这个模型的工作方式与 OSI 模型类似,只是它只包含四层。TCP/IP 模型的每一层对应于 OSI 模型的层。TCP/IP 应用层映射 OSI 模型的第 5、6 和 7 层。TCP/IP 传输层映射 OSI 模型的第 4 层。TCP/IP 互联网层映射 OSI 模型的第 3 层。TCP/IP 链路层映射 OSI 模型的第 1 和 2 层。让我们看下图以了解更多细节:

TCP/IP 参考模型

这些是 TCP/IP 模型中每个层的主要作用:

  • 链路层负责确定在数据传输过程中使用的协议和物理设备。

  • 互联网层负责通过寻址数据包确定最佳的数据传输路由。

  • 传输层负责建立两个设备之间的通信并发送数据包。

  • 应用层负责为计算机上运行的应用程序提供服务。由于缺少会话和表示层,应用程序必须包含任何所需的会话和表示功能。

以下是涉及 TCP/IP 模型的协议和设备:

协议 设备
应用 HTTP、HTTPS、SMTP、POP3 和 DNS 代理服务器和防火墙
传输 TCP 和 UDP -
互联网 IP 和 ICMP 路由器
链路 以太网、令牌环和帧中继 集线器、调制解调器和中继器

理解 TCP 和 UDP

正如我们在本章的传输层部分中讨论的那样,TCP 和 UDP 是用于在网络中传输数据的主要协议。它们的传输机制彼此不同。TCP 在传输数据过程中提供了确认、序列号和流量控制以提供可靠的传输,而 UDP 不提供可靠的传输,但尽最大努力提供传输。

传输控制协议

在协议建立会话之前,TCP 执行三次握手过程。这是为了提供可靠的传输。请参考下图了解三次握手过程:

传输控制协议

从上图中可以想象,Carol 的设备想要向 Bryan 的设备传输数据,并且它们需要执行三次握手过程。首先,Carol 的设备发送一个带有同步(SYN)标志的数据包到 Bryan 的设备。一旦 Bryan 的设备接收到数据包,它会回复发送另一个带有 SYN 和确认(ACK)标志的数据包。最后,Carol 的设备通过发送一个带有 ACK 标志的第三个数据包完成握手过程。现在,两个设备都建立了会话,并确保对方正在工作。会话建立后,数据传输就准备好进行了。

提示

在安全领域,我们知道“SYN-Flood”这个术语,它是一种拒绝服务攻击,攻击者向目标系统发送一系列 SYN 请求,试图消耗足够的服务器资源使系统对合法流量无响应。攻击者只发送 SYN 而不发送预期的 ACK,导致服务器向伪造的 IP 地址发送 SYN-ACK,而伪造的 IP 地址不会发送 ACK,因为它“知道”它从未发送过 SYN。

TCP 还将数据分割成较小的段,并使用序列号来跟踪这些段。每个分离的段被分配不同的序列号,比如 1 到 20。目标设备接收每个段,并使用序列号根据序列的顺序重新组装文件。

例如,假设 Carol 想要从 Bryan 的设备下载一个 JPEG 图像文件。在进行三次握手的过程中建立会话后,两个设备确定单个段的大小以及在确认之间需要发送多少个段。可以同时发送的段的总数称为 TCP 滑动窗口。如果在传输过程中有一个位损坏或丢失,段中的数据将不再有效。TCP 使用循环冗余检查(CRC)来识别损坏或丢失的数据,通过验证每个段中的数据是否完整。如果传输中有任何损坏或丢失的段,Carol 的设备将发送一个负确认(NACK)数据包,然后请求损坏或丢失的段;否则,Carol 的设备将发送一个 ACK 数据包并请求下一个段。

用户数据报协议

UDP 在发送数据之前不执行任何握手过程。它只是直接将数据发送到目标设备;但是,它会尽最大努力转发消息。想象一下,我们正在等待朋友的消息。我们打电话给他/她来接收我们的消息。如果我们的电话没有接听,我们可以发送电子邮件或短信通知我们的朋友。如果我们的朋友没有回复我们的电子邮件或短信,我们可以发送常规电子邮件。然而,我们讨论的所有技术都不能保证我们的消息已被接收。但是,我们仍然尽最大努力转发消息,直到成功为止。我们在发送电子邮件的类比中的最大努力类似于 UDP 的最大努力术语。它将尽最大努力确保接收方接收到数据,即使不能保证数据已被接收。

那么,为什么即使 UDP 不可靠也会使用它呢?有时我们需要进行快速数据传输的通信,即使有一点数据损坏也可以。例如,流媒体音频、流媒体视频和 VoIP 使用 UDP 来确保它们具有快速的数据传输速度。尽管 UDP 可能会丢失数据包,我们仍然能够清晰地接收所有消息。

然而,尽管 UDP 在传输数据之前不检查连接,但它实际上使用校验和来验证数据。校验和可以通过比较校验和值来检查接收到的数据是否被更改。

理解端口

在计算机网络中,端口是发送或接收数据的端点。端口通过其端口号来识别,其中包含一个 16 位数字。逻辑端口号被 TCP 和 UDP 用来跟踪数据包的内容,并在设备接收到数据时帮助 TCP/IP 获取将处理数据的应用程序或服务的数据包。

TCP 端口总共有 65536 个,UDP 端口也有 65536 个。我们可以将 TCP 端口分为三个端口范围,分别是:

  • 从 0 到 1023 的众所周知的端口是由 IANA 注册的,用于与特定协议或应用程序相关联。

  • 从 1024 到 49151 的注册端口是由 IANA 注册的,用于特定协议,但在此范围内未使用的端口可以由计算机应用程序分配。

  • 从 49152 到 65535 的动态端口是未注册的端口,可以用于任何目的。

注意

要获取有关 TCP 和 UDP 中所有端口的更多详细信息,可以访问en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers。此外,要了解所有已由 IANA 注册的已分配端口,请访问www.iana.org/assignments/port-numbers

要理解端口的概念,可以考虑我们的计算机上安装了电子邮件客户端,如 Thunderbird 或 Microsoft Outlook。现在,我们想要将电子邮件发送到 Gmail 服务器,然后从服务器上获取所有传入的电子邮件并将其保存在我们的本地计算机上。发送电子邮件的步骤如下:

  1. 我们的计算机会分配一个随机未使用的端口号,比如48127,用来将电子邮件发送到 Gmail SMTP 服务器的端口25

  2. 当电子邮件到达 SMTP 服务器时,它会识别数据来自端口25,然后将数据转发到处理该服务的 SMTP。

  3. 一旦电子邮件被接收,服务器会将确认发送到我们计算机上的端口48127,以通知计算机已经接收到电子邮件。

  4. 在我们的计算机完全接收到来自端口48127的确认后,它会将电子邮件发送到电子邮件客户端,然后电子邮件客户端将电子邮件从发件箱移动到已发送文件夹。

与发送电子邮件的步骤类似,要接收电子邮件,我们必须处理一个端口。接收电子邮件的步骤如下:

  1. 我们的计算机会分配一个随机未使用的端口号,比如48128,用来向 Gmail POP3 服务器发送请求到端口110

  2. 当电子邮件到达 POP3 服务器时,它会识别数据来自端口110,然后将数据转发到处理该服务的 POP3。

  3. 然后,POP3 服务器会在端口48128向我们的计算机发送电子邮件。

  4. 在我们的计算机从端口48128接收到电子邮件后,它会将电子邮件发送到我们的电子邮件客户端,然后将其移动到收件箱文件夹。它还会自动将邮件保存到本地计算机。

探索 Internet 协议

IP 是一种主要的通信协议,用于在网络上传递数据报。数据报本身是与分组交换网络相关联的传输单元。IP 的作用是根据数据包头部中指定的 IP 地址,从主机传递数据包到主机。目前常用的 IP 版本有两个,即 IPv4 和 IPv6。

Internet 协议版本 4 - IPv4

自 1980 年代以来,IPv4 已成为标准 IP 地址,并用于在网络上从一台计算机到另一台获取 TCP/IP 流量。每个连接到互联网的设备都有唯一的 IP 地址,只要它们有有效的 IP 地址,所有设备都可以在互联网上相互通信。

有效的 IP 地址由四个十进制数构成,用三个点分隔。地址只包含从0255的十进制数。我们可以说10.161.4.25是一个有效的 IP 地址,因为它包含了从0255的四个十进制数,并用三个点分隔,而192.2.256.4是一个无效的 IP 地址,因为它包含了大于255的十进制数。

十进制数实际上将结果从 8 位二进制数字转换而来。因此,对于最大的 8 位数,我们将得到 1111 1111 或者十进制的 255。这就是为什么 IP 地址中十进制数的范围是从 0(0000 0000)到 255(1111 1111)。

要了解我们的 IP 地址配置,我们可以在命令提示符窗口中再次使用ipconfig /all命令。然后,它将显示以下输出:

Wireless LAN adapter Wi-Fi:
 Connection-specific DNS Suffix  . :
 Link-local IPv6 Address . . . . . : fe80::f14e:d5e6:aa0a:5855%3
 IPv4 Address. . . . . . . . . . . : 10.1.6.165
 Subnet Mask . . . . . . . . . . . : 255.255.255.0
 Default Gateway . . . . . . . . . : 10.1.6.1

输出将显示 IPv4 地址和 IPv6 地址中的 IP 地址。我们还可以看到在我的设备中,10.1.6.1被用作系统的默认网关。默认网关参数是计算机网络上的一个点,用于为不匹配的 IP 地址或子网提供路径。

IP 地址必须包含这两个组件:网络 ID用于识别计算机所在的子网络或子网,主机 ID用于识别该子网中的计算机。每个网络 ID 表示网络子网上的一组主机。具有相同网络 ID 的设备必须具有唯一的主机 ID。如果两个或更多设备具有相同的主机 ID 和相同的网络 ID(所有四个十进制数的 IP 地址相同),则会发生 IP 地址冲突。

对于本地网络,子网掩码用于识别 IP 地址中的网络 ID 和主机 ID 部分。以下是一些常见的子网掩码:

  • 255.0.0.0

  • 255.255.0.0

  • 255.255.255.0

假设我们有 IP 地址190.23.4.51和子网掩码255.255.0.0。现在,我们可以使用每个与子网掩码对应的 IP 地址位的布尔AND逻辑来找到网络 ID。以下表将 IP 地址和子网掩码转换为二进制数字,然后使用布尔AND逻辑来找出网络 ID:

第一组 第二组 第三组 第四组
190.23.4.51 1011 1110 0001 0111 0000 0100 0011 0011
255.255.0.0 1111 1111 1111 1111 0000 0000 0000 0000
网络 ID: 1011 1110 0001 0111 0000 0000 0000 0000

从上表中,我们可以得到网络 ID,即190.23.0.0

相邻的最大数字必须应用于子网掩码。这意味着如果决定使用第一个零,剩下的数字必须为零。因此,子网掩码255.0.255.0是无效的。子网掩码也不允许以零开头。这意味着子网掩码0.255.0.0也是无效的。

IPv4 可以分为三个主要地址类:A 类、B 类和 C 类。地址的类由 IP 地址中的第一个数字和每个类的预定义子网掩码来定义。以下是每个类的三个范围:

第一个数字 IP 地址范围 子网掩码
A 类 1 至 126 1.0.0.0 至 126.255.255.254 255.0.0.0
B 类 128 至 191 128.0.0.0 至 191.255.255.254 255.255.0.0
C 类 192 至 223 192.0.0.0 至 223.255.255.254 255.255.255.0

我们的计算机可以通过转换 IP 地址中第一个十进制数后的前两位比特来确定 IP 地址的类。例如,在 A 类中,范围为 1 至 126,二进制数字在 0000 0001 至 0111 1110 之间。前两位可能是 0 和 0 或 0 和 1。B 类的范围从 128 到 191,二进制数字范围为 1000 0000 至 1011 1111。这意味着最高的第一位始终为 1,第二位始终为 0。C 类的范围从 192 到 223,二进制数字范围为 1100 0000 至 1101 1111。前两位将是所有 1。请参考以下表格,以了解计算机如何通过检查 IP 地址的前两位来确定 IP 地址的类(这里,X 被忽略,可以是任何十六进制字符):

二进制数字中的第一个数字
A 类 00XXXXXX01XXXXXX
B 类 10XXXXXX
C 类 11XXXXXX

通过对 IP 地址进行分类,我们还可以通过查看 IP 地址来确定子网掩码,因为每个类都有不同的子网掩码,如下所示:

范围 子网掩码
A 类地址 0-126 255.0.0.0
B 类地址 128 至 191 255.255.0.0
C 类地址 192 至 223 255.255.255.0

通过了解子网掩码,我们可以轻松知道网络 ID。假设我们有以下三个 IP 地址:

  • 174.12.1.8

  • 192.168.1.15

  • 10.70.4.13

现在,我们可以按以下方式确定网络 ID:

IP 地址 子网掩码 网络 ID
174.12.1.8 B 类 255.255.0.0 174.12.0.0
192.168.1.15 C 类 255.255.255.0 192.168.1.0
10.70.4.13 A 类 255.0.0.0 10.0.0.0

子网掩码还可以使用一个称为无类别域间路由(CIDR)的指示器来引用,它是根据位数定义的。例如,子网掩码255.0.0.0使用 8 位(值为0的位被视为未使用的位),因此被引用为/8。同样,子网掩码 255.255.0.0 使用 16 位,可以被引用为/16,子网掩码 255.255.255.0 使用 24 位,可以被引用为/24。这些是我们之前 IP 地址示例的 CIDR 表示法:

IP 地址 子网掩码 CIDR 表示法
174.12.1.8 255.255.0.0 174.12.1.8 /16
192.168.1.15 255.255.255.0 192.168.1.15 /24
10.70.4.13 255.0.0.0 10.70.4.13 /8

互联网协议第 6 版 - IPv6

IPv6 包含 128 位,是为了改进 IPv4 而推出的,IPv4 只有 32 位。在 IPv4 中,32 位可以寻址 4,294,967,296 个地址。一开始这个数字很高,但现在已经不够用了,因为有很多设备需要 IP 地址。IPv6 被创建来解决这个问题,因为它可以寻址超过 340,000,000,000,000,000,000,000,000,000,000,000,000 个地址,或约3.4028e+38,这已经足够多了——至少目前是这样。

注意

IPv5 曾经被开发为 64 位,但从未被采用,因为人们认为如果使用 IPv5,互联网很快就会用完 IP 地址。

IPv4 地址和 IPv6 地址之间的显着区别在于,IPv6 不是用十进制数字表示 IP 地址,而是用十六进制字符表示。我们可以通过一眼就看到的这种格式数字来确定它是 IPv4 还是 IPv6。我们可以调用ipconfig /all命令来了解我们的 IPv6 地址,并在以太网适配器网络中查看它。我的是fe80::f14e:d5e6:aa0a:5855%3,但你的肯定不一样。地址本身是fe80::f14e:d5e6:aa0a:5855,最后的%3变量是一个区域索引,用于标识网络接口卡。第一个 IPv6 地址中的数字fe80被称为链路本地地址,这是一个在网络上自动分配的 IP 地址,因为它没有通过 DHCP 自动配置,也没有手动配置。

我们知道,IPv6 实际上是一组 128 位,并将其位转换为十六进制字符,以简化其表示。考虑到我们有一组二进制数字形成 IPv6,如下所示:

0010 0000 0000 0001 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0100 1111 0000 1001 0111 0011
1111 0101 1111 1110 1111 1000 1011 0110

与其记住所有这些数字,不如将其转换为 IPv6 地址格式。首先,我们将每个四位数字组转换为十六进制字符,我们将得到这些十六进制字符:

2001000000000000004f0973f5fef8b6

其次,我们用冒号分隔每组四个字符,如下所示:

2001:0000:0000:0000:004f:0973:f5fe:f8b6

第三,我们可以去掉每个四位数字集合中的前导零,如下所示:

2001:0:0:0:4f:973:f5fe:f8b6

第四,我们将连续的零组合并成一个空组,如下所示:

2001::4f:973:f5fe:f8b6

现在我们更容易记住这个 IPv6 地址。

注意

一个空组,由两个冒号(::)表示,意味着插入尽可能多的零以形成 128 位的地址。IPv6 地址不允许有多个空组,因为这样会让我们难以确定每个空组中有多少个零。

同样,对于 IPv4,它通过查看第一个数字(实际上是前两位)来对 IP 地址进行分类,IPv6 的类型也可以通过查看其前缀来确定。这就是我们如何写入所有具有以 32 位前缀开头的网络 ID2001:04fe的地址:

2001:04fe:: /32

这意味着所有地址的前 32 位是 0010 0000 0000 0001 000 0100 1111 1110。然而,为了方便阅读这个地址,我们使用十六进制字符。

使用 TCP/IP 工具进行故障排除

以下命令可以用来跟踪任何 TCP/IP 错误。这些命令可以用来检查是否有任何路由器宕机或是否建立了任何连接。然后,它将帮助我们决定适当的解决方案。

ipconfig 命令

我们之前使用ipconfig命令来识别 MAC 地址和 IP 地址。除此之外,我们还可以使用此命令来检查 TCP/IP 配置。我们还可以根据即将介绍的部分来使用此命令。

显示完整的配置信息

要完全显示配置信息,我们可以在控制台上调用以下命令:

ipconfig /all

关于网络适配器的所有配置信息都将显示给我们,例如网络接口卡、无线网卡和以太网适配器,就像我们在本章的数据链路层部分中已经尝试过的那样,当我们寻找 MAC 地址时。

显示 DNS

以下命令将使用以下选项显示 DNS 解析器缓存的内容:

ipconfig /displaydns

通过调用上述命令,我们将得到本地系统中 DNS 的信息,如下所示:

Windows IP Configuration

 ipv4only.arpa
 ----------------------------------------
 Record Name . . . . . : ipv4only.arpa
 Record Type . . . . . : 1
 Time To Live  . . . . : 77871
 Data Length . . . . . : 4
 Section . . . . . . . : Answer
 A (Host) Record . . . : 192.0.0.170

 Record Name . . . . . : ipv4only.arpa
 Record Type . . . . . : 1
 Time To Live  . . . . : 77871
 Data Length . . . . . : 4
 Section . . . . . . . : Answer
 A (Host) Record . . . : 192.0.0.171

 ieonlinews.microsoft.com
 ----------------------------------------
 Record Name . . . . . : ieonlinews.microsoft.com
 Record Type . . . . . : 1
 Time To Live  . . . . : 307
 Data Length . . . . . : 4
 Section . . . . . . . : Answer
 A (Host) Record . . . : 131.253.34.240

显示 DNS 输出中每个字段的含义如下:

  • 记录名称:这是要与 IP 地址关联的 DNS 名称。

  • 记录类型:这是记录的类型,表示为一个数字。

  • 生存时间:这是缓存过期时间,以秒为单位。

  • 数据长度:这是以字节为单位存储记录值文本的内存大小。

  • 部分:如果值为Answer,这意味着它回复了实际查询,但如果值为Additional,这意味着它包含了查找实际答案所需的信息。

  • A(主机)记录:这是实际值存储的位置。

刷新 DNS

以下命令用于移除已解析的 DNS 服务器项目,但不会移除缓存中的项目。在命令提示符中输入以下命令:

ipconfig /flushdns

一旦成功刷新 DNS 解析器缓存,我们将在控制台中看到此消息:

Successfully flushed the DNS Resolver Cache.

如果我们再次调用ipconfig /displaydns命令,已解析的 DNS 服务器已被移除,剩下的是缓存中的项目。

更新 IP 地址

有两个命令可以用来更新 IP 地址,它们是:

ipconfig /renew

上述命令将从 DHCP 服务器更新 IPv4 的租约过程,而以下命令将更新 IPv6 的租约过程:

ipconfig /renew6

释放 IP 地址

使用以下两个命令分别释放从 DHCP 服务器获取的 IPv4 和 IPv6 的租约过程:

ipconfig /release
ipconfig /release6

这些命令只影响由 DHCP 分配(自动分配)的 IP 地址。

ping 命令

ping命令用于检查与其他计算机的连接。它使用Internet 控制消息协议ICMP)向目标计算机发送消息。我们可以使用 IP 地址和主机名来 ping 目标。假设我们有一个名为HOST1的设备,要 ping 自己,我们可以使用以下命令:

ping HOST1

然后,我们将在控制台窗口中得到以下输出:

Pinging HOST1 [fe80::f14e:d5e6:aa0a:5855%3] with 32 bytes of data:
Reply from fe80::f14e:d5e6:aa0a:5855%3: time<1ms
Reply from fe80::f14e:d5e6:aa0a:5855%3: time<1ms
Reply from fe80::f14e:d5e6:aa0a:5855%3: time<1ms
Reply from fe80::f14e:d5e6:aa0a:5855%3: time<1ms

Ping statistics for fe80::f14e:d5e6:aa0a:5855%3:
 Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
 Minimum = 0ms, Maximum = 0ms, Average = 0ms

如果我们得到了 IPv6 地址,而我们想要显示 IPv4 地址,我们可以使用-4选项来强制使用 IPv4 地址,如下所示:

ping HOST1 -4

然后,我们将得到以下输出:

Pinging HOST1 [10.1.6.165] with 32 bytes of data:
Reply from 10.1.6.165: bytes=32 time<1ms TTL=128
Reply from 10.1.6.165: bytes=32 time<1ms TTL=128
Reply from 10.1.6.165: bytes=32 time<1ms TTL=128
Reply from 10.1.6.165: bytes=32 time<1ms TTL=128

Ping statistics for 10.1.6.165:
 Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
 Minimum = 0ms, Maximum = 0ms, Average = 0ms

但是,如果我们显示了 IPv4 地址,而我们需要获取 IPv6 地址,我们可以使用-6选项来强制使用 IPv6 地址,如下所示:

ping HOST1 -6

ping命令中,有两个发生的点。首先,名为HOST1的计算机解析为 IP 地址10.1.6.165。如果主机名解析不起作用,我们将得到如下错误:

Ping request could not find host HOST1\. Please check the name and try again.

其次,该命令向HOST1发送四个数据包并接收四个数据包。这个回复表示名为HOST1的计算机正常工作,并能够响应命令请求。如果HOST1不工作或无法响应请求,我们将看到以下输出:

Pinging HOST1 [10.1.6.165] with 32 bytes of data:
Request timed out.
Request timed out.
Request timed out.
Request timed out.
Ping statistics for 192.168.1.112:
 Packets: Sent = 4, Received = 0, Lost = 4 (100% loss),

当我们发送 ping 命令时,可能会遇到一些错误信息,其中一些如下:

  • 目标主机不可达:这表示路由存在问题。这可能是因为本地计算机或远程计算机默认网关的错误配置。

  • 传输中的 TTL 已过期:这表示 ping 过程已经通过的路由器数量大于 TTL(生存时间)值。每次 ping 通过一个路由器,TTL 值都会减少。如果 ping 必须通过的路由器总数大于 TTL 值,将显示此错误消息。

在 ping 命令中,我们可以使用另一个选项-t。使用此选项,ping 命令将持续发送数据包,直到用户按下Ctrl + C停止。通常在等待断开状态转为连接状态时使用。我们可以通过以下方式将命令发送到控制台:

ping HOST1 -t

tracert 命令

当我们有多个路由器时,可以使用tracert命令跟踪数据包的路径。tracert命令类似于 ping 命令,不同之处在于tracert包含了源设备和目标设备之间的路由器信息。以下是我用于跟踪从我的设备到google.com的通信轨迹的命令:

tracert google.com

我在控制台窗口中得到了这个输出:

Tracing route to google.com [173.194.126.32]
over a maximum of 30 hops:

 1     1 ms     1 ms     1 ms  254.1.168.192.in-addr.arpa [192.168.1.254]
 2    23 ms    26 ms     *     125.166.200.1
 3     *        *      331 ms  189.subnet125-160-11.speedy.telkom.net.id [125.1
 60.11.189]
 4   293 ms    76 ms    84 ms  73.171.94.61.in-addr.arpa [61.94.171.73]
 5   504 ms   612 ms   612 ms  61.94.117.229
 6   698 ms   714 ms   209 ms  42.193.240.180.in-addr.arpa [180.240.193.42]
 7     *        *        *     Request timed out.
 8     *        *        *     Request timed out.
 9     *      668 ms   512 ms  190.221.14.72.in-addr.arpa [72.14.221.190]
 10     *        *        *     Request timed out.
 11     *        *      582 ms  136.142.85.209.in-addr.arpa [209.85.142.136]
 12   184 ms   202 ms   202 ms  233.242.85.209.in-addr.arpa [209.85.242.233]
 13     *        *      563 ms  241.251.85.209.in-addr.arpa [209.85.251.241]
 14   273 ms    96 ms    83 ms  kul01s08-in-f0.1e100.net [173.194.126.32]

Trace complete.

如您所见,有 14 行,每行代表一个跳数ping命令通过路由器的情况)。如果我们将一行除以一列,例如第四行,我们将得到以下表格:

跳数 RTT1 RTT2 RTT3 名称/IP 地址
4 293 毫秒 76 毫秒 84 毫秒 73.171.94.61.in-addr.arpa [61.94.171.73]

每行的解释如下:

  • 跳数:这是第一列,只是路由路径上的跳数。

  • RTT 列:这是数据包到达目的地并返回到我们的计算机的往返时间(RTT)。RTT 分为三列,因为tracecert命令发送三个单独的信号数据包。这是为了显示路由的一致性或不一致性。

  • 域名/IP 列:这是路由器的 IP 地址。如果可用,还将提供域名。

pathping 命令

pathping命令用于验证路由路径。它类似于tracert命令,检查两个设备的路由路径,然后像ping命令一样检查每个路由器的连接性。pathping命令向每个路由器发送 100 个请求命令,并期望得到 100 个回复。对于每个未回复的请求,pathping命令将计为 1%的数据丢失。因此,例如,如果有十个请求没有回复,就会有 10%的数据丢失。数据丢失的百分比越小,连接越好。

我们将尝试使用以下命令向google.com发送pathping命令:

pathping google.com

通过这样做,我们将得到以下输出:

Tracing route to google.com [173.194.126.67]
over a maximum of 30 hops:
 0  HOST1 [10.1.7.101]
 1  10.1.7.1
 2  ns.csl-group.net [192.168.2.4]
 3  101.255.54.25
 4  115.124.80.209
 5  peer-Exch-D2-out.tachyon.net.id [115.124.80.73]
 6  ip-sdi.net.id [103.11.31.1]
 7  ip-31-253.sdi.net.id [103.11.31.253]
 8  209.85.243.158
 9  216.239.40.129
 10  209.85.242.243
 11  209.85.251.175
 12  kul06s05-in-f3.1e100.net [173.194.126.67]

Computing statistics for 300 seconds...
 Source to Here   This Node/Link
Hop  RTT    Lost/Sent = Pct  Lost/Sent = Pct  Address
 0                                           HOST1 [10.1.7.101]
 0/ 100 =  0%   |
 1   33ms     1/ 100 =  1%     1/ 100 =  1%  10.1.7.1
 0/ 100 =  0%   |
 2   24ms     1/ 100 =  1%     1/ 100 =  1%  ns.csl-group.net [192.168.2.4]
 0/ 100 =  0%   |
 3   19ms     1/ 100 =  1%     1/ 100 =  1%  101.255.54.25
 0/ 100 =  0%   |
 4   18ms     1/ 100 =  1%     1/ 100 =  1%  115.124.80.209
 0/ 100 =  0%   |
 5   33ms     1/ 100 =  1%     1/ 100 =  1%  peer-Exch-D2-out.tachyon.net.id [115.124.80.73]
 0/ 100 =  0%   |
 6   53ms     0/ 100 =  0%     0/ 100 =  0%  ip-sdi.net.id [103.11.31.1]
 0/ 100 =  0%   |
 7   38ms     2/ 100 =  2%     2/ 100 =  2%  ip-31-253.sdi.net.id [103.11.31.253]
 0/ 100 =  0%   |
 8   44ms     1/ 100 =  1%     1/ 100 =  1%  209.85.243.158
 0/ 100 =  0%   |
 9   59ms     0/ 100 =  0%     0/ 100 =  0%  216.239.40.129
 4/ 100 =  4%   |
 10  ---     100/ 100 =100%    96/ 100 = 96%  209.85.242.243
 0/ 100 =  0%   |
 11  ---     100/ 100 =100%    96/ 100 = 96%  209.85.251.175
 0/ 100 =  0%   |
 12   62ms     4/ 100 =  4%     0/ 100 =  0%  kul06s05-in-f3.1e100.net [173.194.126.67]

Trace complete.

在第 10 和第 11 行,我们得到了 100%的数据包丢失,因为发送到网络的 100 个数据包丢失了。然而,这不太可能是因为数据未到达目标路由器,而是因为路由器阻止了 ICMP。通过这个命令,我们可以确定在哪个具体的路由器上会遇到大量数据丢失,特别是在连接了许多路由器的大型网络中。

我们还可以使用-q选项来更改发送到路由器的请求数量。我们只需要在选项后面说明新的请求数量,如下所示:

pathping -q 10 google.com

这将发送十个请求到路由器,而不是 100 个请求,速度会更快。

netstat 命令

netstat(代表网络统计)命令用于查看 TCP/IP 统计信息,显示当前设备上关于 TCP/IP 连接的所有信息。它将显示有关网络中涉及的连接、端口和应用程序的信息。我们可以通过在控制台窗口中输入该命令来使用它:

netstat

之后,我们将得到以下输出:

Active Connections

 Proto  Local Address          Foreign Address        State
 TCP    127.0.0.1:50239        HOST1:50240            ESTABLISHED
 TCP    127.0.0.1:50240        HOST1:50239            ESTABLISHED
 TCP    127.0.0.1:50242        HOST1:50243            ESTABLISHED
 TCP    127.0.0.1:50243        HOST1:50242            ESTABLISHED
 TCP    127.0.0.1:60855        HOST1:60856            ESTABLISHED
 TCP    127.0.0.1:60856        HOST1:60855            ESTABLISHED
 TCP    127.0.0.1:60845        HOST1:60846            ESTABLISHED
 TCP    127.0.0.1:60846        HOST1:60845            ESTABLISHED
 TCP    192.168.1.4:50257      a72-246-188-35:http    ESTABLISHED
 TCP    192.168.1.4:50258      a72-246-188-35:http    ESTABLISHED
 TCP    192.168.1.4:50259      a72-246-188-35:http    ESTABLISHED
 TCP    192.168.1.4:50260      a104-78-107-69:http    ESTABLISHED
 TCP    192.168.1.4:50261      a72-246-188-35:http    TIME_WAIT
 TCP    192.168.1.4:50262      a72-246-188-35:http    ESTABLISHED
 TCP    192.168.1.4:50263      151:http               SYN_SENT
 TCP    [::1]:12372            HOST1:49567            ESTABLISHED
 TCP    [::1]:49567            HOST1:12372            ESTABLISHED

我们可以看到netstat命令的输出中有四列。每列的解释如下:

  • Proto:显示协议的名称,即 TCP 或 UDP。

  • Local Address:显示本地计算机的 IP 地址以及正在使用的端口号。如果服务器正在监听所有接口,主机名将显示为星号(*)。如果端口尚未建立,端口号也将显示为星号。

  • Foreign Address:显示套接字连接到的远程计算机的 IP 地址和端口号。如果端口尚未建立,端口号将显示为星号(*)。

  • State:表示 TCP 连接的状态。我们将得到的可能状态如下:

  • SYN_SEND:表示主动打开系统。

  • SYN_RECEIVED:表示服务器刚刚收到来自客户端的 SYN。

  • ESTABLISHED:表示客户端收到了服务器的 SYN,会话已建立。

  • LISTEN:表示服务器准备接受连接。

  • FIN_WAIT_1:表示主动关闭系统。

  • TIMED_WAIT:表示客户端在主动关闭后进入此状态。

  • CLOSE_WAIT:表示被动关闭,即服务器刚刚收到来自客户端的第一个 FIN。

  • FIN_WAIT_2:表示客户端刚刚收到来自服务器的第一个 FIN 的确认。

  • LAST_ACK:表示服务器在发送自己的 FIN 时处于此状态。

  • CLOSED:表示服务器已收到来自客户端的 ACK,连接现在已关闭。

有关这些状态的更多详细信息,您可以访问tools.ietf.org/html/rfc793并参考第三章功能规范

telnet 命令

telnet(代表终端网络)命令用于通过 TCP/IP 网络访问远程计算机。在 Windows 中,有两个 Telnet 功能,即 Telnet 服务器和 Telnet 客户端。前者用于配置 Windows 以侦听传入连接并允许其他人使用它。而后者用于通过 Telnet 与任何服务器连接。

默认情况下,Telnet 在 Windows 系统上未安装,因为存在安全风险。保持 Telnet 禁用更安全,因为攻击者可以使用 Telnet 检查系统上的开放端口。然而,没有人能阻止我们在系统中安装它。我们可以通过执行以下步骤来安装 Telnet。

  1. 通过按下Windows + R打开运行窗口,输入%SYSTEMROOT%\System32\OptionalFeatures.exe,然后按下确定按钮。Windows 功能窗口将随即打开。

  2. 勾选Telnet 客户端Telnet 服务器选项,然后按下确定按钮以确认更改。勾选的选项将看起来像下面的截图:The telnet command

Telnet 现在应该已经安装在我们的计算机上了。打开命令提示窗口,并运行以下命令来启动 Telnet:

telnet

按下Enter键后,您将看到以下输出,并在末尾闪烁的光标:

Welcome to Microsoft Telnet Client
Escape Character is 'CTRL+]'
Microsoft Telnet>_

现在,Telnet 已准备好接收我们的命令。为了测试它,我们可以在其中运行各种命令。Telnet 中可用的命令的完整列表可以在windows.microsoft.com/en-us/windows/telnet-commands找到。

总结

在本章中,当我们谈论网络架构时,我们了解了 OSI 和 TCP/IP 模型中每个层的主要作用。我们探讨了 Internet Protocol,并能够区分 IPv4 和 IPv6 之间的区别。我们还能够确定子网掩码并对 IP 地址进行分类。此外,我们能够使用各种 TCP/IP 工具检测错误是否发生。

在下一章中,我们将讨论 Boost C++库,这个库将使我们在 C++编程中更加高效。现在,让我们准备好我们的编程工具,进入下一章。

第三章:介绍 Boost C++库

许多程序员使用库,因为这简化了编程过程。使用库可以节省大量的代码开发时间,因为他们不再需要从头开始编写函数。在本章中,我们将熟悉 Boost C++库。让我们准备自己的编译器和文本编辑器,以证明 Boost 库的强大功能。在这样做的过程中,我们将讨论以下主题:

  • 介绍 C++标准模板库

  • 介绍 Boost 库

  • 在 MinGW 编译器中准备 Boost C++库

  • 构建 Boost 库

  • 编译包含 Boost C++库的代码

介绍 C++标准模板库

C++ 标准模板库STL)是一个基于模板的通用库,提供了通用容器等功能。程序员可以轻松使用 STL 提供的算法,而不是处理动态数组、链表、二叉树或哈希表。

STL 由容器、迭代器和算法构成,它们的作用如下:

  • 容器:它们的主要作用是管理某种类型的对象的集合,例如整数数组或字符串链表。

  • 迭代器:它们的主要作用是遍历集合的元素。迭代器的工作方式类似于指针。我们可以使用++运算符递增迭代器,并使用*运算符访问值。

  • 算法:它们的主要作用是处理集合的元素。算法使用迭代器遍历所有元素。在迭代元素后,它处理每个元素,例如修改元素。它还可以在迭代所有元素后搜索和排序元素。

通过创建以下代码来检查 STL 结构的三个元素:

/* stl.cpp */
#include <vector>
#include <iostream>
#include <algorithm>

int main(void) {
  int temp;
  std::vector<int> collection;
  std::cout << "Please input the collection of integer numbers, input 0 to STOP!\n";
  while(std::cin >> temp != 0) {
    if(temp == 0) break;
    collection.push_back(temp);
  }
  std::sort(collection.begin(), collection.end());
  std::cout << "\nThe sort collection of your integer numbers:\n";
  for(int i: collection) {
    std::cout << i << std::endl;
  }
}

将前面的代码命名为stl.cpp,并运行以下命令进行编译:

g++ -Wall -ansi -std=c++11 stl.cpp -o stl

在我们解剖这段代码之前,让我们运行它看看会发生什么。这个程序将要求用户输入尽可能多的整数,然后对数字进行排序。要停止输入并要求程序开始排序,用户必须输入0。这意味着0不会包括在排序过程中。由于我们没有阻止用户输入非整数数字,比如 3.14,程序很快就会停止等待用户输入下一个数字。代码产生以下输出:

介绍 C++标准模板库

我们输入了六个整数:43756891224056。最后一个输入是0,以停止输入过程。然后程序开始对数字进行排序,我们得到了按顺序排序的数字:74356915682240

现在,让我们检查我们的代码,以确定 STL 中包含的容器、迭代器和算法:

std::vector<int> collection;

 vector in the code. A vector manages its elements in a dynamic array, and they can be accessed randomly and directly with the corresponding index. In our code, the container is prepared to hold integer numbers so we have to define the type of the value inside the angle brackets <int>. These angle brackets are also called generics in STL:
collection.push_back(temp);
std::sort(collection.begin(), collection.end());

前面代码中的begin()end()函数是 STL 中的算法。它们的作用是处理容器中的数据,用于获取容器中的第一个和最后一个元素。在此之前,我们可以看到push_back()函数,用于将元素追加到容器中:

for(int i: collection) {
 std::cout << i << std::endl;
}

前面的for块将迭代称为collection的整数的每个元素。每次迭代元素时,我们可以单独处理元素。在前面的示例中,我们向用户显示了数字。这就是 STL 中迭代器发挥作用的方式。

#include <vector>
#include <algorithm>

我们包括向量定义以定义所有vector函数和algorithm定义以调用sort()函数。

介绍 Boost C++库

Boost C++库是一组库,用于补充 C++标准库。该集合包含 100 多个库,我们可以使用它们来提高 C++编程的生产力。当我们的需求超出 STL 提供的范围时,也可以使用它。它以 Boost 许可证提供源代码,这意味着它允许我们免费使用、修改和分发这些库,甚至用于商业用途。

Boost 的开发由来自世界各地的 C++开发人员组成的 Boost 社区处理。社区的使命是开发高质量的库,作为 STL 的补充。只有经过验证的库才会被添加到 Boost 库中。

注意

有关 Boost 库的详细信息,请访问www.boost.org。如果您想为 Boost 开发库做出贡献,可以加入开发者邮件列表lists.boost.org/mailman/listinfo.cgi/boost

所有库的完整源代码都可以在官方 GitHub 页面github.com/boostorg上找到。

Boost 库的优势

正如我们所知,使用 Boost 库将提高程序员的生产力。此外,通过使用 Boost 库,我们将获得诸如以下优势:

  • 它是开源的,所以我们可以检查源代码并在需要时进行修改。

  • 它的许可证允许我们开发开源和闭源项目。它还允许我们自由商业化我们的软件。

  • 它有很好的文档,并且我们可以在官方网站上找到所有库的解释,以及示例代码。

  • 它支持几乎所有现代操作系统,如 Windows 和 Linux。它还支持许多流行的编译器。

  • 它是 STL 的补充而不是替代。这意味着使用 Boost 库将简化那些 STL 尚未处理的编程过程。实际上,Boost 的许多部分都包含在标准 C++库中。

为 MinGW 编译器准备 Boost 库

在使用 Boost 库编写 C++应用程序之前,需要配置库以便 MinGW 编译器能够识别。在这里,我们将准备我们的编程环境,以便我们的编译器能够使用 Boost 库。

下载 Boost 库

下载 Boost 的最佳来源是官方下载页面。我们可以通过将互联网浏览器指向www.boost.org/users/download来访问该页面。在当前版本部分找到下载链接。在撰写本书时,Boost 库的当前版本是 1.58.0,但当您阅读本书时,版本可能已经更改。如果是这样,您仍然可以选择当前版本,因为更高的版本必须与更低的版本兼容。但是,您必须根据我们稍后将要讨论的设置进行调整。否则,选择相同的版本将使您能够遵循本书中的所有说明。

有四种文件格式可供选择进行下载;它们是.zip.tar.gz.tar.bz2.7z。这四个文件之间没有区别,只是文件大小不同。ZIP 格式的文件大小最大,而 7Z 格式的文件大小最小。由于文件大小,Boost 建议我们下载 7Z 格式。请参考以下图片进行比较:

下载 Boost 库

从上图可以看出,ZIP 版本的大小为 123.1 MB,而 7Z 版本的大小为 65.2 MB。这意味着 ZIP 版本的大小几乎是 7Z 版本的两倍。因此,他们建议您选择 7Z 格式以减少下载和解压时间。让我们选择boost_1_58_0.7z进行下载,并将其保存到本地存储中。

部署 Boost 库

在本地存储中获得boost_1_58_0.7z后,使用 7ZIP 应用程序对其进行解压,并将解压文件保存到C:\boost_1_58_0

注意

7ZIP 应用程序可以从www.7-zip.org/download.html获取。

然后,该目录应包含以下文件结构:

部署 Boost 库

注意

与其直接浏览到 Boost 下载页面并手动搜索 Boost 版本,不如直接转到sourceforge.net/projects/boost/files/boost/1.58.0。当 1.58.0 版本不再是当前版本时,这将非常有用。

使用 Boost 库

Boost 中的大多数库都是仅头文件;这意味着所有函数的声明和定义,包括命名空间和宏,都对编译器可见,无需单独编译它们。现在我们可以尝试使用 Boost 与程序一起将字符串转换为int值,如下所示:

/* lexical.cpp */
#include <boost/lexical_cast.hpp>
#include <string>
#include <iostream>

int main(void) {
  try 	{
    std::string str;
    std::cout << "Please input first number: ";
    std::cin >> str;
    int n1 = boost::lexical_cast<int>(str);
    std::cout << "Please input second number: ";
    std::cin >> str;
    int n2 = boost::lexical_cast<int>(str);
    std::cout << "The sum of the two numbers is ";
    std::cout << n1 + n2 << "\n";
    return 0;
  }
  catch (const boost::bad_lexical_cast &e) {
    std::cerr << e.what() << "\n";
    return 1;
  }
}

打开 Notepad++应用程序,输入上述代码,并将其保存为lexical.cpp,保存在C:\CPP目录中——这是我们在第一章中创建的目录,简化 C++中的网络编程。现在打开命令提示符,将活动目录指向C:\CPP,然后输入以下命令:

g++ -Wall -ansi lexical.cpp –Ic:\boost_1_58_0 -o lexical

我们在这里有一个新选项,即-I(“包含”选项)。此选项与目录的完整路径一起使用,以通知编译器我们有另一个要包含到我们的代码中的头文件目录。由于我们将 Boost 库存储在c:\ boost_1_58_0中,我们可以使用-Ic:\boost_1_58_0作为附加参数。

lexical.cpp中,我们应用boost::lexical_caststring类型数据转换为int类型数据。程序将要求用户输入两个数字,然后自动找到这两个数字的和。如果用户输入不合适的数字,程序将通知他们发生了错误。

Boost.LexicalCast库由 Boost 提供,用于将一种数据类型转换为另一种数据类型(将数值类型(如intdoublefloat)转换为字符串类型,反之亦然)。现在,让我们解剖lexical.cpp,以便更详细地了解它的功能:

#include <boost/lexical_cast.hpp>
#include <string>
#include <iostream>

我们包括boost/lexical_cast.hpp,以便能够调用boost::lexical_cast函数,因为该函数在lexical_cast.hpp中声明。此外,我们使用string头文件来应用std::string函数,以及使用iostream头文件来应用std::cinstd::coutstd::cerr函数。

其他函数,如std::cinstd::cout,在第一章中已经讨论过,我们知道它们的功能,因此可以跳过这些行:

int n1 = boost::lexical_cast<int>(str);
int n2 = boost::lexical_cast<int>(str);

我们使用上述两个单独的行将用户提供的输入string转换为int数据类型。然后,在转换数据类型后,我们对这两个int值进行求和。

我们还可以在上述代码中看到try-catch块。它用于捕获错误,如果用户输入不合适的数字,除了 0 到 9。

catch (const boost::bad_lexical_cast &e)
{
 std::cerr << e.what() << "\n";
 return 1;
}

boost::bad_lexical_cast. We call the e.what() function to obtain the string of the error message.

现在让我们通过在命令提示符中输入lexical来运行应用程序。我们将得到以下输出:

使用 Boost 库

我为第一个输入放入了10,为第二个输入放入了20。结果是30,因为它只是对两个输入求和。但是如果我输入一个非数字值,例如Packt,会发生什么呢?以下是尝试该条件的输出:

使用 Boost 库

一旦应用程序发现错误,它将忽略下一个语句并直接转到catch块。通过使用e.what()函数,应用程序可以获取错误消息并显示给用户。在我们的示例中,我们获得了bad lexical cast: source type value could not be interpreted作为错误消息,因为我们尝试将string数据分配给int类型变量。

构建 Boost 库

正如我们之前讨论的,Boost 中的大多数库都是仅头文件的,但并非所有库都是如此。有一些库必须单独构建。它们是:

  • Boost.Chrono:用于显示各种时钟,如当前时间、两个时间之间的范围,或者计算过程中经过的时间。

  • Boost.Context:用于创建更高级的抽象,如协程和协作线程。

  • Boost.Filesystem:用于处理文件和目录,例如获取文件路径或检查文件或目录是否存在。

  • Boost.GraphParallel:这是Boost 图形库BGL)的并行和分布式计算扩展。

  • Boost.IOStreams:用于使用流写入和读取数据。例如,它将文件的内容加载到内存中,或者以 GZIP 格式写入压缩数据。

  • Boost.Locale:用于本地化应用程序,换句话说,将应用程序界面翻译成用户的语言。

  • Boost.MPI:用于开发可以并行执行任务的程序。MPI 本身代表消息传递接口

  • Boost.ProgramOptions:用于解析命令行选项。它使用双减号(--)来分隔每个命令行选项,而不是使用main参数中的argv变量。

  • Boost.Python:用于在 C++代码中解析 Python 语言。

  • Boost.Regex:用于在我们的代码中应用正则表达式。但如果我们的开发支持 C++11,我们不再依赖于Boost.Regex库,因为它已经在regex头文件中可用。

  • Boost.Serialization:用于将对象转换为一系列字节,可以保存,然后再次恢复为相同的对象。

  • Boost.Signals:用于创建信号。信号将触发事件来运行一个函数。

  • Boost.System:用于定义错误。它包含四个类:system::error_codesystem::error_categorysystem::error_conditionsystem::system_error。所有这些类都在boost命名空间中。它也支持 C++11 环境,但由于许多 Boost 库使用Boost.System,因此有必要继续包含Boost.System

  • Boost.Thread:用于应用线程编程。它提供了用于同步多线程数据访问的类。在 C++11 环境中,Boost.Thread库提供了扩展,因此我们可以在Boost.Thread中中断线程。

  • Boost.Timer:用于使用时钟来测量代码性能。它基于通常的时钟和 CPU 时间来测量经过的时间,这表明执行代码所花费的时间。

  • Boost.Wave:提供了一个可重用的 C 预处理器,我们可以在我们的 C++代码中使用。

还有一些具有可选的、单独编译的二进制文件的库。它们如下:

  • Boost.DateTime:用于处理时间数据;例如,日历日期和时间。它有一个二进制组件,只有在使用to_stringfrom_string或序列化功能时才需要。如果我们将应用程序定位到 Visual C++ 6.x 或 Borland,也是必需的。

  • Boost.Graph:用于创建二维图形。它有一个二进制组件,只有在我们打算解析GraphViz文件时才需要。

  • Boost.Math:用于处理数学公式。它有用于cmath函数的二进制组件。

  • Boost.Random:用于生成随机数。它有一个二进制组件,只有在我们想要使用random_device时才需要。

  • Boost.Test:用于编写和组织测试程序及其运行时执行。它可以以仅头文件或单独编译模式使用,但对于严肃的使用,建议使用单独编译。

  • Boost.Exception:它用于在抛出异常后向异常添加数据。它为 32 位_MSC_VER==1310_MSC_VER==1400提供了exception_ptr的非侵入式实现,这需要单独编译的二进制文件。这是通过#define BOOST_ENABLE_NON_INTRUSIVE_EXCEPTION_PTR启用的。

让我们尝试重新创建我们在第一章中创建的随机数生成器程序。但现在我们将使用Boost.Random库,而不是 C++标准函数中的std::rand()。让我们看一下以下代码:

/* rangen_boost.cpp */
#include <boost/random/mersenne_twister.hpp>
#include <boost/random/uniform_int_distribution.hpp>
#include <iostream>

int main(void) {
  int guessNumber;
  std::cout << "Select number among 0 to 10: ";
  std::cin >> guessNumber;
  if(guessNumber < 0 || guessNumber > 10) {
    return 1;
  }
  boost::random::mt19937 rng;
  boost::random::uniform_int_distribution<> ten(0,10);
  int randomNumber = ten(rng);
  if(guessNumber == randomNumber) {
    std::cout << "Congratulation, " << guessNumber << " is your lucky number.\n";
  }
  else {
    std::cout << "Sorry, I'm thinking about number " << randomNumber << "\n"; 
  }
  return 0;
}

我们可以使用以下命令编译前面的源代码:

g++ -Wall -ansi -Ic:/boost_1_58_0 rangen_boost.cpp -o rangen_boost

现在,让我们运行程序。不幸的是,在我运行程序的三次中,我总是得到相同的随机数,如下所示:

构建 Boost 库

正如我们从这个例子中看到的,我们总是得到数字 8。这是因为我们应用了 Mersenne Twister,一个伪随机数生成器PRNG),它使用默认种子作为随机性的来源,因此每次运行程序时都会生成相同的数字。当然,这不是我们期望的程序。

现在,我们将再次修改程序,只需两行。首先,找到以下行:

#include <boost/random/mersenne_twister.hpp>

将其更改如下:

#include <boost/random/random_device.hpp>

接下来,找到以下行:

boost::random::mt19937 rng;

将其更改如下:

boost::random::random_device rng;

然后,将文件保存为rangen2_boost.cpp,并使用与我们编译rangen_boost.cpp相同的命令来编译rangen2_boost.cpp文件。命令将如下所示:

g++ -Wall -ansi -Ic:/boost_1_58_0 rangen2_boost.cpp -o rangen2_boost

遗憾的是,会出现一些问题,编译器将显示以下错误消息:

cc8KWVvX.o:rangen2_boost.cpp:(.text$_ZN5boost6random6detail20generate_uniform_intINS0_13random_deviceEjEET0_RT_S4_S4_N4mpl_5bool_ILb1EEE[_ZN5boost6random6detail20generate_uniform_intINS0_13random_deviceEjEET0_RT_S4_S4_N4mpl_5bool_ILb1EEE]+0x24f): more undefined references to boost::random::random_device::operator()()' follow
collect2.exe: error: ld returned 1 exit status

这是因为,正如我们之前看到的,如果我们想要使用random_device属性,Boost.Random库需要单独编译。

Boost 库有一个系统来编译或构建 Boost 本身,称为Boost.Build库。我们必须完成两个步骤来安装Boost.Build库。首先,通过将命令提示符中的活动目录指向C:\boost_1_58_0,并键入以下命令来运行Bootstrap

bootstrap.bat mingw

我们使用我们在第一章中安装的 MinGW 编译器作为我们在编译 Boost 库时的工具集。等一下,如果过程成功,我们将得到以下输出:

Building Boost.Build engine

Bootstrapping is done. To build, run:

    .\b2

To adjust configuration, edit 'project-config.jam'.
Further information:

    - Command line help:
    .\b2 --help

    - Getting started guide:
    http://boost.org/more/getting_started/windows.html

    - Boost.Build documentation:
    http://www.boost.org/build/doc/html/index.html

在这一步中,我们将在 Boost 库的根目录中找到四个新文件。它们是:

  • b2.exe:这是一个可执行文件,用于构建 Boost 库

  • bjam.exe:这与b2.exe完全相同,但它是一个旧版本

  • bootstrap.log:这包含了bootstrap过程的日志

  • project-config.jam:这包含了在运行b2.exe时将用于构建过程的设置

我们还发现,这一步在C:\boost_1_58_0\tools\build\src\engine\bin.ntx86中创建了一个新目录,其中包含与需要编译的 Boost 库相关的一堆.obj文件。

之后,在命令提示符下键入以下命令来运行第二步:

b2 install toolset=gcc

在运行该命令后,喝杯咖啡,因为这个过程将花费大约二十到五十分钟的时间,这取决于您的系统规格。我们将得到的最后输出将如下所示:

...updated 12562 targets...

这意味着过程已经完成,我们现在已经构建了 Boost 库。如果我们在资源管理器中检查,Boost.Build库将添加C:\boost_1_58_0\stage\lib,其中包含一系列静态和动态库,我们可以直接在我们的程序中使用。

注意

bootstrap.batb2.exe使用msvc(Microsoft Visual C++编译器)作为默认工具集,许多 Windows 开发人员已经在他们的机器上安装了msvc。由于我们安装了 GCC 编译器,我们在 Boost 的构建中设置了mingwgcc工具集选项。如果您也安装了mvsc并希望在 Boost 的构建中使用它,可以省略工具集选项。

现在,让我们再次尝试使用以下命令编译rangen2_boost.cpp文件:

c:\CPP>g++ -Wall -ansi -Ic:/boost_1_58_0 rangen2_boost.cpp -Lc:\boost_1_58_0\stage\lib -lboost_random-mgw49-mt-1_58 -lboost_system-mgw49-mt-1_58 -o rangen2_boost

这里有两个新选项,它们是-L-l-L选项用于定义包含库文件的路径,如果库文件不在活动目录中。-l选项用于定义库文件的名称,但省略文件名前面的lib单词。在这种情况下,原始库文件名为libboost_random-mgw49-mt-1_58.a,我们省略了lib短语和选项-l的文件扩展名。

新文件rangen2_boost.exe将在C:\CPP中创建。但在运行程序之前,我们必须确保程序安装的目录包含程序所依赖的两个库文件。这些是libboost_random-mgw49-mt-1_58.dlllibboost_system-mgw49-mt-1_58.dll,我们可以从库目录c:\boost_1_58_0_1\stage\lib中获取它们。

为了方便我们运行该程序,运行以下copy命令将两个库文件复制到C:\CPP

copy c:\boost_1_58_0_1\stage\lib\libboost_random-mgw49-mt-1_58.dll c:\cpp
copy c:\boost_1_58_0_1\stage\lib\libboost_system-mgw49-mt-1_58.dll c:\cpp

现在程序应该可以顺利运行了。

为了创建一个网络应用程序,我们将使用Boost.Asio库。我们在非仅头文件库中找不到Boost.Asio——我们将用它来创建网络应用程序的库。看来我们不需要构建 Boost 库,因为Boost.Asio是仅头文件库。这是正确的,但由于Boost.Asio依赖于Boost.System,而Boost.System需要在使用之前构建,因此在创建网络应用程序之前,首先构建 Boost 是很重要的。

提示

对于选项-I-L,编译器不在乎我们在路径中使用反斜杠(\)还是斜杠(/)来分隔每个目录名称,因为编译器可以处理 Windows 和 Unix 路径样式。

总结

我们看到 Boost C++库是为了补充标准 C++库而开发的。我们还能够设置我们的 MinGW 编译器,以便编译包含 Boost 库的代码,并构建必须单独编译的库的二进制文件。在下一章中,我们将深入研究 Boost 库,特别是关于Boost.Asio库(我们将用它来开发网络应用程序)。请记住,尽管我们可以将Boost.Asio库作为仅头文件库使用,但最好使用Boost.Build库构建所有 Boost 库。这样我们就可以轻松使用所有库,而不必担心编译失败。

第四章:使用 Boost.Asio 入门

我们已经对 Boost C++库有了一般了解。现在是时候更多地了解 Boost.Asio 了,这是我们用来开发网络应用程序的库。Boost.Asio 是一组库,用于异步处理数据,因为 Asio 本身代表异步 I/O输入和输出)。异步意味着程序中的特定任务将在不阻塞其他任务的情况下运行,并且 Boost.Asio 将在完成该任务时通知程序。换句话说,任务是同时执行的。

在本章中,我们将讨论以下主题:

  • 区分并发和非并发编程

  • 理解 I/O 服务,Boost.Asio 的大脑和心脏

  • 将函数动态绑定到函数指针

  • 同步访问任何全局数据或共享数据

接近 Boost.Asio 库

假设我们正在开发一个音频下载应用程序,并且希望用户能够在下载过程中导航到应用程序的所有菜单。如果我们不使用异步编程,应用程序将被下载过程阻塞,用户必须等到文件下载完成才能继续使用。但由于异步编程,用户不需要等到下载过程完成才能继续使用应用程序。

换句话说,同步过程就像在剧院售票处排队。只有当我们到达售票处之后,我们才会被服务,而在此之前,我们必须等待前面排队的其他顾客的所有流程完成。相比之下,我们可以想象异步过程就像在餐厅用餐,其中服务员不必等待顾客的订单被厨师准备。服务员可以在不阻塞时间并等待厨师的情况下去接受其他顾客的订单。

Boost库还有Boost.Thread库,用于同时执行任务,但Boost.Thread库用于访问内部资源,如 CPU 核心资源,而Boost.Asio库用于访问外部资源,如网络连接,因为数据是通过网络卡发送和接收的。

让我们区分并发和非并发编程。看一下以下代码:

/* nonconcurrent.cpp */
#include <iostream>

void Print1(void) {
  for(int i=0; i<5; i++) {
    std::cout << "[Print1] Line: " << i << "\n";
  }
}

void Print2(void) {
  for(int i=0; i<5; i++) {
    std::cout << "[Print2] Line: " << i << "\n";
  }
}

int main(void) {
  Print1();
  Print2();
  return 0;
}

上面的代码是一个非并发程序。将代码保存为nonconcurrent.cpp,然后使用以下命令进行编译:

g++ -Wall -ansi nonconcurrent.cpp -o nonconcurrent

运行nonconcurrent.cpp后,将显示如下输出:

接近 Boost.Asio 库

我们想要运行两个函数:Print1()Print2()。在非并发编程中,应用程序首先运行Print1()函数,然后完成函数中的所有指令。程序继续调用Print2()函数,直到指令完全运行。

现在,让我们将非并发编程与并发编程进行比较。为此,请看以下代码:

/* concurrent.cpp */
#include <boost/thread.hpp>
#include <boost/chrono.hpp>
#include <iostream>

void Print1() {
  for (int i=0; i<5; i++) {
    boost::this_thread::sleep_for(boost::chrono::milliseconds{500});
    std::cout << "[Print1] Line: " << i << '\n';
  }
}

void Print2() {
  for (int i=0; i<5; i++) {
    boost::this_thread::sleep_for(boost::chrono::milliseconds{500});
    std::cout << "[Print2] Line: " << i << '\n';
  }
}

int main(void) {
  boost::thread_group threads;
  threads.create_thread(Print1);
  threads.create_thread(Print2);
  threads.join_all();
}

将上述代码保存为concurrent.cpp,并使用以下命令进行编译:

g++ -ansi -std=c++11 -I ../boost_1_58_0 concurrent.cpp -o concurrent -L ../boost_1_58_0/stage/lib -lboost_system-mgw49-mt-1_58 -lws2_32 -l boost_thread-mgw49-mt-1_58 -l boost_chrono-mgw49-mt-1_58

运行程序以获得以下输出:

接近 Boost.Asio 库

我们可以从上面的输出中看到,Print1()Print2()函数是同时运行的。Print2()函数不需要等待Print1()函数执行完所有要调用的指令。这就是为什么我们称之为并发编程。

提示

如果在代码中包含库,请不要忘记复制相关的动态库文件。例如,如果使用-l选项包含boost_system-mgw49-mt-1_58,则必须复制libboost_system-mgw49-mt-1_58.dll文件并将其粘贴到与输出可执行文件相同的目录中。

检查 Boost.Asio 库中的 I/O 服务

Boost::Asio命名空间的核心对象是io_serviceI/O service是一个通道,用于访问操作系统资源,并在我们的程序和执行 I/O 请求的操作系统之间建立通信。还有一个I/O 对象,其作用是提交 I/O 请求。例如,tcp::socket对象将从我们的程序向操作系统提供套接字编程请求。

使用和阻塞 run()函数

在 I/O 服务对象中最常用的函数之一是run()函数。它用于运行io_service对象的事件处理循环。它将阻塞程序的下一个语句,直到io_service对象中的所有工作都完成,并且没有更多的处理程序需要分派。如果我们停止io_service对象,它将不再阻塞程序。

注意

在编程中,event是程序检测到的一个动作或事件,将由程序使用event handler对象处理。io_service对象有一个或多个实例,用于处理事件的event processing loop

现在,让我们看一下以下代码片段:

/* unblocked.cpp */
#include <boost/asio.hpp>
#include <iostream>

int main(void) {
  boost::asio::io_service io_svc;

  io_svc.run();

  std::cout << "We will see this line in console window." << std::endl;

  return 0;
}

我们将上述代码保存为unblocked.cpp,然后运行以下命令进行编译:

g++ -Wall -ansi -I ../boost_1_58_0 unblocked.cpp -o unblocked -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32

当我们运行程序时,将显示以下输出:

We will see this line in console window.

然而,为什么即使我们之前知道run()函数在被调用后会阻塞下一个函数,我们仍然在控制台中获取到文本行呢?这是因为我们没有给io_service对象任何工作。由于io_service没有工作要做,io_service对象不应该阻塞程序。

现在,让我们给io_service对象一些工作要做。这个程序将如下所示:

/* blocked.cpp */
#include <boost/asio.hpp>
#include <iostream>

int main(void) {
  boost::asio::io_service io_svc;
  boost::asio::io_service::work worker(io_svc);

  io_svc.run();

  std::cout << "We will not see this line in console window :(" << std::endl;

  return 0;
}

给上述代码命名为blocked.cpp,然后在控制台窗口中输入以下命令进行编译:

g++ -Wall -ansi -I ../boost_1_58_0 blocked.cpp -o blocked -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32

如果我们在控制台中输入blocked来运行程序,由于我们添加了以下代码行,我们将不再看到文本行:

boost::asio::io_service::work work(io_svc);

work类负责告诉io_service对象工作何时开始和何时结束。它将确保io_service对象中的run()函数在工作进行时不会退出。此外,它还将确保run()函数在没有未完成的工作时退出。在我们的上述代码中,work类通知io_service对象它有工作要做,但我们没有定义工作是什么。因此,程序将被无限阻塞,不会显示输出。它被阻塞的原因是因为即使我们仍然可以通过按Ctrl + C来终止程序,run()函数仍然被调用。

使用非阻塞的 poll()函数

现在,我们将暂时离开run()函数,尝试使用poll()函数。poll()函数用于运行就绪处理程序,直到没有更多的就绪处理程序,或者直到io_service对象已停止。然而,与run()函数相反,poll()函数不会阻塞程序。

让我们输入以下使用poll()函数的代码,并将其保存为poll.cpp

/* poll.cpp */
#include <boost/asio.hpp>
#include <iostream>

int main(void) {
  boost::asio::io_service io_svc;

  for(int i=0; i<5; i++) {
    io_svc.poll();
    std::cout << "Line: " << i << std::endl;
  }

  return 0;
}

然后,使用以下命令编译poll.cpp

g++ -Wall -ansi -I ../boost_1_58_0 poll.cpp -o poll -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32

因为io_service对象没有工作要做,所以程序应该显示以下五行文本:

使用非阻塞的 poll()函数

然而,如果我们在使用poll()函数时给io_service对象分配工作会怎样呢?为了找出答案,让我们输入以下代码并将其保存为pollwork.cpp

/* pollwork.cpp */
#include <boost/asio.hpp>
#include <iostream>

int main(void) {
  boost::asio::io_service io_svc;
  boost::asio::io_service::work work(io_svc);

  for(int i=0; i<5; i++) {
    io_svc.poll();
    std::cout << "Line: " << i << std::endl;
  }

  return 0;
}

要编译pollwork.cpp,使用以下命令:

g++ -Wall -ansi -I ../boost_1_58_0 pollwork.cpp -o pollwork -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32

poll.cpp文件和pollwork.cpp文件之间的区别只有以下一行:

boost::asio::io_service::work work(io_svc);

然而,如果我们运行pollwork.exe,我们将获得与poll.exe相同的输出。这是因为,正如我们之前所知道的,poll()函数在有更多工作要做时不会阻塞程序。它将执行当前工作,然后返回值。

移除 work 对象

我们也可以通过从io_service对象中移除work对象来解除程序的阻塞,但是我们必须使用指向work对象的指针来移除work对象本身。我们将使用Boost库提供的智能指针shared_ptr指针。

让我们使用修改后的blocked.cpp代码。代码如下:

/* removework.cpp */
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <iostream>

int main(void) {
  boost::asio::io_service io_svc;
  boost::shared_ptr<boost::asio::io_service::work> worker(
    new boost::asio::io_service::work(io_svc)
  );

  worker.reset();

  io_svc.run();

  std::cout << "We will not see this line in console window :(" << std::endl;

  return 0;
}

将上述代码保存为removework.cpp,并使用以下命令进行编译:

g++ -Wall -ansi -I ../boost_1_58_0 removework.cpp -o removework -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32

当我们运行removework.cpp时,与blocked.cpp相比,它将无限期地阻塞程序,将显示以下文本:

移除 work 对象

现在,让我们解析代码。如前所述,我们在上面的代码中使用了shared_ptr指针来实例化work对象。有了 Boost 提供的这个智能指针,我们不再需要手动删除内存分配以存储指针,因为它保证了指向的对象在最后一个指针被销毁或重置时将被删除。不要忘记在boost目录中包含shared_ptr.hpp,因为shared_ptr指针是在头文件中定义的。

我们还添加了reset()函数来重置io_service对象,以便准备进行后续的run()函数调用。在任何run()poll()函数调用之前必须调用reset()函数。它还会告诉shared_ptr指针自动销毁我们创建的指针。有关shared_ptr指针的更多信息,请访问www.boost.org/doc/libs/1_58_0/libs/smart_ptr/shared_ptr.htm

上面的程序解释了我们已成功从io_service对象中移除了work对象。即使尚未完成所有挂起的工作,我们也可以使用这个功能。

处理多个线程

到目前为止,我们只处理了一个io_service对象的一个线程。如果我们想在单个io_service对象中处理更多的线程,以下代码将解释如何做到这一点:

/* multithreads.cpp */
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
#include <iostream>

boost::asio::io_service io_svc;
int a = 0;

void WorkerThread() {
  std::cout << ++a << ".\n";
  io_svc.run();
  std::cout << "End.\n";
}

int main(void) {
  boost::shared_ptr<boost::asio::io_service::work> worker(
    new boost::asio::io_service::work(io_svc)
  );

  std::cout << "Press ENTER key to exit!" << std::endl;

  boost::thread_group threads;
  for(int i=0; i<5; i++)
    threads.create_thread(WorkerThread);

  std::cin.get();

  io_svc.stop();

  threads.join_all();

  return 0;
}

给上述代码命名为mutithreads.cpp,然后使用以下命令进行编译:

g++ -Wall -ansi -I ../boost_1_58_0 multithreads.cpp -o multithreads -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l boost_thread-mgw49-mt-1_58

我们包含thread.hpp头文件,以便我们可以使用头文件中定义的thread对象。线程本身是一系列可以独立运行的指令,因此我们可以同时运行多个线程。

现在,在我们的控制台中运行mutithreads.exe。我通过运行它获得了以下输出:

处理多个线程

您可能会得到不同的输出,因为作为线程池设置的所有线程彼此等效。io_service对象可能会随机选择其中任何一个并调用其处理程序,因此我们无法保证io_service对象是否会按顺序选择线程:

for(int i=0; i<5; i++)
 threads.create_thread(WorkerThread);

使用上面的代码片段,我们可以创建五个线程来显示文本行,就像在之前的屏幕截图中所看到的那样。这五行文本足以用于此示例以查看非并发流的顺序:

std::cout << ++a << ".\n";
io_svc.run();

在创建的每个线程中,程序将调用run()函数来运行io_service对象的工作。只调用一次run()函数是不够的,因为所有非工作线程将在run()对象完成所有工作后被调用。

创建了五个线程后,程序运行了io_service对象的工作:

std::cin.get();

在所有工作运行之后,程序会等待您使用上面的代码片段从键盘上按Enter键。

io_svc.stop();

stop() function will notify the io_service object that all the work should be stopped. This means that the program will stop the five threads that we have:
threads.join_all();

WorkerThread() block:
std::cout << "End.\n";

因此,在我们按下Enter键后,程序将完成其余的代码,我们将得到以下其余的输出:

处理多线程

理解 Boost.Bind 库

我们已经能够使用io_service对象并初始化work对象。在继续向io_service服务提供工作之前,我们需要了解boost::bind库。

Boost.Bind库用于简化函数指针的调用。它将语法从晦涩和令人困惑的东西转换为易于理解的东西。

包装函数调用

让我们看一下以下代码,以了解如何包装函数调用:

/* uncalledbind.cpp */
#include <boost/bind.hpp>
#include <iostream>

void func() {
  std::cout << "Binding Function" << std::endl;
}

int main(void) {
  boost::bind(&func);
  return 0;
}

将上述代码保存为uncalledbind.cpp,然后使用以下命令进行编译:

g++ -Wall -ansi -I ../boost_1_58_0 uncalledbind.cpp -o uncalledbind

我们将不会得到任何文本行作为输出,因为我们只是创建了一个函数调用,但实际上并没有调用它。我们必须将其添加到()运算符中来调用函数,如下所示:

/* calledbind.cpp */
#include <boost/bind.hpp>
#include <iostream>

void func() {
  std::cout << "Binding Function" << std::endl;
}

int main(void) {
  boost::bind(&func)();
  return 0;
}

将上述代码命名为calledbind.cpp并运行以下命令进行编译:

g++ -Wall -ansi -I ../boost_1_58_0 calledbind.cpp -o calledbind

如果我们运行程序,现在将会得到一行文本作为输出,当然,我们将看到bind()函数作为输出:

boost::bind(&func)();

Now, let's use the function that has arguments to pass. We will use boost::bind for this purpose in the following code:
/* argumentbind.cpp */
#include <boost/bind.hpp>
#include <iostream>

void cubevolume(float f) {
  std::cout << "Volume of the cube is " << f * f * f << std::endl;
}

int main(void) {
  boost::bind(&cubevolume, 4.23f)();
  return 0;
}

运行以下命令以编译上述argumentbind.cpp文件:

g++ -Wall -ansi -I ../boost_1_58_0 argumentbind.cpp -o argumentbind

我们成功地使用boost::bind调用了带有参数的函数,因此我们得到了以下输出:

Volume of the cube is 75.687

需要记住的是,如果函数有多个参数,我们必须完全匹配函数签名。以下代码将更详细地解释这一点:

/* signaturebind.cpp */
#include <boost/bind.hpp>
#include <iostream>
#include <string>

void identity(std::string name, int age, float height) {
  std::cout << "Name   : " << name << std::endl;
  std::cout << "Age    : " << age << " years old" << std::endl;
  std::cout << "Height : " << height << " inch" << std::endl;
}

int main(int argc, char * argv[]) {
  boost::bind(&identity, "John", 25, 68.89f)();
  return 0;
}

使用以下命令编译signaturebind.cpp代码:

g++ -Wall -ansi -I ../boost_1_58_0 signaturebind.cpp -o signaturebind

身份函数的签名是std::stringintfloat。因此,我们必须分别用std::stringintfloat填充bind参数。

因为我们完全匹配了函数签名,我们将得到以下输出:

包装函数调用

我们已经能够在boost::bind中调用global()函数。现在,让我们继续在boost::bind中调用类中的函数。这方面的代码如下所示:

/* classbind.cpp */
#include <boost/bind.hpp>
#include <iostream>
#include <string>

class TheClass {
public:
  void identity(std::string name, int age, float height) {
    std::cout << "Name   : " << name << std::endl;
    std::cout << "Age    : " << age << " years old" << std::endl;
    std::cout << "Height : " << height << " inch" << std::endl;
  }
};

int main(void) {
  TheClass cls;
  boost::bind(&TheClass::identity, &cls, "John", 25, 68.89f)();
  return 0;
}

使用以下命令编译上述classbind.cpp代码:

g++ -Wall -ansi -I ../boost_1_58_0 classbind.cpp -o classbind

这将与signaturebind.cpp代码的输出完全相同,因为函数的内容也完全相同:

boost::bind(&TheClass::identity, &cls, "John", 25, 68.89f)();

boost:bind arguments with the class and function name, object of the class, and parameter based on the function signature.

使用 Boost.Bind 库

到目前为止,我们已经能够使用boost::bind来调用全局和类函数。然而,当我们使用io_service对象与boost::bind时,我们会得到一个不可复制的错误,因为io_service对象无法被复制。

现在,让我们再次看一下multithreads.cpp。我们将修改代码以解释boost::bind用于io_service对象,并且我们仍然需要shared_ptr指针的帮助。让我们看一下以下代码片段:

/* ioservicebind.cpp */
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
#include <boost/bind.hpp>
#include <iostream>

void WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {
  std::cout << counter << ".\n";
  iosvc->run();
  std::cout << "End.\n";
}

int main(void) {
  boost::shared_ptr<boost::asio::io_service> io_svc(
    new boost::asio::io_service
  );

  boost::shared_ptr<boost::asio::io_service::work> worker(
    new boost::asio::io_service::work(*io_svc)
  );

  std::cout << "Press ENTER key to exit!" << std::endl;

  boost::thread_group threads;
  for(int i=1; i<=5; i++)
    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));

  std::cin.get();

  io_svc->stop();

  threads.join_all();

  return 0;
}

我们将上述代码命名为ioservicebind.cpp并使用以下命令进行编译:

g++ -Wall -ansi -I ../boost_1_58_0 ioservicebind.cpp -o ioservicebind –L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l boost_thread-mgw49-mt-1_58

当我们运行ioservicebind.exe时,我们会得到与multithreads.exe相同的输出,但当然,程序会随机排列所有线程的顺序:

boost::shared_ptr<boost::asio::io_service> io_svc(
 new boost::asio::io_service
);

我们在shared_ptr指针中实例化io_service对象,以使其可复制,以便我们可以将其绑定到作为线程处理程序使用的worker thread()函数:

void WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter)

io_service object can be passed to the function. We do not need to define an int global variable as we did in the multithreads.cpp code snippet, since we can also pass the int argument to the WorkerThread() function:
std::cout << counter << ".\n";

for loop in the main block.

如果我们看一下create_thread()函数,在ioservicebind.cppmultithreads.cpp文件中看到它得到的不同参数。我们可以将指向不带参数的void()函数的指针作为create_thread()函数的参数传递,就像我们在multithreads.cpp文件中看到的那样。我们还可以将绑定函数作为create_thread()函数的参数传递,就像我们在ioservicebind.cpp文件中看到的那样。

使用 Boost.Mutex 库同步数据访问

当您运行multithreads.exeioservicebind.exe可执行文件时,您是否曾经得到以下输出?

使用 Boost.Mutex 库同步数据访问

我们可以在上面的截图中看到这里存在格式问题。因为std::cout对象是一个全局对象,同时从不同的线程写入它可能会导致输出格式问题。为了解决这个问题,我们可以使用mutex对象,它可以在thread库提供的boost::mutex对象中找到。Mutex 用于同步对任何全局数据或共享数据的访问。要了解更多关于 Mutex 的信息,请看以下代码:

/* mutexbind.cpp */
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
#include <boost/bind.hpp>
#include <iostream>

boost::mutex global_stream_lock;

void WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {
  global_stream_lock.lock();
  std::cout << counter << ".\n";
  global_stream_lock.unlock();

  iosvc->run();

  global_stream_lock.lock();
  std::cout << "End.\n";
  global_stream_lock.unlock();
}

int main(void) {
  boost::shared_ptr<boost::asio::io_service> io_svc(
    new boost::asio::io_service
  );

  boost::shared_ptr<boost::asio::io_service::work> worker(
    new boost::asio::io_service::work(*io_svc)
  );

  std::cout << "Press ENTER key to exit!" << std::endl;

  boost::thread_group threads;
  for(int i=1; i<=5; i++)
    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));

  std::cin.get();

  io_svc->stop();

  threads.join_all();

  return 0;
}

将上述代码保存为mutexbind.cpp,然后使用以下命令编译它:

g++ -Wall -ansi -I ../boost_1_58_0 mutexbind.cpp -o mutexbind -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l boost_thread-mgw49-mt-1_58

现在,运行mutexbind.cpp文件,我们将不再面临格式问题:

boost::mutex global_stream_lock;

我们实例化了新的mutex对象global_stream_lock。有了这个对象,我们可以调用lock()unlock()函数。lock()函数将阻塞其他访问相同函数的线程,等待当前线程完成。只有当前线程调用了unlock()函数,其他线程才能访问相同的函数。需要记住的一件事是,我们不应该递归调用lock()函数,因为如果lock()函数没有被unlock()函数解锁,那么线程死锁将发生,并且会冻结应用程序。因此,在使用lock()unlock()函数时,我们必须小心。

给 I/O 服务一些工作

现在,是时候给io_service对象一些工作了。了解更多关于boost::bindboost::mutex将帮助我们给io_service对象一些工作。io_service对象中有两个成员函数:post()dispatch()函数,我们经常会使用它们来做这件事。post()函数用于请求io_service对象在我们排队所有工作后运行io_service对象的工作,因此不允许我们立即运行工作。而dispatch()函数也用于请求io_service对象运行io_service对象的工作,但它会立即执行工作而不是排队。

使用 post()函数

通过创建以下代码来检查post()函数。我们将使用mutexbind.cpp文件作为我们的基础代码,因为我们只会修改源代码:

/* post.cpp */
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
#include <boost/bind.hpp>
#include <iostream>

boost::mutex global_stream_lock;

void WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {
  global_stream_lock.lock();
  std::cout << counter << ".\n";
  global_stream_lock.unlock();

  iosvc->run();

  global_stream_lock.lock();
  std::cout << "End.\n";
  global_stream_lock.unlock();
}

size_t fac(size_t n) {
  if ( n <= 1 ) {
    return n;
  }
  boost::this_thread::sleep(
    boost::posix_time::milliseconds(1000)
  );
  return n * fac(n - 1);
}

void CalculateFactorial(size_t n) {
  global_stream_lock.lock();
  std::cout << "Calculating " << n << "! factorial" << std::endl;
  global_stream_lock.unlock();

  size_t f = fac(n);

  global_stream_lock.lock();
  std::cout << n << "! = " << f << std::endl;
  global_stream_lock.unlock();
}

int main(void) {
  boost::shared_ptr<boost::asio::io_service> io_svc(
    new boost::asio::io_service
  );

  boost::shared_ptr<boost::asio::io_service::work> worker(
    new boost::asio::io_service::work(*io_svc)
  );

  global_stream_lock.lock();
  std::cout << "The program will exit once all work has finished." << std::endl;
  global_stream_lock.unlock();

  boost::thread_group threads;
  for(int i=1; i<=5; i++)
    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));

  io_svc->post(boost::bind(CalculateFactorial, 5));
  io_svc->post(boost::bind(CalculateFactorial, 6));
  io_svc->post(boost::bind(CalculateFactorial, 7));

  worker.reset();

  threads.join_all();

  return 0;
}

将上述代码命名为post.cpp,并使用以下命令编译它:

g++ -Wall -ansi -I ../boost_1_58_0 post.cpp -o post -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l boost_thread-mgw49-mt-1_58

在运行程序之前,让我们检查代码以了解其行为:

size_t fac(size_t n) {
 if (n <= 1) {
 return n;
 }
 boost::this_thread::sleep(
 boost::posix_time::milliseconds(1000)
 );
 return n * fac(n - 1);
}

我们添加了fac()函数来递归计算n的阶乘。为了看到我们的工作线程的工作,有一个时间延迟来减慢进程:

io_svc->post(boost::bind(CalculateFactorial, 5));
io_svc->post(boost::bind(CalculateFactorial, 6));
io_svc->post(boost::bind(CalculateFactorial, 7));

main块中,我们使用post()函数在io_service对象上发布了三个函数对象。我们在初始化五个工作线程后立即这样做。然而,因为我们在每个线程内调用了io_service对象的run()函数,所以io_service对象的工作将运行。这意味着post()函数将起作用。

现在,让我们运行post.cpp并看看这里发生了什么:

使用 post()函数

正如我们在前面的截图输出中所看到的,程序从线程池中运行线程,并在完成一个线程后,调用io_service对象的post()函数,直到所有三个post()函数和所有五个线程都被调用。然后,它计算每个三个n数字的阶乘。在得到worker.reset()函数后,它被通知工作已经完成,然后通过threads.join_all()函数加入所有线程。

使用dispatch()函数

现在,让我们检查dispatch()函数,给io_service函数一些工作。我们仍然会使用mutexbind.cpp文件作为我们的基础代码,并稍微修改它,使其变成这样:

/* dispatch.cpp */
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
#include <boost/bind.hpp>
#include <iostream>

boost::mutex global_stream_lock;

void WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc) {
  global_stream_lock.lock();
  std::cout << "Thread Start.\n";
  global_stream_lock.unlock();

  iosvc->run();

  global_stream_lock.lock();
  std::cout << "Thread Finish.\n";
  global_stream_lock.unlock();
}

void Dispatch(int i) {
  global_stream_lock.lock();
  std::cout << "dispath() Function for i = " << i <<  std::endl;
  global_stream_lock.unlock();
}

void Post(int i) {
  global_stream_lock.lock();
  std::cout << "post() Function for i = " << i <<  std::endl;
  global_stream_lock.unlock();
}

void Running(boost::shared_ptr<boost::asio::io_service> iosvc) {
  for( int x = 0; x < 5; ++x ) {
    iosvc->dispatch(boost::bind(&Dispatch, x));
    iosvc->post(boost::bind(&Post, x));
    boost::this_thread::sleep(boost::posix_time::milliseconds(1000));
  }
}

int main(void) {
  boost::shared_ptr<boost::asio::io_service> io_svc(
    new boost::asio::io_service
  );

  boost::shared_ptr<boost::asio::io_service::work> worker(
    new boost::asio::io_service::work(*io_svc)
  );

  global_stream_lock.lock();
  std::cout << "The program will exit automatically once all work has finished." << std::endl;
  global_stream_lock.unlock();

  boost::thread_group threads;

  threads.create_thread(boost::bind(&WorkerThread, io_svc));

  io_svc->post(boost::bind(&Running, io_svc));

  worker.reset();

  threads.join_all();

  return 0;
}

给上述代码命名为dispatch.cpp,并使用以下命令进行编译:

g++ -Wall -ansi -I ../boost_1_58_0 dispatch.cpp -o dispatch -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l boost_thread-mgw49-mt-1_58

现在,让我们运行程序以获得以下输出:

使用 dispatch()函数

post.cpp文件不同,在dispatch.cpp文件中,我们只创建一个工作线程。此外,我们添加了两个函数dispatch()post()来理解两个函数之间的区别:

iosvc->dispatch(boost::bind(&Dispatch, x));
iosvc->post(boost::bind(&Post, x));

Running() function, we expect to get the ordered output between the dispatch() and post() functions. However, when we see the output, we find that the result is different because the dispatch() function is called first and the post() function is called after it. This happens because the dispatch() function can be invoked from the current worker thread, while the post() function has to wait until the handler of the worker is complete before it can be invoked. In other words, the dispatch() function's events can be executed from the current worker thread even if there are other pending events queued up, while the post() function's events have to wait until the handler completes the execution before being allowed to be executed.

摘要

有两个函数可以让我们使用io_service对象工作:run()poll()成员函数。run()函数会阻塞程序,因为它必须等待我们分配给它的工作,而poll()函数不会阻塞程序。当我们需要给io_service对象一些工作时,我们只需使用poll()run()函数,取决于我们的需求,然后根据需要调用post()dispatch()函数。post()函数用于命令io_service对象运行给定的处理程序,但不允许处理程序在此函数内部被io_service对象调用。而dispatch()函数用于在调用run()poll()函数的线程中调用处理程序。dispatch()post()函数之间的根本区别在于,dispatch()函数会立即完成工作,而post()函数总是将工作排队。

我们了解了io_service对象,如何运行它,以及如何给它一些工作。现在,让我们转到下一章,了解更多关于Boost.Asio库的内容,我们将离创建网络编程更近一步。

第五章:深入了解 Boost.Asio 库

现在我们能够运行io_service对象并给它一些工作要做,是时候了解更多关于Boost.Asio库中的其他对象,以开发网络应用程序。我们之前使用的io_service对象的所有工作都是异步运行的,但不是按顺序进行的,这意味着我们无法确定将运行io_service对象的工作的顺序。此外,我们还必须考虑如果我们的应用程序在运行时遇到任何错误会怎么做,并考虑运行任何io_service对象工作的时间间隔。因此,在本章中,我们将讨论以下主题:

  • 串行执行io_service对象的工作

  • 捕获异常并正确处理它们

  • 在所需的时间内执行工作

串行化 I/O 服务工作

假设我们想要排队要做的工作,但顺序很重要。如果我们只应用异步方法,我们就不知道我们将得到的工作顺序。我们需要确保工作的顺序是我们想要的,并且已经设计好了。例如,如果我们按顺序发布 Work A,Work B 和 Work C,我们希望在运行时保持该顺序。

使用 strand 函数

Strandio_service对象中的一个类,它提供了处理程序执行的串行化。它可以用来确保我们的工作将被串行执行。让我们来看一下下面的代码,以了解如何使用strand函数进行串行化。但首先,我们将在不使用strand()lock()函数的情况下开始:

/* nonstrand.cpp */
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/bind.hpp>
#include <iostream>

boost::mutex global_stream_lock;

void WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {
  global_stream_lock.lock();
  std::cout << "Thread " << counter << " Start.\n";
  global_stream_lock.unlock();

  iosvc->run();

  global_stream_lock.lock();
  std::cout << "Thread " << counter << " End.\n";
global_stream_lock.unlock();
}

void Print(int number) {
  std::cout << "Number: " << number << std::endl;
}

int main(void) {
  boost::shared_ptr<boost::asio::io_service> io_svc(
    new boost::asio::io_service
  );

  boost::shared_ptr<boost::asio::io_service::work> worker(
    new boost::asio::io_service::work(*io_svc)
  );

  global_stream_lock.lock();
  std::cout << "The program will exit once all work has finished.\n";
  global_stream_lock.unlock();

  boost::thread_group threads;
  for(int i=1; i<=5; i++)
    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));

  boost::this_thread::sleep(boost::posix_time::milliseconds(500));

  io_svc->post(boost::bind(&Print, 1));
  io_svc->post(boost::bind(&Print, 2));
  io_svc->post(boost::bind(&Print, 3));
  io_svc->post(boost::bind(&Print, 4));
  io_svc->post(boost::bind(&Print, 5));

  worker.reset();

  threads.join_all();

  return 0;
}

将上述代码保存为nonstrand.cpp,并使用以下命令编译它:

g++ -Wall -ansi -I ../boost_1_58_0 nonstrand.cpp -o nonstrand -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58

然后,在控制台窗口中输入nonstrand来运行它。我们将得到类似以下的输出:

使用 strand 函数

你可能会得到不同的输出,事实上,多次运行程序会产生不同顺序的结果。这是因为,正如我们在上一章中讨论的,没有lock对象,输出将是不同步的,如下所示。我们可以注意到结果看起来是无序的:

Number: Number: 1
Number: 5
Number: 3
2
Number: 4

lock object to synchronize the output. This is why we get the output as shown in the preceding screenshot.
void Print(int number) {
 std::cout << "Number: " << number << std::endl;
}

现在,让我们应用strand函数来同步程序的流程。输入以下代码并将其保存为strand.cpp

/* strand.cpp */
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/bind.hpp>
#include <iostream>

boost::mutex global_stream_lock;

void WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {
  global_stream_lock.lock();
  std::cout << "Thread " << counter << " Start.\n";
  global_stream_lock.unlock();

  iosvc->run();

  global_stream_lock.lock();
  std::cout << "Thread " << counter << " End.\n";
  global_stream_lock.unlock();
}

void Print(int number) {
  std::cout << "Number: " << number << std::endl;
}

int main(void) {
  boost::shared_ptr<boost::asio::io_service> io_svc(
    new boost::asio::io_service
  );

  boost::shared_ptr<boost::asio::io_service::work> worker(
    new boost::asio::io_service::work(*io_svc)
  );

  boost::asio::io_service::strand strand(*io_svc);

  global_stream_lock.lock();
  std::cout << "The program will exit once all work has finished.\n";
  global_stream_lock.unlock();

  boost::thread_group threads;
  for(int i=1; i<=5; i++)
    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));

  boost::this_thread::sleep(boost::posix_time::milliseconds(500));

  strand.post(boost::bind(&Print, 1));
  strand.post(boost::bind(&Print, 2));
  strand.post(boost::bind(&Print, 3));
  strand.post(boost::bind(&Print, 4));
  strand.post(boost::bind(&Print, 5));

  worker.reset();

  threads.join_all();

  return 0;
}

使用以下命令编译上述代码:

g++ -Wall -ansi -I ../boost_1_58_0 strand.cpp -o strand -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58

我们只对nonstrand.cpp进行了一点修改,改为strand.cpp,但影响很大。在运行程序之前,让我们区分一下nonstrand.cppstrand.cpp之间的代码:

io_svc->post(boost::bind(&Print, 1));
io_svc->post(boost::bind(&Print, 2));
io_svc->post(boost::bind(&Print, 3));
io_svc->post(boost::bind(&Print, 4));
io_svc->post(boost::bind(&Print, 5));

我们使用post()函数在io_service对象中给它工作。但是通过使用这种方法,程序的流程是不可预测的,因为它不是同步的:

strand.post(boost::bind(&Print, 1));
strand.post(boost::bind(&Print, 2));
strand.post(boost::bind(&Print, 3));
strand.post(boost::bind(&Print, 4));
strand.post(boost::bind(&Print, 5));

然后,我们使用strand对象将工作交给io_service对象。通过使用这种方法,我们将确保工作的顺序与我们在代码中声明的顺序完全相同。为了证明这一点,让我们来看一下以下输出:

使用 strand 函数

工作的顺序与我们代码中的工作顺序相同。我们以数字顺序显示工作的输出,即:

Number: 1
Number: 2
Number: 3
Number: 4
Number: 5

而且,如果你记得,我们继续从Print()函数中省略lock()函数,但由于strand对象的使用,它仍然可以正常运行。现在,无论我们重新运行程序多少次,结果总是按升序排列。

通过 strand 对象包装处理程序

boost::asio::strand中有一个名为wrap()的函数。根据官方 Boost 文档,它创建一个新的处理程序函数对象,当调用时,它将自动将包装的处理程序传递给strand对象的调度函数。让我们看一下以下代码来解释它:

/* strandwrap.cpp */
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/bind.hpp>
#include <iostream>

boost::mutex global_stream_lock;

void WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {
  global_stream_lock.lock();
  std::cout << "Thread " << counter << " Start.\n";
  global_stream_lock.unlock();

  iosvc->run();

  global_stream_lock.lock();
  std::cout << "Thread " << counter << " End.\n";
  global_stream_lock.unlock();
}

void Print(int number) {
  std::cout << "Number: " << number << std::endl;
}

int main(void) {
  boost::shared_ptr<boost::asio::io_service> io_svc(
    new boost::asio::io_service
  );

  boost::shared_ptr<boost::asio::io_service::work> worker(
    new boost::asio::io_service::work(*io_svc)
  );

  boost::asio::io_service::strand strand(*io_svc);

  global_stream_lock.lock();
  std::cout << "The program will exit once all work has finished." <<  std::endl;
  global_stream_lock.unlock();

  boost::thread_group threads;
  for(int i=1; i<=5; i++)
    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));

  boost::this_thread::sleep(boost::posix_time::milliseconds(100));
  io_svc->post(strand.wrap(boost::bind(&Print, 1)));
  io_svc->post(strand.wrap(boost::bind(&Print, 2)));

  boost::this_thread::sleep(boost::posix_time::milliseconds(100));
  io_svc->post(strand.wrap(boost::bind(&Print, 3)));
  io_svc->post(strand.wrap(boost::bind(&Print, 4)));

  boost::this_thread::sleep(boost::posix_time::milliseconds(100));
  io_svc->post(strand.wrap(boost::bind(&Print, 5)));
  io_svc->post(strand.wrap(boost::bind(&Print, 6)));

  worker.reset();

  threads.join_all();

  return 0;
}

给上述代码命名为strandwrap.cpp,然后使用以下命令编译它:

g++ -Wall -ansi -I ../boost_1_58_0 strandwrap.cpp -o strandwrap -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58

现在运行程序,我们将得到以下输出:

通过 strand 对象包装处理程序

然而,如果我们多次运行程序,可能会产生如下随机输出:

Number: 2
Number: 1
Number: 3
Number: 4
Number: 6
Number: 5

虽然工作保证按顺序执行,但实际发生的工作顺序并不是保证的,这是由于内置的处理程序包装器。如果顺序真的很重要,我们必须在使用strand对象时查看内置的处理程序包装器本身。

处理异常和错误

有时,我们的代码会在运行时抛出异常或错误。正如你可能记得在我们讨论第三章中的lexical.cpp时,介绍 Boost C++库,我们有时必须在代码中使用异常处理,现在我们将挖掘它来深入了解异常和错误处理。

处理异常

异常是一种在代码出现异常情况时通过将控制权转移给处理程序来对情况做出反应的方式。为了处理异常,我们需要在代码中使用try-catch块;然后,如果出现异常情况,异常将被抛出到异常处理程序。

现在,看一下以下代码,看看异常处理是如何使用的:

/* exception.cpp */
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/bind.hpp>
#include <iostream>

boost::mutex global_stream_lock;

void WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {
  global_stream_lock.lock();
  std::cout << "Thread " << counter << " Start.\n";
  global_stream_lock.unlock();

  try {
    iosvc->run();

    global_stream_lock.lock();
    std::cout << "Thread " << counter << " End.\n";
    global_stream_lock.unlock();
  }
  catch(std::exception & ex) {
    global_stream_lock.lock();
    std::cout << "Message: " << ex.what() << ".\n";
    global_stream_lock.unlock();
  }
}

void ThrowAnException(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {
  global_stream_lock.lock();
  std::cout << "Throw Exception " << counter << "\n" ;
  global_stream_lock.unlock();

  throw(std::runtime_error("The Exception !!!"));
}

int main(void) {
  boost::shared_ptr<boost::asio::io_service> io_svc(
    new boost::asio::io_service
  );

  boost::shared_ptr<boost::asio::io_service::work> worker(
    new boost::asio::io_service::work(*io_svc)
  );

  global_stream_lock.lock();
  std::cout << "The program will exit once all work has finished.\n";
  global_stream_lock.unlock();

  boost::thread_group threads;
  for(int i=1; i<=2; i++)
    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));

  io_svc->post(boost::bind(&ThrowAnException, io_svc, 1));
  io_svc->post(boost::bind(&ThrowAnException, io_svc, 2));
  io_svc->post(boost::bind(&ThrowAnException, io_svc, 3));
  io_svc->post(boost::bind(&ThrowAnException, io_svc, 4));
  io_svc->post(boost::bind(&ThrowAnException, io_svc, 5));

  threads.join_all();

  return 0;
}

将前面的代码保存为exception.cpp,并运行以下命令编译它:

g++ -Wall -ansi -I ../boost_1_58_0 exception.cpp -o exception -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58

然后,运行程序,你应该会得到以下输出:

处理异常

正如我们所看到的,由于异常,我们没有看到std::cout << "Thread " << counter << " End.\n";这一行。当io_service对象的工作运行时,它总是使用throw关键字抛出异常,以便异常将被WorkerThread函数内的catch块捕获,因为iosvc->run()函数在try块内。

我们还可以看到,尽管我们为io_service对象发布了五次工作,但异常处理只处理了两次异常,因为一旦线程完成,线程中的join_all()函数将完成线程并退出程序。换句话说,我们可以说一旦异常被处理,线程就退出以加入调用。可能会抛出异常的其他代码将永远不会被调用。

如果我们将io_service对象的工作调用递归放入呢?这会导致一个无限运行的程序吗?让我们尝试无限抛出异常。代码将如下所示:

/* exception2.cpp */
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/bind.hpp>
#include <iostream>

boost::mutex global_stream_lock;

void WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {
  global_stream_lock.lock();
  std::cout << "Thread " << counter << " Start.\n";
  global_stream_lock.unlock();

  try {
    iosvc->run();

    global_stream_lock.lock();
    std::cout << "Thread " << counter << " End.\n";
    global_stream_lock.unlock();
  }
  catch(std::exception &ex) {
    global_stream_lock.lock();
    std::cout << "Message: " << ex.what() << ".\n";
    global_stream_lock.unlock();
  }
}

void ThrowAnException(boost::shared_ptr<boost::asio::io_service> iosvc) {
  global_stream_lock.lock();
  std::cout << "Throw Exception\n" ;
  global_stream_lock.unlock();

  iosvc->post(boost::bind(&ThrowAnException, iosvc));

  throw(std::runtime_error("The Exception !!!"));
}

int main(void) {
  boost::shared_ptr<boost::asio::io_service> io_svc(
    new boost::asio::io_service
  );

  boost::shared_ptr<boost::asio::io_service::work> worker(
    new boost::asio::io_service::work(*io_svc)
  );

  global_stream_lock.lock();
  std::cout << "The program will exit once all work has finished.\n";
  global_stream_lock.unlock();

  boost::thread_group threads;
  for(int i=1; i<=5; i++)
    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));

  io_svc->post(boost::bind(&ThrowAnException, io_svc));

  threads.join_all();

  return 0;
}

将前面的代码保存为exception2.cpp,并使用以下命令编译它:

g++ -Wall -ansi -I ../boost_1_58_0 exception2.cpp -o exception2 -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58

现在,让我们检查代码:

iosvc->post(boost::bind(&ThrowAnException, iosvc));

ThrowAnException function. Every time the ThrowAnException function is called, it will call itself. Then, it should be an infinite program since there is a recursive function. Let us run the program to prove this by typing the exception2 command in the console window. The output will be like the following:

处理异常

幸运的是,程序能够成功完成。这是因为异常通过run()函数传播,工作线程退出。之后,所有线程都完成了,并且调用了join_all()函数。这就是为什么程序退出,即使io_service对象中还有工作未完成。

处理错误

在我们之前的例子中,我们使用了run()函数而没有任何参数,但实际上,该函数有两个重载方法,std::size_t run()std::size_t run(boost::system::error_code & ec)。后一个方法有一个错误代码参数,如果发生错误,它将被设置。

现在,让我们尝试在run()函数中使用错误代码作为输入参数。看一下以下代码:

/* errorcode.cpp */
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/bind.hpp>
#include <iostream>

boost::mutex global_stream_lock;

void WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {
  global_stream_lock.lock();
  std::cout << "Thread " << counter << " Start.\n";
  global_stream_lock.unlock();

  boost::system::error_code ec;
  iosvc->run(ec);

  if(ec) {
    global_stream_lock.lock();
    std::cout << "Message: " << ec << ".\n";
    global_stream_lock.unlock();
  }

  global_stream_lock.lock();
  std::cout << "Thread " << counter << " End.\n";
  global_stream_lock.unlock();
}

void ThrowAnException(boost::shared_ptr<boost::asio::io_service> iosvc) {
  global_stream_lock.lock();
  std::cout << "Throw Exception\n" ;
  global_stream_lock.unlock();

  iosvc->post(boost::bind(&ThrowAnException, iosvc));

  throw(std::runtime_error("The Exception !!!"));
}

int main(void) {
  boost::shared_ptr<boost::asio::io_service> io_svc(
    new boost::asio::io_service
  );

  boost::shared_ptr<boost::asio::io_service::work> worker(
    new boost::asio::io_service::work(*io_svc)
  );

  global_stream_lock.lock();
  std::cout << "The program will exit once all work has finished.\n";
  global_stream_lock.unlock();

  boost::thread_group threads;
  for(int i=1; i<=5; i++)
    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));

  io_svc->post(boost::bind(&ThrowAnException, io_svc));

  threads.join_all();

  return 0;
}

将前面的代码保存为errorcode.cpp,并使用以下命令编译代码:

g++ -Wall -ansi -I ../boost_1_58_0 errorcode.cpp -o errorcode -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58

现在,在控制台中输入errorcode命令运行程序。由于这样做,程序将崩溃。以下截图显示了输出:

处理错误

我们打算通过以下代码检索错误代码:

iosvc->run(ec);

我们可以使用if块来捕获错误,如下所示:

if(ec)

然而,在错误变量方法中,用户异常会转换为boost::asio异常;因此,错误变量ec不会将用户异常解释为错误,因此处理程序不会捕获异常。如果Boost.Asio库需要抛出错误,如果没有错误变量,它将变为异常,或者将转换为错误变量。最好继续使用try-catch块来捕获任何异常或错误。

此外,我们还需要检查异常的类型,即系统故障或上下文故障。如果是系统故障,我们必须调用io_service类中的stop()函数,以确保工作对象已被销毁,以便程序能够退出。相反,如果异常是上下文故障,我们需要工作线程再次调用run()函数,以防止线程死亡。现在,让我们看以下代码以理解这个概念:

/* errorcode2.cpp */
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/bind.hpp>
#include <iostream>

boost::mutex global_stream_lock;

void WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {
  global_stream_lock.lock();
  std::cout << "Thread " << counter << " Start.\n";
  global_stream_lock.unlock();

  while(true) {
    try {
      boost::system::error_code ec;
      iosvc->run(ec);
      if(ec) {
        global_stream_lock.lock();
        std::cout << "Error Message: " << ec << ".\n";
        global_stream_lock.unlock();
      }
      break;
    }
    catch(std::exception &ex) {
      global_stream_lock.lock();
      std::cout << "Exception Message: " << ex.what() << ".\n";
      global_stream_lock.unlock();
    }
  }

  global_stream_lock.lock();
  std::cout << "Thread " << counter << " End.\n";
  global_stream_lock.unlock();
}

void ThrowAnException(boost::shared_ptr<boost::asio::io_service> iosvc) {
  global_stream_lock.lock();
  std::cout << "Throw Exception\n" ;
  global_stream_lock.unlock();

  iosvc->post(boost::bind(&ThrowAnException, iosvc));

  throw(std::runtime_error("The Exception !!!"));
}

int main(void) {
  boost::shared_ptr<boost::asio::io_service> io_svc(
    new boost::asio::io_service
  );

  boost::shared_ptr<boost::asio::io_service::work> worker(
    new boost::asio::io_service::work(*io_svc)
  );

  global_stream_lock.lock();
  std::cout << "The program will exit once all work has finished.\n";
  global_stream_lock.unlock();

  boost::thread_group threads;
  for(int i=1; i<=5; i++)
    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));

  io_svc->post(boost::bind(&ThrowAnException, io_svc));

  threads.join_all();

  return 0;
}

将上述代码保存为errorcode2.cpp,然后通过执行以下命令进行编译:

g++ -Wall -ansi -I ../boost_1_58_0 errorcode2.cpp -o errorcode2 -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58

如果我们运行程序,会发现它不会退出,我们必须按Ctrl + C来停止程序:

处理错误

如果我们看到以下代码片段:

while(true) {
 try {
 . . .
 iosvc->run(ec);
 if(ec)
 . . .
 }
 catch(std::exception &ex) {
 . . .
 }
}

工作线程正在循环。当输出结果中发生异常时(由Throw ExceptionException Message: The Exception!!!输出表示),再次调用run()函数,这样它将向队列中发布一个新事件。当然,我们不希望这种情况发生在我们的应用程序中。

使用定时器类来计时工作执行

Boost C++库中有一个类,它提供了对定时器进行阻塞或异步等待的能力,称为截止定时器。截止定时器表示两种状态之一:到期或未到期。

一个即将到期的定时器

在这里,我们将创建一个在 10 秒后到期的定时器。让我们看一下以下代码:

/* timer.cpp */
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/bind.hpp>
#include <iostream>

boost::mutex global_stream_lock;

void WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {
  global_stream_lock.lock();
  std::cout << "Thread " << counter << " Start.\n";
  global_stream_lock.unlock();

  while(true) {
    try {
      boost::system::error_code ec;
      iosvc->run(ec);
      if(ec) {
        global_stream_lock.lock();
        std::cout << "Message: " << ec << ".\n";
        global_stream_lock.unlock();
      }
      break;
    }
    catch(std::exception &ex) {
      global_stream_lock.lock();
      std::cout << "Message: " << ex.what() << ".\n";
      global_stream_lock.unlock();
    }
  }

  global_stream_lock.lock();
  std::cout << "Thread " << counter << " End.\n";
  global_stream_lock.unlock();
}

void TimerHandler(const boost::system::error_code & ec) {
  if(ec) {
    global_stream_lock.lock();
    std::cout << "Error Message: " << ec << ".\n";
    global_stream_lock.unlock();
  }
  else {
    global_stream_lock.lock();
    std::cout << "You see this line because you have waited for 10 seconds.\n";
    std::cout << "Now press ENTER to exit.\n";
    global_stream_lock.unlock();
  }
}

int main(void) {
  boost::shared_ptr<boost::asio::io_service> io_svc(
    new boost::asio::io_service
  );

  boost::shared_ptr<boost::asio::io_service::work> worker(
    new boost::asio::io_service::work(*io_svc)
  );

  global_stream_lock.lock();
  std::cout << "Wait for ten seconds to see what happen, ";
  std::cout << "otherwise press ENTER to exit!\n";
  global_stream_lock.unlock();

  boost::thread_group threads;
  for(int i=1; i<=5; i++)
    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));

  boost::asio::deadline_timer timer(*io_svc);
  timer.expires_from_now(boost::posix_time::seconds(10));
  timer.async_wait(TimerHandler);

  std::cin.get();

  io_svc->stop();

  threads.join_all();

  return 0;
}

将上述代码保存为timer.cpp,并运行以下命令进行编译:

g++ -Wall -ansi -I ../boost_1_58_0 timer.cpp -o timer -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58

现在,让我们在运行之前区分一下代码:

boost::asio::deadline_timer timer(*io_svc);
timer.expires_from_now(boost::posix_time::seconds(10));
timer.async_wait(TimerHandler);

在程序调用TimerHandler函数之前,它必须等待 10 秒,因为我们使用了timer对象的expires_from_now函数。async_wait()函数将等待直到定时器到期:

void TimerHandler(const boost::system::error_code & ec) {
 if(ec)
 . . .
}
else {
 global_stream_lock.lock();
 std::cout << "You see this line because you have waited for 10 seconds.\n";
 std::cout << "Now press ENTER to exit.\n";
 global_stream_lock.unlock();
}

定时器到期后,将调用TimerHandler函数,由于没有错误,程序将执行else块内的代码。让我们运行程序,看完整的输出:

一个即将到期的定时器

并且,由于我们使用了async_wait()函数,我们可以在看到这行之前按下Enter键退出程序,现在按 Enter 键退出

使用定时器和 boost::bind 函数

让我们尝试创建一个循环定时器。我们必须初始化全局定时器对象,以便该对象成为共享对象。为了实现这一点,我们需要shared_ptr指针和boost::bind方法的帮助,以使线程安全,因为我们将使用共享对象:

/* timer2.cpp */
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/bind.hpp>
#include <iostream>

boost::mutex global_stream_lock;

void WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {
  global_stream_lock.lock();
  std::cout << "Thread " << counter << " Start.\n";
  global_stream_lock.unlock();

  while( true ) {
    try {
      boost::system::error_code ec;
      iosvc->run(ec);
      if(ec) {
        global_stream_lock.lock();
        std::cout << "Message: " << ec << ".\n";
        global_stream_lock.unlock();
      }
      break;
    }
    catch(std::exception &ex) {
      global_stream_lock.lock();
      std::cout << "Message: " << ex.what() << ".\n";
      global_stream_lock.unlock();
    }
  }

  global_stream_lock.lock();
  std::cout << "Thread " << counter << " End.\n";
  global_stream_lock.unlock();
}

void TimerHandler(
  const boost::system::error_code &ec,
  boost::shared_ptr<boost::asio::deadline_timer> tmr
)
{
  if(ec) {
    global_stream_lock.lock();
    std::cout << "Error Message: " << ec << ".\n";
    global_stream_lock.unlock();
  }
  else {
    global_stream_lock.lock();
    std::cout << "You see this every three seconds.\n";
    global_stream_lock.unlock();

    tmr->expires_from_now( boost::posix_time::seconds(3));
    tmr->async_wait(boost::bind(&TimerHandler, _1, tmr));
  }
}

int main(void) {
  boost::shared_ptr<boost::asio::io_service> io_svc(
    new boost::asio::io_service
  );

  boost::shared_ptr<boost::asio::io_service::work> worker(
    new boost::asio::io_service::work(*io_svc)
  );

  global_stream_lock.lock();
  std::cout << "Press ENTER to exit!\n";
  global_stream_lock.unlock();

  boost::thread_group threads;
  for(int i=1; i<=5; i++)
    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));

  boost::shared_ptr<boost::asio::deadline_timer> timer(
    new boost::asio::deadline_timer(*io_svc)
  );
  timer->expires_from_now( boost::posix_time::seconds(3));
  timer->async_wait(boost::bind(&TimerHandler, _1, timer));

  std::cin.get();

  io_svc->stop();

  threads.join_all();

  return 0;
}

将上述代码保存为timer2.cpp,并运行以下命令进行编译:

g++ -Wall -ansi -I ../boost_1_58_0 timer2.cpp -o timer2 -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58

现在运行程序。我们会得到一个重复的输出,可以通过按Enter键来停止:

使用定时器和 boost::bind 函数

从输出中我们可以看到,定时器每三秒触发一次,当用户按下Enter键后工作将停止。现在,让我们看以下代码片段:

timer->async_wait(boost::bind(&TimerHandler, _1, timer));

boost::bind函数帮助我们使用全局定时器对象。如果我们深入研究,我们可以使用_1参数来进行boost::bind函数。如果我们阅读boost::bind函数的文档,我们会发现_1参数是一个占位符参数,将被第一个输入参数替换。

注意

有关使用占位符绑定的更多信息,请查看官方 Boost 文档www.boost.org/doc/libs/1_58_0/libs/bind/doc/html/bind.html

关于占位参数的更多信息,请参见en.cppreference.com/w/cpp/utility/functional/placeholders

使用定时器和 boost::strand 函数

由于定时器是异步执行的,可能定时器的执行不是一个序列化的过程。定时器可能在一个线程中执行,同时另一个事件也在执行。正如我们之前讨论过的,我们可以利用 strand 函数来序列化执行顺序。让我们来看下面的代码片段:

/* timer3.cpp */
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/bind.hpp>
#include <iostream>

boost::mutex global_stream_lock;

void WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {
  global_stream_lock.lock();
  std::cout << "Thread " << counter << " Start.\n";
  global_stream_lock.unlock();

  while( true ) {
    try {
      boost::system::error_code ec;
      iosvc->run(ec);
      if(ec) {
        global_stream_lock.lock();
        std::cout << "Message: " << ec << ".\n";
        global_stream_lock.unlock();
      }
      break;
    }
    catch(std::exception &ex) {
      global_stream_lock.lock();
      std::cout << "Message: " << ex.what() << ".\n";
      global_stream_lock.unlock();
    }
  }

  global_stream_lock.lock();
  std::cout << "Thread " << counter << " End.\n";
  global_stream_lock.unlock();
}

void TimerHandler(
  const boost::system::error_code &ec,
  boost::shared_ptr<boost::asio::deadline_timer> tmr,
  boost::shared_ptr<boost::asio::io_service::strand> strand
)
{
  if(ec) {
    global_stream_lock.lock();
    std::cout << "Error Message: " << ec << ".\n";
    global_stream_lock.unlock();
  }
  else {
    global_stream_lock.lock();
    std::cout << "You see this every three seconds.\n";
    global_stream_lock.unlock();

    tmr->expires_from_now( boost::posix_time::seconds(1));
    tmr->async_wait(
      strand->wrap(boost::bind(&TimerHandler, _1, tmr, strand))
    );
  }
}

void Print(int number) {
  std::cout << "Number: " << number << std::endl;
  boost::this_thread::sleep( boost::posix_time::milliseconds(500));
}

int main(void) {
  boost::shared_ptr<boost::asio::io_service> io_svc(
    new boost::asio::io_service
  );

  boost::shared_ptr<boost::asio::io_service::work> worker(
    new boost::asio::io_service::work(*io_svc)
  );
  boost::shared_ptr<boost::asio::io_service::strand> strand(
    new boost::asio::io_service::strand(*io_svc)
  );

  global_stream_lock.lock();
  std::cout << "Press ENTER to exit!\n";
  global_stream_lock.unlock();

  boost::thread_group threads;
  for(int i=1; i<=5; i++)
    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));

  boost::this_thread::sleep(boost::posix_time::seconds(1));

  strand->post(boost::bind(&Print, 1));
  strand->post(boost::bind(&Print, 2));
  strand->post(boost::bind(&Print, 3));
  strand->post(boost::bind(&Print, 4));
  strand->post(boost::bind(&Print, 5));

  boost::shared_ptr<boost::asio::deadline_timer> timer(
    new boost::asio::deadline_timer(*io_svc)
  );

  timer->expires_from_now( boost::posix_time::seconds(1));
  timer->async_wait( 
    strand->wrap(boost::bind(&TimerHandler, _1, timer, strand))
  );

  std::cin.get();

  io_svc->stop();

  threads.join_all();

  return 0;
}

将上述代码保存为timer3.cpp,并通过运行以下命令进行编译:

g++ -Wall -ansi -I ../boost_1_58_0 timer3.cpp -o timer3 -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58

现在,在控制台中输入timer3命令运行程序,我们将得到以下输出:

使用定时器和 boost::strand 函数

从输出中,我们可以看到前五个 work 对象首先被执行,因为它们必须被串行执行,然后执行 TimerHandler()函数。在定时器线程执行之前,必须先完成 work 对象。如果我们移除 strand 包装,程序的流程将变得混乱,因为我们没有在 Print()函数内部锁定 std::cout 函数。

总结

我们已成功通过使用 strand 对象对 io_service 对象的工作进行了序列化,因此我们可以确保我们设计的工作顺序。我们还可以通过使用错误和异常处理来确保我们的程序能够顺利运行而不会崩溃。最后,在本章中,我们讨论了等待时间,因为在创建网络应用程序时这一点非常重要。

现在,让我们继续下一章,讨论创建一个服务器-客户端应用程序,使得服务器和客户端之间的通信成为可能。

第六章:创建一个客户端-服务器应用程序

在上一章中,我们深入研究了Boost.Asio库,这对于开发网络应用程序非常重要。现在,我们将深入讨论一个客户端-服务器应用程序,它可以在两台或多台计算机之间的计算机网络上相互通信。其中一个称为客户端,另一个称为服务器

我们将讨论服务器的开发,它能够从客户端发送和接收数据流量,并创建一个客户端程序来接收数据流量。在本章中,我们将讨论以下主题:

  • 在客户端和服务器之间建立连接

  • 在客户端和服务器之间发送和接收数据

  • 通过包装最常用的代码来简化编程过程,避免代码重用

建立连接

我们在第二章中讨论了两种类型的 Internet 协议(IP),即传输控制协议(TCP)和用户数据报协议(UDP)。TCP 是面向连接的,这意味着在建立连接后可以发送数据。相反,UDP 是无连接的 Internet 协议,这意味着协议直接将数据发送到目标设备。在本章中,我们只讨论 TCP;因此,我们必须首先建立连接。只有在两方,即客户端和服务器,在本例中接受连接时,连接才能建立。在这里,我们将尝试同步和异步地建立连接。

一个同步客户端

我们首先要建立与远程主机的同步连接。它充当客户端,将打开到 Packt Publishing 网站(www.packtpub.com)的连接。我们将使用 TCP 协议,正如我们在第二章中讨论的那样,理解网络概念。以下是代码:

/* connectsync.cpp */
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/bind.hpp>
#include <boost/lexical_cast.hpp>

boost::mutex global_stream_lock;

void WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {
  global_stream_lock.lock();
  std::cout << "Thread " << counter << " Start.\n";
  global_stream_lock.unlock();

  while(true) {
    try {
      boost::system::error_code ec;
      iosvc->run(ec);
      if(ec) {
        global_stream_lock.lock();
        std::cout << "Message: " << ec << ".\n";
        global_stream_lock.unlock();
      }
      break;
    }
    catch(std::exception &ex) {
      global_stream_lock.lock();
      std::cout << "Message: " << ex.what() << ".\n";
      global_stream_lock.unlock();
    }
  }

  global_stream_lock.lock();
  std::cout << "Thread " << counter << " End.\n";
  global_stream_lock.unlock();
}

int main(void) {
  boost::shared_ptr<boost::asio::io_service> io_svc(
    new boost::asio::io_service
  );

  boost::shared_ptr<boost::asio::io_service::work> worker(
    new boost::asio::io_service::work(*io_svc)
  );
  boost::shared_ptr<boost::asio::io_service::strand> strand(
    new boost::asio::io_service::strand(*io_svc)
  );

  global_stream_lock.lock();
  std::cout << "Press ENTER to exit!\n";
  global_stream_lock.unlock();

  boost::thread_group threads;
  for(int i=1; i<=2; i++)
    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));

  boost::asio::ip::tcp::socket sckt(*io_svc);

  try {
    boost::asio::ip::tcp::resolver resolver(*io_svc);
    boost::asio::ip::tcp::resolver::query query("www.packtpub.com", 
      boost::lexical_cast<std::string>(80)
    );
    boost::asio::ip::tcp::resolver::iterator iterator = resolver.resolve(query);
    boost::asio::ip::tcp::endpoint endpoint = *iterator;

    global_stream_lock.lock();
    std::cout << "Connecting to: " << endpoint << std::endl;
    global_stream_lock.unlock();

    sckt.connect(endpoint); 
    std::cout << "Connected!\n";
  }
  catch(std::exception &ex) {
    global_stream_lock.lock();
    std::cout << "Message: " << ex.what() << ".\n";
    global_stream_lock.unlock();
  }

  std::cin.get();

  boost::system::error_code ec;
  sckt.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
  sckt.close(ec);

  io_svc->stop();

  threads.join_all();

  return 0;
}

将上述代码保存为connectsync.cpp,并运行以下命令来编译代码:

g++ -Wall -ansi -I ../boost_1_58_0 connectsync.cpp -o connectsync -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58

在控制台中输入connectsync来运行程序,我们应该会得到以下输出:

同步客户端

一旦我们按下Enter键,程序将退出。

现在让我们分析代码。正如我们在前面的代码中所看到的,我们使用了之前的示例代码并插入了一行代码,以便能够建立连接。让我们注意我们插入的那一行:

boost::asio::ip::tcp::socket sckt(*io_svc);

现在我们有了一个全局变量,即socket。这个变量将用于提供套接字功能。它来自命名空间boost::asio::ip::tcp,因为我们使用 TCP 作为我们的协议:

boost::asio::ip::tcp::resolver resolver(*io_svc);
boost::asio::ip::tcp::resolver::query query("www.packtpub.com",
 boost::lexical_cast<std::string>(80)
);
boost::asio::ip::tcp::resolver::iterator iterator =
resolver.resolve(query);

我们还使用了命名空间boost::asio::ip::tcp::resolver。它用于获取我们想要连接的远程主机的地址。使用query()类,我们将 Internet 地址和端口作为参数传递。但是因为我们使用整数类型作为端口号,所以我们必须使用lexical_cast将其转换为字符串。查询类用于描述可以传递给解析器的查询。然后,通过使用iterator类,我们将从解析器返回的结果中定义迭代器:

boost::asio::ip::tcp::endpoint endpoint = *iterator;

成功创建迭代器后,我们将其提供给endpoint类型变量。端点将存储由resolver生成的ip地址列表:

sckt.connect(endpoint);

然后,connect()成员函数将套接字连接到我们之前指定的端点。如果一切正常,没有错误或异常抛出,连接现在已经建立:

boost::system::error_code ec;
sckt.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
sckt.close(ec);

为了释放连接,我们必须首先使用shutdown()成员函数在套接字上禁用发送和接收数据过程;然后,我们调用close()成员函数关闭套接字。

当我们运行程序并得到类似上图的输出时,它会通知我们连接已建立。我们可以更改端口号,例如在query()类中将端口号更改为110,即远程 TELNET 服务协议。

boost::asio::ip::tcp::resolver::query query("www.packtpub.com",
 boost::lexical_cast<std::string>(110)
);

然后,程序将抛出异常,输出如下:

一个同步客户端

从输出中,我们可以得出结论,连接被目标机器拒绝,因为我们计划连接的端口是关闭的。这意味着通过使用端口80,即超文本传输协议HTTP),我们可以与 Packt Publishing 网站建立连接。

一个异步客户端

我们已经能够同步建立连接。但是,如果我们需要异步连接到目标,以便程序在尝试建立连接时不会冻结,该怎么办呢?让我们看一下以下代码,找到答案:

/* connectasync.cpp */
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/bind.hpp>
#include <boost/lexical_cast.hpp>
#include <iostream>
#include <string>

boost::mutex global_stream_lock;

void WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {
  global_stream_lock.lock();
  std::cout << "Thread " << counter << " Start.\n";
  global_stream_lock.unlock();

  while(true) {
    try {
      boost::system::error_code ec;
      iosvc->run(ec);
      if(ec) {
        global_stream_lock.lock();
        std::cout << "Message: " << ec << ".\n";
        global_stream_lock.unlock();
      }
      break;
    }
    catch(std::exception &ex) {
      global_stream_lock.lock();
      std::cout << "Message: " << ex.what() << ".\n";
      global_stream_lock.unlock();
    }
  }

  global_stream_lock.lock();
  std::cout << "Thread " << counter << " End.\n";
  global_stream_lock.unlock();
}

void OnConnect(const boost::system::error_code &ec) {
  if(ec) {
    global_stream_lock.lock();
    std::cout << "OnConnect Error: " << ec << ".\n";
    global_stream_lock.unlock();
  }
  else {
    global_stream_lock.lock();
    std::cout << "Connected!.\n";
    global_stream_lock.unlock();
  }
}

int main(void) {
  boost::shared_ptr<boost::asio::io_service> io_svc(
    new boost::asio::io_service
  );

  boost::shared_ptr<boost::asio::io_service::work> worker(
    new boost::asio::io_service::work(*io_svc)
  );

  boost::shared_ptr<boost::asio::io_service::strand> strand(
    new boost::asio::io_service::strand(*io_svc)
  );

  global_stream_lock.lock();
  std::cout << "Press ENTER to exit!\n";
  global_stream_lock.unlock();

  boost::thread_group threads;
  for(int i=1; i<=2; i++)
    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));

  boost::shared_ptr<boost::asio::ip::tcp::socket> sckt(
    new boost::asio::ip::tcp::socket(*io_svc)
  );

  try {
    boost::asio::ip::tcp::resolver resolver(*io_svc);
    boost::asio::ip::tcp::resolver::query query("www.packtpub.com",
      boost::lexical_cast<std::string>(80)
    );
    boost::asio::ip::tcp::resolver::iterator iterator = resolver.resolve( query );
    boost::asio::ip::tcp::endpoint endpoint = *iterator;

    global_stream_lock.lock();
    std::cout << "Connecting to: " << endpoint << std::endl;
    global_stream_lock.unlock();

    sckt->async_connect(endpoint, boost::bind(OnConnect, _1));
  }
  catch(std::exception &ex) {
    global_stream_lock.lock();
    std::cout << "Message: " << ex.what() << ".\n";
    global_stream_lock.unlock();
  }

  std::cin.get();

  boost::system::error_code ec;
  sckt->shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
  sckt->close(ec);

  io_svc->stop();

  threads.join_all();

  return 0;
}

然后,将上述代码保存为connectasync.cpp,并运行以下命令来编译代码:

g++ -Wall -ansi -I ../boost_1_58_0 connectasync.cpp -o connectasync -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58

尝试运行程序,你应该会得到以下输出:

一个异步客户端

正如我们在上述代码中所看到的,我们添加了OnConnect()函数。因为socket对象是不可复制的,我们需要确保在处理程序等待调用时它仍然有效,所以我们必须使用boost::shared_ptr命名空间。我们还使用boost::bind命名空间来调用处理程序,也就是OnConnect()函数。

一个异步服务器

我们已经知道如何同步和异步连接到远程主机。现在,我们将创建服务器程序,与之前创建的客户端程序进行通信。因为我们将处理boost::asio命名空间中的异步程序,我们只会讨论异步服务器中的客户端程序。让我们看一下以下代码:

/* serverasync.cpp */
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/bind.hpp>
#include <boost/lexical_cast.hpp>
#include <iostream>
#include <string>

boost::mutex global_stream_lock;

void WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {
  global_stream_lock.lock();
  std::cout << "Thread " << counter << " Start.\n";
  global_stream_lock.unlock();

  while(true) {
    try {
      boost::system::error_code ec;
      iosvc->run(ec);
      if(ec) {
        global_stream_lock.lock();
        std::cout << "Message: " << ec << ".\n";
        global_stream_lock.unlock();
      }
      break;
    }
    catch(std::exception &ex) {
      global_stream_lock.lock();
      std::cout << "Message: " << ex.what() << ".\n";
      global_stream_lock.unlock();
    }
  }

  global_stream_lock.lock();
  std::cout << "Thread " << counter << " End.\n";
  global_stream_lock.unlock();
}

void OnAccept(const boost::system::error_code &ec) {
  if(ec) {
    global_stream_lock.lock();
    std::cout << "OnAccept Error: " << ec << ".\n";
    global_stream_lock.unlock();
  }
  else {
    global_stream_lock.lock();
    std::cout << "Accepted!" << ".\n";
    global_stream_lock.unlock();
  }
}

int main(void) {
  boost::shared_ptr<boost::asio::io_service> io_svc(
    new boost::asio::io_service
  );

  boost::shared_ptr<boost::asio::io_service::work> worker(
    new boost::asio::io_service::work(*io_svc)
  );

  boost::shared_ptr<boost::asio::io_service::strand> strand(
    new boost::asio::io_service::strand(*io_svc)
  );

  global_stream_lock.lock();
  std::cout << "Press ENTER to exit!\n";
  global_stream_lock.unlock();

  boost::thread_group threads;
  for(int i=1; i<=2; i++)
    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));

  boost::shared_ptr< boost::asio::ip::tcp::acceptor > acceptor(
    new boost::asio::ip::tcp::acceptor(*io_svc)
  );

  boost::shared_ptr<boost::asio::ip::tcp::socket> sckt(
    new boost::asio::ip::tcp::socket(*io_svc)
  );

  try {
    boost::asio::ip::tcp::resolver resolver(*io_svc);
    boost::asio::ip::tcp::resolver::query query(
      "127.0.0.1", 
      boost::lexical_cast<std::string>(4444)
    );
    boost::asio::ip::tcp::endpoint endpoint = *resolver.resolve(query);
    acceptor->open(endpoint.protocol());
    acceptor->set_option(
      boost::asio::ip::tcp::acceptor::reuse_address(false));
    acceptor->bind(endpoint);
    acceptor->listen(boost::asio::socket_base::max_connections);
    acceptor->async_accept(*sckt, boost::bind(OnAccept, _1));

    global_stream_lock.lock();
    std::cout << "Listening on: " << endpoint << std::endl;
    global_stream_lock.unlock();
  }
  catch(std::exception &ex) {
    global_stream_lock.lock();
    std::cout << "Message: " << ex.what() << ".\n";
    global_stream_lock.unlock();
  }

  std::cin.get();

  boost::system::error_code ec;
  acceptor->close(ec);

  sckt->shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
  sckt->close(ec);

  io_svc->stop();

  threads.join_all();

  return 0;
}

将上述代码保存为serverasync.cpp,并运行以下命令来编译代码:

g++ -Wall -ansi -I ../boost_1_58_0 serverasync.cpp -o serverasync -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58 –l mswsock

在运行程序之前,让我们区分一下代码。我们现在有一个新对象,即tcp::acceptor。这个对象用于接受新的套接字连接。由于使用了accept()函数,我们需要在编译过程中添加mswsock库:

acptor->open(endpoint.protocol());
acptor->set_option
(boost::asio::ip::tcp::acceptor::reuse_address(false));
acptor->bind(endpoint);
acptor->listen(boost::asio::socket_base::max_connections);
acptor->async_accept(*sckt, boost::bind(OnAccept, _1));

open() function to open the acceptor by using the protocol that is retrieved from the endpoint variable. Then, by using the set_option function, we set an option on the acceptor to not reuse the address. The acceptor is also bound to the endpoint using the bind() function. After that, we invoke the listen() function to put the acceptor into the state where it will listen for new connections. Finally, the acceptor will accept new connections by using the async_accept() function, which will start an asynchronous accept.

现在,是时候运行程序了。我们需要在这里打开两个命令控制台。第一个控制台是用于程序本身的,第二个是用于调用telnet命令连接到服务器的。我们只需要在运行serverasync程序后立即运行命令telnet 127.0.0.1 4444(我们可以参考第二章中的理解网络概念,在命令提示符中调用telnet命令)。输出应该如下所示:

一个异步服务器

从上图中,我们可以看到程序在启动时监听端口4444,并且在我们调用telnet命令连接到端口4444时,程序接受了连接。然而,因为我们只有一个套接字对象,并且只调用了一次async_accept()函数,程序只会接受一个连接。

读取和写入套接字

我们现在正式能够建立客户端-服务器连接。现在,我们将写入和读取套接字,使连接更有用。我们将修改之前的代码serverasync.cpp,并添加basic_stream_socket对象,提供面向流的套接字功能。

注意

要获取有关basic_stream_socket对象的更详细信息,可以访问www.boost.org/doc/libs/1_58_0/doc/html/boost_asio/reference/basic_stream_socket.html

现在,让我们看一下包含读取和写入套接字过程的以下代码:

/* readwritesocket.cpp */
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/bind.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/cstdint.hpp>
#include <boost/enable_shared_from_this.hpp>
#include <iostream>
#include <string>

boost::mutex global_stream_lock;

void WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {
  global_stream_lock.lock();
  std::cout << "Thread " << counter << " Start.\n";
  global_stream_lock.unlock();

  while(true) {
    try {
      boost::system::error_code ec;
      iosvc->run(ec);
      if(ec) {
        global_stream_lock.lock();
        std::cout << "Message: " << ec << ".\n";
        global_stream_lock.unlock();
      }
      break;
    }
    catch(std::exception &ex) {
      global_stream_lock.lock();
      std::cout << "Message: " << ex.what() << ".\n";
      global_stream_lock.unlock();
    }
  }

  global_stream_lock.lock();
  std::cout << "Thread " << counter << " End.\n";
  global_stream_lock.unlock();
}

struct ClientContext : public boost::enable_shared_from_this<ClientContext> {
  boost::asio::ip::tcp::socket m_socket;

  std::vector<boost::uint8_t> m_recv_buffer;
  size_t m_recv_buffer_index;

  std::list<std::vector<boost::uint8_t> > m_send_buffer;

  ClientContext(boost::asio::io_service & io_service)
  : m_socket(io_service), m_recv_buffer_index(0) {
    m_recv_buffer.resize(4096);
  }

  ~ClientContext() {
  }

  void Close() {
    boost::system::error_code ec;
    m_socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
    m_socket.close(ec);
  }

  void OnSend(const boost::system::error_code &ec, std::list<std::vector<boost::uint8_t> >::iterator itr) {
    if(ec) {
      global_stream_lock.lock();
      std::cout << "OnSend Error: " << ec << ".\n";
      global_stream_lock.unlock();

      Close();
    }
    else {
      global_stream_lock.lock();
      std::cout << "Sent " << (*itr).size() << " bytes." << std::endl;
      global_stream_lock.unlock();
    }
    m_send_buffer.erase(itr);

    // Start the next pending send
    if(!m_send_buffer.empty()) {
      boost::asio::async_write(
        m_socket,
        boost::asio::buffer(m_send_buffer.front()),
        boost::bind(
          &ClientContext::OnSend,
          shared_from_this(),
          boost::asio::placeholders::error,
          m_send_buffer.begin()
        )
      );
    }
  }

  void Send(const void * buffer, size_t length) {
    bool can_send_now = false;

    std::vector<boost::uint8_t> output;
    std::copy((const boost::uint8_t *)buffer, (const boost::uint8_t *)buffer + length, std::back_inserter(output));

    // Store if this is the only current send or not
    can_send_now = m_send_buffer.empty();

    // Save the buffer to be sent
    m_send_buffer.push_back(output);

    // Only send if there are no more pending buffers waiting!
    if(can_send_now) {
      // Start the next pending send
      boost::asio::async_write(
        m_socket,
        boost::asio::buffer(m_send_buffer.front()),
        boost::bind(
          &ClientContext::OnSend,
          shared_from_this(),
          boost::asio::placeholders::error,
          m_send_buffer.begin()
        )
      );
    }
  }

  void OnRecv(const boost::system::error_code &ec, size_t bytes_transferred) {
    if(ec) {
      global_stream_lock.lock();
      std::cout << "OnRecv Error: " << ec << ".\n";
      global_stream_lock.unlock();

      Close();
    }
    else 	{
      // Increase how many bytes we have saved up
      m_recv_buffer_index += bytes_transferred;

      // Debug information
      global_stream_lock.lock();
      std::cout << "Recv " << bytes_transferred << " bytes." << std::endl;
      global_stream_lock.unlock();

      // Dump all the data
      global_stream_lock.lock();
      for(size_t x = 0; x < m_recv_buffer_index; ++x) {

        std::cout << (char)m_recv_buffer[x] << " ";
        if((x + 1) % 16 == 0) {
          std::cout << std::endl;
        }
      }
      std::cout << std::endl << std::dec;
      global_stream_lock.unlock();

      // Clear all the data
      m_recv_buffer_index = 0;

      // Start the next receive cycle
      Recv();
    }
  }

  void Recv() {
    m_socket.async_read_some(
      boost::asio::buffer(
        &m_recv_buffer[m_recv_buffer_index],
        m_recv_buffer.size() - m_recv_buffer_index),
      boost::bind(&ClientContext::OnRecv, shared_from_this(), _1, _2)
    );
  }
};

void OnAccept(const boost::system::error_code &ec, boost::shared_ptr<ClientContext> clnt) {
  if(ec) {
    global_stream_lock.lock();
    std::cout << "OnAccept Error: " << ec << ".\n";
    global_stream_lock.unlock();
  }
  else {
    global_stream_lock.lock();
    std::cout << "Accepted!" << ".\n";
    global_stream_lock.unlock();

    // 2 bytes message size, followed by the message
    clnt->Send("Hi there!", 9);
    clnt->Recv();
  }
}

int main(void) {
  boost::shared_ptr<boost::asio::io_service> io_svc(
    new boost::asio::io_service
  );

  boost::shared_ptr<boost::asio::io_service::work> worker(
    new boost::asio::io_service::work(*io_svc)
  );

  boost::shared_ptr<boost::asio::io_service::strand> strand(
    new boost::asio::io_service::strand(*io_svc)
  );

  global_stream_lock.lock();
  std::cout << "Press ENTER to exit!\n";
  global_stream_lock.unlock();

  // We just use one worker thread 
  // in order that no thread safety issues
  boost::thread_group threads;
  threads.create_thread(boost::bind(&WorkerThread, io_svc, 1));

  boost::shared_ptr< boost::asio::ip::tcp::acceptor > acceptor(
    new boost::asio::ip::tcp::acceptor(*io_svc)
  );

  boost::shared_ptr<ClientContext> client(
    new ClientContext(*io_svc)
  );

  try {
    boost::asio::ip::tcp::resolver resolver(*io_svc);
    boost::asio::ip::tcp::resolver::query query(
      "127.0.0.1",
      boost::lexical_cast<std::string>(4444)
    );
    boost::asio::ip::tcp::endpoint endpoint = *resolver.resolve(query);
    acceptor->open(endpoint.protocol());
    acceptor->set_option(boost::asio::ip::tcp::acceptor::reuse_address(false));
    acceptor->bind(endpoint);
    acceptor->listen(boost::asio::socket_base::max_connections);
    acceptor->async_accept(client->m_socket, boost::bind(OnAccept, _1, client));

    global_stream_lock.lock();
    std::cout << "Listening on: " << endpoint << std::endl;
    global_stream_lock.unlock();
  }
  catch(std::exception &ex) {
    global_stream_lock.lock();
    std::cout << "Message: " << ex.what() << ".\n";
    global_stream_lock.unlock();
  }

  std::cin.get();

  boost::system::error_code ec;
  acceptor->close(ec);

  io_svc->stop();

  threads.join_all();

  return 0;
}

将上述代码保存为readwritesocket.cpp,并使用以下命令编译代码:

g++ -Wall -ansi -I ../boost_1_58_0 readwritesocket.cpp -o readwritesocket -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58 -l mswsock

如果我们将readwritesocket.cpp文件的代码与serverasync.cpp文件进行比较,我们会发现我们添加了一个名为ClientContext的新类。它包含五个成员函数:Send()OnSend()Recv()OnRecv()Close()

Send()和 OnSend()函数

Send()函数中,我们输入一个字符数组和它们的长度。在函数发送字符数组之前,它必须检查m_send_buffer参数是否为空。只有在缓冲区不为空时,发送过程才能发生。

boost::asio::async_write命名空间写入套接字并调用OnSend()函数处理程序。然后,它擦除缓冲区并发送下一个待处理的数据(如果有的话)。现在,每当我们在telnet窗口中按下任何键时,它都会显示我们输入的内容,因为readwritesocket项目会将我们输入的内容发送回telnet窗口。

Recv()和 OnRecv()函数

Send()函数相比,Recv()函数将调用async_read_some()函数来接收数据集,并且OnRecv()函数处理程序将对接收到的数据进行十六进制格式化。

包装网络代码

为了方便起见,让我们为网络应用程序创建一个包装器。使用这个包装器,我们不需要一遍又一遍地重用我们的代码;因此,使我们的编程过程更简单、更高效。现在,只需创建两个文件,名为wrapper.hwrapper.cpp,我们将在下一个代码中包含它们在编译过程中。因为源代码长度较长,不方便在本书中打印,我已将它们制作成可下载的文件,您可以在本书的存储库中访问,网址为www.packtpub.com/networking-and-servers/boostasio-c-network-programming-second-edition。转到代码文件部分。

开发客户端和服务器程序

我们已经使用Boost.Asio库的网络包装器代码简化了开发网络应用程序的编程过程。现在,让我们使用我们的包装器代码创建一个客户端和服务器程序。

创建一个简单的回显服务器

我们将创建一个服务器程序,它将回显从客户端接收到的所有流量。在这种情况下,我们将使用telnet作为客户端,就像以前做过的那样。文件必须保存为echoserver.cpp,内容如下:

/* echoserver.cpp */
#include "wrapper.h"
#include <conio.h>
#include <boost/thread/mutex.hpp>

boost::mutex global_stream_lock;

class MyConnection : public Connection {
private:
  void OnAccept(const std::string &host, uint16_t port) {
    global_stream_lock.lock();
    std::cout << "[OnAccept] " << host << ":" << port << "\n";
    global_stream_lock.unlock();

    Recv();
  }

  void OnConnect(const std::string & host, uint16_t port) {
    global_stream_lock.lock();
    std::cout << "[OnConnect] " << host << ":" << port << "\n";
    global_stream_lock.unlock();

    Recv();
  }

  void OnSend(const std::vector<uint8_t> & buffer) {
    global_stream_lock.lock();
    std::cout << "[OnSend] " << buffer.size() << " bytes\n";
    for(size_t x=0; x<buffer.size(); x++) {

      std::cout << (char)buffer[x];
      if((x + 1) % 16 == 0)
        std::cout << std::endl;
    }
    std::cout << std::endl;
    global_stream_lock.unlock();
  }

  void OnRecv(std::vector<uint8_t> &buffer) {
    global_stream_lock.lock();
    std::cout << "[OnRecv] " << buffer.size() << " bytes\n";
    for(size_t x=0; x<buffer.size(); x++) {

      std::cout << (char)buffer[x];
      if((x + 1) % 16 == 0)
        std::cout << std::endl;
    }
    std::cout << std::endl;
    global_stream_lock.unlock();

    // Start the next receive
    Recv();

    // Echo the data back
    Send(buffer);
  }

  void OnTimer(const boost::posix_time::time_duration &delta) {
    global_stream_lock.lock();
    std::cout << "[OnTimer] " << delta << "\n";
    global_stream_lock.unlock();
  }

  void OnError(const boost::system::error_code &error) {
    global_stream_lock.lock();
    std::cout << "[OnError] " << error << "\n";
    global_stream_lock.unlock();
  }

public:
  MyConnection(boost::shared_ptr<Hive> hive)
    : Connection(hive) {
  }

  ~MyConnection() {
  }
};

class MyAcceptor : public Acceptor {
private:
  bool OnAccept(boost::shared_ptr<Connection> connection, const std::string &host, uint16_t port) {
    global_stream_lock.lock();
    std::cout << "[OnAccept] " << host << ":" << port << "\n";
    global_stream_lock.unlock();

    return true;
  }

  void OnTimer(const boost::posix_time::time_duration &delta) {
    global_stream_lock.lock();
    std::cout << "[OnTimer] " << delta << "\n";
    global_stream_lock.unlock();
  }

  void OnError(const boost::system::error_code &error) {
    global_stream_lock.lock();
    std::cout << "[OnError] " << error << "\n";
    global_stream_lock.unlock();
  }

public:
  MyAcceptor(boost::shared_ptr<Hive> hive)
    : Acceptor(hive) {
  }

  ~MyAcceptor() {
  }
};

int main(void) {
  boost::shared_ptr<Hive> hive(new Hive());

  boost::shared_ptr<MyAcceptor> acceptor(new MyAcceptor(hive));
  acceptor->Listen("127.0.0.1", 4444);

  boost::shared_ptr<MyConnection> connection(new MyConnection(hive));
  acceptor->Accept(connection);

  while(!_kbhit()) {
    hive->Poll();
    Sleep(1);
  }

  hive->Stop();

  return 0;
}

然后,使用以下命令编译上述代码。在这里,我们可以看到在编译过程中包含了wrapper.cpp,以利用我们的包装器代码:

g++ -Wall -ansi -I ../boost_1_58_0 wrapper.cpp echoserver.cpp -o echoserver -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58 -l mswsock

我们可以通过在控制台窗口中输入echoserver来尝试上述程序;之后,我们应该会得到以下输出:

创建一个简单的回显服务器

第一次运行程序时,它将在localhost的端口4444上监听。我们可以在main块中看到,如果没有键盘输入,程序会调用Hive类中的poll()函数。这意味着如果按下任何键,程序将关闭,因为它将调用Hive类中的Stop()函数,这将停止io_service对象。每 1000 毫秒,定时器将会触发,因为Acceptor类的构造函数初始化了 1000 毫秒的定时器间隔。

现在,打开另一个控制台窗口,并输入命令telnet 127.0.0.1 4444,将telnet作为我们的客户端。在echoserver接受连接之后,每当我们在键盘上按下字母数字选项时,echoserver都会将字符发送回telnet。以下图片描述了echoservertelnet服务器之间的连接接受情况:

创建一个简单的回显服务器

当服务器接受来自客户端的连接时,将立即调用OnAccept()函数处理程序。我在telnet窗口中分别按下了ABC键,然后echoserver接收到字符并将它们发送回客户端。telnet窗口还显示了ABC

创建一个简单的客户端程序

我们已经成功创建了一个服务器端程序。现在,我们将继续开发客户端程序。它将通过HTTP GET命令接收 Packt Publishing 网站的内容,代码将如下所示:

/* clienthttpget.cpp */
#include "wrapper.h"
#include <conio.h>
#include <boost/thread/mutex.hpp>

boost::mutex global_stream_lock;

class MyConnection : public Connection {
private:
  void OnAccept(const std::string &host, uint16_t port) {
    global_stream_lock.lock();
    std::cout << "[OnAccept] " << host << ":" << port << "\n";
    global_stream_lock.unlock();

    // Start the next receive
    Recv();
  }

  void OnConnect(const std::string &host, uint16_t port) {
    global_stream_lock.lock();
    std::cout << "[OnConnect] " << host << ":" << port << "\n";
    global_stream_lock.unlock();

    // Start the next receive
    Recv();

    std::string str = "GET / HTTP/1.0\r\n\r\n";

    std::vector<uint8_t> request;
    std::copy(str.begin(), str.end(), std::back_inserter(request));
    Send(request);
  }

  void OnSend(const std::vector<uint8_t> &buffer) {
    global_stream_lock.lock();
    std::cout << "[OnSend] " << buffer.size() << " bytes\n";
    for(size_t x=0; x<buffer.size(); x++) {

      std::cout << (char)buffer[x];
      if((x + 1) % 16 == 0)
        std::cout << "\n";
    }
    std::cout << "\n";
    global_stream_lock.unlock();
  }

  void OnRecv(std::vector<uint8_t> &buffer) {
    global_stream_lock.lock();
    std::cout << "[OnRecv] " << buffer.size() << " bytes\n";
    for(size_t x=0; x<buffer.size(); x++) {

      std::cout << (char)buffer[x];
      if((x + 1) % 16 == 0)
        std::cout << "\n";
    }
    std::cout << "\n";
    global_stream_lock.unlock();

    // Start the next receive
    Recv();
  }

  void OnTimer(const boost::posix_time::time_duration &delta) {
    global_stream_lock.lock();
    std::cout << "[OnTimer] " << delta << std::endl;
    global_stream_lock.unlock();
  }

  void OnError(const boost::system::error_code &error) {
    global_stream_lock.lock();
    std::cout << "[OnError] " << error << "\n";
    global_stream_lock.unlock();
  }

public:
  MyConnection(boost::shared_ptr<Hive> hive)
    : Connection(hive) {
  }

  ~MyConnection() {
  }
};

int main(void) {
  boost::shared_ptr<Hive> hive(new Hive());

  boost::shared_ptr<MyConnection> connection(new MyConnection(hive));
  connection->Connect("www.packtpub.com", 80);

  while(!_kbhit()) {
    hive->Poll();
    Sleep(1);
  }

  hive->Stop();

  return 0;
}

将上述代码保存为clienthttpget.cpp,并使用以下命令编译代码:

g++ -Wall -ansi -I ../boost_1_58_0 wrapper.cpp clienthttpget.cpp -o clienthttpget -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 –l libboost_thread-mgw49-mt-1_58 -l mswsock

当我们运行程序时,将显示以下输出:

创建一个简单的客户端程序

连接建立后,程序将使用以下代码片段向www.packtpub.com的端口80发送HTTP GET命令。

std::string str = "GET / HTTP/1.0\r\n\r\n";
std::vector<uint8_t> request;
std::copy(str.begin(), str.end(), std::back_inserter(request));
Send(request)

Send() function is as follows:
m_io_strand.post(boost::bind(&Connection::DispatchSend, shared_from_this(), buffer));

正如我们所看到的,我们使用strand对象来允许所有事件按顺序运行。此外,由于strand对象的存在,每次事件发生时我们都不必使用lock对象。

请求发送后,程序将使用以下代码片段轮询传入的数据:

m_io_service.poll();

然后,一旦数据到来,它将通过OnRecv()函数处理程序在控制台中显示,就像我们在上面的图像中看到的那样。

总结

在开发网络应用程序时,有三个基本步骤。第一步包括建立源和目标之间的连接,也就是客户端和服务器。我们可以配置socket对象以及acceptor对象来建立连接。

其次,我们通过读写套接字来交换数据。为此,我们可以使用basic_stream_socket函数集合。在我们之前的示例中,我们使用了boost::asio::async_write()方法来发送数据,使用了boost::asio::async_read()方法来接收数据。最后,最后一步是释放连接。通过在ip::tcp::socket对象中使用shutdown()方法,我们可以禁用套接字上的数据发送和接收。然后,在shutdown()函数之后调用close()方法将关闭套接字并释放处理程序。我们还已经为所有函数创建了一个包装器,通过访问Boost.Asio库在网络应用程序编程中最常用。这意味着我们可以简单高效地开发网络应用程序,因为我们不需要一遍又一遍地重用代码。

第七章:调试代码和解决错误

在上一章中,我们成功开发了一个服务器-客户端程序。我们也顺利地运行了我们创建的程序。然而,有时当我们运行应用程序时,会遇到一些问题,比如收到意外的结果或应用程序在运行时崩溃。在这种情况下,调试工具有能力帮助我们解决这些问题。在本章中讨论调试工具时,我们将涵盖以下主题:

  • 选择适合我们使用的调试工具,并保持简单和轻量级

  • 设置调试工具并准备要调试的可执行文件

  • 熟悉调试工具中使用的命令

选择调试工具

许多调试工具都与程序设计语言的集成开发环境IDE)一起提供。例如,Visual Studio有用于 C、C++、C#和 Visual Basic 的调试工具。或者,您可能听说过 CodeBlock 和 Bloodshed Dev-C++,它们也有自己的调试工具。然而,如果您还记得我们在第一章 简化 C++中的网络编程中讨论过的内容,我们决定不使用 IDE,因为它的重负载不会给我们的计算机带来太多资源。我们需要一个轻量级的工具来开发我们的网络应用程序。

我们选择的工具是GNU 调试器GDB)。GDB 是一个基于命令行工具的强大调试工具;这意味着我们不需要复杂的图形用户界面GUI)。换句话说,我们只需要键盘,甚至不需要鼠标,因此系统也变得轻量级。

GDB 可以做四件事来帮助我们解决代码问题,具体如下:

  • 逐行运行我们的代码:当 GDB 运行我们的程序时,我们可以看到当前正在执行哪一行

  • 在特定行停止我们的代码:当我们怀疑某一行导致了错误时,这是很有用的

  • 检查怀疑的行:当我们成功停在怀疑的行时,我们可以继续检查它,例如,通过检查涉及的变量的值

  • 更改变量的值:如果我们发现了导致错误的意外变量值,我们可以在 GDB 运行时用我们期望的值替换该值,以确保值的更改将解决问题

安装调试工具

幸运的是,如果您按照第一章 简化 C++中的网络编程中与 MinGW-w64 安装相关的所有步骤,您将不需要安装其他任何东西,因为安装程序包中也包含了 GDB 工具。现在我们需要做的是在命令控制台中运行 GDB 工具,以检查它是否正常运行。

在命令提示符的任何活动目录中,键入以下命令:

gdb

我们应该在控制台窗口中得到以下输出:

C:\CPP>gdb
GNU gdb (GDB) 7.8.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-w64-mingw32".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
(gdb)_

正如我们在控制台上得到的输出中所看到的,我们有版本 7.8.1(这不是最新版本,因为我们刚刚从 MinGW-w64 安装程序包中获得它)。在最后一行中,我们还有(gdb),旁边有一个闪烁的光标;这意味着 GDB 已准备好接收命令。然而,目前,我们需要知道的命令是quit(或者,我们可以使用q作为快捷方式)来退出 GDB。只需输入q并按Enter,您将回到命令提示符。

为调试准备一个文件

GDB 需要至少一个可执行文件进行调试。为此,我们将回到上一章,从那里借用源代码。你还记得我们在第一章中创建了一个游戏,Simplifying Your Network Programming in C++,在那里我们必须猜测计算机所想的随机数吗?如果你记得,我们有源代码,我们在第一章中保存为rangen.cpp,并且我们通过添加Boost库对其进行了修改,将其保存为第三章中的rangen_boost.cppIntroducing the Boost C++ Libraries。在下一节中,我们将使用rangen_boost.cpp源代码来演示 GDB 的使用。另外,对于那些忘记源代码的人,我已经为你们重新写了它:

/* rangen_boost.cpp */
#include <boost/random/mersenne_twister.hpp>
#include <boost/random/uniform_int_distribution.hpp>
#include <iostream>

int main(void) {
  int guessNumber;
  std::cout << "Select number among 0 to 10: ";
  std::cin >> guessNumber;
  if(guessNumber < 0 || guessNumber > 10) {
    return 1;
  }
  boost::random::mt19937 rng;
  boost::random::uniform_int_distribution<> ten(0,10);
  int randomNumber = ten(rng);

  if(guessNumber == randomNumber) {
    std::cout << "Congratulation, " << guessNumber << " is your lucky number.\n";
  }
  else {
    std::cout << "Sorry, I'm thinking about number " << randomNumber << "\n"; 
  }
  return 0;
}

我们将修改编译命令,以便在 GDB 中使用。我们将使用-g选项,以便创建的可执行文件包含 GDB 将读取的调试信息和符号。我们将使用以下命令从rangen_boost.cpp文件中生成包含调试信息和符号的rangen_boost_gdb.exe可执行文件:

g++ -Wall -ansi -I ../boost_1_58_0 rangen_boost.cpp -o rangen_boost_gdb -g

正如我们在前面的命令中所看到的,我们在编译命令中添加了-g选项,以便在可执行文件中记录调试信息和符号。现在,我们应该在我们的活动目录中有一个名为rangen_boost_gdb.exe的文件。在下一节中,我们将使用 GDB 对其进行调试。

提示

我们只能调试使用-g选项编译的可执行文件。换句话说,如果没有调试信息和符号,我们将无法调试可执行文件。此外,我们无法调试源代码文件(*.cpp文件)或头文件(*.h文件)。

在 GDB 下运行程序

准备包含调试信息和符号的可执行文件后,让我们运行 GDB 从文件中读取所有符号并进行调试。运行以下命令开始调试过程:

gdb rangen_boost_gdb

我们的输出将如下:

C:\CPP>gdb rangen_boost_gdb
GNU gdb (GDB) 7.8.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-w64-mingw32".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from rangen_boost_gdb...done.
(gdb)_

我们得到了与之前 GDB 输出相同的输出,除了(gdb)之前的最后一行。这一行告诉我们,GDB 已成功读取所有调试符号,并准备启动调试过程。在这一步中,我们还可以指定参数,如果我们的程序需要。由于我们的程序不需要指定任何参数,我们现在可以忽略它。

开始调试过程

要开始调试过程,我们可以调用runstart命令。前者将在 GDB 下启动我们的程序,而后者将类似地行为,但将逐行执行代码。不同之处在于,如果我们尚未设置断点,如果调用run命令,程序将像往常一样运行,而如果我们使用start命令开始,调试器将自动在主代码块中设置断点,如果程序达到该点,程序将停止。

现在,让我们使用start命令进行调试过程。只需在 GDB 提示符中输入start,控制台将附加以下输出:

(gdb) start
Temporary breakpoint 1 at 0x401506: file rangen_boost.cpp, line 10.
Starting program: C:\CPP\rangen_boost_gdb.exe
[New Thread 10856.0x213c]

Temporary breakpoint 1, main () at rangen_boost.cpp:10
10              std::cout << "Select number among 0 to 10: ";

调试过程已经开始。从输出中,我们可以发现一个断点自动创建在main块内,位于第 10 行。当没有断点时,调试器将选择主块内的第一个语句。这就是为什么我们得到line 10作为我们的自动断点。

继续和步进调试过程

成功在 GDB 下启动程序后,下一步是继续和步进。我们可以使用以下命令之一来继续和步进调试过程:

  • 继续: 这个命令将恢复程序的执行,直到程序正常完成。如果它找到一个断点,执行将停在设置断点的那一行。

  • step:此命令将执行程序的下一步。step可能意味着源代码的一行或一条机器指令。如果它找到函数的调用,它将进入函数并在函数内运行一步。

  • next:此命令类似于step命令,但它只会继续执行当前堆栈帧中的下一行。换句话说,如果next命令找到函数的调用,它将不会进入函数。

现在,让我们使用next命令。在调用start命令后,立即在 GDB 提示符中键入next命令。我们应该得到以下输出:

(gdb) next
Select number among 0 to 10: 11         std::cin >> guessNumber;

GDB 执行第 10 行,然后继续执行第 11 行。我们将再次调用next命令以继续调试过程。但是,如果我们只是按下Enter键,GDB 将执行我们之前的命令。这就是为什么现在我们只需要按下Enter键,这将给我们一个闪烁的光标。现在,我们必须输入我们猜测的数字以存储在guessNumber变量中。我将输入数字4,但您可以输入您喜欢的数字。再次按下Enter键,以继续调试,直到正常退出程序为止。以下输出将被附加:

(gdb)
4
12              if(guessNumber < 0 || guessNumber > 10)
(gdb)
17              boost::random::mt19937 rng;
(gdb)
19              boost::random::uniform_int_distribution<> ten(0,10);
(gdb)
20              int randomNumber = ten(rng);
(gdb)
22              if(guessNumber == randomNumber)
(gdb)
28                      std::cout << "Sorry, I'm thinking about number " << randomNumber << "\n";
(gdb)
Sorry, I'm thinking about number 8
30              return 0;
(gdb)
31      }(gdb)
0x00000000004013b5 in __tmainCRTStartup ()
(gdb)
Single stepping until exit from function __tmainCRTStartup, which has no line number information.
[Inferior 1 (process 11804) exited normally]

正如我们在前面的输出中所看到的,当我们输入猜测的数字后,程序执行if语句以确保我们输入的数字不超出范围。如果我们猜测的数字有效,程序将继续生成一个随机数。然后我们猜测的数字将与程序生成的随机数进行比较。无论这两个数字是否相同,程序都会给出不同的输出。不幸的是,我的猜测数字与随机数不同。如果您能正确猜出数字,您可能会得到不同的输出。

打印源代码

有时,我们可能希望在运行调试过程时检查我们的源文件。由于调试信息和符号记录在我们的程序中,即使它是一个可执行文件,GDB 也可以打印源代码。要打印源代码,我们可以在 GDB 提示符中键入list(或使用l命令进行快捷方式)。默认情况下,GDB 在每次调用命令时会打印十行。但是,我们可以使用set listsize命令更改此设置。此外,要知道list命令将显示的行数,我们可以调用show listsize命令。让我们看看以下命令行输出:

(gdb) show listsize
Number of source lines gdb will list by default is 10.
(gdb) set listsize 20
(gdb) show listsize
Number of source lines gdb will list by default is 20.
(gdb)_

我们使用list命令增加要显示的行数。现在,每次调用list命令时,输出将显示二十行源代码。

以下是几种list命令的形式,这是最常见的:

  • list:此命令将显示与列表大小定义的行数相同的源代码。如果再次调用它,它将显示剩余的行数,与列表大小定义的行数相同。

  • list [linenumber]:此命令将显示以linenumber为中心的行。命令list 10将显示第 5 行到第 14 行,因为第 10 行位于中心。

  • list [functionname]:此命令将显示以functionname变量开头的行。命令list main将在列表的中心显示int main(void)函数。

  • list [first,last]:此命令将显示从第一行到最后一行的内容。命令list 15,16将仅显示第 15 行和第 16 行。

  • list [,last]:此命令将显示以last结尾的行。命令list ,5将显示第 1 行到第 5 行。

  • list [first,]:此命令将显示从指定行开始的所有行。命令list 5,将显示第 5 行到最后一行,如果行数超过指定行数。否则,它将显示与列表大小设置相同的行数。

  • list +:此命令将显示上次显示的行后面的所有行。

  • list -:此命令将显示在上次显示的行之前的所有行。

设置和删除断点

如果我们怀疑某一行出错,我们可以在那一行设置一个断点,这样调试器就会在那一行停止调试过程。要设置断点,我们可以调用break [linenumber]命令。假设我们想在第 20 行停下来,其中包含以下代码:

int randomNumber = ten(rng);

在这里,我们需要在加载程序到 GDB 后立即调用break 20命令,在第 20 行设置一个断点。下面的输出控制台说明了这一点:

(gdb) break 20
Breakpoint 1 at 0x401574: file rangen_boost.cpp, line 20.
(gdb) run
Starting program: C:\CPP\rangen_boost_gdb.exe
[New Thread 1428.0x13f4]
Select number among 0 to 10: 2

Breakpoint 1, main () at rangen_boost.cpp:20
20              int randomNumber = ten(rng);
(gdb) next
22              if(guessNumber == randomNumber)
(gdb)
28                      std::cout << "Sorry, I'm thinking about number " << randomNumber << "\n";
(gdb)
Sorry, I'm thinking about number 8
30              return 0;
(gdb)
31      }(gdb)
0x00000000004013b5 in __tmainCRTStartup ()
(gdb)
Single stepping until exit from function __tmainCRTStartup,
which has no line number information.
[Inferior 1 (process 1428) exited normally]
(gdb)_

在前面的输出控制台中,我们在 GDB 下加载程序后,调用了break 20命令。然后调试器在第 20 行设置了一个新的断点。与之前一样,我们不再调用start命令,而是调用run命令来执行程序,并让它在找到断点时停止。在我们输入猜测的数字,例如2后,调试器停在第 20 行,这正是我们期望它停下来的地方。然后,我们调用next命令继续调试器,并按下Enter键多次直到程序退出。

如果我们想要删除一个断点,只需使用delete N命令,其中N是设置的所有断点的顺序。如果我们不记得我们设置的所有断点的位置,我们可以调用info break命令来获取所有断点的列表。我们还可以使用delete命令(不带N),它将删除所有断点。

打印变量值

我们已经能够停在我们想要的行上。我们还可以发现我们程序中使用的变量的值。我们可以调用print [variablename]命令来打印任何变量的值。使用前面的断点,我们将打印变量randomNumber的值。在调试器命中第 20 行的断点后,我们将调用打印randomNumber命令。然后,我们调用next命令并再次打印randomNumber变量。看一下命令调用的下面说明:

(gdb) break 20
Breakpoint 1 at 0x401574: file rangen_boost.cpp, line 20.
(gdb) run
Starting program: C:\CPP\rangen_boost_gdb.exe
[New Thread 5436.0x1b04]
Select number among 0 to 10: 3

Breakpoint 1, main () at rangen_boost.cpp:20
20              int randomNumber = ten(rng);
(gdb) print randomNumber
$1 = 0
(gdb) next
22              if(guessNumber == randomNumber)
(gdb) print randomNumber
$2 = 8
(gdb)_

正如我们在前面的输出中所看到的,以下一行是设置断点的地方:

int randomNumber = ten(rng);

在执行该行之前,我们窥视randomNumber变量的值。变量的值为0。然后,我们调用next命令指示调试器执行该行。之后,我们再次窥视变量的值,这次是8。当然,在这个实验中,你可能得到的值与 8 不同。

修改变量值

我们将通过修改一个变量的值来欺骗我们的程序。可以使用set var [variablename]=[newvalue]命令重新分配变量的值。为了确保我们要修改的变量的类型,我们可以调用whatis [variablename]命令来获取所需的变量类型。

现在,让我们在程序为变量分配一个随机数后更改randomNumber变量的值。我们将重新启动调试过程,删除我们已经设置的所有断点,设置一个新的断点在第 22 行,并通过输入continue命令继续调试过程,直到调试器在第 22 行命中断点。在这种情况下,我们可以重新分配randomNumber变量的值,使其与guessNumber变量的值完全相同。现在,再次调用continue命令。之后,我们将因猜对数字而受到祝贺。

要了解更多细节,请看下面的输出控制台,它将说明前面的步骤:

(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Temporary breakpoint 2 at 0x401506: file rangen_boost.cpp, line 10.
Starting program: C:\CPP\rangen_boost_gdb.exe
[New Thread 6392.0x1030]

Temporary breakpoint 2, main () at rangen_boost.cpp:10
10              std::cout << "Select number among 0 to 10: ";
(gdb) info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000401574 in main()
 at rangen_boost.cpp:20
(gdb) delete 1
(gdb) info break
No breakpoints or watchpoints.
(gdb) break 22
Breakpoint 3 at 0x40158d: file rangen_boost.cpp, line 22.
(gdb) continue
Continuing.
Select number among 0 to 10: 5

Breakpoint 3, main () at rangen_boost.cpp:22
22              if(guessNumber == randomNumber)
(gdb) whatis randomNumber
type = int
(gdb) print randomNumber
$3 = 8
(gdb) set var randomNumber=5
(gdb) print randomNumber
$4 = 5
(gdb) continue
Continuing.
Congratulation, 5 is your lucky number.
[Inferior 1 (process 6392) exited normally]
(gdb)_

正如我们在前面的输出中所看到的,当我们调用start命令时,调试器要求我们停止先前的调试过程,因为它仍在运行。只需键入Y键并按Enter键回答查询。我们可以使用info break命令列出所有可用的断点,然后根据从info break命令获取的顺序删除所需的断点。我们调用continue命令恢复调试过程,当调试器触发断点时,我们将randomNumber变量重新分配为guessNumber变量的值。我们继续调试过程,并成功在运行时修改了randomNumber变量的值,因为程序向我们表示祝贺。

如果程序中有很多变量,我们可以使用info locals命令打印所有变量的值,而不是逐个打印所有变量。

调用命令提示符

我偶尔在 GDB 提示符内调用 Windows shell 命令,比如cls命令清除屏幕,dir命令列出活动目录的内容,甚至编译命令。如果你也想执行 Windows shell 命令,可以使用的 GDB 命令是shell [Windows shell command]。实际上,只需在需要时在 Windows shell 命令和参数前添加shell命令。让我们看看以下控制台输出,以了解在 GDB 提示符内执行 Windows shell 命令。让我们看看以下输出:

C:\CPP>gdb
GNU gdb (GDB) 7.8.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-w64-mingw32".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
(gdb) shell dir rangen_boost* /w
 Volume in drive C is SYSTEM
 Volume Serial Number is 8EA6-1DBE

 Directory of C:\CPP

rangen_boost.cpp       rangen_boost.exe       rangen_boost_gdb.exe
 3 File(s)        190,379 bytes
 0 Dir(s)  141,683,314,688 bytes free
(gdb) shell g++ -Wall -ansi -I ../boost_1_58_0 rangen_boost.cpp -o rangen_boost_gdb_2 -g
(gdb) shell dir rangen_boost* /w
 Volume in drive C is SYSTEM
 Volume Serial Number is 8EA6-1DBE

 Directory of C:\CPP

rangen_boost.cpp         rangen_boost.exe         rangen_boost_gdb.exe
rangen_boost_gdb_2.exe
 4 File(s)        259,866 bytes
 0 Dir(s)  141,683,249,152 bytes free

在前面的控制台输出中,我们调用dir命令列出活动目录中以rangen_boost开头的所有文件。然后,我们调用编译命令在活动目录中生成rangen_boost_gdb_2.exe可执行文件。然后,我们再次调用dir命令,以确保rangen_boost_gdb_2.exe可执行文件已成功创建。

提示

您可以使用apropos shell命令获取有关 GDB 中 shell 命令的更多信息。

解决错误

在第五章深入了解 Boost.Asio 库中,我们讨论了处理异常和错误。如果我们遵循本书中的所有源代码,可能永远不会得到任何错误代码来困扰我们。然而,如果我们尝试修改源代码,即使只是一点点,可能会抛出一个错误代码,而程序不会给我们任何描述。由于Boost库抛出的错误代码基于 Windows 系统错误代码,并且超出了本书的范围,我们可以在Microsoft Developer NetworkMSDN)网站上找到描述,网址为msdn.microsoft.com/en-us/library/windows/desktop/ms681381%28v=vs.85%29.aspx。在这里,我们可以找到从错误 0 到 15999 的所有错误代码的翻译。使用 GDB 和 MSDN 的错误代码翻译将成为解决程序中出现的错误的强大工具。

让我们回到第六章创建客户端-服务器应用程序并运行serverasync程序。当程序运行时,它会在127.0.0.1上的端口4444监听客户端,这将在我们的示例中由 telnet 模拟。但是,如果客户端不响应会发生什么?要进一步了解,让我们在不运行 telnet 的情况下运行serverasync程序。由于客户端未响应,将显示以下错误:

解决错误

我们得到了系统错误代码995。现在,有了这个错误代码,我们可以访问 MSDN 系统错误代码并找到错误描述,即I/O 操作已因线程退出或应用程序请求而中止。 (ERROR_OPERATION_ABORTED)

接下来呢?

我们熟悉了基本的 GDB 命令。GDB 中还有许多命令我们无法在这本书中讨论。GDB 有一个官方网站,我们可以访问www.gnu.org/software/gdb/documentation/。在这里,我们可以找到所有我们尚未讨论的完整命令。

注意

我们还可以在官方网站www.boost.org上获取更详细的 Boost C++库信息,特别是Boost.Asio库的文档,可以在www.boost.org/doc/libs/1_58_0/doc/html/boost_asio.html上找到。

总结

调试过程是我们通过逐步运行程序来分析程序的重要工作。当我们的程序产生意外结果或在执行过程中崩溃时,除了运行调试过程别无选择。GDB 是我们的选择,因为它与 C++语言兼容,随 MinGW-w64 安装程序包提供,并且加载时很轻量级。

GDB 只能运行使用-g选项编译的可执行文件。这个选项会添加调试信息和符号,这在调试过程中很重要。如果没有使用-g选项编译可执行文件,你将无法对其进行调试。

在成功加载程序到 GDB 后,我们可以选择使用runstart命令来执行调试过程。run命令会像平常一样执行程序,但如果调试器找到断点,程序会停下来;而start命令会在程序的第一次执行时停在main块处。

当调试器停在某一行时,我们必须决定是否继续调试过程。我们可以选择使用continue命令运行程序直到退出,或者在找到断点时运行调试器的逐步调试使用next命令。

要使调试器在调试过程的执行中停下来,可以使用break [linenumber]命令设置断点。如果我们想确保设置了正确的行号,可以使用list命令打印源代码。然后使用delete N命令删除断点,其中N可以在info break命令中找到。

当查找错误时,获取变量的值也很重要。如果程序产生意外输出,我们可以通过打印变量的值来追踪变量的值。我们可以使用print [variablename]命令来做到这一点。对于我们怀疑引起错误的变量,我们可以使用set var [variablename]=[newvalue]命令重新分配该变量的值。然后我们可以再次运行调试器,直到获得预期的输出。当我们修复了所有错误,并确信一切完美无缺时,我们可以通过在 GDB 提示符中调用编译命令来重新编译我们的程序,使用shell [Windows shell command]命令。

posted @ 2024-05-04 22:43  绝不原创的飞龙  阅读(24)  评论(0编辑  收藏  举报