VC++ 打造小型HTTP服务器

VC++ 打造小型HTTP服务器

一、概述

使用VC++实现一个轻量级HTTP服务器,支持基本的HTTP/1.1协议,能够处理静态文件请求、目录列表和简单的CGI脚本执行。服务器采用多线程架构,支持并发连接,适合作为嵌入式设备或小型应用的Web服务器。

二、系统架构

2.1 整体架构

graph TD
    A[客户端浏览器] -->|HTTP请求| B[主服务器]
    B -->|分发请求| C[工作线程1]
    B -->|分发请求| D[工作线程2]
    B -->|分发请求| E[工作线程3]
    C -->|文件请求| F[文件系统]
    D -->|CGI请求| G[CGI处理器]
    E -->|目录请求| H[目录列表生成器]

2.2 核心组件

  1. 主服务器模块:监听端口,接受连接,管理线程池
  2. 请求处理模块:解析HTTP请求,路由到相应处理器
  3. 文件服务模块:提供静态文件服务
  4. CGI处理器:执行简单脚本
  5. 目录列表模块:生成目录浏览页面
  6. 日志模块:记录访问日志和错误日志

三、核心源代码实现

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 4

class 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);
    
    // 解码URL
    path = 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);
            
            // 添加到HTML
            html << "<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 环境要求

4.2 编译步骤

  1. 创建新的Win32控制台应用程序项目

  2. 添加上述三个源文件(HttpServer.h, HttpServer.cpp, main.cpp)

  3. 配置项目属性:

    • 链接器 → 输入 → 附加依赖项:添加 ws2_32.lib
    • C/C++ → 预处理器 → 预处理器定义:添加 _CRT_SECURE_NO_WARNINGS
  4. 编译并运行

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/contentcst/122432.html

五、功能扩展

5.1 添加POST请求支持

// 在ProcessRequest函数中添加
else if (method == "POST") {
    // 解析Content-Length
    size_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 运行服务器

  1. 编译并运行HTTP服务器程序
  2. 打开浏览器访问:http://localhost:8080
  3. 浏览网站内容,测试各种功能

七、性能优化建议

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服务器实现了以下功能:

  1. 核心HTTP协议支持

    • GET请求处理
    • 静态文件服务
    • 目录列表生成
    • 简单CGI脚本执行
  2. 关键技术点

    • Winsock网络编程
    • 多线程并发处理
    • MIME类型识别
    • URL解码
    • HTTP响应生成
  3. 扩展能力

    • 支持HTTPS(通过OpenSSL)
    • 支持POST请求
    • 支持Gzip压缩
    • 支持缓存机制
  4. 实际应用

    • 嵌入式设备Web管理界面
    • 本地文件共享服务器
    • 开发测试服务器
    • 物联网设备控制接口

专注于matlab/simulink,电子电路,编程