基于C语言的简单HTTP Web服务器实现

发布于:2025-03-14 ⋅ 阅读:(18) ⋅ 点赞:(0)

1. 概述

本案例使用C语言实现了一个简单的HTTP服务器,能够处理客户端的GET请求,并返回静态文件(如HTML、图片等)。在此案例中案例,我们主要使用的知识点有:

  • Socket编程:基于TCP协议的Socket通信。

  • HTTP协议:HTTP请求和响应的基本格式。

  • 多线程:使用多线程处理客户端请求。

  • 文件操作:读取本地文件并发送给客户端。

  • MIME类型:根据文件扩展名设置正确的Content-Type


2. 主要知识点

2.1 Socket编程

Socket是网络通信的基础,本案例使用Windows下的Socket API(winsock2.h)实现TCP通信。主要函数包括:

  • WSAStartup:初始化Winsock库。

  • socket:创建套接字。

  • bind:绑定套接字到本地地址和端口。

  • listen:监听客户端连接。

  • accept:接受客户端连接。

  • send/recv:发送和接收数据。

  • closesocket:关闭套接字。

2.2 HTTP协议

HTTP是一种无状态的请求-响应协议。本案例实现了HTTP/1.0的基本功能:

  • 请求格式

  • GET /path HTTP/1.0
    Host: 127.0.0.1:8080

    响应格式

  • HTTP/1.0 200 OK
    Content-Type: text/html
    
    <html>...</html>

    2.3 多线程

    为了支持多个客户端同时连接,本案例使用Windows的CreateThread函数创建新线程处理每个客户端请求。

2.4 文件操作

服务器需要读取本地文件并发送给客户端。本案例使用fopenfread等函数操作文件。

2.5 MIME类型

根据文件扩展名设置正确的Content-Type,例如:

  • .html -> text/html

  • .jpg -> image/jpeg

  • .png -> image/png

3. 实现思路

3.1 服务器启动流程

  1. 初始化Winsock库:调用WSAStartup初始化网络通信。

  2. 创建套接字:调用socket创建TCP套接字。

  3. 绑定地址和端口:调用bind绑定套接字到本地地址和端口。

  4. 监听连接:调用listen开始监听客户端连接。

  5. 接受连接:调用accept接受客户端连接,并为每个连接创建新线程。

3.2 处理客户端请求

  1. 读取请求行:从客户端读取HTTP请求的第一行,解析请求方法和URL。

  2. 解析URL:根据URL确定请求的文件路径。

  3. 检查文件是否存在:使用stat函数检查文件是否存在。

  4. 发送响应头:根据文件类型设置Content-Type,并发送HTTP响应头。

  5. 发送文件内容:读取文件内容并发送给客户端。

3.3 多线程处理

每个客户端连接由一个独立的线程处理,避免阻塞主线程。

4. 代码细节分析

4.1 初始化网络和创建套接字

int startup(unsigned short* port) {
    WSADATA wsaData;
    int ret = WSAStartup(MAKEWORD(1, 1), &wsaData);
    if (ret) {
        printf("初始化网络通信失败\n");
        return -1;
    }

    int server_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (server_sock == INVALID_SOCKET) {
        error_die("socket()失败");
    }

    // 设置端口复用
    int opt = 1;
    setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt));

    // 绑定地址和端口
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(*port);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    ret = bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr));
    if (ret == SOCKET_ERROR) {
        error_die("bind()失败");
    }

    // 动态分配端口
    if (*port == 0) {
        int len = sizeof(server_addr);
        getsockname(server_sock, (struct sockaddr*)&server_addr, &len);
        *port = ntohs(server_addr.sin_port);
    }

    // 监听连接
    ret = listen(server_sock, 5);
    if (ret == SOCKET_ERROR) {
        error_die("listen()失败");
    }

    return server_sock;
}

4.2 读取HTTP请求

int get_line(int sock, char* buf, int size) {
    int i = 0;
    char c = 0;
    while (i < size - 1 && c != '\n') {
        int n = recv(sock, &c, 1, 0);
        if (n <= 0) break;
        if (c == '\r') {
            n = recv(sock, &c, 1, MSG_PEEK);
            if (n > 0 && c == '\n') recv(sock, &c, 1, 0);
            c = '\n';
        }
        buf[i++] = c;
    }
    buf[i] = '\0';
    return i;
}

