C++ 秘籍:问题解决方法(六)
十二、网络
通过互联网进行通信正日益成为许多现代计算机程序不可或缺的一部分。很难找到任何程序不连接到同一程序的另一个实例或为程序或应用程序的某个部分提供基本功能的 web 服务器。这为专门从事网络编程领域的开发人员创造了机会。当编写连接程序时,您可以采用几种不同的方法,使用高级库是一种有效的技术;然而,本章着眼于可以在 OS X、Linux 和 Windows 上使用的 Berkeley 套接字库。
Berkeley 套接字于 1983 年首次出现在 Unix 操作系统中。该操作系统在 20 世纪 80 年代末不再受版权问题的困扰,使得 Berkeley Sockets API 成为今天大多数操作系统上使用的标准实现。即使 Windows 不直接支持 Berkeley,但它的网络 API 几乎与 Berkeley 标准 API 完全相同。
这一章讲述了如何创建和使用套接字来编写可以通过网络(如互联网)相互通信的程序。配方 14-1、14-2 和 14-3 涵盖了当今使用的主要操作系统的相同材料。你应该阅读与你正在开发的系统相关的配方,然后继续阅读配方 12-4。
12-1.在 OS X 上设置 Berkeley Sockets 应用程序
问题
您想要创建一个可以在 OS X 上使用的网络套接字程序。
解决办法
OS X 将 Berkeley Sockets API 作为操作系统的一部分提供,无需借助外部库即可使用。
它是如何工作的
苹果提供了 Xcode IDE ,你可以用它从苹果电脑上构建 OS X 应用程序。Xcode 可以从 App Store 免费获得。安装后,您可以使用 Xcode 创建要在您选择的电脑上运行的程序。这个方法创建了一个命令行程序,它连接到互联网并打开一个到服务器的套接字。
首先,您必须为应用程序创建一个有效的项目。打开 Xcode,选择图 12-1 所示的新建 Xcode 项目选项。
图 12-1 。带有创建新 Xcode 项目选项的 Xcode 欢迎屏幕
系统会要求您选择希望创建的应用程序类型。选择 OS X 应用类别下的命令行工具选项;图 12-2 显示了这个窗口。
图 12-2 。OS X 应用程序命令行工具选项
接下来,要求您指定一个文件夹来存储您的项目文件。之后,Xcode 主窗口打开,您可以从左侧的项目视图中选择源文件。用清单 12-1 中的代码替换新 CPP 文件中的代码,创建一个打开 Google HTTP web 服务器套接字的应用程序。
清单 12-1 。打开伯克利插座
#include <iostream>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
using SOCKET = int;
using namespace std;
int main(int argc, const char * argv[])
{
addrinfo hints{};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
addrinfo *servinfo{};
getaddrinfo("www.google.com", "80", &hints, &servinfo);
SOCKET sockfd{
socket(servinfo->ai_family, servinfo->ai_socktype, servinfo->ai_protocol)
};
int connectionResult{ connect(sockfd, servinfo->ai_addr, servinfo->ai_addrlen) };
if (connectionResult == -1)
{
cout << "Connection failed!" << endl;
}
else
{
cout << "Connection successful!" << endl;
}
freeaddrinfo(servinfo);
return 0;
}
清单 12-1 中的代码需要一个关于互联网如何工作的简短入门,以便你完全理解正在发生的事情。在连接到服务器之前,您需要知道它所在的地址。最好使用域名服务(DNS) 找到它。DNS 的工作原理是保存给定主机名的服务器地址缓存。在本例中,您向 DNS 请求与www.google.com相关联的地址。如果您正在创建一个在您自己的网络上运行的程序,您可以手动指定服务器的 IP 地址,但是对于使用互联网访问信息的程序来说,这通常是不可能的。服务器可以移动,IP 地址可以在不同的时间为不同的系统更改或重新使用。getaddrinfo功能向 DNS 请求与端口 80 上的www.google.com相关联的地址。
特定服务的服务器地址通常由两部分组成:要连接的计算机的 IP 地址和您希望与之通信的服务器上特定服务的端口。万维网使用 HTTP 协议进行通信,该协议通常配置为使用端口 80 提供数据。您可以在清单 12-1 中看到,这是您试图在远程计算机上建立连接的端口。
getaddrinfo函数将网址、端口和两个addrinfo struct作为参数。这些struct中的第一个为 DNS 服务提供了一些提示,关于你想与远程计算机建立的连接类型。此时最重要的两个是ai_family和ai_socktype字段。
ai_family字段指定您想要为您的程序检索的地址类型。这允许您指定是需要 IPv4、IPv6、NetBIOS、红外线还是蓝牙地址。清单 12-1 中提供的选项是未指定的,它允许getaddrinfo函数返回所请求网址的所有有效 IP 地址。这些有效的 IP 地址由相同的addrinfo struct表示,并通过提供给getaddrinfo第四个参数的指针传回程序。
ai_socktype字段允许您指定与相关插座一起使用的传输机制的类型。清单 12-1 中的SOCK_STREAM选项创建了一个使用 TCP/IP 作为传输机制的套接字。这种类型的套接字允许您发送保证按顺序到达目的地的信息包。本章中使用的另一种传动机构是SOCK_DGRAM型。这种传输机制不保证数据包会到达,也不保证它们会按预期的顺序到达;然而,它们没有 TCP/IP 机制带来的开销,因此可以在计算机之间以更低的延迟发送数据包。
由getaddrinfo函数返回的servinfo可以用来创建一个套接字。从socket函数中获得一个套接字文件描述符,该函数从servinfo结构中传递信息。在这个例子中,servinfo结构可以是一个链表,因为 Google 支持 IPv4 和 IPv6 地址格式。您可以在这里编写代码来选择要使用的地址并适当地操作。只要列表中有更多的元素,字段ai_next就存储指向列表中下一个元素的指针。ai_family、ai_socktype和ai_protocol变量都被传递到socket函数中,以创建一个有效的套接字来使用。一旦有了有效的套接字,就可以调用connect函数。connect函数获取套接字 ID、来自包含地址的servinfo对象的ai_addr字段和ai_addrlen来确定地址的长度。如果连接没有成功,您将从connect收到一个返回值-1。清单 12-1 显示了连接是否成功。
12-2.在 Ubuntu 上的 Eclipse 中设置 Berkeley Sockets 应用程序
问题
您想使用 Eclipse 创建一个可以在 Ubuntu 上使用的网络套接字程序。
解决办法
Ubuntu 提供了 Berkeley Sockets API 作为操作系统的一部分,可以在不借助外部库的情况下使用。
它是如何工作的
Eclipse IDE 可用于在运行 Linux 的计算机上构建应用程序。Eclipse 可以从 Ubuntu 软件中心免费获得。一旦安装完毕,您就可以使用 Eclipse 创建在您选择的计算机上运行的程序。这个方法创建了一个命令行程序,它连接到互联网并打开一个到服务器的套接字。
首先,您必须为应用程序创建一个有效的项目。打开 Eclipse,从菜单栏中选择项目 New 选项。新建项目向导打开,如图图 12-3 所示。
图 12-3 。Eclipse 新项目向导
新建项目向导允许您选择 C++ 项目作为选项。然后,点击下一步,你会看到如图 12-4 所示的 C++ 项目设置窗口。
图 12-4 。Eclipse C++ 项目设置窗口
在此窗口中,您可以给项目命名,并决定应该在哪个文件夹中创建项目。在项目类型下,选择可执行文件 Hello World C++ 项目。这样做将创建一个项目,该项目被配置为构建为可执行文件,并且包含一个用于添加您自己的代码的源文件。
本章中的示例代码使用了 C++11 语言规范中的功能。默认的 Eclipse 项目没有启用这一功能。您可以通过右键单击您的项目并选择 Properties 来打开它。你应该会看到图 12-5 中所示的设置窗口,左边是类别。要启用 C++11 支持,选择 C/C++ Build 下的设置,将–std=c++11添加到 All Options 字段,然后单击 OK。
图 12-5 。向您的 Eclipse 项目添加 C++11 支持
用清单 12-2 中的代码替换新 CPP 文件中的代码,创建一个打开 Google HTTP web 服务器套接字的应用程序。
注以下代码和描述与配方 12-1 完全相同。如果你已经读过这份材料,你可能希望跳到食谱 12-4。如果你跳过了食谱 12-1,因为 OS X 与你无关,那么继续读下去。
清单 12-2 。打开伯克利插座
#include <iostream>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
using SOCKET = int;
using namespace std;
int main(int argc, const char * argv[])
{
addrinfo hints{};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
addrinfo *servinfo{};
getaddrinfo("www.google.com", "80", &hints, &servinfo);
SOCKET sockfd{
socket(servinfo->ai_family, servinfo->ai_socktype, servinfo->ai_protocol)
};
int connectionResult{ connect(sockfd, servinfo->ai_addr, servinfo->ai_addrlen) };
if (connectionResult == -1)
{
cout << "Connection failed!" << endl;
}
else
{
cout << "Connection successful!" << endl;
}
freeaddrinfo(servinfo);
return 0;
}
清单 12-2 中的代码需要一个关于互联网如何工作的简短入门,以便你完全理解正在发生的事情。在连接到服务器之前,您需要知道它所在的地址。最好使用域名服务(DNS)找到它。DNS 的工作原理是保存给定主机名的服务器地址缓存。在本例中,您向 DNS 请求与www.google.com相关联的地址。如果你正在创建一个在你自己的网络上运行的程序,你可以手动指定服务器的 IP 地址,但是这对于通过互联网访问信息的程序来说通常是不可能的。服务器可以移动,IP 地址可以在不同的时间为不同的系统更改或重新使用。getaddrinfo功能向 DNS 请求与端口 80 上的www.google.com相关的地址。
特定服务的服务器地址通常由两部分组成:要连接的计算机的 IP 地址和您希望与之通信的服务器上特定服务的端口。万维网使用 HTTP 协议进行通信,该协议通常配置为使用端口 80 提供数据。您可以在清单 12-2 中看到,这是您试图在远程计算机上建立连接的端口。
getaddrinfo函数将网址、端口和两个addrinfo struct作为参数。这些struct中的第一个为 DNS 服务提供了一些提示,关于你想与远程计算机建立的连接类型。此时最重要的两个是ai_family和ai_socktype字段。
ai_family字段指定您想要为您的程序检索的地址类型。这允许您指定是否需要 IPv4、IPv6、NetBIOS、红外线或蓝牙地址。在清单 12-2 中提供的选项是未指定的,它允许getaddrinfo函数为请求的 web 地址返回所有有效的 IP 地址。这些有效的 IP 地址由相同的addrinfo struct表示,并通过提供给getaddrinfo第四个参数的指针传回程序。
ai_socktype字段允许您指定与相关插座一起使用的传输机制的类型。清单 12-2 中的SOCK_STREAM选项创建了一个使用 TCP/IP 作为传输机制的套接字。这种类型的套接字允许您发送保证按顺序到达目的地的信息包。本章中使用的另一种传动机构是SOCK_DGRAM型。这种传输机制不保证数据包会到达,也不保证它们会按预期的顺序到达;然而,它们没有 TCP/IP 机制带来的开销,因此可以在计算机之间以更低的延迟发送数据包。
由getaddrinfo函数返回的servinfo可以用来创建一个套接字。从socket函数中获得一个套接字文件描述符,该函数从servinfo结构中传递信息。在这个例子中,servinfo结构可以是一个链表,因为 Google 支持 IPv4 和 IPv6 地址格式。您可以在这里编写代码来选择要使用的地址并适当地操作。只要列表中有更多的元素,字段ai_next就存储指向列表中下一个元素的指针。ai_family、ai_socktype和ai_protocol变量都被传递到socket函数中,以创建一个有效的套接字来使用。一旦有了有效的套接字,就可以调用connect函数。connect函数获取套接字 ID、来自包含地址的servinfo对象的ai_addr字段和ai_addrlen来确定地址的长度。如果连接没有成功,您将从connect收到一个返回值-1。清单 12-2 显示了连接是否成功。
12-3.在 Windows 上的 Visual Studio 中设置 Winsock 2 应用程序
问题
您想要创建一个可以在 Windows 机器上使用的网络套接字程序。
解决办法
微软提供了 Winsock 库,它支持计算机之间基于套接字的通信。
它是如何工作的
Windows 操作系统没有像 OS X 或 Ubuntu 那样自带本地的 Berkeley 套接字实现。相反,微软提供了 Winsock 库。幸运的是,这个库与 Berkeley Sockets 库非常相似,在某种程度上,大部分代码可以在三个平台之间互换。通过打开 Visual Studio 并选择 File New
项目选项,可以创建一个使用 Winsock 的新 C++ 应用程序。这样做将打开新项目向导,如图图 12-6 所示。
图 12-6 。Visual Studio 新建项目向导
您希望创建一个 Win32 控制台应用程序来运行本章中的示例代码。选择这种类型的应用程序,输入名称,并选择存储数据的文件夹。然后单击确定。
您将进入 Win32 应用程序向导。点击下一步,进入图 12-7 中所示的对话框。
图 12-7 。Win32 应用程序向导
取消选择预编译头和安全开发生命周期(SDL) 检查选项,然后单击完成。当你这样做的时候,你会看到一个工作项目。不过,该项目不支持套接字,因为 Windows 要求您链接一个库来提供套接字支持。您可以通过在“解决方案资源管理器”窗口中右击项目并选择“属性”来实现这一点。在配置属性链接器
输入部分指定要链接的库。图 12-8 显示了选择了特定选项的窗口。
图 12-8 。Visual Studio 链接器输入选项
您希望向附加依赖项部分添加一个新库。选择该选项,点击向下箭头,打开如图图 12-9 所示的对话框。
图 12-9 。附加依赖对话框
Winsock API 由Ws2_32.lib静态库提供。在文本框中输入该值,然后点击确定。这允许您在程序中毫无问题地使用 Winsock 2 API。
用清单 12-3 中的代码替换新 CPP 文件中的代码,创建一个打开 Google HTTP web 服务器套接字的应用程序。
注以下代码和描述与配方 12-1 中的大部分相同。但是,有些部分是 Windows 独有的。如果你已经阅读了这份材料,你可能希望涵盖 Windows 特有的方面,然后跳到食谱 12-4。如果您跳过了配方 12-1 和配方 12-2,请继续阅读。
清单 12-3 。打开 Winsock 套接字
#include <iostream>
#include <winsock2.h>
#include <WS2tcpip.h>
using namespace std;
int main(int argc, char* argv[])
{
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
return 1;
}
addrinfo hints{};
hints.ai_family = AF_UNSPEC; // don't care IPv4 or IPv6
hints.ai_socktype = SOCK_STREAM; // TCP stream sockets
// get ready to connect
addrinfo* servinfo{}; // will point to the results
getaddrinfo("www.google.com", "80", &hints, &servinfo);
SOCKET sockfd{ socket(servinfo->ai_family, servinfo->ai_socktype, servinfo->ai_protocol) };
int connectionResult{ connect(sockfd, servinfo->ai_addr, servinfo->ai_addrlen) };
if (connectionResult == -1)
{
cout << "Connection failed!" << endl;
}
else
{
cout << "Connection successful!" << endl;
}
freeaddrinfo(servinfo);
WSACleanup();
return 0;
}
在清单 12-3 中用粗体显示的代码部分是 Windows socket 库独有的,不能转移到 Unix 和 OS X 的 Berkeley Sockets 实现中。Windows 要求您的程序启动并关闭 Winsock 库。这是通过使用WSAStartup和WSACleanup功能实现的。另一个细微的区别是,Winsock API 将SOCKET类型指定为unsigned int。OS X 和 Ubuntu 中的 Berkeley 实现都从socket函数返回一个标准的int。清单 12-1 和清单 12-2 中的代码使用类型别名来指定SOCKET类型,使代码看起来更具可移植性;然而,平台之间的类型仍然不同。
为了让你完全理解正在发生的事情,这个代码需要一个关于互联网如何工作的简短入门。在连接到服务器之前,您需要知道它所在的地址。最好使用域名服务(DNS)找到它。DNS 的工作原理是保存给定主机名的服务器地址缓存。在这个例子中,您向 DNS 请求与www.google.com相关联的地址。如果您正在创建一个在您自己的网络上运行的程序,您可以手动指定服务器的 IP 地址,但是对于使用互联网访问信息的程序来说,这通常是不可能的。服务器可以移动,IP 地址可以在不同的时间为不同的系统更改或重新使用。getaddrinfo功能向 DNS 请求与端口 80 上的www.google.com相关的地址。
特定服务的服务器地址通常由两部分组成:要连接的计算机的 IP 地址和您希望与之通信的服务器上特定服务的端口。万维网使用 HTTP 协议进行通信,该协议通常配置为使用端口 80 提供数据。您可以在清单 12-3 中看到,这是您试图在远程计算机上建立连接的端口。
getaddrinfo函数将网址、端口和两个addrinfo struct作为参数。这些struct中的第一个为 DNS 服务提供了一些提示,关于你想与远程计算机建立的连接类型。此时最重要的两个是ai_family和ai_socktype字段。
ai_family字段指定您想要为您的程序检索的地址类型。这允许您指定是需要 IPv4、IPv6、NetBIOS、红外线还是蓝牙地址。清单 12-3 中提供的选项是未指定的,它允许getaddrinfo函数返回所请求网址的所有有效 IP 地址。这些有效的 IP 地址由相同的addrinfo struct表示,并通过提供给getaddrinfo第四个参数的指针传回程序。
ai_socktype字段允许您指定与相关插座一起使用的传输机制的类型。清单 12-3 中的SOCK_STREAM选项创建了一个使用 TCP/IP 作为传输机制的套接字。这种类型的套接字允许您发送保证按顺序到达目的地的信息包。本章中使用的另一种传动机构是SOCK_DGRAM型。这种传输机制不保证数据包会到达,也不保证它们会按预期的顺序到达;然而,它们没有 TCP/IP 机制带来的开销,因此可以在计算机之间以更低的延迟发送数据包。
由getaddrinfo函数返回的servinfo 可以用来创建一个套接字。从socket函数中获得一个套接字文件描述符,该函数从servinfo结构中传递信息。在这个例子中,servinfo结构可以是一个链表,因为 Google 支持 IPv4 和 IPv6 地址格式。您可以在这里编写代码来选择要使用的地址并适当地操作。只要列表中有更多的元素,字段ai_next就存储指向列表中下一个元素的指针。ai_family、ai_socktype和ai_protocol变量都被传递到socket函数中,以创建一个有效的套接字来使用。一旦有了有效的套接字,就可以调用connect函数。connect函数获取套接字 ID、来自包含地址的servinfo对象的ai_addr字段和ai_addrlen来确定地址的长度。如果连接没有成功,您将从connect收到一个返回值-1。清单 12-3 显示了连接是否成功。
12-4.在两个程序之间创建套接字连接
问题
你想写一个网络客户端程序和一个服务器程序,可以通过网络进行通信。
解决办法
您可以使用 Berkeley Sockets API 通过套接字发送和接收数据。
它是如何工作的
Berkeley 套接字被设计成通过网络发送和接收信息。API 提供了send和recv函数来实现这个目标。实现这一点的困难在于,您必须确保为数据传输正确配置您的套接字。设置套接字时,接收数据所需的操作与发送数据所需的操作非常不同。该方法还创建了可以在多种平台上运行的代码,并使用 Microsoft Visual Studio、Xcode 或在 Linux 机器上使用 Clang 作为编译器进行编译。
注意
Socket类在使用 GCC 时不会编译,因为编译器还不支持stringstream类的move构造函数。如果您使用 GCC,您可以修改示例代码以避免用stringstream调用move。
当程序构建为在 Windows 机器上运行时,要查看的第一个类启动和停止 Winsock。当您在 OS X 或 Linux 计算机上构建和运行时,这个类不应该有任何影响。清单 12-4 显示了这是如何实现的。
清单 12-4 。包装 Winsock
#include <iostream>
using namespace std;
#ifdef _MSC_VER
#pragma comment(lib, "Ws2_32.lib")
#include <WinSock2.h>
#include <WS2tcpip.h>
#define UsingWinsock 1
using ssize_t = SSIZE_T;
#else
#define UsingWinsock 0
#endif
class WinsockWrapper
{
public:
WinsockWrapper()
{
#if UsingWinsock
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
exit(1);
}
#ifndef NDEBUG
cout << "Winsock started!" << endl;
#endif
#endif
}
~WinsockWrapper()
{
#if UsingWinsock
WSACleanup();
#ifndef NDEBUG
cout << "Winsock shut down!" << endl;
#endif
#endif
}
};
int main(int argc, char* argv[])
{
WinsockWrapper myWinsockWrapper;
return 0;
}
清单 12-4 中的代码使用预处理器检测微软 Visual Studio 的存在。Visual Studio 在构建时定义了符号 _MSC_VER 。您可以在用 Visual Studio 构建 Windows 程序时使用它来包含特定于 Windows 的文件,就像我在这里所做的那样。仅当使用 Visual Studio 生成时,Winsock 2 库才使用 pragma 包含在此程序中;必要的 Winsock 头文件也包括在内。设置了一个专门用于该程序的define。在 Visual Studio 中构建代码时,UsingWinsock宏被定义为 1;当代码不是使用 Visual Studio 构建时,它被设置为 0。Windows 构建还要求您创建一个类型别名来将SSIZE_T映射到ssize_t,因为当不在 Windows 计算机上构建时,该类型使用小写拼写。
WinsockWrapper类在其构造函数和析构函数中检测UsingWinsock的值。如果该值为 1,则启动和停止 Winsock API 的函数在。当不使用 Visual Studio 构建时,不编译此代码;因此以这种方式包含是安全的。
main函数在其第一行创建一个WinsockWrapper对象。这将导致在 Windows 计算机上调用构造函数并初始化 Winsock 它对非 Windows 版本没有影响。当该对象超出范围时,Winsock API 也会关闭,因为会调用析构函数。现在,您有了一种方便的方法,可以跨多个平台移植来启动和停止 Winsock。
类是从一个程序到另一个程序通信的组成部分。它负责为基于 C 的 Berkeley Sockets API 提供面向对象的包装。socket本身由一个描述符表示,这个描述符本质上是一个int。一种方法创建一个类,该类将创建 Berkeley 套接字所需的数据与处理套接字所需的代码相关联。Socket类的完整源代码显示在清单 12-5 中。
清单 12-5 。创建面向对象的Socket Class
class Socket
{
private:
#if !UsingWinsock
using SOCKET = int;
#endif
addrinfo* m_ServerInfo{ nullptr };
SOCKET m_Socket{ static_cast<SOCKET>(0xFFFFFFFF) };
sockaddr_storage m_AcceptedSocketStorage{};
socklen_t m_AcceptedSocketSize{ sizeof(m_AcceptedSocketStorage) };
void CreateSocket(string& webAddress, string& port, addrinfo& hints)
{
getaddrinfo(webAddress.c_str(), port.c_str(), &hints, &m_ServerInfo);
m_Socket = socket(
*m_ServerInfo->ai_family,*
*m_ServerInfo->ai_socktype,*
m_ServerInfo->ai_protocol);
}
Socket(int newSocket, sockaddr_storage&& socketStorage)
: m_Socket{ newSocket }
, m_AcceptedSocketStorage(move(socketStorage))
{
}
public:
Socket(string& port)
{
#ifndef NDEBUG
stringstream portStream{ port };
int portValue{};
portStream >> portValue;
assert(portValue > 1024); // Ports under 1024 are reserved for certain applications and protocols!
#endif
addrinfo hints{};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
string address{ "" };
CreateSocket(address, port, hints);
}
Socket(string& webAddress, string& port)
{
addrinfo hints{};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
CreateSocket(webAddress, port, hints);
}
Socket(string& webAddress, string& port, addrinfo& hints)
{
CreateSocket(webAddress, port, hints);
}
~Socket()
{
Close();
}
bool IsValid()
{
return m_Socket != -1;
}
int Connect()
{
*int connectionResult{*
connect(m_Socket, m_ServerInfo->ai_*addr, m_ServerInfo->ai_addrlen)*
};
#ifndef NDEBUG
if (connectionResult == -1)
{
cout << "Connection failed!" << endl;
}
else
{
cout << "Connection successful!" << endl;
}
#endif
return connectionResult;
}
int Bind()
{
int bindResult{ ::bind(m_Socket, m_ServerInfo->ai_addr, m_ServerInfo->ai_addrlen) };
#ifndef NDEBUG
if (bindResult == -1)
{
cout << "Bind Failed!" << endl;
}
else
{
cout << "Bind Successful" << endl;
}
#endif
return bindResult;
}
int Listen(int queueSize)
{
int listenResult{ listen(m_Socket, queueSize) };
#ifndef NDEBUG
if (listenResult == -1)
{
cout << "Listen Failed" << endl;
}
else
{
cout << "Listen Succeeded" << endl;
}
#endif
return listenResult;
}
Socket Accept()
{
SOCKET newSocket{
*accept(m_Socket,*
reinterpret_cast<sockad*dr*>(*&*m_AcceptedSocketStorage),*
&*m_AcceptedSocketSize)*
};
#ifndef NDEBUG
if (newSocket == -1)
{
cout << "Accept Failed" << endl;
}
else
{
cout << "Accept Succeeded" << endl;
}
#endif
m_AcceptedSocketSize = sizeof(m_AcceptedSocketStorage);
return Socket(newSocket, move(m_AcceptedSocketStorage));
}
void Close()
{
#ifdef _MSC_VER
closesocket(m_Socket);
#else
close(m_Socket);
#endif
m_Socket = -1;
freeaddrinfo(m_ServerInfo);
}
ssize_t Send(stringstream data)
{
string packetData{ data.str() };
ssize_t sendResult{ send(m_Socket, packetData.c_str(), packetData.length(), 0) };
#ifndef NDEBUG
if (sendResult == -1)
{
cout << "Send Failed" << endl;
}
else
{
cout << "Send Succeeded" << endl;
}
#endif
return sendResult;
}
stringstream Receive()
{
const int size{ 1024 };
char dataReceived[size];
ssize_t receiveResult{ recv(m_Socket, dataReceived, size, 0) };
#ifndef NDEBUG
if (receiveResult == -1)
{
cout << "Receive Failed" << endl;
}
else if (receiveResult == 0)
{
cout << "Receive Detected Closed Connection!" << endl;
Close();
}
else
{
dataReceived[receiveResult] = '\0';
cout << "Receive Succeeded" << endl;
}
#endif
stringstream data{ dataReceived };
return move(data);
}
};
Socket class有三个不同的构造函数,允许你为不同的目的创建套接字。第一个公共构造函数只接受一个port作为参数。这种构造方法适用于用于监听传入连接的Socket对象。构造函数中的hints addrinfo struct将ai_flags参数设置为AI_PASSIVE值,并为address传递一个空的string。这告诉getaddrinfo函数填写本地计算机的 IP 地址作为套接字使用的地址。以这种方式使用本地地址可以让您打开套接字来监听计算机—当您希望从外部源接收程序中的数据时,这是一项重要的任务。
第二个公共构造函数接受一个地址和一个端口。这允许您创建一个Socket,它自动使用 IPv6 或 IPv4 和 TCP/IP 来创建一个可用于发送数据的套接字。第一个和第二个构造函数都是为了方便起见——它们都可以被删除,以支持第三个公共构造函数,它接受一个地址、一个端口和一个addrinfo struct,并允许用户按照自己的意愿配置一个Socket。
最后一个构造函数是私有构造函数。当外部程序连接到套接字侦听连接时,使用这种类型的构造函数。您可以看到这是如何在Accept方法中使用的。
IsValid方法确定Socket是否已经用适当的描述符初始化。CreateSocket中的socket函数在失败的结果中返回-1;m_Socket的默认值也是-1。
当你希望建立一个到远程计算机的连接,并且对接收来自其他程序的连接不感兴趣时,可以使用Connect方法。它主要用于客户端-服务器关系的客户端;然而,不难想象,您可以编写使用不同套接字监听和连接他人的对等程序。Connect调用 Berkeley connect函数,但是能够从对象中使用m_Socket和m_ServerInfo对象,而不必从外部位置手动传递它们。
当您希望接收输入连接时,使用Bind方法。Berkeley bind函数负责协商访问您希望与操作系统一起使用的端口。操作系统负责发送和接收网络流量,端口用于让计算机知道哪个程序正在哪个端口上等待数据。当using namespace std;语句存在时,bind函数上的scope操作符对于该代码是必需的。这告诉编译器从全局名称空间而不是从std名称空间使用bind方法。来自std名称空间的bind方法用于创建仿函数,与套接字无关。
Listen方法出现在对Bind的调用之后,它告诉套接字开始对来自远程机器的连接进行排队。queueSize参数指定队列的大小;一旦队列满了,操作系统就会丢弃连接。您的操作系统可以支持的连接数量会有所不同。桌面操作系统通常支持比服务器专用操作系统少得多的排队连接。大多数情况下,5 这样的数字就可以了。
Accept方法从调用Listen时创建的队列中提取连接。Accept调用 Berkeley accept函数,该函数将m_Socket变量作为其第一个参数。第二个和第三个参数是m_AcceptedSocketStorage和m_AcceptedSocketSize变量。m_AcceptedSocketStorage成员变量属于sockaddr_storage类型,而不是accept方法所期望的sockaddr类型。sockaddr_storage类型足够大,可以处理 IPv4 和 IPv6 地址,但是accept方法仍然需要一个指向sockaddr类型的指针。这并不理想;但是,可以使用reinterpret_cast来解决这个问题,因为 accept 也会考虑被传递对象的大小。如果返回的对象小于传入的大小,则改变大小;因此,在函数返回之前,大小被重置。将m_AcceptedSocketStorage对象移动到从函数返回的新的Socket对象中,以确保初始Socket中的副本无效。
Close方法负责在不再需要Socket时关闭它。在 Windows 上调用closesocket函数,在非 Windows 平台上使用close函数。freeaddrinfo对象也在该类的析构函数中被释放。
接下来的方法是Send。不出所料,这个方法将数据发送到连接另一端的机器。Send被设置为发送一个stringstream对象,因为正确序列化二进制数据超出了本书的范围。您可以看到,调用send Berkeley 函数时使用了m_Socket描述符以及从传入的stringstream对象中提取的字符串数据和大小。
Receive方法负责从远程连接引入数据。这个调用会一直阻塞,直到准备好从套接字连接读取数据。Receive函数可以返回三种类型的值:-1(当遇到错误时),0(当连接被远程计算机关闭时),或者一个表示接收到的字节数的正值。接收到的数据被读入一个char数组,然后传递给一个stringstream对象,该对象将使用move构造函数从函数中返回。
现在你已经有了一个全功能的Socket类,你可以创建程序来发送和接收数据。清单 12-6 中的代码可以用来创建一个等待远程连接和单个接收消息的程序。
清单 12-6 。创建一个可以接收数据的程序
#include <cassert>
#include <iostream>
#include <type_traits>
#include <vector>
#ifndef NDEBUG
#include <sstream>
#endif
using namespace std;
#ifdef _MSC_VER
#pragma comment(lib, "Ws2_32.lib")
#include <WinSock2.h>
#include <WS2tcpip.h>
#define UsingWinsock 1
using ssize_t = SSIZE_T;
#else
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#define UsingWinsock 0
#endif
class WinsockWrapper
{
public:
WinsockWrapper()
{
#if UsingWinsock
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
exit(1);
}
cout << "Winsock started!" << endl;
#endif
}
~WinsockWrapper()
{
#if UsingWinsock
WSACleanup();
cout << "Winsock shut down!" << endl;
#endif
}
};
class Socket
{
private:
#if !UsingWinsock
using SOCKET = int;
#endif
addrinfo* m_ServerInfo{ nullptr };
SOCKET m_Socket{ static_cast<SOCKET>(0xFFFFFFFF) };
sockaddr_storage m_AcceptedSocketStorage{};
socklen_t m_AcceptedSocketSize{ sizeof(m_AcceptedSocketStorage) };
void CreateSocket(string& webAddress, string& port, addrinfo& hints)
{
getaddrinfo(webAddress.c_str(), port.c_str(), &hints, &m_ServerInfo);
m_Socket = socket(
m_ServerInfo->ai_family,
m_ServerInfo->ai_socktype,
m_ServerInfo->ai_protocol);
}
Socket(int newSocket, sockaddr_storage&& socketStorage)
: m_Socket{ newSocket }
, m_AcceptedSocketStorage(move(socketStorage))
{
}
public:
Socket(string& port)
{
#ifndef NDEBUG
stringstream portStream{ port };
int portValue{};
portStream >> portValue;
assert(portValue > 1024);
// Ports under 1024 are reserved for certain applications and protocols!
#endif
addrinfo hints{};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
string address{ "" };
CreateSocket(address, port, hints);
}
Socket(string& webAddress, string& port)
{
addrinfo hints{};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
CreateSocket(webAddress, port, hints);
}
Socket(string& webAddress, string& port, addrinfo& hints)
{
CreateSocket(webAddress, port, hints);
}
~Socket()
{
Close();
}
bool IsValid()
{
return m_Socket != -1;
}
int Connect()
{
int connectionResult{
connect(m_Socket, m_ServerInfo->ai_addr, m_ServerInfo->ai_addrlen)
};
#ifndef NDEBUG
if (connectionResult == -1)
{
cout << "Connection failed!" << endl;
}
else
{
cout << "Connection successful!" << endl;
}
#endif
return connectionResult;
}
int Bind()
{
int bindResult{ ::bind(m_Socket, m_ServerInfo->ai_addr, m_ServerInfo->ai_addrlen) };
#ifndef NDEBUG
if (bindResult == -1)
{
cout << "Bind Failed!" << endl;
}
else
{
cout << "Bind Successful" << endl;
}
#endif
return bindResult;
}
int Listen(int queueSize)
{
int listenResult{ listen(m_Socket, queueSize) };
#ifndef NDEBUG
if (listenResult == -1)
{
cout << "Listen Failed" << endl;
}
else
{
cout << "Listen Succeeded" << endl;
}
#endif
return listenResult;
}
Socket Accept()
{
SOCKET newSocket{
accept(m_Socket,
reinterpret_cast<sockaddr*>(&m_AcceptedSocketStorage),
&m_AcceptedSocketSize)
};
#ifndef NDEBUG
if (newSocket == -1)
{
cout << "Accept Failed" << endl;
}
else
{
cout << "Accept Succeeded" << endl;
}
#endif
m_AcceptedSocketSize = sizeof(m_AcceptedSocketStorage);
return Socket(newSocket, move(m_AcceptedSocketStorage));
}
void Close()
{
#ifdef _MSC_VER
closesocket(m_Socket);
#else
close(m_Socket);
#endif
m_Socket = -1;
freeaddrinfo(m_ServerInfo);
}
ssize_t Send(stringstream data)
{
string packetData{ data.str() };
ssize_t sendResult{ send(m_Socket, packetData.c_str(), packetData.length(), 0) };
#ifndef NDEBUG
if (sendResult == -1)
{
cout << "Send Failed" << endl;
}
else
{
cout << "Send Succeeded" << endl;
}
#endif
return sendResult;
}
stringstream Receive()
{
const int size{ 1024 };
char dataReceived[size];
ssize_t receiveResult{ recv(m_Socket, dataReceived, size, 0) };
#ifndef NDEBUG
if (receiveResult == -1)
{
cout << "Receive Failed" << endl;
}
else if (receiveResult == 0)
{
cout << "Receive Detected Closed Connection!" << endl;
Close();
}
else
{
dataReceived[receiveResult] = '\0';
cout << "Receive Succeeded" << endl;
}
#endif
stringstream data{ dataReceived };
return move(data);
}
};
int main(int argc, char* argv[])
{
WinsockWrapper myWinsockWrapper;
string port{ "3000" };
Socket myBindingSocket(port);
myBindingSocket.Bind();
int listenResult{ myBindingSocket.Listen(5) };
assert(listenResult != -1);
Socket acceptResult{ myBindingSocket.Accept() };
assert(acceptResult.IsValid());
stringstream data{ acceptResult.Receive() };
string message;
getline(data, message, '\0');
cout << "Received Message: " << message << endl;
return 0;
}
清单 12-6 中的代码创建了一个程序,该程序有一个套接字,它等待从远程连接接收一条消息。由于封装在WinsockWrapper 和Socket类中的困难工作,main函数最终只包含几行代码。如果运行在 Visual Studio for Windows 计算机构建的服务器上,main函数首先创建一个WinsockWrapper来初始化 Winsock。然后用一个空地址将一个Socket初始化到端口 3000。此端口将用于侦听本地计算机上的连接。你可以看到是这样的,因为main函数接着调用Bind,然后调用队列大小为 5 的Listen,最后才调用Accept。Accept调用会阻塞,直到队列中出现一个远程连接。Accept返回一个用于接收数据的单独的Socket对象。对那个Socket的Receive调用也是一个阻塞调用,程序在那里等待,直到数据可用。在返回之前,程序通过打印出接收到的消息来结束。
一旦构建并运行了服务器程序,就需要一个客户机程序来连接它并发送消息。这显示在清单 12-7 中。
清单 12-7 。客户端程序
#include <cassert>
#include <iostream>
#include <type_traits>
#ifndef NDEBUG
#include <sstream>
#endif
using namespace std;
#ifdef _MSC_VER
#pragma comment(lib, "Ws2_32.lib")
#include <WinSock2.h>
#include <WS2tcpip.h>
#define UsingWinsock 1
using ssize_t = SSIZE_T;
#else
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#define UsingWinsock 0
#endif
class WinsockWrapper
{
public:
WinsockWrapper()
{
#if UsingWinsock
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
exit(1);
}
#ifndef NDEBUG
cout << "Winsock started!" << endl;
#endif
#endif
}
~WinsockWrapper()
{
#if UsingWinsock
WSACleanup();
#ifndef NDEBUG
cout << "Winsock shut down!" << endl;
#endif
#endif
}
};
class Socket
{
private:
#if !UsingWinsock
using SOCKET = int;
#endif
addrinfo* m_ServerInfo{ nullptr };
SOCKET m_Socket{ static_cast<SOCKET>(0xFFFFFFFF) };
sockaddr_storage m_AcceptedSocketStorage{};
socklen_t m_AcceptedSocketSize{ sizeof(m_AcceptedSocketStorage) };
void CreateSocket(string& webAddress, string& port, addrinfo& hints)
{
getaddrinfo(webAddress.c_str(), port.c_str(), &hints, &m_ServerInfo);
m_Socket = socket(m_ServerInfo->ai_family,
m_ServerInfo->ai_socktype,
m_ServerInfo->ai_protocol);
}
Socket(int newSocket, sockaddr_storage&& socketStorage)
: m_Socket{ newSocket }
, m_AcceptedSocketStorage(move(socketStorage))
{
}
public:
Socket(string& port)
{
#ifndef NDEBUG
stringstream portStream{ port };
int portValue{};
portStream >> portValue;
assert(portValue > 1024);
// Ports under 1024 are reserved for certain applications and protocols!
#endif
addrinfo hints{};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
string address{ "" };
CreateSocket(address, port, hints);
}
Socket(string& webAddress, string& port)
{
addrinfo hints{};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
CreateSocket(webAddress, port, hints);
}
Socket(string& webAddress, string& port, addrinfo& hints)
{
CreateSocket(webAddress, port, hints);
}
~Socket()
{
Close();
}
bool IsValid()
{
return m_Socket != -1;
}
int Connect()
{
int connectionResult{ connect(
m_Socket,
m_ServerInfo->ai_addr,
m_ServerInfo->ai_addrlen)
};
#ifndef NDEBUG
if (connectionResult == -1)
{
cout << "Connection failed!" << endl;
}
else
{
cout << "Connection successful!" << endl;
}
#endif
return connectionResult;
}
int Bind()
{
int bindResult{ ::bind(m_Socket, m_ServerInfo->ai_addr, m_ServerInfo->ai_addrlen) };
#ifndef NDEBUG
if (bindResult == -1)
{
cout << "Bind Failed!" << endl;
}
else
{
cout << "Bind Successful" << endl;
}
#endif
return bindResult;
}
int Listen(int queueSize)
{
int listenResult{ listen(m_Socket, queueSize) };
#ifndef NDEBUG
if (listenResult == -1)
{
cout << "Listen Failed" << endl;
}
else
{
cout << "Listen Succeeded" << endl;
}
#endif
return listenResult;
}
Socket Accept()
{
SOCKET newSocket{ accept(m_Socket, reinterpret_cast<sockaddr*>(&m_AcceptedSocketStorage), &m_AcceptedSocketSize) };
#ifndef NDEBUG
if (newSocket == -1)
{
cout << "Accept Failed" << endl;
}
else
{
cout << "Accept Succeeded" << endl;
}
#endif
m_AcceptedSocketSize = sizeof(m_AcceptedSocketStorage);
return Socket(newSocket, move(m_AcceptedSocketStorage));
}
void Close()
{
#ifdef _MSC_VER
closesocket(m_Socket);
#else
close(m_Socket);
#endif
m_Socket = -1;
freeaddrinfo(m_ServerInfo);
}
ssize_t Send(stringstream data)
{
string packetData{ data.str() };
ssize_t sendResult{ send(m_Socket, packetData.c_str(), packetData.length(), 0) };
#ifndef NDEBUG
if (sendResult == -1)
{
cout << "Send Failed" << endl;
}
else
{
cout << "Send Succeeded" << endl;
}
#endif
return sendResult;
}
stringstream Receive()
{
const int size{ 1024 };
char dataReceived[size];
ssize_t receiveResult{ recv(m_Socket, dataReceived, size, 0) };
#ifndef NDEBUG
if (receiveResult == -1)
{
cout << "Receive Failed" << endl;
}
else if (receiveResult == 0)
{
cout << "Receive Detected Closed Connection!" << endl;
Close();
}
else
{
dataReceived[receiveResult] = '\0';
cout << "Receive Succeeded" << endl;
}
#endif
stringstream data{ dataReceived };
return move(data);
}
};
int main(int argc, char* argv[])
{
WinsockWrapper myWinsockWrapper;
string address("192.168.178.44");
string port("3000");
Socket myConnectingSocket(address, port);
myConnectingSocket.Connect();
string message("Sending Data Over a Network!");
stringstream data;
data << message;
myConnectingSocket.Send(move(data));
return 0;
}
清单 12-7 显示了相同的Socket类可以在服务器和客户机上使用。客户端的main函数也使用WinsockWrapper对象来处理 Winsock 库的启动和关闭。然后创建一个连接到 IP 地址 192.168.178.44 的Socket。(这是我用来托管服务器程序的计算机的地址。)在创建了Socket之后,调用Connect方法,以在不同计算机上运行的两个程序之间建立连接。Send方法是最后一个函数调用,发送字符串“通过网络发送数据!”图 12-10 显示了在 MacBook Pro 上运行服务器和在 Windows 8.1 桌面 PC 上运行客户端所获得的输出。
图 12-10 。在 OS X 上运行服务器生成的输出
12-5.在两个程序之间创建网络协议
问题
您希望创建两个能够遵循标准模式相互通信的程序。
解决办法
您可以创建一个两个程序都可以遵循的协议,这样每个程序都知道如何响应给定的请求。
它是如何工作的
在两个程序之间建立的套接字连接可以用来双向发送数据:从发起连接的程序到接收者,也可以从接收者返回到发起者。这个特性允许您编写能够响应请求的网络应用程序,甚至可以构建需要在单个应用程序中来回发送多条消息的更复杂的协议。
您可能熟悉的当今使用的最常见的协议示例是 HTTP。HTTP 是支持万维网的网络协议。它是一个请求和响应协议,让客户端程序从服务器请求数据。当浏览器向服务器请求网页时,可以看到常见的应用程序,但移动应用程序使用 HTTP 在其应用程序和服务器后端之间传输数据也并不罕见。其他常见的协议有 FTP(用于促进计算机之间的文件传输)以及 POP 和 SMTP 电子邮件协议。
这个菜谱展示了一个非常简单的网络协议,它向服务器提出一个问题,让客户机用一个答案进行响应,并让服务器告诉客户机答案是否正确。与 HTTP 这样复杂的例子相比,这个协议微不足道,但是它是一个很好的起点。
该协议由四条消息组成:QUESTION、ANSWER、QUIT和FINISHED。当用户应该被询问一个问题时,QUESTION消息从客户端发送到服务器。服务器通过向客户端发送一个问题来响应此消息。客户端通过向服务器发送ANSWER 以及用户的回答来响应问题。客户端可以在任何时候向服务器发送QUIT 来终止套接字连接。一旦服务器将所有问题发送到服务器,来自客户端的后续QUESTION请求将导致FINISHED 被发送到客户端;那么连接将被终止。
这个方法中的服务器程序可以同时处理多个客户端连接。它通过使用Socket::Accept方法接受单个连接,然后使用async函数将连接到客户端的Socket交给一个thread来实现这一点。你可以在清单 12-8 的中看到服务器程序的源代码。
清单 12-8 。协议服务器程序
#include <array>
#include <cassert>
#include <future>
#include <iostream>
#include <thread>
#include <type_traits>
#include <vector>
#ifndef NDEBUG
#include <sstream>
#endif
using namespace std;
#ifdef _MSC_VER
#pragma comment(lib, "Ws2_32.lib")
#include <WinSock2.h>
#include <WS2tcpip.h>
#define UsingWinsock 1
using ssize_t = SSIZE_T;
#else
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#define UsingWinsock 0
#endif
class WinsockWrapper
{
public:
WinsockWrapper()
{
#if UsingWinsock
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
exit(1);
}
cout << "Winsock started!" << endl;
#endif
}
~WinsockWrapper()
{
#if UsingWinsock
WSACleanup();
cout << "Winsock shut down!" << endl;
#endif
}
};
class Socket
{
private:
#if !UsingWinsock
using SOCKET = int;
#endif
addrinfo* m_ServerInfo{ nullptr };
SOCKET m_Socket{ static_cast<SOCKET>(0xFFFFFFFF) };
sockaddr_storage m_AcceptedSocketStorage{};
socklen_t m_AcceptedSocketSize{ sizeof(m_AcceptedSocketStorage) };
void CreateSocket(string& webAddress, string& port, addrinfo& hints)
{
getaddrinfo(webAddress.c_str(), port.c_str(), &hints, &m_ServerInfo);
m_Socket = socket(m_ServerInfo->ai_family,
m_ServerInfo->ai_socktype,
m_ServerInfo->ai_protocol);
}
Socket(int newSocket, sockaddr_storage&& socketStorage)
: m_Socket{ newSocket }
, m_AcceptedSocketStorage(move(socketStorage))
{
}
public:
Socket(string& port)
{
#ifndef NDEBUG
stringstream portStream{ port };
int portValue{};
portStream >> portValue;
assert(portValue > 1024);
// Ports under 1024 are reserved for certain applications and protocols!
#endif
addrinfo hints{};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
`string address{ "" };`
`CreateSocket(address, port, hints);`
`}`
`Socket(string& webAddress, string& port)`
`{`
`addrinfo hints{};`
`hints.ai_family = AF_UNSPEC;`
`hints.ai_socktype = SOCK_STREAM;`
`CreateSocket(webAddress, port, hints);`
`}`
`Socket(string& webAddress, string& port, addrinfo& hints)`
`{`
`CreateSocket(webAddress, port, hints);`
`}`
`~Socket()`
`{`
`Close();`
`}`
`Socket(const Socket& other) = delete;`
`Socket(Socket&& other)`
`: m_ServerInfo( other.m_ServerInfo )`
`, m_Socket( other.m_Socket )`
`, m_AcceptedSocketStorage( other.m_AcceptedSocketStorage )`
`, m_AcceptedSocketSize( other.m_AcceptedSocketSize )`
`{`
`if (this != &other)`
`{`
`other.m_ServerInfo = nullptr;`
`other.m_Socket = -1;`
`other.m_AcceptedSocketStorage = sockaddr_storage{};`
`other.m_AcceptedSocketSize = sizeof(other.m_AcceptedSocketStorage);`
`}`
`}`
`bool IsValid()`
`{`
`return m_Socket != -1;`
`}`
`int Connect()`
`{`
`int connectionResult{`
`connect(m_Socket,`
`m_ServerInfo->ai_addr,`
`m_ServerInfo->ai_addrlen)`
`};`
`#ifndef NDEBUG`
`if (connectionResult == -1)`
`{`
`cout << "Connection failed!" << endl;`
`}`
`else`
`{`
`cout << "Connection successful!" << endl;`
`}`
`#endif`
`return connectionResult;`
`}`
`int Bind()`
`{`
`int bindResult{ ::bind(m_Socket, m_ServerInfo->ai_addr, m_ServerInfo->ai_addrlen) };`
`#ifndef NDEBUG`
`if (bindResult == -1)`
`{`
`cout << "Bind Failed!" << endl;`
`}`
`else`
`{`
`cout << "Bind Successful" << endl;`
`}`
`#endif`
`return bindResult;`
`}`
`int Listen(int queueSize)`
`{`
`int listenResult{ listen(m_Socket, queueSize) };`
`#ifndef NDEBUG`
`if (listenResult == -1)`
`{`
`cout << "Listen Failed" << endl;`
`}`
`else`
`{`
`cout << "Listen Succeeded" << endl;`
`}`
`#endif`
`return listenResult;`
`}`
`Socket Accept()`
`{`
`SOCKET newSocket{`
`accept(m_Socket,`
`reinterpret_cast<sockaddr*>(&m_AcceptedSocketStorage),`
`&m_AcceptedSocketSize)`
`};`
`#ifndef NDEBUG`
`if (newSocket == -1)`
`{`
`cout << "Accept Failed" << endl;`
`}`
`else`
`{`
`cout << "Accept Succeeded" << endl;`
`}`
`#endif`
`m_AcceptedSocketSize = sizeof(m_AcceptedSocketStorage);`
`return Socket(newSocket, move(m_AcceptedSocketStorage));`
`}`
`void Close()`
`{`
`#ifdef _MSC_VER`
`closesocket(m_Socket);`
`#else`
`close(m_Socket);`
`#endif`
`m_Socket = -1;`
`freeaddrinfo(m_ServerInfo);`
`}`
`ssize_t Send(stringstream data)`
`{`
`string packetData{ data.str() };`
`ssize_t sendResult{ send(m_Socket, packetData.c_str(), packetData.length(), 0) };`
`#ifndef NDEBUG`
`if (sendResult == -1)`
`{`
`cout << "Send Failed" << endl;`
`}`
`else`
`{`
`cout << "Send Succeeded" << endl;`
`}`
`#endif`
`return sendResult;`
`}`
`stringstream Receive()`
`{`
`const int size{ 1024 };`
`char dataReceived[size];`
`ssize_t receiveResult{ recv(m_Socket, dataReceived, size, 0) };`
`#ifndef NDEBUG`
`if (receiveResult == -1)`
`{`
`cout << "Receive Failed" << endl;`
`}`
`else if (receiveResult == 0)`
`{`
`cout << "Receive Detected Closed Connection!" << endl;`
`Close();`
`}`
`else`
`{`
`dataReceived[receiveResult] = '\0';`
`cout << "Receive Succeeded" << endl;`
`}`
`#endif`
`stringstream data{ dataReceived };`
`return move(data);`
`}`
`};`
`namespace`
`{`
`const int NUM_QUESTIONS{ 2 };`
`const array<string, NUM_QUESTIONS> QUESTIONS`
`{`
`"What is the capital of Australia?",`
`"What is the capital of the USA?"`
`};`
`const array<string, NUM_QUESTIONS> ANSWERS{ "Canberra", "Washington DC" };`
`}`
`bool ProtocolThread(reference_wrapper<Socket> connectionSocketRef)`
`{`
`Socket socket{ move(connectionSocketRef.get()) };`
`int currentQuestion{ 0 };`
`string message;`
`while (message != "QUIT")`
`{`
`stringstream sstream{ socket.Receive() };`
`if (sstream.rdbuf()->in_avail() == 0)`
`{`
`break;`
`}`
`sstream >> message;`
`stringstream output;`
`if (message == "QUESTION")`
`{`
`if (currentQuestion >= NUM_QUESTIONS)`
`{`
`output << "FINISHED";`
`socket.Send(move(output));`
`cout << "Quiz Complete!" << endl;`
`break;`
`}`
`output << QUESTIONS[currentQuestion];`
`}`
`else if (message == "ANSWER")`
`{`
`string answer;`
`sstream >> answer;`
`if (answer == ANSWERS[currentQuestion])`
`{`
`output << "You are correct!";`
`}`
`else`
`{`
`output << "Sorry the correct answer is " << ANSWERS[currentQuestion];`
`}`
`++currentQuestion;`
`}`
`socket.Send(move(output));`
`}`
`return true;`
`}`
`int main(int argc, char* argv[])`
`{`
`WinsockWrapper myWinsockWrapper;`
`string port("3000");`
`Socket myListeningSocket(port);`
`int bindResult{ myListeningSocket.Bind() };`
`assert(bindResult != -1);`
`if (bindResult != -1)`
`{`
`int listenResult{ myListeningSocket.Listen(5) };`
`assert(listenResult != -1);`
`if (listenResult != -1)`
`{`
`while (true)`
`{`
`Socket acceptedSocket{ myListeningSocket.Accept() };`
`async(launch::async, ProtocolThread, ref(acceptedSocket));`
`}`
`}`
`}`
`return 0;`
`}`
清单 12-8 中的服务器程序使用的Socket类与配方 12-4 中详细描述的相同。main函数负责同时处理多个客户端。它通过创建一个Socket并将其绑定到端口 3000 来实现这一点。然后,要求绑定的Socket监听传入的连接;它使用长度为 5 的队列来实现。main的最后一部分使用一个while循环来接受任何传入的连接,并将它们交给async函数。async函数创建一个thread来处理从Socket::Accept获取的每个Socket;第一个参数是launch::async`。
ProtocolThread功能响应连接客户端的请求,并支持简单问答网络协议的服务器端。通过将字符串打包到每个数据包中,在客户端和服务器之间传输数据。message变量保存来自stringstream 的单个消息。这个协议可以用一个基本的if...else if模块来处理。当收到QUESTION消息时,服务器将当前问题打包成输出stringstream。如果消息是ANSWER,那么服务器检查用户是否正确,并将适当的响应打包到输出中。使用最初接收数据的同一个Socket将输出stringstream发送到客户端,这表明Socket连接不一定是单向通信通道。如果接收到QUESTION消息,并且已经发送了服务器可用的所有问题,则服务器向客户端发送FINISHED消息,并退出循环;这导致Socket超出范围,进而关闭连接。
所有这些活动都需要连接一个客户机来与服务器程序通信。你可以在清单 12-9 的中看到一个基本的客户端实现。
清单 12-9 。一个简单的测验协议客户端
#include <cassert>
#include <iostream>
#include <type_traits>
#ifndef NDEBUG
#include <sstream>
#endif
using namespace std;
#ifdef _MSC_VER
#pragma comment(lib, "Ws2_32.lib")
#include <WinSock2.h>
#include <WS2tcpip.h>
#define UsingWinsock 1
using ssize_t = SSIZE_T;
#else
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#define UsingWinsock 0
#endif
class WinsockWrapper
{
public:
WinsockWrapper()
{
#if UsingWinsock
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
exit(1);
}
cout << "Winsock started!" << endl;
#endif
}
~WinsockWrapper()
{
#if UsingWinsock
WSACleanup();
cout << "Winsock shut down!" << endl;
#endif
}
};
class Socket
{
private:
#if !UsingWinsock
using SOCKET = int;
#endif
addrinfo* m_ServerInfo{ nullptr };
SOCKET m_Socket{ static_cast<SOCKET>(0xFFFFFFFF) };
sockaddr_storage m_AcceptedSocketStorage{};
socklen_t m_AcceptedSocketSize{ sizeof(m_AcceptedSocketStorage) };
void CreateSocket(string& webAddress, string& port, addrinfo& hints)
{
getaddrinfo(webAddress.c_str(), port.c_str(), &hints, &m_ServerInfo);
m_Socket = socket(
m_ServerInfo->ai_family,
m_ServerInfo->ai_socktype,
m_ServerInfo->ai_protocol);
}
Socket(int newSocket, sockaddr_storage&& socketStorage)
: m_Socket{ newSocket }
, m_AcceptedSocketStorage(move(socketStorage))
{
}
public:
Socket(string& port)
{
#ifndef NDEBUG
stringstream portStream{ port };
int portValue{};
portStream >> portValue;
assert(portValue > 1024);
// Ports under 1024 are reserved for certain applications and protocols!
#endif
addrinfo hints{};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
string address{ "" };
CreateSocket(address, port, hints);
}
Socket(string& webAddress, string& port)
{
addrinfo hints{};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
CreateSocket(webAddress, port, hints);
}
Socket(string& webAddress, string& port, addrinfo& hints)
{
CreateSocket(webAddress, port, hints);
}
~Socket()
{
Close();
}
Socket(const Socket& other) = delete;
Socket(Socket&& other)
: m_ServerInfo(other.m_ServerInfo)
, m_Socket(other.m_Socket)
, m_AcceptedSocketStorage(other.m_AcceptedSocketStorage)
, m_AcceptedSocketSize(other.m_AcceptedSocketSize)
{
if (this != &other)
{
other.m_ServerInfo = nullptr;
other.m_Socket = -1;
other.m_AcceptedSocketStorage = sockaddr_storage{};
other.m_AcceptedSocketSize = sizeof(other.m_AcceptedSocketStorage);
}
}
bool IsValid()
{
return m_Socket != -1;
}
int Connect()
{
int connectionResult{ connect(
m_Socket,
m_ServerInfo->ai_addr,
m_ServerInfo->ai_addrlen)
};
#ifndef NDEBUG
if (connectionResult == -1)
{
cout << "Connection failed!" << endl;
}
else
{
cout << "Connection successful!" << endl;
}
#endif
return connectionResult;
}
int Bind()
{
int bindResult{ ::bind(m_Socket, m_ServerInfo->ai_addr, m_ServerInfo->ai_addrlen) };
#ifndef NDEBUG
if (bindResult == -1)
{
cout << "Bind Failed!" << endl;
}
else
{
cout << "Bind Successful" << endl;
}
#endif
return bindResult;
}
int Listen(int queueSize)
{
int listenResult{ listen(m_Socket, queueSize) };
#ifndef NDEBUG
if (listenResult == -1)
{
cout << "Listen Failed" << endl;
}
else
{
cout << "Listen Succeeded" << endl;
}
#endif
return listenResult;
}
Socket Accept()
{
SOCKET newSocket{ accept(
m_Socket,
reinterpret_cast<sockaddr*>(&m_AcceptedSocketStorage),
&m_AcceptedSocketSize)
};
#ifndef NDEBUG
if (newSocket == -1)
{
cout << "Accept Failed" << endl;
}
else
{
cout << "Accept Succeeded" << endl;
}
#endif
m_AcceptedSocketSize = sizeof(m_AcceptedSocketStorage);
return Socket(newSocket, move(m_AcceptedSocketStorage));
}
void Close()
{
#ifdef _MSC_VER
closesocket(m_Socket);
#else
close(m_Socket);
#endif
m_Socket = -1;
freeaddrinfo(m_ServerInfo);
}
ssize_t Send(stringstream data)
{
string packetData{ data.str() };
ssize_t sendResult{ send(m_Socket, packetData.c_str(), packetData.length(), 0) };
#ifndef NDEBUG
if (sendResult == -1)
{
cout << "Send Failed" << endl;
}
else
{
cout << "Send Succeeded" << endl;
}
#endif
return sendResult;
}
stringstream Receive()
{
const int size{ 1024 };
char dataReceived[size];
ssize_t receiveResult{ recv(m_Socket, dataReceived, size, 0) };
#ifndef NDEBUG
if (receiveResult == -1)
{
cout << "Receive Failed" << endl;
}
else if (receiveResult == 0)
{
cout << "Receive Detected Closed Connection!" << endl;
Close();
}
else
{
dataReceived[receiveResult] = '\0';
cout << "Receive Succeeded" << endl;
}
#endif
stringstream data{ dataReceived };
return move(data);
}
};
int main(int argc, char* argv[])
{
WinsockWrapper myWinsockWrapper;
string address("192.168.178.44");
string port("3000");
Socket mySocket(address, port);
int connectionResult{ mySocket.Connect() };
if (connectionResult != -1)
{
stringstream output{ "QUESTION" };
mySocket.Send(move(output));
stringstream input{ mySocket.Receive() };
if (input.rdbuf()->in_avail() > 0)
{
string question;
getline(input, question, '\0');
input.clear();
while (question != "FINISHED")
{
cout << question << endl;
string answer;
cin >> answer;
output << "ANSWER ";
output << answer;
mySocket.Send(move(output));
input = mySocket.Receive();
if (input.rdbuf()->in_avail() == 0)
{
break;
}
string result;
getline(input, result, '\0');
cout << result << endl;
output << "QUESTION";
mySocket.Send(move(output));
input = mySocket.Receive();
getline(input, question, '\0');
input.clear();
}
}
}
return 0;
}
清单 12-9 中的客户端程序可以连接到清单 12-8 中的服务器,并向玩家展示服务器测验。客户端代码比服务器简单,因为它只需要考虑一个连接,因此不需要线程或处理多个套接字。客户端不需要知道要连接的服务器的地址;IP 地址是我在家庭网络上运行服务器的 MacBook Pro 的 IP 地址。客户端将QUESTION发送给服务器,然后在Receive调用中等待响应。Receive是阻塞呼叫;因此,客户端会等待,直到数据可用。然后,它从玩家那里获得输入并发送回服务器,并等待关于用户是否正确的响应。这个过程循环重复,直到服务器通知客户端测验已经结束。
以这种方式实现的网络协议的美妙之处在于它们可以在不同的程序中重用。如果您想扩展这个例子,您可以使用 Qt 之类的框架轻松地创建一个 GUI 版本,让所有对Receive的调用都发生在一个线程中,并让 UI 动画化一个旋转的徽标,以向用户表明程序正在等待数据通过远程连接。您还可以扩展服务器应用程序来存储结果,并添加到协议中,让用户重新开始正在进行的测验。最后,该协议简单地规定了两个程序应该如何相互通信,以便于从一台计算机向另一台计算机提供服务。`
十三、脚本
C++ 是一种功能强大的编程语言,可以以多种方式使用,并支持几种不同的编程范例。它允许高级面向对象的抽象和通用编程,但它也允许您在考虑 CPU 特性(如缓存行的长度)的非常低的级别进行编码。这种能力是以需要将语言编译成机器代码为代价的。编译、构建和链接 C++ 是程序员需要承担的一项任务,对于非程序员来说并不容易理解。
脚本语言有助于降低对程序进行与代码相关的更改的障碍,并使艺术和设计团队能够控制高级任务。像屏幕布局和 UI 流这样的东西用脚本语言编写并不少见,这样团队中的非编程成员就可以很容易地修改它们。有几种流行的脚本语言可用,其中之一是 Lua 。本章着眼于 Lua 编程语言与 C++ 的不同之处,以及如何将 Lua 解释器和引擎整合到 C++ 程序中。
13-1.在 Visual Studio 中创建 Lua 库项目
问题
您希望使用 Visual Studio 编写一个包含 Lua 脚本语言的程序。
解决办法
Lua 编程语言提供了制作一个有效的 Lua 程序所需的所有源文件。您可以将这些文件包含到一个单独的 Visual Studio 项目中,该项目可用于生成静态库。
它是如何工作的
Visual Studio 程序可以由几个组成部分组成。Visual Studio 通过为包含多个项目的应用程序创建解决方案文件来支持这一点。Visual Studio 中的项目可以配置为创建 EXE、静态库或动态库。对于这个菜谱,您将创建一个包含两个项目的解决方案:一个构建包含 Lua 库的静态库,另一个创建静态链接到 Lua 项目并在其代码中使用 Lua 的应用程序。按照以下步骤创建一个项目,该项目构建一个链接到 Lua C 库的应用程序:
-
打开 Visual Studio,从“开始”屏幕或“文件”菜单中选择创建新项目的选项。
-
单击“已安装的模板”下的“Visual C++”类别,并为新应用程序选择 Win32 项目模板。
-
Give your project a name, choose a location to store its files, and click OK to proceed. Figure 13-1 shows the New Project Wizard.
图 13-1 。步骤 3 中的 Visual Studio 新建项目向导
-
在应用程序向导中,选择控制台应用程序,并取消选中预编译头和安全开发生命周期(SDL)选项。
-
单击完成。
-
在 Solution Explorer 窗口中右键单击新创建的解决方案,并选择 Add
New Project。
-
再次选择 Win32 项目,将项目命名为 Lua,然后单击“确定”。
-
在应用程序向导中单击下一步,然后选择静态库选项。
-
取消选中预编译头选项和 SDL 选项,然后单击完成。
-
从
www.lua.org下载 Lua 源代码。 -
使用 7-Zip 之类的应用程序解压下载的
tar.gz文件,并将src文件夹复制到您用来创建 Lua 项目的文件夹中。 -
在 Visual Studio 解决方案资源管理器窗口中,右键单击 Lua 项目中的源文件文件夹,并选择 Add
Existing Item。
-
从您复制到项目目录的
srcLua 文件夹中添加以下文件: *lapi.c*lauxlib.c*lbaselib.c*lbitlib.c*lcode.c*lcorolib.c*lctype.c*ldblib.c*ldebug.c*ldo.c*ldump.c*lfunc.c*lgc.c*linit.c*liolib.c*llex.c*lmathlib.c*lmem.c*loadlib.c*lobject.c*lopcodes.c*loslib.c *lparser.c*lstate.c*lstring.c*lstrlib.c *ltable.c*ltablib.c*ltm.c*lundump.c*lutf8lib.c*lvm.c*lzio.c -
右键单击您的 Lua 项目,然后单击 Build,可以看到生成的
Lua.lib文件没有错误。 -
右键单击第十三章项目,并选择 Properties。
-
展开“通用属性”部分,然后单击“引用”。
-
单击添加新引用。
-
检查 Lua 项目,并选择 OK。
-
展开配置属性下的 C/C++ 部分,然后单击常规。
-
确保配置选项设置为所有配置。
-
编辑附加的包含目录选项,这样它就有了复制到 Lua 项目文件夹中的 Lua 源文件夹的路径。
-
Replace your
mainfunction source in the project CPP file with the code from Listing 13-1.
***清单 13-1*** 。一个简单的 Lua 程序
```cpp
#include "lua.hpp"
int main(int argc, char* argv[])
{
lua_State* pLuaState{ luaL_newstate() };
if (pLuaState)
{
luaL_openlibs(pLuaState);
lua_close(pLuaState);
}
return 0;
}
```
23. 构建并执行您的程序,查看 Lua 静态库是否成功包含在您的项目中。
按照这些步骤,您可以创建一个 Lua 静态库项目,您可以在本章余下的食谱中使用它。
13-2.在 Eclipse 中创建 Lua 库项目
问题
您希望使用 Lua 作为脚本语言创建一个 C++ 程序,并且您正在安装了 Eclipse 的 Linux 计算机上进行开发。
解决办法
Lua 作为源代码提供,您可以创建一个 Eclipse 项目,该项目可以构建到一个静态库中,以包含在其他程序中。
它是如何工作的
Eclipse IDE 允许您创建可以链接到应用程序项目的新静态库项目。按照以下步骤创建一个 Eclipse 项目,为您的 Lua 项目构建一个 Linux 兼容的静态库:
- 打开您的 Eclipse IDE,并导航到 C/C++ 透视图。
- 在项目浏览器窗口中右键单击,并选择 New
C++ Project。
- 展开“静态库”类别,并选择“空项目”。
- 为项目命名,并选取一个文件夹来存储项目。
- 单击完成。
- 在 Project Explorer 窗口中右键单击您的新项目,并选择 New
Source Folder。给它起个名字。
- 从
www.lua.org下载 Lua 源代码。 - 解压您获得的
tar.gz文件,并将.c和.h文件从src文件夹复制到您新创建的项目源文件夹中。 - 在“项目资源管理器”窗口中右键单击您的项目,然后选择“刷新”。
- 注意到 Lua 源文件和头文件出现在 Project Explorer 窗口中。
- 右键单击项目,并选择 Build 以确保源代码正确编译。
- 右键单击项目浏览器窗口中的空白区域,并选择 New
C++ Project。
- 选择可执行的
Hello World C++ 项目。
- 设置项目名称字段。
- 选择一个位置。
- 单击完成。
- 在“项目资源管理器”窗口中右键单击新的可执行项目,并选择“属性”。
- 单击 C/C++ Build 类别,并确保将 Configuration 设置为 Debug。
- 展开 C/C++ Build 类别,然后单击 Settings。
- 选择 GCC C++ 链接器类别下的库选项。
- 在 Libraries 部分点击 Add 选项,输入 Lua (不需要输入liblua . a——lib 和。零件是自动添加的)。
- 单击库搜索路径选项上的添加选项。
- 单击工作区。
- 选择 Lua 项目中的 Debug 文件夹。
- 对发布配置重复步骤 18–24(在生成发布文件夹和库之前,您需要在发布配置中构建 Lua 项目)。
- 在 C/C++ Build
设置对话框中选择 GCC C++ 编译器
Includes 部分。
- 将配置设置为所有配置。
- 单击“包含路径”部分中的“添加”选项。
- 单击工作区按钮。
- 选择您在步骤 6 中添加到 Lua 项目中的源文件夹。
- 在 C/C++ Build
Settings 部分的 GCC C++ 编译器设置下选择 Miscellaneous 部分。
- 将
–std=c++11添加到其他标志字段。 - Replace your
mainfunction with the source code in Listing 13-2.
***清单 13-2*** 。一个简单的 Lua 程序
```cpp
#include "lua.hpp"
int main()
{
luaState* pLuaState{ luaL_newstate() };
if (pLuaState)
{
luaL_openlibs(pLuaState);
lua_close(pLuaState);
}
return 0;
}
```
34. 调试你的应用程序,并逐步确保清单 13-2 中的变量是有效的,并且一切按预期完成。
这个方法中的步骤允许您在 Eclipse 中创建一个 Lua 静态库项目,您现在可以在本章的剩余部分中使用它。
13-3.在 Xcode 中创建 Lua 项目
问题
您想在 Xcode 中创建一个使用 Lua 编程语言编写脚本的 C++ 程序。
解决办法
您可以在 Xcode 中创建项目,这些项目允许您生成要链接到 C++ 应用程序中的静态库。
它是如何工作的
Xcode IDE 允许您创建可以构建可执行文件或库的项目。这个菜谱向您展示了如何配置一个项目,将 Lua 源代码构建为一个静态库,并将它链接到另一个生成可执行文件的项目中。按照以下步骤设置您的项目:
- 打开 Xcode。
- 选择“创建新的 Xcode 项目”。
- 在 OS X 框架和库部分选择库选项。
- 单击下一步。
- 将产品名称设置为 Lua。
- 将框架更改为无。
- 将类型更改为静态。
- 选择一个文件夹来储存 Xcode 资源库项目。
- 从
www.lua.org下载 Lua 源代码。 - 解压从网页上获取的
tar,gz文件。 - 将源文件从
src文件夹复制到步骤 8 中创建的 Lua 项目文件夹。 - 在 Xcode 中右键单击项目,并选择 Add Files to Lua。
- 关闭 Xcode。
- 打开 Xcode。
- 选择“创建新的 Xcode 项目”。
- 从 OS X 应用程序部分选择命令行工具选项。
- 设置产品名称字段。
- 取消选中使用故事板选项。
- 单击下一步。
- 选择一个文件夹来存储项目。
- 打开 Finder,浏览到包含 Lua 项目的文件夹。
- 将
xcodeproj文件拖到 Xcode 窗口的app项目中。现在你应该在app项目下有了 Lua 项目。 - 点击
app项目,然后点击 Build Phases 选项。 - 展开将二进制文件与库链接选项。
- 单击加号。
- 从工作区部分选择
libLua.a。 - 单击构建设置。
- 双击标题搜索路径选项。
- 单击加号,并在您的 Lua 项目中输入 Lua 源代码的路径。
- Replace the code in
AppDelegate.mwith the code in Listing 13-3.
***清单 13-3*** 。一个简单的 Lua 程序
```cpp
#import "AppDelegate.h"
#include "lua.hpp"
@property (weak) IBOutlet NSWindow *window;
@end
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotofication *)aNotification {
lua_State* pLuaState{ luaL_newstate() };
if (pLuaState)
{
luaL_openlibs(pLuaState);
lua_close(pLuaState);
}
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {
}
@end
```
31. 构建并调试您的程序,使用断点来确保 Lua 状态被正确初始化。
本章提供的步骤和代码是使用 Xcode 6.1.1 生成的。您可能需要修改本章中剩余的例子,用applicationDidFinishLaunching Objective-C 方法替换main函数。如果您的程序无法编译,请尝试在标识和类型设置中将源文件的类型从 Objective-C 更改为 Objective-C++。
13-4.使用 Lua 编程语言
问题
您是一名 C++ 程序员,希望在将 Lua 编程语言添加到您自己的应用程序之前先学习一下。
解决办法
Lua 编程语言文档可在/www.lua.org/manual/5.3获得,测试代码的现场演示可在www.lua.org/demo.html获得。
它是如何工作的
Lua 编程语言与 C++ 几乎完全不同。C++ 是一种直接在 CPU 上执行的编译语言。另一方面,Lua 是一种解释型语言,由运行在 CPU 上的虚拟机执行。Lua 语言附带了一个用 C 编写的虚拟机,并提供了源代码。这意味着您可以将虚拟机嵌入到您编写的任何 C 或 C++ 程序中,并使用脚本语言来编写和控制应用程序的高级功能。
在承担这样的任务之前,学习 Lua 编程语言的一些特性是一个好主意。
使用变量
C++ 变量是静态类型的。这意味着它们的类型是在声明变量时指定的,并且在将来的任何时候都不能更改。一个int在其整个生命周期内保持为一个int。这有助于使 C++ 程序在 CPU 上运行时具有可预测性和高性能,因为正确的指令可以用于正确类型的变量。另一方面,Lua 代码运行在虚拟机中;因此,对变量可以表示的类型限制较少。这导致 Lua 被称为动态类型语言。清单 13-4 显示了动态类型对正在执行的程序的影响。
清单 13-4 。使用 Lua 变量
variable = 1
print(variable)
print(type(variable))
variable = "1"
print(variable)
print(type(variable))
你可以复制清单 13-4 中的代码,直接粘贴到www.lua.org/demo.html的 Lua Live 演示中。这个演示有运行、清除、恢复和重启 Lua 虚拟机的控件。粘贴或键入清单 13-4 中的代码后,点击运行,网页会生成以下输出:
1
number
1
string
这个输出让您看到动态类型的作用。清单 13-4 最初给variable分配一个整数值。这由print函数输出,作为日志中的数字 1。type函数在被调用时返回一个代表变量类型的字符串。对type的第一次调用返回number作为变量的类型。然后将值“1”的字符串表示分配给variable。print函数以与整数值相同的方式表示字符串值 1。没有办法判断变量中当前存储的是哪种类型。对type函数的第二次调用表明该值实际上是一个字符串,而不再是一个数字。
如果你不小心的话,动态类型语言会让你的程序发生有趣的事情。在 C++ 中,除非重载赋值操作符来处理这种特殊情况,否则无法给字符串添加数字。Lua 可以轻松处理这样的操作。清单 13-5 展示了这一点。
清单 13-5 。向字符串中添加数字
variable = 1
print(variable)
print(type(variable))
variable = "1"
print(variable)
print(type(variable))
variable = variable + 1
print(variable)
print(type(variable))
清单 13-5 给原本显示在清单 13-4 中的代码增加了一个额外的操作。该操作将值 1 加到variable上。回想一下前面的输出,在variable中的值最后是由一个字符串表示的。以下输出显示了执行清单 13-5 后发生的情况:
1
number
1
string
2.0
number
该变量现在保存一个由浮点值 2.0 表示的数字。然而,并不是所有的字符串都是平等的。如果您试图将一个数字添加到一个不能转换为数字的字符串中,那么您将会收到一个错误。清单 13-6 显示了尝试这样做的代码。
清单 13-6 。向非数字字符串添加数字
variable = "name"
variable = variable + 1
该代码导致 Lua 虚拟机产生以下错误:
input:2: attempt to perform arithmetic on a string value (global 'variable')
所有的 Lua 算术运算符都可以转换类型。如果两个变量都是整数,那么结果值也是整数。如果一个或两个值都是浮点数,则结果是浮点数。最后,如果一个或两个值都是可以转换成数字的字符串,那么结果值就是浮点数。你可以在清单 13-4 的输出中看到这一点,其中print显示的值是 2.0,0 代表一个浮点数。有些运算符,如除法运算符和指数运算符,总是返回用浮点数 表示的值。
这些例子展示了 Lua 编程语言的一个特性,它使非程序员更容易使用。你不需要像使用 C++ 时那样牢牢掌握变量的底层类型。不需要考虑是否有足够数量的字节来表示值 512,也不需要在char、short或int之间进行选择。你也不需要关心如何处理 C 风格的字符串或者 C++ STL 字符串。只要在代码中随时给变量赋值,任何变量都可以存储 Lua 支持的任何类型。
使用函数
上一节展示了 Lua 有一些您可以调用的内置函数。也可以使用function关键字创建自己的函数。清单 13-7 创建一个 Lua 函数。
清单 13-7 。创建和调用函数
variable = "name"
function ChangeName()
variable = "age"
end
print(variable)
ChangeName()
print(variable)
清单 13-7 从定义一个存储值“name”的变量开始。接下来是一个函数定义,将variable的值改为“age”。函数中的代码在函数定义时不会被调用。这可以在打印调用生成的输出中看到。对print的第一次调用产生输出name,第二次调用产生输出age。
这是一个有用的例子,因为它显示了默认情况下,Lua 变量本质上是全局的。由variable存储的值被打印两次:一次在调用ChangeName之前,一次在调用之后。如果variable不是全球化的,你会期望两次的值是相同的。Lua 确实支持创建局部变量,但是您必须小心使用它们。清单 13-8 显示了当你使variable局部化时会发生什么。
清单 13-8 。使variable本地化
local variable = "name"
function ChangeName()
variable = "age"
end
print(variable)
ChangeName()
print(variable)
在清单 13-8 的中给variable添加local说明符对所示代码没有任何作用。将它设为局部变量实际上是告诉 Lua 虚拟机,可以在当前作用域的任何地方访问该变量——这意味着可以在当前文件的任何地方访问。如果您正在使用 Lua 演示,您可以想象用于输入代码的文本框是一个单独的 Lua 文件。为了防止ChangeName函数访问variable的同一个实例,你也必须在这个变量上使用local关键字,如清单 13-9 所示。
清单 13-9 。制作ChangeName variable L ocal
local variable = "name"
function ChangeName()
local variable = "age"
end
print(variable)
ChangeName()
print(variable)
对清单 13-9 中的中的print的两次调用都导致值“name”被打印到输出窗口。我建议将所有变量都设为局部变量,以确保代码不太可能引入难以追踪的错误,这些错误是由于一次在多个地方无意中使用了相同的变量名而导致的。
Lua 中的函数总是返回值。清单 13-9 中的ChangeName函数没有指定返回值,所以它隐式返回nil。这可以在清单 13-10 中看到。
清单 13-10 。功能返回nil
function GetValue()
local variable = "age"
end
local value = GetValue()
print(value)
该代码将nil返回给variable value,,并由print函数打印出来。nil值是 C++ 中nullptr的 Lua 等价物。它意味着没有值,而不是表示 0。试图操纵nil值会导致如下所示的 Lua 错误:
input:8: attempt to perform arithmetic on a nil value (local 'value')
当value存储nil时,试图将 1 加到value会产生此错误。您可以通过从GetValue函数中正确返回值来避免错误,如清单 13-11 所示。
清单 13-11 。从函数中正确返回
function GetValue()
return "age"
end
local value = GetValue()
print(value)
这个清单显示了在 C++ 中可以像使用函数一样使用函数return。不过,Lua 的return语句与 C++ 中的return不同。您可以使用逗号操作符(,)从函数中返回多个值。清单 13-12 展示了这一点。
清单 13-12 。多个返回值
function GetValues()
return "name", "age"
end
local name, age = GetValues()
print(name)
print(age)
清单 13-12 显示,要从一个函数中返回和存储多个值,在定义函数和调用函数时,必须在return语句和赋值语句中使用逗号运算符。
使用表格
Lua 提供了表作为存储信息集合的手段。一个表既可以用作基于整数的索引的标准数组,也可以用作键值对的关联数组。你用花括号创建一个表格,如清单 13-13 所示。
清单 13-13 。创建表格
newTable = {}
这段代码只是创建了一个现在可以用来存储值的表。关联表可以使用任何类型的变量作为关键字。对于字符串、浮点、整数,甚至其他表都是如此。清单 13-14 显示了如何使用 Lua 表作为关联数组。
清单 13-14 。向关联数组中添加值
newTable = {}
newTable["value"] = 3.14
newTable[3.14] = "value"
keyTable = {}
newTable[keyTable] = "VALID"
print(newTable["value"])
print(newTable[3.14])
print(newTable[keyTable])
清单 13-14 使用键向 Lua 表添加值。在这个清单中有使用字符串、浮点数和其他表作为键的例子,您可以看到如何使用数组操作符将值赋给表中的键以及从表中读取值。试图读取newTable[3.14]处的值将导致在将任何值分配给该键之前返回nil。这也是你从表中删除值的方法:将nil分配给你想要删除的键。清单 13-15 显示了从表格中移除对象。
清单 13-15 。从表格中移除对象
newTable = {}
newTable["nilValue1"] = 1
newTable["nilValue2"] = 2
print(newTable["nilValue1"])
print(newTable["nilValue2"])
newTable["nilValue1"] = nil
print(newTable["nilValue1"])
print(newTable["nilValue2"])
Lua 表也可以用作 C 风格的数组,Lua 语言提供了帮助函数来帮助管理这些类型的数组。清单 13-16 显示了一个数组表的创建及其元素的修改。
清单 13-16 。创建 Lua 数组
newTable = {}
table.insert(newTable, "first")
table.insert(newTable, "second")
table.insert(newTable, "third")
print(newTable[2])
print(newTable[2])
table.insert(newTable, 2, "fourth")
print(newTable[2])
table.remove(newTable, 1)
print(newTable[1])
print(newTable[2])
print(newTable[3])
print(newTable[4])
清单 13-16 使用了table.insert和table.remove Lua 函数。您可以通过两种方式使用insert函数:不使用索引,将元素添加到数组末尾;或者用一个索引作为第二个参数,将元素插入到数组中,并从该点开始向上移动一个位置。这向您展示了 Lua 数组的行为更像 C++ vector。remove函数获取您希望从数组中移除的索引。
Lua 还提供了一个#操作符,可以用于数组样式的表。清单 13-17 展示了它的实际应用。
清单 13-17 。使用#操作符
newTable = {}
table.insert(newTable, "first")
table.insert(newTable, "second")
table.insert(newTable, "third")
print(#newTable)
newTable[9] = "fourth"
print(newTable[9])
print(#newTable)
清单 13-17 中的操作符返回它能找到的最后一个连续索引。使用insert方法,添加前三个元素没有问题;因此它们有连续的索引。然而,在 9 中手动添加的元素却没有。这使得您无法使用#操作符来计算数组中元素的数量,除非您可以确定数组中的所有索引都是连续的。
使用流量控制
Lua 提供了一个if语句、一个for循环和一个while循环来帮助你构建你的程序。这些可以用来做决策和循环表中的所有元素。清单 13-18 显示了 Lua if语句。
清单 13-18 。使用 Lua if语句
value1 = 1
value2 = 2
if value1 == value2 then
print("Are equal")
elseif value1 ~= value2 then
print("Not equal")
else
print("Shouldn't be here!")
end
Lua 的if语句是通过在if…then语句中创建一个计算结果为 not nil和 not false的表达式而形成的。if块中的代码创建了自己的作用域,可以由自己的local变量组成。提供elseif语句是为了允许按顺序计算多个表达式,而else语句可以提供一个默认的 required 行为。elseif和else语句都是可选的,不是必需的。使用关键字end终止整个if语句块。
当从 C++ 迁移到 Lua 并使用像if这样的流控制语句时,有一些事情需要考虑。当使用if语句时,将 0 值赋给变量会导致阳性测试。if语句对not nil和not false求值,因此值为 0 表示真。清单 13-18 还显示了不等运算符,它在 Lua 中使用~字符代替 C++ 语言中使用的!。
这些情况也适用于while语句,如清单 13-19 所示。
清单 13-19 。使用 Lua while循环
value1 = 2
while value1 do
print("We got here! " .. value1)
value1 = value1 - 1
if value1 == -1 then
value1 = nil
end
end
这段代码使用了一个while循环来显示值 0 在 Lua 控制语句中评估为真。输出如下所示:
We got here! 2
We got here! 1
We got here! 0
在if语句被触发并将value1的值设置为nil后,循环最终终止。在清单 13-20 中显示了控制while循环终止的更好方法。
清单 13-20 。更好的终止
value1 = 2
while value1 do
print("We got here! " .. value1)
value1 = value1 - 1
if value1 == -1 then
break
end
end
清单 13-20 使用一个break语句来退出while循环的执行。当来自 C++ 时,break语句的工作方式与你预期的完全一样。清单 13-21 中显示了另一个退出循环的选项。
清单 13-21 。使用比较运算符离开循环
value1 = 2
while value1 >= 0 do
print("We got here! " .. value1)
value1 = value1 - 1
end
尽管值 0 在while循环测试中导致真结果,但在正常操作环境下,0 的比较或任何其他有效比较最终返回假。这里将value1的值与 0 进行比较,一旦值低于 0,循环就停止执行。
你可以使用 Lua for循环来迭代算法。清单 13-22 显示了一个简单的for循环。
清单 13-22 。Lua for循环
for i=0, 10, 2 do
print(i)
end
这个for循环打印数字 0、2、4、6、8 和 10。生成一个for循环的语句需要一个起始位置(在本例中是一个变量及其值)、一个限制,最后是一个步骤。此示例创建一个变量并将其赋值为 0,循环直到变量大于限制,并在每次迭代时将步长添加到变量中。循环从 0 开始,每次迭代增加 2,当变量的值大于 10 时结束。如果步长为负,它将循环,直到变量的值小于极限值。
您还可以使用一个for循环来迭代使用pairs或ipairs函数的表。清单 13-23 展示了这些在实际中的应用。
清单 13-23 。使用pairs和ipairs
newTable = {}
newTable["first"] = 1
newTable["second"] = 2
newTable["third"] = 3
for key, value in pairs(newTable) do
print(key .. ": " .. value)
end
newTable = {}
table.insert(newTable, "first")
table.insert(newTable, "second")
table.insert(newTable, "third")
for index, value in ipairs(newTable) do
print(index .. ": " .. value)
end
pairs函数返回关联数组表中每个元素的键和值,ipairs函数返回数组样式表的数字索引。这段代码展示了 Lua 从一个函数返回多个值的能力的好处。
13-5.从 C++ 调用 Lua 函数
问题
您的程序中有一个任务将受益于 Lua 脚本提供的快速迭代能力。
解决办法
Lua 编程语言带有源代码,允许您在程序运行时编译和执行脚本。
它是如何工作的
Lua C++ API 为 Lua 状态的堆栈提供了一个编程接口。C++ API 可以操纵这个堆栈将参数传递给 Lua 代码,并从 Lua 接收值作为回报。这个功能允许您创建 Lua 源文件,然后这些文件可以充当 Lua 函数。这些 Lua 函数可以在你的程序运行时更新,允许你比单独使用 C++ 更快地迭代你的程序逻辑。
Lua APIs 是使用 C 编程语言提供的。这意味着,如果您希望采用更 C++ 风格的方法来使用 Lua,您必须创建代理对象。清单 13-24 展示了如何创建一个程序,从 C++ 中加载并执行一个 Lua 脚本作为函数。
清单 13-24 。调用一个简单的 Lua 脚本作为函数
#include <iostream>
#include "lua.hpp"
using namespace std;
class Lua
{
private:
lua_State* m_pLuaState{ nullptr };
public:
Lua()
: m_pLuaState{ luaL_newstate() }
{
if (m_pLuaState)
{
luaL_openlibs(m_pLuaState);
}
}
~Lua()
{
lua_close(m_pLuaState);
}
Lua(const Lua& other) = delete;
Lua& operator=(const Lua& other) = delete;
Lua(Lua&& rvalue) = delete;
Lua& operator=(Lua&& rvalue) = delete;
bool IsValid() const
{
return m_pLuaState != nullptr;
}
int LoadFile(const string& filename)
{
int status{ luaL_loadfile(m_pLuaState, filename.c_str()) };
if (status == 0)
{
lua_setglobal(m_pLuaState, filename.c_str());
}
return status;
}
int PCall()
{
return lua_pcall(m_pLuaState, 0, LUA_MULTRET, 0);
}
};
class LuaFunction
{
private:
Lua& m_Lua;
string m_Filename;
int PCall()
{
return m_Lua.PCall();
}
public:
LuaFunction(Lua& lua, const string& filename)
: m_Lua{ lua }
, m_Filename(filename)
{
m_Lua.LoadFile(m_Filename);
}
~LuaFunction() = default;
LuaFunction(const LuaFunction& other) = delete;
LuaFunction& operator=(const LuaFunction& other) = delete;
LuaFunction(LuaFunction&& rvalue) = delete;
LuaFunction& operator=(LuaFunction&& rvalue) = delete;
int Call()
{
m_Lua.GetGlobal(m_Filename);
return m_Lua.PCall();
}
};
int main(int argc, char* argv[])
{
Lua lua;
if (lua.IsValid())
{
const string filename{ "LuaCode1.lua" };
LuaFunction function(lua, filename);
function.Call();
}
return 0;
}
清单 13-24 展示了一种在单个类实现中包含所有 Lua C 函数的方法。这让您可以将所有这些方法的定义放在一个 C++ 文件中,并在整个程序中限制对 Lua 的依赖。因此,Lua类负责维护管理程序 Lua 上下文的lua_State指针。本示例创建一个限制复制或移动Lua对象的能力的类;您可能需要做到这一点,但对于这些示例来说,这不是必需的。
Lua类的构造函数调用luaL_newstate函数。这个函数调用lua_newstate函数并传递默认参数。如果您想为 Lua 状态机提供自己的内存分配器,可以直接调用lua_newstate。对luaL_newstate的成功调用导致m_pLuaState字段存储该州的有效地址。如果这是真的,那么调用luaL_openlibs函数。这个函数自动将 Lua 提供的库加载到您创建的状态中。如果不需要 Lua 内置的库功能,可以避免调用这个函数。
Lua类析构函数负责调用lua_close销毁luaL_newstate在构造函数中创建的 Lua 上下文。IsValid函数为您的调用代码提供了一个简单的方法来确定 Lua 上下文是否在构造函数中正确初始化。
LuaFunction类存储了它用于上下文的Lua类的引用。这个类再次阻止了复制和移动。构造函数引用了为其提供功能的Lua对象和一个包含要加载的包含 Lua 源代码的文件名的字符串。构造函数使用m_Lua对象调用LoadFile方法并传递m_Filename字段。LoadFile方法调用luaL_loadfile,后者读取文件,编译 Lua 源代码,并使用编译后的代码将一个 Lua 函数对象推到 Lua 堆栈的顶部。如果luaL_loadfile调用成功,则调用lua_setglobal函数。该函数从堆栈中获取顶层对象,并将其分配给一个具有指定名称的全局对象。在这种情况下,由luaL_loadfile创建的函数对象被分配给一个以源文件名命名的全局变量。
main函数用一个名为LuaCode1.lua 的文件创建一个LuaFunction对象。该文件的来源如清单 13-25 所示。
清单 13-25 。来自LuaCode1.lua的代码
print("Printing From Lua!")
这个 Lua 代码导致一个简单的消息被打印到控制台。这发生在main函数调用LuaFunction::Call方法 时。该方法使用Lua::GetGlobal函数 将具有给定名称的全局对象移动到堆栈顶部。在这种情况下,m_Filename变量将在LoadFile方法中创建的函数对象移动到堆栈上。Lua::PCall方法 然后调用离栈顶最近的函数。该程序生成的输出如图图 13-2 所示。
图 13-2 。运行清单 13-24 和清单 13-25 中的代码生成的输出
清单 13-24 没有初始化任何被 Lua 脚本消费的数据。您可以通过创建表示 Lua 类型的类来处理这个问题。清单 13-26 创建一个LuaTable类来用 C++ 创建 Lua 表,然后 Lua 可以访问这些表。
清单 13-26 。在 C++ 中创建 Lua 表
#include <iostream>
#include "lua.hpp"
#include <vector>
using namespace std;
class Lua
{
private:
lua_State* m_pLuaState{ nullptr };
public:
Lua()
: m_pLuaState{ luaL_newstate() }
{
if (m_pLuaState)
{
luaL_openlibs(m_pLuaState);
}
}
~Lua()
{
lua_close(m_pLuaState);
}
Lua(const Lua& other) = delete;
Lua& operator=(const Lua& other) = delete;
Lua(Lua&& rvalue) = delete;
Lua& operator=(Lua&& rvalue) = delete;
bool IsValid() const
{
return m_pLuaState != nullptr;
}
int LoadFile(const string& filename)
{
int status{ luaL_loadfile(m_pLuaState, filename.c_str()) };
if (status == 0)
{
lua_setglobal(m_pLuaState, filename.c_str());
Pop(1);
}
return status;
}
int PCall()
{
return lua_pcall(m_pLuaState, 0, LUA_MULTRET, 0);
}
void NewTable(const string& name)
{
lua_newtable(m_pLuaState);
lua_setglobal(m_pLuaState, name.c_str());
}
void GetGlobal(const string& name)
{
lua_getglobal(m_pLuaState, name.c_str());
}
void PushNumber(double number)
{
lua_pushnumber(m_pLuaState, number);
}
void SetTableValue(double index, double value)
{
PushNumber(index);
PushNumber(value);
lua_rawset(m_pLuaState, -3);
}
double GetNumber()
{
return lua_tonumber(m_pLuaState, -1);
}
void Pop(int number)
{
lua_pop(m_pLuaState, number);
}
};
class LuaTable
{
private:
Lua& m_Lua;
string m_Name;
public:
LuaTable(Lua& lua, const string& name)
: m_Lua{ lua }
, m_Name(name)
{
m_Lua.NewTable(m_Name);
}
void Set(const vector<int>& values)
{
Push();
for (unsigned int i = 0; i < values.size(); ++i)
{
m_Lua.SetTableValue(i + 1, values[i]);
}
m_Lua.Pop(1);
}
void Push()
{
m_Lua.GetGlobal(m_Name);
}
};
class LuaFunction
{
private:
Lua& m_Lua;
string m_Filename;
int PCall()
{
return m_Lua.PCall();
}
protected:
int Call()
{
m_Lua.GetGlobal(m_Filename);
return m_Lua.PCall();
}
double GetReturnValue()
{
double result{ m_Lua.GetNumber() };
m_Lua.Pop(1);
return result;
}
public:
LuaFunction(Lua& lua, const string& filename)
: m_Lua{ lua }
, m_Filename( filename )
{
int status{ m_Lua.LoadFile(m_Filename) };
}
};
class PrintTable
: public LuaFunction
{
public:
PrintTable(Lua& lua, const string& filename)
: LuaFunction(lua, filename)
{
}
double Call(LuaTable& table)
{
double sum{};
int status{ LuaFunction::Call() };
if (status)
{
throw(status);
}
else
{
sum = LuaFunction::GetReturnValue();
}
return sum;
}
};
int main(int argc, char* argv[])
{
Lua lua;
if (lua.IsValid())
{
int loop = 2;
while (loop > 0)
{
const string tableName("cTable");
LuaTable table(lua, tableName);
vector<int> values{ 1, 2, 3, 4, 5 };
table.Set(values);
const string filename{ "LuaCode.lua" };
PrintTable printTableFunction(lua, filename);
try
{
double result{ printTableFunction.Call(table) };
cout << "Result: " << result << endl;
}
catch (int error)
{
cout << "Call error: " << error << endl;
}
cout << "Waiting" << endl;
int input;
cin >> input;
--loop;
}
}
return 0;
}
清单 13-26 向Lua类添加了一个LuaTable类以及相关的方法来管理表格。lua_newtable函数创建一个新表,并将其推送到堆栈中。然后在LuaTable构造函数中用提供的名字将element赋给一个全局变量。使用Lua::SetTableValue方法 将值添加到表格中。这个方法只支持表的数字索引,并通过将两个数字压入堆栈来工作:表中要分配的索引和分配给该索引的值。lua_rawset函数将一个值赋给表上的一个索引,所讨论的表存在于所提供的索引处。堆栈上的第一个元素被-1 引用,这将是值;此时堆栈上的第二个元素是索引;第三个元素是表,所以值-3 被传递给lua_rawset函数。该调用从堆栈中弹出索引和值,因此再次在位置-1 找到该表。
LuaFunction类被继承到一个名为PrintTable的新类中。这个类提供了一个新的call方法,该方法知道如何从提供的 Lua 脚本中检索返回值。清单 13-27 中的 Lua 代码展示了为什么这是必要的。
清单 13-27 。LuaCode2.lua来源
local x = 0
for i = 1, #cTable do
print(i, cTable[i])
x = x + cTable[i]
end
return x
这段代码遍历用 C++ 建立的cTable表并打印出值。它还计算表中所有值的总和,并使用堆栈将它们返回给调用代码。
C++ main函数创建一个表,并使用一个vector给它分配五个整数。PrintTable类用LuaCode2.lua文件创建了一个 C++ Lua 函数。调用这个函数,使用Lua::GetReturnValue函数 从堆栈中检索返回值。
在main中最值得注意的是重新加载 Lua 脚本和更新运行时执行的代码的能力。使用cin时main功能停止。在等待的过程中,您可以修改 Lua 脚本,并在解除阻塞执行后看到反映的变化。图 13-3 显示了证明这可能发生的输出。
图 13-3 。显示脚本可以在运行时更改的输出
该输出显示,更改 Lua 代码并重新加载函数会替换给定全局变量处的代码。我在脚本中添加了一行输出:您可以在图中看到这一行“我更改了它!”已打印。
13-6.从 Lua 调用 C 函数
问题
您有一些高度复杂的代码,它们将受益于 C/C++ 代码提供的高性能,但是您希望能够从 Lua 调用这些函数。
解决办法
Lua 提供了lua_CFunction类型,让您创建可以被 Lua 代码引用的 C 函数。
它是如何工作的
Lua API 提供了一个类型lua_CFunction,它本质上决定了可以与 C 函数一起使用的签名,以允许从 Lua 调用它。清单 13-28 展示了一个例子,它创建了一个函数,可以添加 Lua 提供给它的所有参数。
清单 13-28 。从 Lua 调用 C 函数
#include <iostream>
#include "lua.hpp"
#include <vector>
using namespace std;
namespace
{
int Sum(lua_State *L)
{
unsigned int numArguments{ static_cast<unsigned int>(lua_gettop(L)) };
lua_Number sum{ 0 };
for (unsigned int i = 1; i <= numArguments; ++i)
{
if (!lua_isnumber(L, i))
{
lua_pushstring(L, "incorrect argument");
lua_error(L);
}
sum += lua_tonumber(L, i);
}
lua_pushnumber(L, sum / numArguments);
lua_pushnumber(L, sum);
return 2;
}
}
class Lua
{
private:
lua_State* m_pLuaState{ nullptr };
public:
Lua()
: m_pLuaState{ luaL_newstate() }
{
if (m_pLuaState)
{
luaL_openlibs(m_pLuaState);
}
}
~Lua()
{
lua_close(m_pLuaState);
}
Lua(const Lua& other) = delete;
Lua& operator=(const Lua& other) = delete;
Lua(Lua&& rvalue) = delete;
Lua& operator=(Lua&& rvalue) = delete;
bool IsValid() const
{
return m_pLuaState != nullptr;
}
int LoadFile(const string& filename)
{
int status{ luaL_loadfile(m_pLuaState, filename.c_str()) };
if (status == 0)
{
lua_setglobal(m_pLuaState, filename.c_str());
}
return status;
}
int PCall()
{
return lua_pcall(m_pLuaState, 0, LUA_MULTRET, 0);
}
void NewTable(const string& name)
{
lua_newtable(m_pLuaState);
lua_setglobal(m_pLuaState, name.c_str());
}
void GetGlobal(const string& name)
{
lua_getglobal(m_pLuaState, name.c_str());
}
void PushNumber(double number)
{
lua_pushnumber(m_pLuaState, number);
}
void SetTableValue(double index, double value)
{
PushNumber(index);
PushNumber(value);
lua_rawset(m_pLuaState, -3);
}
double GetNumber()
{
return lua_tonumber(m_pLuaState, -1);
}
void Pop(int number)
{
lua_pop(m_pLuaState, number);
}
void CreateCFunction(const string& name, lua_CFunction function)
{
lua_pushcfunction(m_pLuaState, function);
lua_setglobal(m_pLuaState, name.c_str());
}
};
class LuaTable
{
private:
Lua& m_Lua;
string m_Name;
public:
LuaTable(Lua& lua, const string& name)
: m_Lua{ lua }
, m_Name(name)
{
m_Lua.NewTable(m_Name);
}
void Set(const vector<int>& values)
{
Push();
for (unsigned int i = 0; i < values.size(); ++i)
{
m_Lua.SetTableValue(i + 1, values[i]);
}
m_Lua.Pop(1);
}
void Push()
{
m_Lua.GetGlobal(m_Name);
}
};
class LuaFunction
{
private:
Lua& m_Lua;
string m_Filename;
protected:
int PCall()
{
m_Lua.GetGlobal(m_Filename);
return m_Lua.PCall();
}
double GetReturnValue()
{
double result{ m_Lua.GetNumber() };
m_Lua.Pop(1);
return result;
}
public:
LuaFunction(Lua& lua, const string& filename)
: m_Lua{ lua }
, m_Filename(filename)
{
int status{ m_Lua.LoadFile(m_Filename) };
}
};
class PrintTable
: public LuaFunction
{
public:
PrintTable(Lua& lua, const string& filename)
: LuaFunction(lua, filename)
{
}
double Call(LuaTable& table)
{
double sum{};
int status{ LuaFunction::PCall() };
if (status)
{
throw(status);
}
else
{
sum = LuaFunction::GetReturnValue();
}
return sum;
}
};
int main(int argc, char* argv[])
{
Lua lua;
if (lua.IsValid())
{
const string functionName("Sum");
lua.CreateCFunction(functionName, Sum);
const string tableName("cTable");
LuaTable table(lua, tableName);
vector<int> values{ 1, 2, 3, 4, 5 };
table.Set(values);
const string filename{ "LuaCode3.lua" };
PrintTable printTableFunction(lua, filename);
try
{
double result{ printTableFunction.Call(table) };
cout << "Result: " << result << endl;
}
catch (int error)
{
cout << "Call error: " << error << endl;
}
cout << "Waiting" << endl;
int input;
cin >> input;
}
return 0;
}
清单 13-28 中的Sum函数展示了 C 函数必须如何与 Lua 接口。签名很简单:可以从 Lua 调用的 C 函数返回一个整数,并接收一个指向lua_State对象的指针作为参数。当 Lua 调用一个 C 函数时,它将传递的参数数量推到 Lua 栈顶。该值由调用的函数读取,然后该函数可以循环并从堆栈中提取适当数量的元素。然后,C 函数将适当数量的结果推送到堆栈上,并返回调用代码必须从堆栈中弹出的元素数量。
Lua::CreateCFunction方法使用lua_pushcfunction方法将一个lua_CFunction对象推到堆栈上,然后使用lua_setglobal将其分配给全局上下文中的一个命名对象。main函数简单地调用CreateCFunction ,并提供要在 Lua 中使用的名字以及要使用的函数指针。调用这个函数的 Lua 代码如清单 13-29 所示。
清单 13-29 。Lua 代码调用 C 函数
local x = 0
for i = 1, #cTable do
print(i, cTable[i])
x = x + cTable[i]
end
local average, sum = Sum(cTable[1], cTable[2], cTable[3])
print("Average: " .. average)
print("Sum: " .. sum)
return sum
这个 Lua 代码显示了对Sum的调用,并检索了average和sum的值。
13-7.创建异步 Lua 函数
问题
您有一个长时间运行的 Lua 操作,您希望防止它阻塞程序的执行。
解决办法
Lua 允许您创建协程。这些可以从中产生,以让您的程序继续执行,并允许创建行为良好、长期运行的 Lua 任务。每个协程接收它自己独特的 Lua 上下文。
它是如何工作的
Lua 编程语言允许创建协程。协程与普通函数的不同之处在于,它们可以从 Lua 调用coroutine.yield函数来通知状态机它们的执行被挂起了。C API 提供了一个resume函数,您可以调用它在一段时间后唤醒协程,以允许线程检查它所等待的情况是否已经发生。这可能是因为您想要等待动画完成,或者 Lua 脚本正在等待从 I/O 进程获取信息,例如从文件中读取或访问服务器上的数据。
使用lua_newthread函数创建一个 Lua 协程。尽管名字如此,Lua 协程是在发出lua_resume调用的线程中执行的。向lua_resume调用传递一个指向包含协程堆栈的lua_State对象的指针。在栈上执行的代码是 Lua 函数对象,在调用lua_resume时,它最靠近栈顶。清单 13-30 显示了设置 Lua 线程并执行其代码所需的 C++ 代码。
清单 13-30 。创建 Lua 协程
#include <iostream>
#include <lua.hpp>
using namespace std;
class Lua
{
private:
lua_State* m_pLuaState{ nullptr };
bool m_IsThread{ false };
public:
Lua()
: m_pLuaState{ luaL_newstate() }
{
if (m_pLuaState)
{
luaL_openlibs(m_pLuaState);
}
}
Lua(lua_State* pLuaState)
: m_pLuaState{ pLuaState }
{
if (m_pLuaState)
{
luaL_openlibs(m_pLuaState);
}
}
~Lua()
{
if (!m_IsThread && m_pLuaState)
{
lua_close(m_pLuaState);
}
}
Lua(const Lua& other) = delete;
Lua& operator=(const Lua& other) = delete;
Lua(Lua&& rvalue)
: m_pLuaState( rvalue.m_pLuaState )
, m_IsThread( rvalue.m_IsThread )
{
rvalue.m_pLuaState = nullptr;
}
Lua& operator=(Lua&& rvalue)
{
if (this != &rvalue)
{
m_pLuaState = rvalue.m_pLuaState;
m_IsThread = rvalue.m_IsThread;
rvalue.m_pLuaState = nullptr;
}
}
bool IsValid() const
{
return m_pLuaState != nullptr;
}
int LoadFile(const string& filename)
{
int status{ luaL_loadfile(m_pLuaState, filename.c_str()) };
if (status == 0)
{
lua_setglobal(m_pLuaState, filename.c_str());
}
return status;
}
void GetGlobal(const string& name)
{
lua_getglobal(m_pLuaState, name.c_str());
}
Lua CreateThread()
{
Lua threadContext(lua_newthread(m_pLuaState));
threadContext.m_IsThread = true;
return move(threadContext);
}
int ResumeThread()
{
return lua_resume(m_pLuaState, m_pLuaState, 0);
}
};
class LuaFunction
{
private:
Lua& m_Lua;
string m_Filename;
public:
LuaFunction(Lua& lua, const string& filename)
: m_Lua{ lua }
, m_Filename(filename)
{
int status{ m_Lua.LoadFile(m_Filename) };
}
void Push()
{
m_Lua.GetGlobal(m_Filename);
}
};
class LuaThread
{
private:
Lua m_Lua;
LuaFunction m_LuaFunction;
int m_Status{ -1 };
public:
LuaThread(Lua&& lua, const string& functionFilename)
: m_Lua(move(lua))
, m_LuaFunction(m_Lua, functionFilename)
{
}
~LuaThread() = default;
LuaThread(const LuaThread& other) = delete;
LuaThread& operator=(const LuaThread& other) = delete;
LuaThread(LuaThread&& rvalue) = delete;
LuaThread& operator=(LuaThread&& rvalue) = delete;
void Resume()
{
if (!IsFinished())
{
if (m_Status == -1)
{
m_LuaFunction.Push();
}
m_Status = m_Lua.ResumeThread();
}
}
bool IsFinished() const
{
return m_Status == LUA_OK;
}
};
int main(int argc, char* argv[])
{
Lua lua;
if (lua.IsValid())
{
const string functionName("LuaCode4.lua");
LuaThread myThread(lua.CreateThread(), functionName);
while (!myThread.IsFinished())
{
myThread.Resume();
cout << "myThread yielded or finished!" << endl;
}
cout << "myThread finished!" << endl;
}
return 0;
}
清单 13-30 中的Lua类包含一个指向lua_State对象的指针和一个bool变量,该变量指示是否创建了一个特定的对象来处理 Lua 线程。这对于确保只有一个Lua对象负责在其析构函数中调用lua_close是必要的。您可以看到这个bool值是在~Lua方法中检查的。
在Lua::CreateThread方法中m_IsThread bool 被设置为真。这个方法调用lua_newthread函数,并将新的lua_State指针传递给一个新构造的Lua对象。然后这个对象将m_IsThread bool设置为真,并从函数中返回。使用 move 语义返回Lua对象。这确保了在任何时候都不会有单个Lua对象的任何副本,这是通过在复制构造函数和复制赋值操作符中指定的delete关键字来实现的。仅定义了移动构造函数和移动赋值运算符。
Lua::Resume方法 也显示在清单 13-30 中。这个方法负责启动或恢复 Lua 协程的执行。
LuaThread类负责管理一个 Lua 协程。构造函数接受一个对Lua对象的rvalue引用和一个包含要加载的文件名的string。class有存储Lua对象和一个LuaFunction对象的字段,该对象将用于将函数Push到协程的堆栈上。m_Status字段确定协程何时完成执行。它被初始化为-1,因为 Lua 不使用这个值来表示状态。当协程执行完成时,从lua_resume返回LUA_OK值,当协程让步时,返回LUA_YIELD值。LuaThread::Resume功能首先检查状态是否已经设置为LUA_OK;如果有,那就什么都不做。如果m_Status变量包含-1,那么m_LuaFunction对象被压入堆栈。用Lua::ResumeThread返回的值更新m_Status变量。
main函数通过创建一个LuaThread对象并在一个while循环中调用LuaThread::Resume来使用所有这些功能,该循环一直执行到IsFinished在myThread对象上返回 true。LuaCode4.lua文件包含来自清单 13-31 的 Lua 代码,它在一个循环中包含几个产量。
清单 13-31 。LuaCode4.lua来源
for i=1, 10, 1 do
print("Going for yield " .. i .. "!")
coroutine.yield()
end
这是一个如何在 Lua 代码中使用coroutine.yield函数的简单例子。在运行中的 Lua 脚本中执行这个 Lua 函数时,lua_resume C 函数返回LUA_YIELD。图 13-4 显示了运行包含清单 13-30 中的 C++ 代码和清单 13-31 中的 Lua 代码的组合的结果。
图 13-4 。结合执行清单 13-30 和清单 13-31 产生的输出