Skip to content

C++ Web 编程

重要提示: 尽管本教程涵盖了使用 C++ 的通用网关接口(Common Gateway Interface,CGI),但请务必理解 CGI 对于现代 Web 开发来说是一项过时的技术,尤其是在 C++ 中。由于性能限制(每个请求都会生成一个新进程)、安全问题以及更高效和强大的 C++ Web 框架及替代架构的出现,如今很少直接使用 C++ 进行 CGI 脚本编程。

现代替代方案包括专用的 C++ Web 框架(如 Crow, Drogon, oat++, Pistache)、使用 C++ 构建供其他 Web 服务器访问的后端 API(例如 REST, gRPC),甚至使用 WebAssembly 进行客户端执行。然而,为了理解历史背景和服务器端执行的基本原理,本教程将解释 CGI。

  • 通用网关接口(Common Gateway Interface,CGI) 是一个标准协议,定义了 Web 服务器如何执行外部程序(CGI 脚本)以动态生成 Web 内容。
  • Web 服务器不是简单地返回一个静态文件,而是运行 CGI 程序,捕获其标准输出,并将该输出发送回客户端浏览器。
  • CGI 脚本可以使用各种语言编写,包括 C++、Perl、Python 或 shell 脚本。
  1. 用户浏览器向 Web 服务器请求与 CGI 脚本关联的特定 URL。

  2. Web 服务器识别出该请求的目标是 CGI 脚本(通常基于其位置或文件扩展名)。

  3. 服务器执行 CGI 程序(例如,您编译好的 C++ 可执行文件)。

  4. CGI 程序执行其任务(例如,访问数据库、执行计算)。

  5. 至关重要的一点是,CGI 程序将其输出打印到标准输出(stdout)。此输出必须以 HTTP 头部开始,后跟一个空行,然后是实际内容(通常是 HTML)。

  6. Web 服务器捕获 CGI 程序打印到标准输出的所有内容。

  7. 服务器将捕获到的输出(头部和内容)发送回用户浏览器。

  8. 浏览器渲染接收到的内容。

要运行 CGI 脚本,需要对 Web 服务器(例如 Apache, Nginx)进行配置:

  • 指定允许存放 CGI 脚本的目录(通常是 /cgi-bin/)。
  • 启用该目录下文件的 CGI 执行权限,或针对具有特定扩展名(例如 .cgi)的文件启用。
  • 确保脚本文件具有执行权限。
  • 具体的配置指令因 Web 服务器软件而异。

让我们创建一个简单的 C++ 程序,通过 CGI 输出一个基本的 HTML 页面。

#include <iostream>
int main() {
// 1. 打印强制的 HTTP Content-Type 头部
// 后跟一个空行(\n\n 或 \r\n\r\n)
std::cout << "Content-type: text/html\n\n";
// 2. 打印 HTML 内容
std::cout << "<!DOCTYPE html>\n";
std::cout << "<html>\n";
std::cout << "<head>\n";
std::cout << "<title>Hello C++ CGI</title>\n";
std::cout << "</head>\n";
std::cout << "<body>\n";
std::cout << "<h1>Hello World!</h1>\n";
std::cout << "<p>This is my first C++ CGI program.</p>\n";
std::cout << "</body>\n";
std::cout << "</html>\n";
// 向服务器指示成功执行
return 0;
}

