- 监听指定端口。
- 接受客户端连接。
- 解析客户端发来的HTTP请求。
- 根据请求的路径,从本地文件系统读取文件内容。
- 作为HTTP响应发送回客户端。
- 支持处理404(文件未找到)错误。
- 支持并发处理多个客户端连接(使用
select实现)。
第一步:理解核心概念
在开始编码前,我们需要了解几个关键的C语言和网络库概念:

(图片来源网络,侵删)
- Socket (套接字):是网络编程的API,它允许程序发送和接收数据,我们可以把它想象成一个通信的端点。
sys/socket.h:创建、绑定、监听和接受socket连接的头文件。netinet/in.h:定义了IP地址和端口号的数据结构(如struct sockaddr_in)。arpa/inet.h:提供了IP地址转换函数(如inet_addr)。unistd.h:提供了文件描述符(socket也是一个文件描述符)操作函数,如read,write,close。select函数:这是一个I/O多路复用函数,它允许我们监视多个文件描述符(多个socket),当其中任何一个描述符“就绪”(有数据可读)时,select函数会返回,这是实现简单并发服务器的好方法。
第二步:项目结构
我们将创建一个名为simple_webserver.c的文件。
第三步:完整代码与详细解释
下面是完整的代码,并附有详细的注释来解释每一部分的功能。
#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 <sys/stat.h>
#include <fcntl.h>
// 定义服务器监听的端口
#define PORT 8080
// 定义缓冲区大小
#define BUFFER_SIZE 4096
// 定义最大同时连接的客户端数
#define MAX_CLIENTS 100
// 错误处理函数
void handle_error(const char *msg) {
perror(msg);
exit(EXIT_FAILURE);
}
// 发送HTTP响应
void send_http_response(int client_socket, int status_code, const char *status_msg, const char *content_type, const char *body) {
char response[BUFFER_SIZE];
int response_len;
// 构建HTTP响应头
response_len = snprintf(response, BUFFER_SIZE,
"HTTP/1.1 %d %s\r\n"
"Content-Type: %s\r\n"
"Content-Length: %ld\r\n"
"Connection: close\r\n" // 告诉客户端连接将在响应后关闭
"\r\n", // 空行,表示头部结束
status_code, status_msg, content_type, strlen(body));
// 发送响应头
if (send(client_socket, response, response_len, 0) < 0) {
handle_error("send() failed for header");
}
// 发送响应体
if (send(client_socket, body, strlen(body), 0) < 0) {
handle_error("send() failed for body");
}
}
// 处理客户端请求
void handle_client(int client_socket) {
char request_buffer[BUFFER_SIZE];
int bytes_received;
// 1. 接收客户端的HTTP请求
bytes_received = recv(client_socket, request_buffer, BUFFER_SIZE - 1, 0);
if (bytes_received <= 0) {
// 客户端关闭连接或出错
close(client_socket);
return;
}
request_buffer[bytes_received] = '\0'; // 确保字符串以null结尾
printf("Received request:\n%s\n", request_buffer);
// 2. 解析请求行,获取请求的文件路径
char method[16], path[256], version[16];
sscanf(request_buffer, "%15s %255s %15s", method, path, version);
// 3. 处理路径,默认为根目录下的index.html
if (strcmp(path, "/") == 0) {
strcpy(path, "/index.html");
}
// 移除路径开头的'/',以便与本地文件系统路径对应
char file_path[512];
snprintf(file_path, sizeof(file_path), "./www%s", path);
// 4. 尝试打开请求的文件
int file_fd = open(file_path, O_RDONLY);
if (file_fd < 0) {
// 文件不存在,发送404 Not Found
const char *not_found_body = "<html><body><h1>404 Not Found</h1><p>The requested resource was not found on this server.</p></body></html>";
send_http_response(client_socket, 404, "Not Found", "text/html", not_found_body);
} else {
// 文件存在,读取文件内容
struct stat file_stat;
if (fstat(file_fd, &file_stat) < 0) {
handle_error("fstat failed");
}
char *file_content = (char *)malloc(file_stat.st_size + 1);
if (read(file_fd, file_content, file_stat.st_size) < 0) {
handle_error("read failed");
}
file_content[file_stat.st_size] = '\0';
// 5. 根据文件扩展名确定Content-Type
const char *content_type = "text/plain"; // 默认类型
if (strstr(path, ".html")) {
content_type = "text/html";
} else if (strstr(path, ".css")) {
content_type = "text/css";
} else if (strstr(path, ".js")) {
content_type = "application/javascript";
} else if (strstr(path, ".jpg") || strstr(path, ".jpeg")) {
content_type = "image/jpeg";
} else if (strstr(path, ".png")) {
content_type = "image/png";
}
// 6. 发送200 OK响应和文件内容
send_http_response(client_socket, 200, "OK", content_type, file_content);
free(file_content);
close(file_fd);
}
// 7. 关闭客户端连接
close(client_socket);
}
int main() {
int server_socket, client_socket;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
fd_set read_fds; // 用于select的文件描述符集合
int max_fd; // 集合中最大的文件描述符
// 1. 创建TCP socket (AF_INET for IPv4, SOCK_STREAM for TCP)
server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket < 0) {
handle_error("socket() failed");
}
// 设置socket选项,允许地址重用,避免"Address already in use"错误
int opt = 1;
if (setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
handle_error("setsockopt() failed");
}
// 2. 绑定socket到指定端口和IP地址 (INADDR_ANY表示接受任何网络接口的连接)
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT); // htons将端口号从主机字节序转为网络字节序
if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
handle_error("bind() failed");
}
// 3. 开始监听连接
if (listen(server_socket, 10) < 0) { // 10是等待连接的队列长度
handle_error("listen() failed");
}
printf("Server listening on port %d...\n", PORT);
// 4. 主循环,使用select实现并发
while (1) {
// 清空文件描述符集合
FD_ZERO(&read_fds);
// 将服务器socket加入集合
FD_SET(server_socket, &read_fds);
max_fd = server_socket;
// 检查所有已连接的客户端socket
// 注意:为了简化,这里只处理了新连接和客户端数据。
// 一个更完整的实现会维护一个活跃客户端列表。
// 在这个简单版本中,我们只处理服务器socket上的新连接事件。
// 客户端数据读取在handle_client中同步处理。
// 使用select等待事件
if (select(max_fd + 1, &read_fds, NULL, NULL, NULL) < 0) {
handle_error("select() failed");
}
// 检查是否有新的连接请求
if (FD_ISSET(server_socket, &read_fds)) {
client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_socket < 0) {
perror("accept() failed");
continue; // 继续等待下一个连接
}
printf("Accepted connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// 处理客户端请求(这里同步处理,会阻塞直到请求完成)
// 在更高级的版本中,这里会创建一个线程或将socket加入一个队列,
// 然后立即accept下一个连接。
handle_client(client_socket);
}
}
// 理论上,上面的while(1)永远不会退出,所以这里不会执行
close(server_socket);
return 0;
}
第四步:如何编译和运行
-
创建一个
www目录:为了存放要提供的网页文件,我们需要在运行服务器的同一目录下创建一个www文件夹。mkdir www
-
在
www目录中创建一些测试文件:
(图片来源网络,侵删)www/index.html:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Welcome to My Simple Server!</title> <style> body { font-family: sans-serif; text-align: center; margin-top: 50px; } h1 { color: #333; } </style> </head> <body> <h1>Hello from a C Web Server!</h1> <p>This page is served by our custom C program.</p> <a href="/style.css">Check out the CSS</a> </body> </html>www/style.css:body { background-color: #f0f0f0; } h1 { color: #0056b3; } a { color: #d9534f; font-weight: bold; }
-
编译C代码:打开终端,使用
gcc编译simple_webserver.c。gcc simple_webserver.c -o my_webserver
-
运行服务器:
./my_webserver
你应该会看到输出:
Server listening on port 8080... -
测试服务器:
(图片来源网络,侵删)- 打开你的网页浏览器。
- 在地址栏输入
http://localhost:8080或http://127.0.0.1:8080。 - 你应该能看到
index.html的内容。 - 点击 "Check out the CSS" 链接,你应该能看到应用了CSS样式的页面。
- 尝试访问一个不存在的文件,
http://localhost:8080/notfound.html,你应该会看到一个 "404 Not Found" 错误页面。
第五步:进阶与改进
这个简单的服务器有很多可以改进的地方:
-
真正的并发:当前版本在
handle_client中是同步处理请求的,如果一个请求需要很长时间(比如一个大文件下载),服务器将无法处理其他客户端,改进方法:- 多线程/多进程:在
accept到一个新连接后,创建一个新的线程或进程来处理handle_client。 - I/O多路复用 (epoll/kqueue):在Linux上,
epoll是比select更高效的I/O多路复用机制,特别适合处理大量并发连接。
- 多线程/多进程:在
-
更健壮的HTTP解析:当前的解析使用
sscanf,非常脆弱,如果请求格式稍有不同,就会出错,应该编写一个更健壮的解析器,能够正确处理头部、Content-Length等。 -
支持HTTP/1.1持久连接:当前服务器在发送完响应后立即关闭连接,HTTP/1.1支持
Keep-Alive,可以在一个连接上处理多个请求,减少握手开销。 -
支持动态内容:目前只能提供静态文件,可以通过CGI(Common Gateway Interface)或集成一个简单的解释器(如Lua)来支持动态生成内容。
-
安全性:没有进行任何输入验证,可能存在路径遍历等安全风险,需要对客户端请求的路径进行清理。
-
性能优化:使用
sendfile系统调用在内核中直接将文件从一个文件描述符发送到另一个(socket),避免在用户空间和内核空间之间拷贝数据,能大大提高文件传输效率。
这个项目是学习C语言网络编程的绝佳起点,从它开始,你可以逐步添加新功能,最终构建一个功能强大的Web服务器。