4.3 处理客户端请求

DWORD WINAPI accept_request(LPVOID arg) {
    int client = (SOCKET)arg;
    char buf[1024], method[255], url[255], path[255];
    int numchars = get_line(client, buf, sizeof(buf));

    // 解析请求方法和URL
    sscanf(buf, "%s %s", method, url);

    // 检查请求方法
    if (_stricmp(method, "GET") && _stricmp(method, "POST")) {
        unimplemented(client);
        return 0;
    }

    // 构造文件路径
    sprintf(path, "htdocs%s", url);
    if (path[strlen(path) - 1] == '/') strcat(path, "index.html");

    // 检查文件是否存在
    struct stat st;
    if (stat(path, &st) == -1) {
        while ((numchars > 0) && strcmp("\n", buf))
            numchars = get_line(client, buf, sizeof(buf));
        not_found(client);
    } else {
        if ((st.st_mode & S_IFMT) == S_IFDIR) strcat(path, "/index.html");
        server_file(client, path);
    }

    closesocket(client);
    return 0;
}

4.4 发送文件内容

void cat(int client_sock, FILE* resource) {
    char buf[4096];
    int count = 0;
    while (1) {
        int ret = fread(buf, sizeof(char), sizeof(buf), resource);
        if (ret <= 0) break;
        send(client_sock, buf, ret, 0);
        count += ret;
    }
    printf("总共发送了%d字节\n", count);
}

5. 总结

      通过这个案例,我们实现了一个简单的HTTP服务器,支持静态文件的请求和响应。核心知识点包括Socket编程、HTTP协议、多线程和文件操作。这个案例是学习网络编程的入门项目,后续可以扩展支持更多功能,如POST请求、动态内容生成等。

静态资源的访问位置记得改成自己的,这是我存放的静态资源位置。

如果edge浏览器访问不了可以多刷新几次,或者使用谷歌等其他浏览器。

如果通过路径访问的资源不存在,则返回404信息

案例完整代码如下:

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")

#include <string.h>
#include <ctype.h>
#include <sys/stat.h> //访问文件的属性

#define PRINTF(str) printf("[%s - %d] "#str" = %s\r\n",__func__,__LINE__,str);

#define ISspace(x) isspace((int)(x))

void error_die(const char* msg) {
    // 打印错误信息
    printf("%s\n", msg);
    // 退出程序
    exit(1);
}

// 初始化网络并创建服务端的套接字
int startup(unsigned short* port) {
    // 1. 网络通信初始化
    WSADATA wsaData;
    int ret = WSAStartup(MAKEWORD(1, 1), &wsaData);
    if (ret) {
        printf("初始化网络通信失败\n");
        return -1;
    }

    // 2. 创建服务端的套接字
    int server_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (server_sock == INVALID_SOCKET) {
        error_die("socket()失败");
    }

    // 设置端口号可复用
    int opt = 1;
    ret = setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt));
    if (ret == -1) {
        error_die("setsockopt()失败");
    }

    // 配置服务端套接字地址
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(struct sockaddr_in)); // 清空结构体
    server_addr.sin_family = AF_INET; // 地址族,这里是IPv4
    server_addr.sin_port = htons(*port); // 端口号
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址,这里是任意IP

    // 绑定套接字与服务端地址
    ret = bind(server_sock, (struct sockaddr*)&server_addr, sizeof(struct sockaddr));
    if (ret == SOCKET_ERROR) {
        error_die("bind()失败");
    }

    // 动态分配一个端口号
    if (*port == 0) {
        int len = sizeof(struct sockaddr);
        getsockname(server_sock, (struct sockaddr*)&server_addr, &len);
        *port = ntohs(server_addr.sin_port);
    }

    // 创建监听队列
    ret = listen(server_sock, 5);
    if (ret == SOCKET_ERROR) {
        error_die("listen()失败");
    }

    return server_sock; // 返回server_sock而不是0
}

