原文:
annas-archive.org/md5/8b9e46aef0499a9bda54207bb9fe14f9
译者:飞龙
前言
大约 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 GCC(MinGW)作为我的 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
文件完全下载。请参考以下屏幕截图以找到下载按钮:
现在,执行安装程序文件。您将会看到一个欢迎对话框。只需按下一步按钮,进入设置设置对话框。在此对话框中,选择最新的 GCC 版本(在撰写本文时,是4.9.2),其余选项选择如下:
点击下一步按钮继续并进入安装位置选项。在这里,您可以更改默认安装位置。我将更改安装位置为C:\MinGW-w64
,以便使我们的下一个设置更容易,但如果您愿意,也可以保留此默认位置。
点击下一步按钮,进入下一步,并等待片刻,直到文件下载和安装过程完成。
设置路径环境
现在您已经在计算机上安装了 C++编译器,但只能从其安装目录访问它。为了从系统中的任何目录访问编译器,您必须通过执行以下步骤设置PATH 环境:
-
通过按Windows + R键以管理员身份运行命令提示符。在文本框中键入
cmd
,而不是按Enter键,按Ctrl + Shift + Enter以以管理员模式运行命令提示符。然后将出现用户账户控制对话框。选择是以确认您打算以管理员模式运行命令提示符。如果您正确执行此操作,您将获得一个标有管理员:命令提示符的标题栏。如果您没有获得它,您可能没有管理员权限。在这种情况下,您必须联系计算机的管理员。 -
在管理员模式下的命令提示符中键入以下命令:
rundll32.exe sysdm.cpl,EditEnvironmentVariables
- 按下Enter键,命令提示符将立即运行环境变量窗口。然后,转到系统变量,选择名为Path的变量,单击编辑按钮打开编辑系统变量对话框,然后在最后的变量值参数中添加以下字符串:
;C:\MinGW-w64\mingw64\bin
(否则,如果您使用默认位置,安装向导中给出的安装目录路径将需要进行调整)
- 单击编辑系统变量对话框中的确定按钮,然后在环境变量对话框中再次单击确定按钮以保存这些更改。
是时候尝试我们的环境变量设置了。在任何活动目录中打开一个新的命令提示符窗口,可以是管理员模式或非管理员模式,但不能是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 之间选择数字。按照指示选择0
到10
之间的数字。然后,按下Enter,程序将输出一个数字。将其与你自己的数字进行比较。如果两个数字相同,你将受到祝贺。然而,如果你选择的数字与代码生成的数字不同,你将得到相同的通知。程序的输出将如下截图所示:
很遗憾,我在三次尝试中从未猜对正确的数字。事实上,即使每次生成数字时都使用新的种子,也很难猜到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,只需将n与1
相加。
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>
头文件中定义。
如果您发现很难猜出程序生成的数字,那是因为我们使用当前时间作为随机生成器种子,这样做的结果是每次调用程序时生成的数字都会不同。以下是我在大约六到七次尝试后成功猜出生成的随机数的屏幕截图(对于所有程序调用,我们都猜错了数字,除了最后一次尝试):
编译多个源文件
有时,当代码存在错误或 bug 时,我们必须修改我们的代码。如果我们只制作一个包含所有代码行的单个文件,当我们想要修改源代码时,我们会感到困惑,或者我们很难理解程序的流程。为了解决这个问题,我们可以将代码拆分成多个文件,每个文件只包含两到三个函数,这样就容易理解和维护了。
我们已经能够生成随机数,现在,让我们来看一下密码生成器程序。我们将使用它来尝试编译多个源文件。我将创建三个文件来演示如何编译多个源文件,它们是pwgen_fn.h
、pwgen_fn.cpp
和passgen.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.cpp
和pwgen_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.cpp
,pwgen_fn.cpp
和pwgen_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++ 1998、C++ 2003和C++ 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
来运行程序。
如您在前面的屏幕截图中所见,我调用了程序两次,并将Packt和packt作为输入。尽管我只改变了一个字符,但整个哈希值发生了巨大变化。这就是为什么哈希用于检测数据或文件的任何更改,以确保数据没有被更改。
有关 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-version
或major-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:IPv4和IPv6。在 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 模型的协议和设备:
层 | 协议 | 设备 |
---|---|---|
应用 | 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 服务器,然后从服务器上获取所有传入的电子邮件并将其保存在我们的本地计算机上。发送电子邮件的步骤如下:
-
我们的计算机会分配一个随机未使用的端口号,比如
48127
,用来将电子邮件发送到 Gmail SMTP 服务器的端口25
。 -
当电子邮件到达 SMTP 服务器时,它会识别数据来自端口
25
,然后将数据转发到处理该服务的 SMTP。 -
一旦电子邮件被接收,服务器会将确认发送到我们计算机上的端口
48127
,以通知计算机已经接收到电子邮件。 -
在我们的计算机完全接收到来自端口
48127
的确认后,它会将电子邮件发送到电子邮件客户端,然后电子邮件客户端将电子邮件从发件箱移动到已发送文件夹。
与发送电子邮件的步骤类似,要接收电子邮件,我们必须处理一个端口。接收电子邮件的步骤如下:
-
我们的计算机会分配一个随机未使用的端口号,比如
48128
,用来向 Gmail POP3 服务器发送请求到端口110
。 -
当电子邮件到达 POP3 服务器时,它会识别数据来自端口
110
,然后将数据转发到处理该服务的 POP3。 -
然后,POP3 服务器会在端口
48128
向我们的计算机发送电子邮件。 -
在我们的计算机从端口
48128
接收到电子邮件后,它会将电子邮件发送到我们的电子邮件客户端,然后将其移动到收件箱文件夹。它还会自动将邮件保存到本地计算机。
探索 Internet 协议
IP 是一种主要的通信协议,用于在网络上传递数据报。数据报本身是与分组交换网络相关联的传输单元。IP 的作用是根据数据包头部中指定的 IP 地址,从主机传递数据包到主机。目前常用的 IP 版本有两个,即 IPv4 和 IPv6。
Internet 协议版本 4 - IPv4
自 1980 年代以来,IPv4 已成为标准 IP 地址,并用于在网络上从一台计算机到另一台获取 TCP/IP 流量。每个连接到互联网的设备都有唯一的 IP 地址,只要它们有有效的 IP 地址,所有设备都可以在互联网上相互通信。
有效的 IP 地址由四个十进制数构成,用三个点分隔。地址只包含从0
到255
的十进制数。我们可以说10.161.4.25
是一个有效的 IP 地址,因为它包含了从0
到255
的四个十进制数,并用三个点分隔,而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。
-
通过按下Windows + R打开运行窗口,输入
%SYSTEMROOT%\System32\OptionalFeatures.exe
,然后按下确定按钮。Windows 功能窗口将随即打开。 -
勾选Telnet 客户端和Telnet 服务器选项,然后按下确定按钮以确认更改。勾选的选项将看起来像下面的截图:
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,程序很快就会停止等待用户输入下一个数字。代码产生以下输出:
我们输入了六个整数:43
,7
,568
,91
,2240
和56
。最后一个输入是0
,以停止输入过程。然后程序开始对数字进行排序,我们得到了按顺序排序的数字:7
,43
,56
,91
,568
和2240
。
现在,让我们检查我们的代码,以确定 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 格式。请参考以下图片进行比较:
从上图可以看出,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 版本,不如直接转到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_cast
将string
类型数据转换为int
类型数据。程序将要求用户输入两个数字,然后自动找到这两个数字的和。如果用户输入不合适的数字,程序将通知他们发生了错误。
Boost.LexicalCast
库由 Boost 提供,用于将一种数据类型转换为另一种数据类型(将数值类型(如int
、double
或float
)转换为字符串类型,反之亦然)。现在,让我们解剖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::cin
、std::cout
和std::cerr
函数。
其他函数,如std::cin
和std::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
来运行应用程序。我们将得到以下输出:
我为第一个输入放入了10
,为第二个输入放入了20
。结果是30
,因为它只是对两个输入求和。但是如果我输入一个非数字值,例如Packt
,会发生什么呢?以下是尝试该条件的输出:
一旦应用程序发现错误,它将忽略下一个语句并直接转到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_code
、system::error_category
、system::error_condition
和system::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_string
、from_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
现在,让我们运行程序。不幸的是,在我运行程序的三次中,我总是得到相同的随机数,如下所示:
正如我们从这个例子中看到的,我们总是得到数字 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.bat
和b2.exe
使用msvc
(Microsoft Visual C++编译器)作为默认工具集,许多 Windows 开发人员已经在他们的机器上安装了msvc
。由于我们安装了 GCC 编译器,我们在 Boost 的构建中设置了mingw
和gcc
工具集选项。如果您也安装了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.dll
和libboost_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
后,将显示如下输出:
我们想要运行两个函数: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
运行程序以获得以下输出:
我们可以从上面的输出中看到,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_service
。I/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()
函数时给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
相比,它将无限期地阻塞程序,将显示以下文本:
现在,让我们解析代码。如前所述,我们在上面的代码中使用了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::string
、int
和float
。因此,我们必须分别用std::string
、int
和float
填充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.cpp
和multithreads.cpp
文件中看到它得到的不同参数。我们可以将指向不带参数的void()
函数的指针作为create_thread()
函数的参数传递,就像我们在multithreads.cpp
文件中看到的那样。我们还可以将绑定函数作为create_thread()
函数的参数传递,就像我们在ioservicebind.cpp
文件中看到的那样。
使用 Boost.Mutex 库同步数据访问
当您运行multithreads.exe
或ioservicebind.exe
可执行文件时,您是否曾经得到以下输出?
我们可以在上面的截图中看到这里存在格式问题。因为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::bind
和boost::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
并看看这里发生了什么:
正如我们在前面的截图输出中所看到的,程序从线程池中运行线程,并在完成一个线程后,调用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
现在,让我们运行程序以获得以下输出:
与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
库的内容,我们将离创建网络编程更近一步。