一、概述
使用VC++实现一个轻量级HTTP服务器,支持基本的HTTP/1.1协议,能够处理静态文件请求、目录列表和简单的CGI脚本执行。服务器采用多线程架构,支持并发连接,适合作为嵌入式设备或小型应用的Web服务器。
二、系统架构
2.1 整体架构
graph TDA[客户端浏览器] -->|HTTP请求| B[主服务器]B -->|分发请求| C[工作线程1]B -->|分发请求| D[工作线程2]B -->|分发请求| E[工作线程3]C -->|文件请求| F[文件系统]D -->|CGI请求| G[CGI处理器]E -->|目录请求| H[目录列表生成器]
2.2 核心组件
- 主服务器模块:监听端口,接受连接,管理线程池
- 请求处理模块:解析HTTP请求,路由到相应处理器
- 文件服务模块:提供静态文件服务
- CGI处理器:执行简单脚本
- 目录列表模块:生成目录浏览页面
- 日志模块:记录访问日志和错误日志
三、核心源代码实现
3.1 主服务器类 (HttpServer.h)
#if !defined(AFX_HTTPSERVER_H__)
#define AFX_HTTPSERVER_H__#include <winsock2.h>
#include <ws2tcpip.h>
#include <vector>
#include <map>
#include <string>
#include <process.h>
#include <fstream>
#include <sstream>
#include <iomanip>
#include <direct.h>#pragma comment(lib, "ws2_32.lib")#define DEFAULT_PORT 8080
#define MAX_CONNECTIONS 100
#define BUFFER_SIZE 4096
#define THREAD_POOL_SIZE 4class CHttpServer {
public:CHttpServer();~CHttpServer();bool Start(unsigned short port = DEFAULT_PORT);void Stop();private:static unsigned __stdcall WorkerThread(void* param);void ProcessRequest(SOCKET clientSocket);void SendResponse(SOCKET clientSocket, const std::string& status, const std::string& contentType, const std::string& content);void SendFile(SOCKET clientSocket, const std::string& filePath);void SendDirectoryListing(SOCKET clientSocket, const std::string& path);void ExecuteCGI(SOCKET clientSocket, const std::string& scriptPath);std::string GetContentType(const std::string& extension);std::string UrlDecode(const std::string& str);void LogRequest(const std::string& method, const std::string& path, const std::string& protocol, int statusCode);private:SOCKET m_listenSocket;bool m_running;unsigned short m_port;std::map<std::string, std::string> m_mimeTypes;
};#endif // AFX_HTTPSERVER_H__
3.2 主服务器实现 (HttpServer.cpp)
#include "HttpServer.h"
#include <iostream>
#include <algorithm>
#include <ctime>CHttpServer::CHttpServer() : m_listenSocket(INVALID_SOCKET), m_running(false), m_port(DEFAULT_PORT) {// 初始化MIME类型映射m_mimeTypes[".html"] = "text/html";m_mimeTypes[".htm"] = "text/html";m_mimeTypes[".txt"] = "text/plain";m_mimeTypes[".css"] = "text/css";m_mimeTypes[".js"] = "application/javascript";m_mimeTypes[".jpg"] = "image/jpeg";m_mimeTypes[".jpeg"] = "image/jpeg";m_mimeTypes[".png"] = "image/png";m_mimeTypes[".gif"] = "image/gif";m_mimeTypes[".ico"] = "image/x-icon";m_mimeTypes[".pdf"] = "application/pdf";m_mimeTypes[".zip"] = "application/zip";m_mimeTypes[".json"] = "application/json";m_mimeTypes[".xml"] = "application/xml";
}CHttpServer::~CHttpServer() {Stop();
}bool CHttpServer::Start(unsigned short port) {WSADATA wsaData;if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {std::cerr << "WSAStartup failed." << std::endl;return false;}m_port = port;m_listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (m_listenSocket == INVALID_SOCKET) {std::cerr << "Socket creation failed: " << WSAGetLastError() << std::endl;WSACleanup();return false;}// 设置SO_REUSEADDR选项int opt = 1;setsockopt(m_listenSocket, SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt));sockaddr_in serverAddr;serverAddr.sin_family = AF_INET;serverAddr.sin_addr.s_addr = INADDR_ANY;serverAddr.sin_port = htons(m_port);if (bind(m_listenSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {std::cerr << "Bind failed: " << WSAGetLastError() << std::endl;closesocket(m_listenSocket);WSACleanup();return false;}if (listen(m_listenSocket, SOMAXCONN) == SOCKET_ERROR) {std::cerr << "Listen failed: " << WSAGetLastError() << std::endl;closesocket(m_listenSocket);WSACleanup();return false;}std::cout << "HTTP Server started on port " << m_port << std::endl;std::cout << "Press Ctrl+C to stop..." << std::endl;m_running = true;// 创建工作线程for (int i = 0; i < THREAD_POOL_SIZE; i++) {_beginthreadex(NULL, 0, WorkerThread, this, 0, NULL);}// 主线程接受连接while (m_running) {sockaddr_in clientAddr;int clientAddrSize = sizeof(clientAddr);SOCKET clientSocket = accept(m_listenSocket, (sockaddr*)&clientAddr, &clientAddrSize);if (clientSocket == INVALID_SOCKET) {if (m_running) {std::cerr << "Accept failed: " << WSAGetLastError() << std::endl;}continue;}// 将客户端套接字传递给工作线程处理// 这里简化处理:直接在工作线程池中处理// 实际应用中应使用队列和线程池管理ProcessRequest(clientSocket);closesocket(clientSocket);}closesocket(m_listenSocket);WSACleanup();return true;
}void CHttpServer::Stop() {m_running = false;closesocket(m_listenSocket);
}unsigned __stdcall CHttpServer::WorkerThread(void* param) {CHttpServer* pServer = (CHttpServer*)param;while (pServer->m_running) {// 简化处理:实际应使用连接队列Sleep(100);}return 0;
}void CHttpServer::ProcessRequest(SOCKET clientSocket) {char buffer[BUFFER_SIZE];int bytesReceived = recv(clientSocket, buffer, BUFFER_SIZE - 1, 0);if (bytesReceived <= 0) {return;}buffer[bytesReceived] = '\0';std::string request(buffer);// 解析请求行size_t pos1 = request.find(' ');size_t pos2 = request.find(' ', pos1 + 1);if (pos1 == std::string::npos || pos2 == std::string::npos) {SendResponse(clientSocket, "400 Bad Request", "text/plain", "Invalid request format");return;}std::string method = request.substr(0, pos1);std::string path = request.substr(pos1 + 1, pos2 - pos1 - 1);std::string protocol = request.substr(pos2 + 1);// 解码URLpath = UrlDecode(path);// 记录请求LogRequest(method, path, protocol, 200);// 处理请求if (method == "GET") {// 检查是否是CGI脚本if (path.find("/cgi-bin/") == 0) {ExecuteCGI(clientSocket, "." + path.substr(8)); // 去掉/cgi-bin/前缀} // 检查是否是目录else if (path == "/" || path.empty()) {SendDirectoryListing(clientSocket, ".");} else {// 规范化路径if (path[0] == '/') {path = "." + path;}// 检查路径是否存在DWORD attr = GetFileAttributes(path.c_str());if (attr == INVALID_FILE_ATTRIBUTES) {SendResponse(clientSocket, "404 Not Found", "text/html", "<html><body><h1>404 Not Found</h1></body></html>");return;}// 如果是目录if (attr & FILE_ATTRIBUTE_DIRECTORY) {SendDirectoryListing(clientSocket, path);} // 如果是文件else {SendFile(clientSocket, path);}}} else {SendResponse(clientSocket, "501 Not Implemented", "text/plain", "Method not supported");}
}void CHttpServer::SendResponse(SOCKET clientSocket, const std::string& status, const std::string& contentType, const std::string& content) {std::stringstream response;response << "HTTP/1.1 " << status << "\r\n";response << "Server: VC++ HTTP Server\r\n";response << "Content-Type: " << contentType << "\r\n";response << "Content-Length: " << content.length() << "\r\n";response << "Connection: close\r\n";response << "\r\n";response << content;send(clientSocket, response.str().c_str(), response.str().length(), 0);
}void CHttpServer::SendFile(SOCKET clientSocket, const std::string& filePath) {std::ifstream file(filePath, std::ios::binary);if (!file.is_open()) {SendResponse(clientSocket, "404 Not Found", "text/html", "<html><body><h1>404 Not Found</h1></body></html>");return;}// 获取文件扩展名size_t dotPos = filePath.find_last_of('.');std::string extension = (dotPos != std::string::npos) ? filePath.substr(dotPos) : "";std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);// 获取内容类型std::string contentType = GetContentType(extension);if (contentType.empty()) {contentType = "application/octet-stream";}// 读取文件内容std::stringstream contentStream;contentStream << file.rdbuf();std::string content = contentStream.str();// 发送响应SendResponse(clientSocket, "200 OK", contentType, content);
}void CHttpServer::SendDirectoryListing(SOCKET clientSocket, const std::string& path) {std::stringstream html;html << "<html><head><title>Directory Listing</title>";html << "<style>body { font-family: Arial, sans-serif; margin: 20px; }";html << "table { border-collapse: collapse; width: 100%; }";html << "th, td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; }";html << "tr:hover { background-color: #f5f5f5; }";html << "</style></head><body>";html << "<h1>Directory Listing: " << path << "</h1>";html << "<table><tr><th>Name</th><th>Size</th><th>Modified</th></tr>";// 添加上级目录链接if (path != ".") {html << "<tr><td><a href=\"..\">[Parent Directory]</a></td><td>-</td><td>-</td></tr>";}// 遍历目录WIN32_FIND_DATAA findData;HANDLE hFind = FindFirstFile((path + "\\*").c_str(), &findData);if (hFind != INVALID_HANDLE_VALUE) {do {if (strcmp(findData.cFileName, ".") == 0 || strcmp(findData.cFileName, "..") == 0) {continue;}std::string fileName = findData.cFileName;std::string filePath = path + "\\" + fileName;// 获取文件属性DWORD attrs = findData.dwFileAttributes;bool isDirectory = (attrs & FILE_ATTRIBUTE_DIRECTORY);// 格式化文件大小std::string sizeStr;if (isDirectory) {sizeStr = "-";} else {DWORD fileSize = findData.nFileSizeLow;std::stringstream ss;ss << fileSize << " bytes";sizeStr = ss.str();}// 格式化修改时间SYSTEMTIME stUTC, stLocal;FileTimeToSystemTime(&findData.ftLastWriteTime, &stUTC);SystemTimeToTzSpecificLocalTime(NULL, &stUTC, &stLocal);char timeStr[20];sprintf_s(timeStr, "%04d-%02d-%02d %02d:%02d:%02d", stLocal.wYear, stLocal.wMonth, stLocal.wDay,stLocal.wHour, stLocal.wMinute, stLocal.wSecond);// 添加到HTMLhtml << "<tr>";html << "<td>" << (isDirectory ? "[DIR]" : "") << "<a href=\"" << fileName << (isDirectory ? "/" : "") << "\">" << fileName << "</a></td>";html << "<td>" << sizeStr << "</td>";html << "<td>" << timeStr << "</td>";html << "</tr>";} while (FindNextFileA(hFind, &findData));FindClose(hFind);}html << "</table></body></html>";SendResponse(clientSocket, "200 OK", "text/html", html.str());
}void CHttpServer::ExecuteCGI(SOCKET clientSocket, const std::string& scriptPath) {// 简化处理:实际应使用更复杂的CGI处理std::ifstream script(scriptPath);if (!script.is_open()) {SendResponse(clientSocket, "404 Not Found", "text/html", "<html><body><h1>CGI Script Not Found</h1></body></html>");return;}std::stringstream contentStream;contentStream << script.rdbuf();std::string content = contentStream.str();// 替换模板变量(示例)size_t pos = content.find("${DATE}");if (pos != std::string::npos) {time_t now = time(NULL);char* dt = ctime(&now);content.replace(pos, 7, dt);}SendResponse(clientSocket, "200 OK", "text/html", content);
}std::string CHttpServer::GetContentType(const std::string& extension) {auto it = m_mimeTypes.find(extension);if (it != m_mimeTypes.end()) {return it->second;}return "";
}std::string CHttpServer::UrlDecode(const std::string& str) {std::string result;for (size_t i = 0; i < str.length(); i++) {if (str[i] == '%' && i + 2 < str.length()) {int hexValue;if (sscanf_s(str.substr(i + 1, 2).c_str(), "%x", &hexValue) == 1) {result += static_cast<char>(hexValue);i += 2;} else {result += str[i];}} else if (str[i] == '+') {result += ' ';} else {result += str[i];}}return result;
}void CHttpServer::LogRequest(const std::string& method, const std::string& path, const std::string& protocol, int statusCode) {time_t now = time(NULL);char* dt = ctime(&now);dt[strlen(dt) - 1] = '\0'; // 去掉换行符std::cout << "[" << dt << "] " << method << " " << path << " " << protocol << " - " << statusCode << std::endl;
}
3.3 主程序入口 (main.cpp)
#include "HttpServer.h"
#include <iostream>int main() {CHttpServer server;// 设置控制台标题SetConsoleTitle(L"VC++ HTTP Server");// 启动服务器if (!server.Start(8080)) {std::cerr << "Failed to start server." << std::endl;return 1;}return 0;
}
四、项目配置与编译
4.1 环境要求
- Windows 7/10/11
- Visual Studio 2010或更高版本
- Winsock2库
4.2 编译步骤
- 创建新的Win32控制台应用程序项目
- 添加上述三个源文件(HttpServer.h, HttpServer.cpp, main.cpp)
- 配置项目属性:
- 链接器 → 输入 → 附加依赖项:添加
ws2_32.lib - C/C++ → 预处理器 → 预处理器定义:添加
_CRT_SECURE_NO_WARNINGS
- 链接器 → 输入 → 附加依赖项:添加
- 编译并运行
4.3 目录结构
HTTP Server/
├── HttpServer.h # 服务器类声明
├── HttpServer.cpp # 服务器类实现
├── main.cpp # 程序入口
├── wwwroot/ # 网站根目录
│ ├── index.html # 默认首页
│ ├── css/ # CSS样式表
│ ├── js/ # JavaScript文件
│ ├── images/ # 图片资源
│ └── cgi-bin/ # CGI脚本目录
│ └── hello.cgi # 示例CGI脚本
└── logs/ # 日志目录(运行时创建)
参考代码 VC打造小型HTTP服务器 www.youwenfan.com/contentcnt/122432.html
五、功能扩展
5.1 添加POST请求支持
// 在ProcessRequest函数中添加
else if (method == "POST") {// 解析Content-Lengthsize_t contentLengthPos = request.find("Content-Length:");if (contentLengthPos != std::string::npos) {size_t endPos = request.find("\r\n", contentLengthPos);std::string lenStr = request.substr(contentLengthPos + 15, endPos - contentLengthPos - 15);int contentLength = atoi(lenStr.c_str());// 读取POST数据std::string postData = request.substr(request.length() - contentLength);// 处理表单数据// ...}
}
5.2 添加MIME类型自动检测
// 使用Windows API检测文件类型
std::string GetContentTypeFromFile(const std::string& filePath) {HANDLE hFile = CreateFile(filePath.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);if (hFile == INVALID_HANDLE_VALUE) {return "application/octet-stream";}DWORD fileSize = GetFileSize(hFile, NULL);if (fileSize == INVALID_FILE_SIZE || fileSize == 0) {CloseHandle(hFile);return "application/octet-stream";}BYTE buffer[256];DWORD bytesRead;ReadFile(hFile, buffer, min(256, fileSize), &bytesRead, NULL);CloseHandle(hFile);// 简单的魔数检测if (bytesRead >= 4 && buffer[0] == 0xFF && buffer[1] == 0xD8) {return "image/jpeg";}if (bytesRead >= 8 && buffer[0] == 0x89 && buffer[1] == 0x50 && buffer[2] == 0x4E && buffer[3] == 0x47) {return "image/png";}if (bytesRead >= 6 && buffer[0] == 'G' && buffer[1] == 'I' && buffer[2] == 'F' && buffer[3] == '8') {return "image/gif";}return "application/octet-stream";
}
5.3 添加HTTPS支持
// 使用OpenSSL添加HTTPS支持
#include <openssl/ssl.h>
#include <openssl/err.h>class CSSLWrapper {
public:CSSLWrapper(SOCKET socket) : m_socket(socket), m_ssl(NULL) {SSL_library_init();SSL_CTX* ctx = SSL_CTX_new(SSLv23_server_method());SSL_CTX_use_certificate_file(ctx, "server.crt", SSL_FILETYPE_PEM);SSL_CTX_use_PrivateKey_file(ctx, "server.key", SSL_FILETYPE_PEM);m_ssl = SSL_new(ctx);SSL_set_fd(m_ssl, m_socket);SSL_accept(m_ssl);}int Send(const char* data, int length) {return SSL_write(m_ssl, data, length);}int Recv(char* buffer, int length) {return SSL_read(m_ssl, buffer, length);}~CSSLWrapper() {if (m_ssl) {SSL_shutdown(m_ssl);SSL_free(m_ssl);}}private:SOCKET m_socket;SSL* m_ssl;
};
六、使用示例
6.1 创建测试网页
在wwwroot目录下创建index.html:
<!DOCTYPE html>
<html>
<head><title>VC++ HTTP Server</title><style>body { font-family: Arial, sans-serif; margin: 40px; }h1 { color: #0066cc; }.container { max-width: 800px; margin: 0 auto; }</style>
</head>
<body><div class="container"><h1>Welcome to VC++ HTTP Server</h1><p>This is a simple HTTP server built with Visual C++.</p><ul><li><a href="/images/">View Images</a></li><li><a href="/docs/">View Documents</a></li><li><a href="/cgi-bin/hello.cgi">Run CGI Script</a></li></ul></div>
</body>
</html>
6.2 创建CGI脚本
在wwwroot/cgi-bin目录下创建hello.cgi:
#!/usr/bin/perl
print "Content-type: text/html\n\n";
print "<html><body>";
print "<h1>Hello from CGI!</h1>";
print "<p>Current date and time: ";
print scalar localtime;
print "</p>";
print "</body></html>";
6.3 运行服务器
- 编译并运行HTTP服务器程序
- 打开浏览器访问:http://localhost:8080
- 浏览网站内容,测试各种功能
七、性能优化建议
7.1 使用IOCP异步I/O
// 使用IOCP提高并发性能
HANDLE hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);// 将套接字关联到完成端口
CreateIoCompletionPort((HANDLE)m_listenSocket, hCompletionPort, (ULONG_PTR)m_listenSocket, 0);// 使用AcceptEx异步接受连接
7.2 添加缓存机制
// 简单的内存缓存实现
std::map<std::string, std::pair<std::string, time_t>> m_cache;std::string GetCachedContent(const std::string& path) {auto it = m_cache.find(path);if (it != m_cache.end()) {// 检查缓存是否过期(5分钟)if (time(NULL) - it->second.second < 300) {return it->second.first;}m_cache.erase(it);}// 读取文件内容并缓存std::ifstream file(path);std::stringstream buffer;buffer << file.rdbuf();std::string content = buffer.str();m_cache[path] = std::make_pair(content, time(NULL));return content;
}
7.3 启用Gzip压缩
// 添加Gzip压缩支持
#include <zlib.h>std::string CompressString(const std::string& str) {z_stream zs;memset(&zs, 0, sizeof(zs));if (deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) {return "";}zs.next_in = (Bytef*)str.data();zs.avail_in = str.size();int ret;char outbuffer[32768];std::string outstring;do {zs.next_out = reinterpret_cast<Bytef*>(outbuffer);zs.avail_out = sizeof(outbuffer);ret = deflate(&zs, Z_FINISH);if (outstring.size() < zs.total_out) {outstring.append(outbuffer, zs.total_out - outstring.size());}} while (ret == Z_OK);deflateEnd(&zs);if (ret != Z_STREAM_END) {return "";}return outstring;
}
八、项目总结
本VC++ HTTP服务器实现了以下功能:
-
核心HTTP协议支持:
- GET请求处理
- 静态文件服务
- 目录列表生成
- 简单CGI脚本执行
-
关键技术点:
- Winsock网络编程
- 多线程并发处理
- MIME类型识别
- URL解码
- HTTP响应生成
-
扩展能力:
- 支持HTTPS(通过OpenSSL)
- 支持POST请求
- 支持Gzip压缩
- 支持缓存机制
-
实际应用:
- 嵌入式设备Web管理界面
- 本地文件共享服务器
- 开发测试服务器
- 物联网设备控制接口