要使其工作:

  • 编译此代码(例如 g++ hello.cpp -o hello.cgi)。
  • 将可执行文件 hello.cgi 放入 Web 服务器配置的 CGI 目录中(例如 /var/www/cgi-bin/)。
  • 确保它具有执行权限(例如 chmod +x hello.cgi)。
  • 在 Web 浏览器中通过其 URL 访问脚本(例如 http://yourserver.com/cgi-bin/hello.cgi)。

浏览器应显示 “Hello World!” 标题和段落。

对于 CGI 来说,最关键的头部是 Content-type,它告诉浏览器后面跟着的是什么类型的数据(例如 text/html, text/plain, image/jpeg)。它必须后跟一个空行。

其他有用的头部包括:

头部描述
Content-type:内容的 MIME 类型(例如 text/html)。必需的。
Location:用于将浏览器重定向到不同的 URL(例如 Location: http://another-page.com\n\n)。
Set-Cookie:用于向浏览器发送 HTTP cookie(参见 Cookies 部分)。
Status:设置特定的 HTTP 状态码(例如 Status: 404 Not Found\n\n)。通常由服务器处理,但也可由 CGI 设置。

Web 服务器通过环境变量向 CGI 脚本提供信息。您的 C++ 程序可以使用 <cstdlib> 中的 getenv() 函数访问这些变量。

变量名描述
REQUEST_METHOD使用的 HTTP 方法(例如 GET, POST)。
QUERY_STRINGURL 中 ? 之后的部分(用于 GET 请求)。
CONTENT_LENGTH通过 POST 发送的数据长度(以字节为单位)。
CONTENT_TYPE通过 POST 发送的数据的 MIME 类型。
SCRIPT_NAME正在执行脚本的虚拟路径。
REMOTE_ADDR发出请求的客户端的 IP 地址。
REMOTE_HOST客户端的主机名(如果可用)。
SERVER_NAME服务器的主机名或 IP 地址。
SERVER_PORT服务器正在监听的端口号。
HTTP_USER_AGENT浏览器/客户端标识字符串。
HTTP_COOKIE包含客户端发送的 cookies。

示例:显示环境变量

#include <iostream>
#include <cstdlib> // 用于 getenv()
#include <string>
// 常见 CGI 环境变量列表
const char* ENV_VARS[] = {
"REQUEST_METHOD", "QUERY_STRING", "CONTENT_LENGTH", "CONTENT_TYPE",
"SCRIPT_NAME", "REMOTE_ADDR", "REMOTE_HOST", "SERVER_NAME",
"SERVER_PORT", "HTTP_USER_AGENT", "HTTP_COOKIE", nullptr // 结束标记
};
int main() {
std::cout << "Content-type: text/html\n\n";
std::cout << "<!DOCTYPE html><html><head><title>CGI Environment</title></head><body>\n";
std::cout << "<h1>CGI Environment Variables</h1>\n";
std::cout << "<table border='1'><tr><th>Variable</th><th>Value</th></tr>\n";
for (int i = 0; ENV_VARS[i] != nullptr; ++i) {
const char* varName = ENV_VARS[i];
const char* value = std::getenv(varName);
std::cout << "<tr><td>" << varName << "</td><td>";
if (value) {
std::cout << value;
} else {
std::cout << "<i>Not set</i>";
}
std::cout << "</td></tr>\n";
}
std::cout << "</table></body></html>\n";
return 0;
}

Web 表单允许用户向服务器发送数据,通常使用 GET 或 POST 方法。

数据作为查询字符串附加到 URL:script.cgi?name1=value1&name2=value2。它在浏览器的地址栏和日志中可见。适用于非敏感数据,且长度有限(取决于浏览器/服务器,通常约为 2KB)。

在 C++ CGI 脚本中,您可以通过使用 getenv("QUERY_STRING") 读取 QUERY_STRING 环境变量来访问 GET 数据。然后,您需要解析此字符串(例如,按 & 和 = 分割,并对值进行 URL 解码)。手动解析可能很复杂。

数据发送在 HTTP 请求的正文中,而不是 URL 中。适用于传输大量数据和敏感信息。大小限制通常由服务器配置决定。

在 C++ CGI 脚本中,您通过以下方式访问 POST 数据:

  • 读取 CONTENT_LENGTH 环境变量以了解要读取多少字节。
  • 从**标准输入(stdin)**精确读取该数量的字节(例如,使用 std::cin.read())。
  • 解析读取的数据。格式取决于 CONTENT_TYPE(通常是 application/x-www-form-urlencoded,类似于查询字符串格式,或用于文件上传的 multipart/form-data)。

处理表单数据(概念性示例 - 简化解析)

Section titled “处理表单数据(概念性示例 - 简化解析)”

可靠地解析表单数据(特别是 URL 解码和处理边缘情况)需要大量的精力或一个库。原始教程中提到的 cgicc 库已经过时。下面是一个概念性的草图,不是健壮的生产代码,展示了如何读取 POST 数据:

#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>
#include <sstream>
// --- 基本、不安全的 URL 解码(仅用于演示!)---
std::string urlDecode(const std::string& str) {
std::string decoded = "";
char ch;
int i, ii;
for (i = 0; i < str.length(); i++) {
if (str[i] == '%') {
sscanf(str.substr(i + 1, 2).c_str(), "%x", &ii);
ch = static_cast<char>(ii);
decoded += ch;
i = i + 2;
} else if (str[i] == '+') {
decoded += ' ';
} else {
decoded += str[i];
}
}
return decoded;
}
// --- 基本 URL 解码结束 ---
int main() {
std::cout << "Content-type: text/html\n\n";
std::cout << "<html><body><h1>Form Data Received</h1>";
std::string method = std::getenv("REQUEST_METHOD") ? std::getenv("REQUEST_METHOD") : "";
std::string formData = "";
if (method == "POST") {
const char* contentLengthStr = std::getenv("CONTENT_LENGTH");
if (contentLengthStr) {
int len = std::atoi(contentLengthStr);
if (len > 0) {
std::vector<char> buffer(len);
std::cin.read(buffer.data(), len);
formData.assign(buffer.data(), len);
}
}
} else if (method == "GET") {
const char* queryString = std::getenv("QUERY_STRING");
if (queryString) {
formData = queryString;
}
}
std::cout << "<h2>Raw Data:</h2><pre>" << formData << "</pre>";
// 基本解析(仅按 '&' 和 '=' 分割,需要正确的解码)
std::cout << "<h2>Parsed Data (Basic):</h2><ul>";
std::stringstream ss(formData);
std::string pair;
while (std::getline(ss, pair, '&')) {
size_t eqPos = pair.find('=');
if (eqPos != std::string::npos) {
std::string key = urlDecode(pair.substr(0, eqPos));
std::string value = urlDecode(pair.substr(eqPos + 1));
std::cout << "<li>**" << key << ":** " << value << "</li>";
} else {
std::cout << "<li>**" << urlDecode(pair) << ":** (no value)</li>";
}
}
std::cout << "</ul>";
std::cout << "</body></html>" << std::endl;
return 0;
}

警告: 上述解析和解码非常基础且不安全。实际应用需要使用健壮的库来完成此任务。

Cookies 允许服务器在客户端浏览器上存储少量数据,这些数据会在后续请求同一域时发送回服务器。CGI 脚本可以与 cookies 交互。

要设置 cookie,CGI 脚本必须在空行和内容之前打印 Set-Cookie HTTP 头部。

// 示例:设置 cookie
std::cout << "Content-type: text/html\n"; // 普通头部
std::cout << "Set-Cookie: UserID=Alice; Max-Age=3600; Path=/\n"; // 设置 cookie 头部
std::cout << "Set-Cookie: SessionToken=xyz789; HttpOnly; Secure\n"; // 另一个 cookie
std::cout << "\n"; // 空行,在所有头部**之后**
std::cout << "<html><body>Cookies have been set (check browser developer tools).</body></html>";

诸如 Max-Age(生命周期,秒)、Expires(过期日期)、Path(URL 路径范围)、Domain(域范围)、Secure(仅通过 HTTPS 发送)和 HttpOnly(阻止 JavaScript 访问)等属性控制着 cookie 的行为。

浏览器发送的 cookies 以分号分隔的 key=value 对字符串的形式存储在 HTTP_COOKIE 环境变量中。

// 示例:检索 cookie
const char* cookieStr = std::getenv("HTTP_COOKIE");
std::cout << "Content-type: text/html\n\n";
std::cout << "<html><body><h1>Received Cookies:</h1>";
if (cookieStr) {
std::cout << "<pre>" << cookieStr << "</pre>";
// 您需要解析此字符串(按 ';' 分割,然后按 '=' 分割)
// 以获取单个 cookie 值。
} else {
std::cout << "<p>No cookies received.</p>";
}
std::cout << "</body></html>";

同样,解析 cookie 字符串需要仔细实现。

在没有专用库的情况下,通过 C++ CGI 处理文件上传非常复杂。它需要解析来自标准输入的 multipart/form-data 编码,其中包含边界、每个部分的头部(包括文件名和内容类型)以及原始文件数据。

HTML 表单需要设置 enctype="multipart/form-data" 并包含一个 <input type="file"> 元素。

<form enctype="multipart/form-data" action="/cgi-bin/upload.cgi" method="post">
<p>Select file: <input type="file" name="userfile"></p>
<p><input type="submit" value="Upload"></p>
</form>

由于复杂性,本入门教程不包含详细的 C++ CGI 文件上传实现。现代 Web 框架透明地处理了这种抽象。

CGI 提供了一个基本的理解,说明了 Web 服务器如何执行程序来生成动态内容。然而,对于实际的 C++ Web 开发,现代框架提供了显著更好的性能、安全性和开发者生产力。