//返回从套接字读取一行信息,并把数据存入buf中
int get_line(int sock, char* buf, int size) {
    int i = 0;
    int n;
    char c = 0;

    while (i < size - 1 && c != '\n') {
        n = recv(sock, &c, 1, 0);
        if (n <= 0) {
            // 连接关闭或出错,结束循环
            break;
        }

        if (c == '\r') {
            // 查看下一个字符是否是'\n'
            char next_char;
            n = recv(sock, &next_char, 1, MSG_PEEK);
            if (n > 0 && next_char == '\n') {
                // 读取并消耗'\n'
                recv(sock, &next_char, 1, 0);
            }
            c = '\n'; // 统一转换为换行符
        }

        buf[i++] = c;

        if (c == '\n') {
            break; // 换行符结束行读取
        }
    }

    buf[i] = '\0'; // 添加字符串终止符
    return i; // 返回读取的字符数(不含终止符)
}

void unimplemented(int client_sock) {
    // 发送501响应
    char buf[1024];

    strcpy(buf, "HTTP/1.0 501 Method Not Implemented\r\n");
    send(client_sock, buf, strlen(buf), 0);

    strcpy(buf, "Server: RockHTTP/0.1 libcurl/7.22.0\r\n");
    send(client_sock, buf, strlen(buf), 0);

    strcpy(buf, "Content-Type: text/html\r\n");
    send(client_sock, buf, strlen(buf), 0);

    strcpy(buf, "\r\n");
    send(client_sock, buf, strlen(buf), 0);

    // 发送501页面
    char unimplemented_html[] = "<HTML><HEAD><TITLE>Method Not Implemented</TITLE></HEAD><BODY><H1>501 Method Not Implemented</H1></BODY></HTML>";
    send(client_sock, unimplemented_html, strlen(unimplemented_html), 0);
}

void not_found(int client_sock) {
    // 发送404响应
    char buf[1024];

    strcpy(buf, "HTTP/1.0 404 Not Found\r\n");
    send(client_sock, buf, strlen(buf), 0);

    strcpy(buf, "Server: RockHTTP/0.1 libcurl/7.22.0\r\n");
    send(client_sock, buf, strlen(buf), 0);

    sprintf(buf, "Content-Type: text/html\r\n");
    send(client_sock, buf, strlen(buf), 0);

    strcpy(buf, "\r\n");
    send(client_sock, buf, strlen(buf), 0);

    // 发送404页面
    char not_found_html[] = "<HTML><HEAD><TITLE>Not Found</TITLE></HEAD><BODY><H1>404 Not Found</H1></BODY></HTML>";
    send(client_sock, not_found_html, strlen(not_found_html), 0);
}

const char* get_content_type(const char* path) {
    const char* last_dot = strrchr(path, '.');
    if (last_dot) {
        if (strcmp(last_dot, ".html") == 0 || strcmp(last_dot, ".htm") == 0) {
            return "text/html";
        }
        else if (strcmp(last_dot, ".jpg") == 0 || strcmp(last_dot, ".jpeg") == 0) {
            return "image/jpeg";
        }
        else if (strcmp(last_dot, ".png") == 0) {
            return "image/png";
        }
        else if (strcmp(last_dot, ".gif") == 0) {
            return "image/gif";
        }
        else if (strcmp(last_dot, ".css") == 0) {
            return "text/css";
        }
        else if (strcmp(last_dot, ".js") == 0) {
            return "application/javascript";
        }
    }
    return "text/plain";
}

void headers(int client_sock, const char* path) {
    // 发送HTTP头部                         
    char buf[1024];

    strcpy(buf, "HTTP/1.0 200 OK\r\n");
    send(client_sock, buf, strlen(buf), 0);

    sprintf(buf, "Content-Type: %s\r\n", get_content_type(path));
    send(client_sock, buf, strlen(buf), 0);

    strcpy(buf, "\r\n");
    send(client_sock, buf, strlen(buf), 0);
}

void cat(int client_sock, FILE* resource) {
    char buf[4096];
    int count = 0;

    while (1) {
        int ret = fread(buf, sizeof(char), sizeof(buf), resource);
        if (ret <= 0) {
            break;
        }
        send(client_sock, buf, ret, 0);
        count += ret;
    }

    printf("总共发送了%d字节\n", count);
}

