加深理解HTTP协议,如何用C语言打造简易的HTTP服务器

文章详细介绍了HTTP协议的工作原理,区分了GET和POST请求,展示了如何使用C语言的epoll和libevent库实现一个基本的HTTP服务器,涉及I/O多路复用、请求处理和错误处理等内容。
摘要由CSDN通过智能技术生成

首先我们先看一下HTTP协议,以及为什么要定义这些内容。

http请求消息(浏览器发给服务器):
    1. 请求行: 说明请求类型, 要访问的资源以及使用的http版本;
    2. 请求头: 说明服务器要使用的附加信息;
    3. 空行:必须!, 即使没有请求数据;
    4. 请求数据: 也叫主体, 可以添加任意的其他数据;
 
    以下是浏览器发送给服务器的http协议头内容举例:
        GET /hello.c HTTP/1.1
        Host:localhost:2222
        User-Agent:Mozilla/5.0(X11;Ubuntu;Linux i686;rv:24.0)Gecko/201001	01 Firefox/24.0
        Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
        Accept-Language:zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
        Accept-Encoding:gzip,deflate
        Connection:keep-alive
        If-Modified-Since:Fri,18 Jul 2014 08:36:36 GMT
        \r\n
 
 
 
http响应消息(服务器发给浏览器):
    1. 状态行: 包括http协议版本号, 状态码, 状态信息;
    2. 消息报头: 说明客户端要使用的一些附加信息;
    3. 空行: 必须!
    4. 响应正文: 服务器返回给客户端的文本信息(或数据流);
 
    以下是服务器发送给浏览器的http协议头内容举例:
        HTTP/1.1 200 OK
        Server:xhttpd
        Date:Fri,18 Jul 2014 14:34:26 GMT
        Content-Type:text/plain;charset=iso-8859-1
        Content-Length:32
        Content-Language:zh-CN
        Last-Modified:Fri,18,Jul 2014 08:36:36 GMT
        Connection:close
        \r\n
 
 
提交密码信息用get请求,就会明文显示,而post则不会显示出涉密信息
    Get          请求指定的页面信息,并返回实体主体
    Post         向指定资源提交数据进行处理请求(例如提交表单或者上传文件)
                 数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改
 
Head         类似于get请求,但是响应消息没有内容,只是获得报头
Put          从客户端向浏览器传送的数据取代指定的文档内容
Delete       请求服务器删除指定的页面
Connect      HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器
Options      允许客户端查看浏览器的性能
Trace        回显服务器收到的请求,主要用于测试和诊断

这里是用C语言实现一个简单HTTP服务器的几个步骤:

  1. 创建一个socket,绑定到指定的端口进行监听
  2. 使用epoll或者libevent来监听socket的读事件
    • epoll方式:使用epoll_ctl注册socket的EPOLLIN事件,在循环中调用epoll_wait获取活动socket,根据事件类型调用处理函数。(仅限linux)
    • libevent方式: 使用event_new注册socket的读事件,event_base_dispatch进入循环,根据活动socket调用回调处理函数。(跨平台)
  3. 当有新的连接时,接受连接,创建一个新的socket与客户端通讯
  4. 读取客户端请求,解析HTTP请求的方法、路径等信息
  5. 根据请求路径调用相应的请求处理函数
  6. 请求处理函数读取请求数据,生成响应
  7. 向客户端socket发送HTTP响应
  8. 关闭与客户端的连接

下面是一个使用epoll实现的基本HTTP服务器的示例代码,能够处理请求资源文件和服务器运行目录文件名,并包含404错误页的处理。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

#define MAX_EVENTS 100
#define PORT 8080
#define MAX_BUF_SIZE 1024

void send_response(int client_fd, const char *response) {
    send(client_fd, response, strlen(response), 0);
}

void send_file(int client_fd, const char *filename) {
    FILE *file = fopen(filename, "r");
    if (file == NULL) {
        perror("Error opening file");
        return;
    }

    char buffer[MAX_BUF_SIZE];
    size_t bytesRead;

    while ((bytesRead = fread(buffer, 1, sizeof(buffer), file)) > 0) {
        send(client_fd, buffer, bytesRead, 0);
    }

    fclose(file);
}

void handle_request(int client_fd, const char *request) {
    char method[10];
    char path[MAX_BUF_SIZE];
    sscanf(request, "%s %s", method, path);

    // Check if the requested path is a file
    if (access(path, F_OK) == 0) {
        send_file(client_fd, path);
    } else {
        // File not found, send 404 response
        const char *not_found_response = "HTTP/1.1 404 Not Found\nContent-Type: text/html\n\n<html><body><h1>404 Not Found</h1></body></html>";
        send_response(client_fd, not_found_response);
    }
}

