C++ Web 编程
C++ Web 编程(使用 CGI)
Section titled “C++ Web 编程(使用 CGI)”重要提示: 尽管本教程涵盖了使用 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。
什么是 CGI?
Section titled “什么是 CGI?”- 通用网关接口(Common Gateway Interface,CGI) 是一个标准协议,定义了 Web 服务器如何执行外部程序(CGI 脚本)以动态生成 Web 内容。
- Web 服务器不是简单地返回一个静态文件,而是运行 CGI 程序,捕获其标准输出,并将该输出发送回客户端浏览器。
- CGI 脚本可以使用各种语言编写,包括 C++、Perl、Python 或 shell 脚本。
CGI 工作原理(简化版)
Section titled “CGI 工作原理(简化版)”-
用户浏览器向 Web 服务器请求与 CGI 脚本关联的特定 URL。
-
Web 服务器识别出该请求的目标是 CGI 脚本(通常基于其位置或文件扩展名)。
-
服务器执行 CGI 程序(例如,您编译好的 C++ 可执行文件)。
-
CGI 程序执行其任务(例如,访问数据库、执行计算)。
-
至关重要的一点是,CGI 程序将其输出打印到标准输出(stdout)。此输出必须以 HTTP 头部开始,后跟一个空行,然后是实际内容(通常是 HTML)。
-
Web 服务器捕获 CGI 程序打印到标准输出的所有内容。
-
服务器将捕获到的输出(头部和内容)发送回用户浏览器。
-
浏览器渲染接收到的内容。
Web 服务器配置(概念性说明)
Section titled “Web 服务器配置(概念性说明)”要运行 CGI 脚本,需要对 Web 服务器(例如 Apache, Nginx)进行配置:
- 指定允许存放 CGI 脚本的目录(通常是
/cgi-bin/)。 - 启用该目录下文件的 CGI 执行权限,或针对具有特定扩展名(例如
.cgi)的文件启用。 - 确保脚本文件具有执行权限。
- 具体的配置指令因 Web 服务器软件而异。
第一个 C++ CGI 程序
Section titled “第一个 C++ CGI 程序”让我们创建一个简单的 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!” 标题和段落。
HTTP 头部
Section titled “HTTP 头部”对于 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 设置。 |
CGI 环境变量
Section titled “CGI 环境变量”Web 服务器通过环境变量向 CGI 脚本提供信息。您的 C++ 程序可以使用 <cstdlib> 中的 getenv() 函数访问这些变量。
| 变量名 | 描述 |
|---|---|
REQUEST_METHOD | 使用的 HTTP 方法(例如 GET, POST)。 |
QUERY_STRING | URL 中 ? 之后的部分(用于 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;}GET 和 POST 方法
Section titled “GET 和 POST 方法”Web 表单允许用户向服务器发送数据,通常使用 GET 或 POST 方法。
GET 方法
Section titled “GET 方法”数据作为查询字符串附加到 URL:script.cgi?name1=value1&name2=value2。它在浏览器的地址栏和日志中可见。适用于非敏感数据,且长度有限(取决于浏览器/服务器,通常约为 2KB)。
在 C++ CGI 脚本中,您可以通过使用 getenv("QUERY_STRING") 读取 QUERY_STRING 环境变量来访问 GET 数据。然后,您需要解析此字符串(例如,按 & 和 = 分割,并对值进行 URL 解码)。手动解析可能很复杂。
POST 方法
Section titled “POST 方法”数据发送在 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;}警告: 上述解析和解码非常基础且不安全。实际应用需要使用健壮的库来完成此任务。
在 CGI 中使用 Cookies
Section titled “在 CGI 中使用 Cookies”Cookies 允许服务器在客户端浏览器上存储少量数据,这些数据会在后续请求同一域时发送回服务器。CGI 脚本可以与 cookies 交互。
设置 Cookies
Section titled “设置 Cookies”要设置 cookie,CGI 脚本必须在空行和内容之前打印 Set-Cookie HTTP 头部。
// 示例:设置 cookiestd::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"; // 另一个 cookiestd::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
Section titled “检索 Cookies”浏览器发送的 cookies 以分号分隔的 key=value 对字符串的形式存储在 HTTP_COOKIE 环境变量中。
// 示例:检索 cookieconst 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 开发,现代框架提供了显著更好的性能、安全性和开发者生产力。