void server_file(int client_sock, const char* fileName) {
    char numchars = 1;
    char buf[1024];

    // 将请求包剩余数据读完,直到遇到换行符
    while (numchars > 0 && strcmp(buf, "\n")) {
        numchars = get_line(client_sock, buf, sizeof(buf));
        PRINTF(buf);
    }

    // 发送文件内容
    FILE* resource = fopen(fileName, "rb"); // 以二进制模式打开文件
    if (resource == NULL) {
        printf("文件打开失败\n");
        not_found(client_sock);
    }
    else {
        // 返回数据给浏览器
        headers(client_sock, fileName);

        // 发送请求的资源
        cat(client_sock, resource);

        printf("资源发送完毕\n");
    }

    fclose(resource);
}

// 处理客户端的连接请求 
DWORD WINAPI accept_request(LPVOID arg) {
    char buf[1024];
    int numchars;
    char method[255];
    char url[255];
    char path[255];
    size_t i, j;

    struct stat st;
    int cgi = 0;

    int client = (SOCKET)arg;

    // 读取一行信息
    numchars = get_line(client, buf, sizeof(buf));
    printf("read %d bytes of data from client\n", numchars);
    PRINTF(buf);

    char* query_string = NULL;

    i = 0; j = 0;
    while (!ISspace(buf[j]) && (i < sizeof(method) - 1)) {
        method[i] = buf[j];
        i++;
        j++;
    }
    method[i] = 0;  // 解析后, method的值:"GET"或者"POST"
    PRINTF(method);

    // 判断是否为GET或POST请求
    if (_stricmp(method, "GET") && _stricmp(method, "POST")) {
        unimplemented(client);
        return 0;
    }

    // 判断是否为CGI请求
    if (_stricmp(method, "POST") == 0)
        cgi = 1;

    // 解析URL,获得资源路径
    i = 0;
    while (ISspace(buf[j]) && (j < sizeof(buf))) // 跳过buff中的空格
        j++;
    while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf))) // 获得资源url 比如 / 或者 /images/head.png
    {
        url[i] = buf[j];
        i++; j++;
    }
    url[i] = '\0';
    PRINTF(url);

    sprintf(path, "htdocs%s", url);
    if (path[strlen(path) - 1] == '/')
        strcat(path, "index.html"); // 如果路径以"/"结尾,则认为是目录,拼接上默认的HTML文件名
    PRINTF(path);

    struct stat status;

    // 检查访问的资源是否存在
    if (stat(path, &st) == -1) {  // stat获取指定文件的属性信息
        // 如果不能访问它的属性信息,那么这个文件就不存在
        // 此时,就需要把这个请求报文,读完!虽然已经没有用了,但是也要把这个报文读完
        while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
            numchars = get_line(client, buf, sizeof(buf));
        not_found(client);
    }
    else {
        // 如果浏览器的地址输入:http://127.0.0.1:8000/movies 
        // 如果movies是目录,就默认访问这个目录下的index.html
        if ((st.st_mode & S_IFMT) == S_IFDIR)
            strcat(path, "/index.html");

        server_file(client, path);
    }

    closesocket(client);
    return 0;
}

int main() {
    // httpd默认的端口是80,这里指定了8000端口,也可以使用其它端口
    unsigned short port = 8080;

    // 初始化网络,并使用指定端口来创建服务端的套接字
    int server_sock = startup(&port);
    printf("httpd running on port %d\n", port);

    while (1) {
        // 等待客户端的连接
        struct sockaddr_in client_addr;
        int client_len = sizeof(struct sockaddr);

        // 阻塞式等待客户端的连接
        int client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_len);
        if (client_sock == -1) {
            error_die("accept"); // 打印错误信息并结束
        }

        // 创建一个线程来处理客户端请求
        DWORD threadId = 0;

        HANDLE handleFirst = CreateThread(NULL, 0, accept_request, (void*)client_sock, 0, &threadId);
        if (handleFirst == NULL) {
            error_die("CreateThread()失败");
        }
    }

    return 0;
}