int main() {
    int server_fd, epoll_fd, nfds, n;
    struct sockaddr_in address;
    struct epoll_event ev, events[MAX_EVENTS];

    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 设置地址结构
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    // 绑定套接字
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("Bind failed");
        exit(EXIT_FAILURE);
    }

    // 监听
    if (listen(server_fd, 10) < 0) {
        perror("Listen failed");
        exit(EXIT_FAILURE);
    }

    // 创建 epoll 实例
    epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
        perror("Epoll creation failed");
        exit(EXIT_FAILURE);
    }

    // 将服务器套接字添加到 epoll 实例中
    ev.events = EPOLLIN;
    ev.data.fd = server_fd;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev) == -1) {
        perror("Epoll control error");
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port %d...\n", PORT);

    while (1) {
        // 等待事件发生
        nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (nfds == -1) {
            perror("Epoll wait error");
            exit(EXIT_FAILURE);
        }

        // 处理发生的事件
        for (n = 0; n < nfds; ++n) {
            if (events[n].data.fd == server_fd) {
                // 有新连接
                int new_socket = accept(server_fd, (struct sockaddr*)&address, (socklen_t*)&address);
                if (new_socket < 0) {
                    perror("Accept error");
                    exit(EXIT_FAILURE);
                }

                // 将新连接添加到 epoll 实例中
                ev.events = EPOLLIN;
                ev.data.fd = new_socket;
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &ev) == -1) {
                    perror("Epoll control error");
                    exit(EXIT_FAILURE);
                }

                printf("New connection, socket fd is %d, ip is : %s, port : %d\n",
                       new_socket, inet_ntoa(address.sin_addr), ntohs(address.sin_port));
            } else {
                // 读取数据并响应
                char buffer[MAX_BUF_SIZE];
                ssize_t bytesRead = recv(events[n].data.fd, buffer, sizeof(buffer), 0);

                if (bytesRead <= 0) {
                    // 关闭连接
                    close(events[n].data.fd);
                } else {
                    buffer[bytesRead] = '\0';
                    printf("Received message: %s", buffer);

                    // 处理 HTTP 请求
                    handle_request(events[n].data.fd, buffer);

                    // 关闭连接
                    close(events[n].data.fd);
                }
            }
        }
    }

    close(server_fd);
    return 0;
}

下面是使用libevent库实现的基本HTTP服务器示例代码,该服务器可以处理请求资源文件和服务器运行目录文件名,并包括404错误页的处理。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <event2/event.h>
#include <event2/http.h>
#include <event2/buffer.h>
#include <sys/stat.h>
#include <fcntl.h>

#define PORT 8080

void not_found_cb(struct evhttp_request *req, void *arg) {
    struct evbuffer *buf = evbuffer_new();
    if (buf == NULL) {
        fprintf(stderr, "Failed to create response buffer\n");
        return;
    }

    // 构建 404 错误响应
    evbuffer_add_printf(buf, "<html><body><h1>404 Not Found</h1></body></html>");

    // 发送响应
    evhttp_send_reply(req, HTTP_NOTFOUND, "Not Found", buf);

    // 释放资源
    evbuffer_free(buf);
}

void generic_handler(struct evhttp_request *req, void *arg) {
    // 获取请求的 URI
    const char *uri = evhttp_request_get_uri(req);

    // 构建请求文件路径
    char filename[256];
    snprintf(filename, sizeof(filename), ".%s", uri);

    // 打开文件
    int file_fd = open(filename, O_RDONLY);
    if (file_fd == -1) {
        // 文件不存在,使用 404 错误处理回调
        not_found_cb(req, arg);
        return;
    }

    // 读取文件内容
    struct stat file_info;
    fstat(file_fd, &file_info);
    char *file_contents = malloc(file_info.st_size);
    read(file_fd, file_contents, file_info.st_size);

    // 构建响应
    struct evbuffer *buf = evbuffer_new();
    evbuffer_add(buf, file_contents, file_info.st_size);

    // 发送响应
    evhttp_send_reply(req, HTTP_OK, "OK", buf);

    // 释放资源
    close(file_fd);
    free(file_contents);
    evbuffer_free(buf);
}

int main() {
    struct event_base *base;
    struct evhttp *http;

    // 初始化 libevent
    base = event_base_new();
    if (!base) {
        fprintf(stderr, "Couldn't create an event base: exiting\n");
        return 1;
    }

    // 创建 HTTP 服务器
    http = evhttp_new(base);
    if (!http) {
        fprintf(stderr, "couldn't create evhttp. Exiting.\n");
        return 1;
    }

    // 设置请求处理回调
    evhttp_set_cb(http, "/", generic_handler, NULL);
    evhttp_set_gencb(http, not_found_cb, NULL);

    // 监听端口
    if (evhttp_bind_socket(http, "0.0.0.0", PORT) != 0) {
        fprintf(stderr, "couldn't bind to port %d. Exiting.\n", PORT);
        return 1;
    }

    // 进入事件循环
    event_base_dispatch(base);

    // 释放资源
    evhttp_free(http);
    event_base_free(base);

    return 0;
}

这就是一个简单HTTP服务器的基本流程和架构。epoll和libevent都是比较高效的I/O多路复用库,可以用来处理大量并发连接。具体实现要处理许多细节,比如解析请求,生成响应,访问服务器资源等。

PS:本文用于学习和加深理解http以及网络编程,共勉!

  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值