<think>根据您提供的项目结构,我们将实现一个基于epoll的简单Web服务器。服务器将监听80端口,并能够处理GET和POST请求。我们将使用非阻塞I/O和边缘触发模式(EPOLLET)来提高性能。
步骤:
1. 创建监听套接字,绑定到80端口,并设置为非阻塞模式。
2. 创建epoll实例,并将监听套接字加入epoll,关注EPOLLIN事件(同时使用边缘触发)。
3. 进入主循环,等待epoll事件。
4. 当有新的连接时,接受连接,并将新的客户端套接字加入epoll(同样设置为非阻塞和边缘触发)。
5. 当客户端套接字有数据可读时,读取请求并解析(这里我们只处理GET和POST请求)。
6. 根据请求的URL,构建本地文件路径(在web目录下查找文件)。
7. 对于GET请求,发送文件内容;对于POST请求,我们简单处理(例如,可以打印请求体,然后返回一个响应)。
8. 注意处理大文件发送(使用sendfile)和错误处理(如文件不存在返回404)。
9. 使用Makefile构建项目。
注意:由于要监听80端口,程序可能需要以root权限运行(或者赋予可执行文件CAP_NET_BIND_SERVICE能力)。
项目结构:
- include/common.h:包含公共定义和函数声明。
- src/common.c:包含公共函数实现(如路径处理、发送文件等)。
- src/epoll_serv.c:主服务器实现。
- web/:Web根目录,包含HTML、CSS、JS和图片等。
我们将按照以下步骤实现:
1. 修改common.h,定义ROOT_DIR为"../web/"(因为可执行文件在bin目录下,而web目录在项目根目录,所以相对路径是../web/)。但为了灵活性,我们会在Makefile中通过编译选项定义ROOT_DIR。
2. 实现common.c中的辅助函数,如构建安全路径、发送文件、发送错误响应等。
3. 实现epoll_serv.c,包含主函数和事件循环。
4. 编写Makefile,编译生成epoll_serv可执行文件。
由于代码较长,我们将分模块实现。
首先,修改common.h:</think># 基于 epoll 的 Web 服务器实现
下面我将为您实现一个基于 epoll I/O 复用的 Web 服务器,使用非阻塞 I/O 和边缘触发模式,支持 GET/POST 请求处理,兼容各种浏览器。
## 项目结构
根据您提供的目录结构,我将实现以下文件:
```
.
├── bin
│ └── webserver (构建后的可执行文件)
├── include
│ └── common.h
├── Makefile
├── src
│ ├── common.c
│ └── webserver.c
└── web
└── ... (您的网页文件)
```
## 实现代码
### 1. include/common.h
```c
#ifndef __COMMON_H__
#define __COMMON_H__
#define _GNU_SOURCE
#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/sendfile.h>
#include <sys/types.h>
#include <dirent.h>
#include <errno.h>
#include <sys/wait.h>
#include <signal.h>
#include <sys/epoll.h>
#include <time.h>
#include <ctype.h>
#define PORT 80
#define MAX_EVENTS 1024
#define BUFFER_SIZE 4096
#define MAX_PATH_LEN 1024
#define MAX_HEADERS 20
// Web根目录 - 在编译时由Makefile定义
#ifndef ROOT_DIR
#define ROOT_DIR "../web/"
#endif
// HTTP状态码
#define HTTP_200 "200 OK"
#define HTTP_400 "400 Bad Request"
#define HTTP_404 "404 Not Found"
#define HTTP_500 "500 Internal Server Error"
#define HTTP_501 "501 Not Implemented"
// 连接状态
typedef enum {
CONN_READING,
CONN_WRITING,
CONN_CLOSING
} conn_state_t;
// HTTP请求结构
typedef struct {
char method[16]; // GET, POST等
char path[MAX_PATH_LEN]; // 请求路径
char version[16]; // HTTP/1.1
char *headers[MAX_HEADERS]; // 请求头
int header_count; // 请求头数量
char *body; // 请求体
size_t body_length; // 请求体长度
} http_request_t;
// HTTP响应结构
typedef struct {
int status; // 状态码
char *content_type; // 内容类型
size_t content_length; // 内容长度
int fd; // 文件描述符
} http_response_t;
// 连接上下文
typedef struct {
int fd; // 文件描述符
conn_state_t state; // 连接状态
http_request_t request; // HTTP请求
http_response_t response; // HTTP响应
char buffer[BUFFER_SIZE]; // 读写缓冲区
size_t buffer_len; // 缓冲区数据长度
size_t bytes_sent; // 已发送字节数
} connection_t;
// 函数声明
int set_nonblocking(int fd);
void handle_request(connection_t *conn);
void send_response(connection_t *conn);
void send_error(connection_t *conn, const char *status);
const char *get_mime_type(const char *path);
int build_safe_path(char *dest, size_t size, const char *requested_path);
#endif
```
### 2. src/common.c
```c
#include "common.h"
#include <libgen.h>
#include <limits.h>
// 设置文件描述符为非阻塞模式
int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) return -1;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
// 根据文件扩展名获取MIME类型
const char *get_mime_type(const char *path) {
const char *ext = strrchr(path, '.');
if (!ext) return "application/octet-stream";
ext++;
if (strcasecmp(ext, "html") == 0 || strcasecmp(ext, "htm") == 0)
return "text/html";
if (strcasecmp(ext, "css") == 0)
return "text/css";
if (strcasecmp(ext, "js") == 0)
return "application/javascript";
if (strcasecmp(ext, "json") == 0)
return "application/json";
if (strcasecmp(ext, "jpg") == 0 || strcasecmp(ext, "jpeg") == 0)
return "image/jpeg";
if (strcasecmp(ext, "png") == 0)
return "image/png";
if (strcasecmp(ext, "gif") == 0)
return "image/gif";
if (strcasecmp(ext, "ico") == 0)
return "image/x-icon";
return "application/octet-stream";
}
// 构建安全路径,防止目录遍历攻击
int build_safe_path(char *dest, size_t size, const char *requested_path) {
char full_path[PATH_MAX];
char resolved_path[PATH_MAX];
// 构建基础路径
snprintf(full_path, sizeof(full_path), "%s%s", ROOT_DIR, requested_path);
// 解析路径(处理 ../ 等)
if (realpath(full_path, resolved_path) == NULL) {
return -1; // 路径无效
}
// 检查是否在根目录内
size_t root_len = strlen(ROOT_DIR);
if (strncmp(resolved_path, ROOT_DIR, root_len) != 0) {
return -2; // 路径遍历攻击
}
// 复制到目标缓冲区
if (strlen(resolved_path) >= size) {
return -3; // 缓冲区太小
}
strcpy(dest, resolved_path);
return 0;
}
// 发送错误响应
void send_error(connection_t *conn, const char *status) {
const char *html_fmt =
"<html><head><title>Error</title></head>"
"<body><h1>%s</h1></body></html>";
char body[BUFFER_SIZE];
int len = snprintf(body, sizeof(body), html_fmt, status);
// 构建响应头
char headers[BUFFER_SIZE];
int hlen = snprintf(headers, sizeof(headers),
"HTTP/1.1 %s\r\n"
"Content-Type: text/html\r\n"
"Content-Length: %d\r\n"
"Connection: close\r\n\r\n",
status, len);
// 合并响应
memcpy(conn->buffer, headers, hlen);
memcpy(conn->buffer + hlen, body, len);
conn->buffer_len = hlen + len;
conn->state = CONN_WRITING;
conn->response.fd = -1; // 没有文件要发送
}
// 发送文件响应
void send_file_response(connection_t *conn, const char *path) {
struct stat st;
if (stat(path, &st) < 0 || !S_ISREG(st.st_mode)) {
send_error(conn, HTTP_404);
return;
}
// 打开文件
int fd = open(path, O_RDONLY);
if (fd < 0) {
send_error(conn, HTTP_500);
return;
}
// 构建响应头
const char *mime_type = get_mime_type(path);
char headers[BUFFER_SIZE];
int hlen = snprintf(headers, sizeof(headers),
"HTTP/1.1 %s\r\n"
"Content-Type: %s\r\n"
"Content-Length: %ld\r\n"
"Connection: keep-alive\r\n\r\n",
HTTP_200, mime_type, st.st_size);
// 将响应头放入缓冲区
memcpy(conn->buffer, headers, hlen);
conn->buffer_len = hlen;
conn->bytes_sent = 0;
// 设置文件响应
conn->response.fd = fd;
conn->response.content_length = st.st_size;
conn->state = CONN_WRITING;
}
// 处理GET请求
void handle_get(connection_t *conn) {
char safe_path[MAX_PATH_LEN];
// 默认请求index.html
const char *path = conn->request.path;
if (strcmp(path, "/") == 0) {
path = "/Index.html";
}
// 构建安全路径
if (build_safe_path(safe_path, sizeof(safe_path), path) != 0) {
send_error(conn, HTTP_404);
return;
}
// 发送文件
send_file_response(conn, safe_path);
}
// 处理POST请求
void handle_post(connection_t *conn) {
// 简单的POST处理示例
// 在实际应用中,这里可以处理表单提交等
const char *response =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: 12\r\n"
"Connection: close\r\n\r\n"
"POST received";
memcpy(conn->buffer, response, strlen(response));
conn->buffer_len = strlen(response);
conn->state = CONN_WRITING;
}
// 解析HTTP请求
int parse_request(connection_t *conn) {
char *buf = conn->buffer;
size_t len = conn->buffer_len;
// 确保有完整的第一行
char *end = strstr(buf, "\r\n");
if (!end) return -1; // 不完整的请求
// 解析请求行
char method[16], path[MAX_PATH_LEN], version[16];
if (sscanf(buf, "%15s %1023s %15s", method, path, version) != 3) {
return -1; // 格式错误
}
// 保存请求信息
strncpy(conn->request.method, method, sizeof(conn->request.method));
strncpy(conn->request.path, path, sizeof(conn->request.path));
strncpy(conn->request.version, version, sizeof(conn->request.version));
// 解析头部
char *ptr = end + 2; // 跳过第一个\r\n
conn->request.header_count = 0;
while ((ptr - buf) < len) {
char *line_end = strstr(ptr, "\r\n");
if (!line_end) break;
// 空行表示头部结束
if (line_end == ptr) {
ptr += 2;
break;
}
// 保存头部行
if (conn->request.header_count < MAX_HEADERS) {
*line_end = '\0'; // 终止字符串
conn->request.headers[conn->request.header_count++] = ptr;
}
ptr = line_end + 2;
}
// 处理请求体
conn->request.body = ptr;
conn->request.body_length = len - (ptr - buf);
return 0;
}
// 处理请求
void handle_request(connection_t *conn) {
// 解析HTTP请求
if (parse_request(conn) < 0) {
send_error(conn, HTTP_400);
return;
}
// 根据方法处理请求
if (strcasecmp(conn->request.method, "GET") == 0) {
handle_get(conn);
} else if (strcasecmp(conn->request.method, "POST") == 0) {
handle_post(conn);
} else {
send_error(conn, HTTP_501);
}
}
```
### 3. src/webserver.c
```c
#include "common.h"
#include <sys/epoll.h>
#include <signal.h>
// 全局变量
connection_t *connections = NULL;
int epoll_fd = -1;
// 清理连接
void cleanup_connection(connection_t *conn) {
if (conn->response.fd != -1) {
close(conn->response.fd);
conn->response.fd = -1;
}
if (conn->fd != -1) {
close(conn->fd);
conn->fd = -1;
}
memset(conn, 0, sizeof(connection_t));
conn->fd = -1;
conn->state = CONN_CLOSING;
}
// 添加事件到epoll
void epoll_ctl_add(int epoll_fd, int fd, uint32_t events) {
struct epoll_event ev;
ev.events = events;
ev.data.fd = fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) {
perror("epoll_ctl");
exit(EXIT_FAILURE);
}
}
// 修改epoll事件
void epoll_ctl_mod(int epoll_fd, int fd, uint32_t events) {
struct epoll_event ev;
ev.events = events;
ev.data.fd = fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_MOD, fd, &ev) == -1) {
perror("epoll_ctl_mod");
exit(EXIT_FAILURE);
}
}
// 初始化服务器
int init_server() {
int server_fd;
struct sockaddr_in addr;
// 创建套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置SO_REUSEADDR
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
// 绑定地址
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听
if (listen(server_fd, SOMAXCONN) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d\n", PORT);
printf("Web root: %s\n", ROOT_DIR);
return server_fd;
}
// 处理新连接
void handle_new_connection(int server_fd) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd;
while ((client_fd = accept4(server_fd, (struct sockaddr *)&client_addr,
&client_len, SOCK_NONBLOCK)) > 0) {
// 找到空闲连接槽
int i;
for (i = 0; i < MAX_EVENTS; i++) {
if (connections[i].fd == -1) break;
}
if (i == MAX_EVENTS) {
close(client_fd);
continue;
}
// 初始化连接
connection_t *conn = &connections[i];
memset(conn, 0, sizeof(connection_t));
conn->fd = client_fd;
conn->state = CONN_READING;
conn->response.fd = -1;
conn->buffer_len = 0;
conn->bytes_sent = 0;
// 添加到epoll
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
ev.data.ptr = conn;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev);
// 打印连接信息
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, ip_str, INET_ADDRSTRLEN);
printf("New connection from %s:%d\n", ip_str, ntohs(client_addr.sin_port));
}
}
// 处理可读事件
void handle_readable(connection_t *conn) {
ssize_t count;
// 读取数据
count = read(conn->fd, conn->buffer + conn->buffer_len,
sizeof(conn->buffer) - conn->buffer_len - 1);
if (count == -1) {
if (errno != EAGAIN) {
cleanup_connection(conn);
}
return;
} else if (count == 0) {
// 客户端关闭连接
cleanup_connection(conn);
return;
}
conn->buffer_len += count;
conn->buffer[conn->buffer_len] = '\0';
// 检查是否收到完整请求
if (strstr(conn->buffer, "\r\n\r\n") != NULL) {
handle_request(conn);
// 修改epoll事件为写事件
struct epoll_event ev;
ev.events = EPOLLOUT | EPOLLET;
ev.data.ptr = conn;
epoll_ctl(epoll_fd, EPOLL_CTL_MOD, conn->fd, &ev);
}
}
// 处理可写事件
void handle_writable(connection_t *conn) {
// 发送响应头
if (conn->buffer_len > 0) {
ssize_t sent = write(conn->fd, conn->buffer, conn->buffer_len);
if (sent == -1) {
if (errno != EAGAIN) {
cleanup_connection(conn);
}
return;
}
// 更新缓冲区
memmove(conn->buffer, conn->buffer + sent, conn->buffer_len - sent);
conn->buffer_len -= sent;
if (conn->buffer_len > 0) {
// 还有数据要发送,保持写事件
return;
}
}
// 发送文件内容
if (conn->response.fd != -1) {
off_t offset = conn->bytes_sent;
size_t remaining = conn->response.content_length - conn->bytes_sent;
size_t to_send = remaining > BUFFER_SIZE ? BUFFER_SIZE : remaining;
ssize_t sent = sendfile(conn->fd, conn->response.fd, &offset, to_send);
if (sent == -1) {
if (errno != EAGAIN) {
cleanup_connection(conn);
}
return;
}
conn->bytes_sent += sent;
if (conn->bytes_sent < conn->response.content_length) {
// 文件未发送完,保持写事件
return;
}
// 文件发送完成
close(conn->response.fd);
conn->response.fd = -1;
}
// 请求处理完成,准备接收新请求
conn->state = CONN_READING;
conn->buffer_len = 0;
// 修改epoll事件为读事件
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
ev.data.ptr = conn;
epoll_ctl(epoll_fd, EPOLL_CTL_MOD, conn->fd, &ev);
}
// 主函数
int main() {
int server_fd = init_server();
// 创建epoll实例
epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
// 分配连接数组
connections = calloc(MAX_EVENTS, sizeof(connection_t));
for (int i = 0; i < MAX_EVENTS; i++) {
connections[i].fd = -1;
}
// 添加服务器套接字到epoll
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = server_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev);
// 事件循环
struct epoll_event events[MAX_EVENTS];
printf("Server started. Waiting for connections...\n");
while (1) {
int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (n == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}
for (int i = 0; i < n; i++) {
// 新连接
if (events[i].data.fd == server_fd) {
handle_new_connection(server_fd);
}
// 客户端事件
else {
connection_t *conn = (connection_t *)events[i].data.ptr;
if (events[i].events & EPOLLERR) {
// 错误事件,关闭连接
cleanup_connection(conn);
continue;
}
if (events[i].events & EPOLLIN) {
// 可读事件
handle_readable(conn);
}
if (events[i].events & EPOLLOUT) {
// 可写事件
handle_writable(conn);
}
}
}
}
// 清理(理论上不会执行到这里)
close(server_fd);
close(epoll_fd);
free(connections);
return 0;
}
```
### 4. Makefile
```makefile
# Makefile for epoll web server
# 编译器设置
CC = gcc
CFLAGS = -Wall -Wextra -O2 -I./include -std=c11 -D_GNU_SOURCE
LDFLAGS = -lpthread
# 根目录配置(开发环境)
WEB_ROOT ?= ../web/
# 源文件和目标
SRC_DIR = src
BIN_DIR = bin
SRCS = $(wildcard $(SRC_DIR)/*.c)
OBJS = $(patsubst $(SRC_DIR)/%.c,$(BIN_DIR)/%.o,$(SRCS))
TARGET = $(BIN_DIR)/webserver
# 添加根目录到编译选项
CFLAGS += -DROOT_DIR="\"$(WEB_ROOT)\""
# 默认目标
all: $(TARGET)
# 创建目录
$(shell mkdir -p $(BIN_DIR))
# 编译规则
$(BIN_DIR)/%.o: $(SRC_DIR)/%.c
$(CC) $(CFLAGS) -c $< -o $@
# 链接可执行文件
$(TARGET): $(OBJS)
$(CC) $^ -o $@ $(LDFLAGS)
# 清理
clean:
rm -f $(BIN_DIR)/*.o $(TARGET)
# 生产环境构建(使用绝对路径)
prod:
$(MAKE) WEB_ROOT=/var/www/html
# 运行服务器(需要sudo权限绑定80端口)
run: $(TARGET)
sudo $(TARGET)
.PHONY: all clean prod run
```
## 功能说明
1. **epoll I/O 复用**:
- 使用边缘触发(EPOLLET)模式和非阻塞I/O
- 高效处理大量并发连接
- 支持长连接(keep-alive)
2. **HTTP 请求处理**:
- 支持 GET/POST 方法
- 解析 HTTP 请求头和请求体
- 自动处理路径安全(防止目录遍历攻击)
3. **文件服务**:
- 自动识别文件 MIME 类型
- 使用 sendfile 高效传输文件
- 支持大文件传输
4. **错误处理**:
- 400 Bad Request
- 404 Not Found
- 500 Internal Server Error
- 501 Not Implemented
5. **浏览器兼容**:
- 支持 HTTP/1.1
- 兼容 Chrome, Firefox, IE8+ 等主流浏览器
## 构建与运行
1. **构建服务器**:
```bash
make
```
2. **运行服务器**(需要 root 权限绑定 80 端口):
```bash
sudo ./bin/webserver
```
3. **生产环境构建**(使用绝对路径):
```bash
make WEB_ROOT=/var/www/html
```
4. **访问网页**:
在浏览器中访问:
- `http://<your-server-ip>/Index.html`
- `http://<your-server-ip>/About.html`
- `http://<your-server-ip>/Work.html`
- `http://<your-server-ip>/Contact.html`
## 安全注意事项
1. **以非特权用户运行**:
在生产环境中,服务器启动后应降权到非 root 用户:
```c
// 在 main() 中绑定端口后添加
if (setuid(1000) < 0) { // 1000 是普通用户ID
perror("setuid");
exit(EXIT_FAILURE);
}
```
2. **目录遍历防护**:
`build_safe_path()` 函数使用 `realpath()` 规范化路径,并检查是否在根目录内
3. **资源限制**:
建议设置文件描述符限制:
```bash
ulimit -n 10000
```
4. **日志记录**:
在实际部署中应添加详细的访问日志和错误日志
这个实现提供了高性能的静态文件服务,支持基本的 GET/POST 请求处理,并考虑了浏览器兼容性和安全性问题。