前面三十八篇文章,我们从变量、指针、数据结构一路学到系统调用、多线程、信号处理。现在我们终于有底气将它们全部串联起来,写一个真正能用的东西:一个迷你 HTTP 服务器。
这个服务器程序会监听本地端口,接受浏览器请求,解析 GET 请求,从磁盘读取对应的文件(HTML、图片、CSS 等),并返回 HTTP 响应。我们还会加入多线程并发处理和信号优雅退出,让它像一个真正的服务器一样工作。
本文将带你从零开始,一步步构建它。
一、HTTP 服务器在做什么?
HTTP(超文本传输协议)是万维网的基础。它的工作模式很简单:
-
客户端(浏览器)连接服务器的某个端口(通常是 80 或 8080)。
-
客户端 发送一个 HTTP 请求 ,例如:
GET /index.html HTTP/1.1 Host: localhost:8080 -
服务器 解析这个请求,找到请求的资源(如
/index.html)。 -
服务器 构建一个 HTTP 响应 :
HTTP/1.1 200 OK Content-Type: text/html Content-Length: 123 <html>...</html> -
服务器 发送响应,关闭连接(或保持连接)。
我们的服务器将使用 TCP 套接字(Socket) 来实现网络通信。如果你对系统调用有印象(第三十五篇),socket 本质上也是一种特殊的文件描述符------你可以对它进行 read 和 write。这正是系统编程的魅力:文件、设备、网络都统一为"文件描述符"的抽象。
二、TCP Socket 编程速成
在开始写服务器之前,先快速过一下必需的 socket API。它们定义在 <sys/socket.h> 中。
1. 创建套接字:socket()
c
int socket(int domain, int type, int protocol);
domain:协议族,IPv4 用AF_INET。type:套接字类型,TCP 用SOCK_STREAM。protocol:通常传0使用默认协议。- 返回值:文件描述符,失败返回
-1。
c
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) { perror("socket"); return 1; }
2. 绑定地址:bind()
c
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
我们需要指定 IP 地址和端口。结构体 sockaddr_in(定义在 <netinet/in.h>)用于 IPv4:
c
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // 监听所有网络接口
address.sin_port = htons(8080); // 端口,htons 转换字节序
INADDR_ANY 表示接受来自任意网卡的连接。htons 将主机字节序转换为网络字节序(大端)。端口号我们选 8080(普通用户可不经 root 运行)。
3. 监听连接:listen()
c
int listen(int sockfd, int backlog);
backlog 是等待连接队列的最大长度,我们设为 10。
4. 接受连接:accept()
c
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
它会阻塞 ,直到有客户端连接进来。返回一个新的文件描述符 ,用于与该客户端通信。原来的 server_fd 继续监听新连接。
5. 收发数据:send() / recv()
虽然可以用 read/write(socket 也是文件描述符),但 send/recv 提供更多控制选项。
c
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
6. 关闭连接:close()
三、HTTP 请求解析
一个典型的 HTTP 请求长这样:
GET /index.html HTTP/1.1\r\n
Host: localhost:8080\r\n
User-Agent: Mozilla/5.0\r\n
\r\n
我们需要从中提取方法 (GET)、路径(/index.html)、以及知道 HTTP 版本。后续可以忽略头部,只处理最简单的 GET 请求。
解析思路:用 recv 读入请求数据,找第一个空格和第二个空格之间的内容作为路径。如果路径是 /,默认映射到 /index.html。
四、构建 HTTP 响应
根据请求路径,尝试打开对应的本地文件(如 ./www/index.html)。我们用系统调用 open/read 或标准 C 的 fopen/fread 都可以。为了与之前的知识一致,用标准 C 库会更方便处理文本和二进制。
构建响应头部的格式:
HTTP/1.1 <状态码> <状态文本>\r\n
Content-Type: <MIME 类型>\r\n
Content-Length: <文件大小>\r\n
Connection: close\r\n
\r\n
后面紧跟文件内容。
MIME 类型我们只处理常见的几个扩展名:
| 扩展名 | Content-Type |
|---|---|
| .html | text/html |
| .css | text/css |
| .js | application/javascript |
| .png | image/png |
| .jpg | image/jpeg |
| .gif | image/gif |
| .txt | text/plain |
| 其他 | application/octet-stream |
如果文件不存在,返回 404 页面;如果方法不是 GET,返回 405。
五、多线程并发
为了能同时处理多个客户端,每接受一个新连接,就创建一个线程来处理它。主线程继续等待下一个连接。这避免了串行处理导致浏览器排队超时。
我们将处理单个连接的逻辑封装到 handle_client 函数中,线程创建时传递客户端 socket 文件描述符的指针(动态分配内存,在线程中释放)。
c
void *handle_client(void *arg) {
int client_fd = *(int*)arg;
free(arg);
// 解析请求,发送响应
close(client_fd);
return NULL;
}
六、信号处理:优雅退出
当用户按 Ctrl+C 发送 SIGINT 信号时,我们应关闭服务器套接字,等待已连接的线程完成(或直接退出但释放资源)。一个简单做法是设置全局 volatile sig_atomic_t 标志,让主循环检查标志退出。退出前 close(server_fd),已创建的线程如果是分离的(pthread_detach)会自行结束,不会阻塞进程退出。为了简单,我们可以让主线程在退出时调用 close 并结束进程,线程会自动被终止(不优雅但可接受)。更完善的做法是跟踪所有线程并 pthread_join,但这里简化。
七、完整代码
文件结构
project/
server.c
www/
index.html
style.css
404.html
server.c
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#define PORT 8080
#define BUFFER_SIZE 4096
#define WWW_DIR "./www"
volatile sig_atomic_t running = 1;
void handle_signal(int sig) {
(void)sig;
running = 0;
}
const char *get_mime_type(const char *path) {
const char *ext = strrchr(path, '.');
if (!ext) return "application/octet-stream";
if (strcmp(ext, ".html") == 0) return "text/html";
if (strcmp(ext, ".css") == 0) return "text/css";
if (strcmp(ext, ".js") == 0) return "application/javascript";
if (strcmp(ext, ".png") == 0) return "image/png";
if (strcmp(ext, ".jpg") == 0) return "image/jpeg";
if (strcmp(ext, ".jpeg") == 0) return "image/jpeg";
if (strcmp(ext, ".gif") == 0) return "image/gif";
if (strcmp(ext, ".txt") == 0) return "text/plain";
return "application/octet-stream";
}
/* 循环发送,确保所有数据都发送出去 */
ssize_t send_all(int fd, const void *buf, size_t len) {
const char *p = buf;
size_t total = 0;
while (total < len) {
ssize_t n = send(fd, p + total, len - total, 0);
if (n == -1) {
if (errno == EINTR) continue;
return -1;
}
total += n;
}
return total;
}
void send_response(int client_fd, int status_code, const char *status_text,
const char *content_type, const void *body, size_t body_len) {
char header[BUFFER_SIZE];
int header_len = snprintf(header, sizeof(header),
"HTTP/1.1 %d %s\r\n"
"Content-Type: %s\r\n"
"Content-Length: %zu\r\n"
"Connection: close\r\n"
"\r\n",
status_code, status_text, content_type, body_len);
if (header_len < 0 || (size_t)header_len >= sizeof(header)) {
/* 响应头过长,发送简单错误 */
const char *err = "HTTP/1.1 500 Internal Server Error\r\n\r\n";
send(client_fd, err, strlen(err), 0);
return;
}
if (send_all(client_fd, header, header_len) == -1) {
perror("send header failed");
return;
}
if (body && body_len > 0) {
if (send_all(client_fd, body, body_len) == -1) {
perror("send body failed");
}
}
}
/* 更严谨的目录遍历防护:规范化路径后检查 */
int is_path_safe(const char *path) {
/* 拒绝包含 .. 的路径 */
if (strstr(path, "..")) {
return 0;
}
/* 拒绝空路径或不以 / 开头的路径 */
if (!path || path[0] != '/') {
return 0;
}
return 1;
}
/* 分块读取并发送文件,避免大文件一次性加载到内存 */
void serve_file_chunked(int client_fd, const char *filepath, const char *mime) {
int fd = open(filepath, O_RDONLY);
if (fd == -1) {
const char *msg = "404 Not Found";
send_response(client_fd, 404, "Not Found", "text/plain", msg, strlen(msg));
return;
}
struct stat st;
if (fstat(fd, &st) == -1) {
close(fd);
const char *msg = "500 Internal Server Error";
send_response(client_fd, 500, "Internal Server Error", "text/plain", msg, strlen(msg));
return;
}
off_t file_size = st.st_size;
/* 发送响应头 */
char header[BUFFER_SIZE];
int header_len = snprintf(header, sizeof(header),
"HTTP/1.1 200 OK\r\n"
"Content-Type: %s\r\n"
"Content-Length: %ld\r\n"
"Connection: close\r\n"
"\r\n",
mime, (long)file_size);
if (header_len < 0 || (size_t)header_len >= sizeof(header)) {
close(fd);
return;
}
if (send_all(client_fd, header, header_len) == -1) {
close(fd);
return;
}
/* 分块读取并发送 */
char buf[BUFFER_SIZE];
ssize_t n;
while ((n = read(fd, buf, sizeof(buf))) > 0) {
if (send_all(client_fd, buf, n) == -1) {
break;
}
}
close(fd);
}
void serve_file(int client_fd, const char *path) {
if (!is_path_safe(path)) {
const char *msg = "403 Forbidden";
send_response(client_fd, 403, "Forbidden", "text/plain", msg, strlen(msg));
return;
}
char filepath[512];
int n = snprintf(filepath, sizeof(filepath), "%s%s", WWW_DIR, path);
if (n < 0 || n >= (int)sizeof(filepath)) {
const char *msg = "404 Not Found";
send_response(client_fd, 404, "Not Found", "text/plain", msg, strlen(msg));
return;
}
/* 检查是否是目录 */
struct stat st;
if (stat(filepath, &st) == 0 && S_ISDIR(st.st_mode)) {
/* 目录请求,尝试 index.html */
strncat(filepath, "/index.html", sizeof(filepath) - strlen(filepath) - 1);
}
/* 使用分块发送 */
const char *mime = get_mime_type(filepath);
serve_file_chunked(client_fd, filepath, mime);
}
void parse_request(const char *request, char *method, char *path) {
method[0] = '\0';
path[0] = '\0';
/* 跳过前导空白 */
const char *p = request;
while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
if (!*p) return;
/* 解析 method */
const char *m_start = p;
while (*p && *p != ' ') p++;
size_t method_len = p - m_start;
if (method_len >= 16) method_len = 15;
memcpy(method, m_start, method_len);
method[method_len] = '\0';
/* 跳过空格 */
while (*p == ' ') p++;
/* 解析 path */
const char *path_start = p;
while (*p && *p != ' ' && *p != '\r' && *p != '\n') p++;
size_t path_len = p - path_start;
if (path_len >= 256) path_len = 255;
memcpy(path, path_start, path_len);
path[path_len] = '\0';
}
void *handle_client(void *arg) {
int client_fd = *(int*)arg;
free(arg);
char buffer[BUFFER_SIZE];
ssize_t bytes = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
if (bytes <= 0) {
close(client_fd);
return NULL;
}
buffer[bytes] = '\0';
char method[16];
char path[256];
parse_request(buffer, method, path);
/* 如果解析失败,返回 400 */
if (method[0] == '\0' || path[0] == '\0') {
const char *msg = "400 Bad Request";
send_response(client_fd, 400, "Bad Request", "text/plain", msg, strlen(msg));
close(client_fd);
return NULL;
}
if (strcmp(method, "GET") != 0) {
const char *msg = "405 Method Not Allowed";
send_response(client_fd, 405, "Method Not Allowed", "text/plain", msg, strlen(msg));
} else {
if (strcmp(path, "/") == 0) {
strcpy(path, "/index.html");
}
serve_file(client_fd, path);
}
close(client_fd);
return NULL;
}
int main(void) {
/* 使用 sigaction 替代 signal */
struct sigaction sa;
sa.sa_handler = handle_signal;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGINT, &sa, NULL) == -1 || sigaction(SIGTERM, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket");
return 1;
}
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
perror("setsockopt");
close(server_fd);
return 1;
}
struct sockaddr_in address;
memset(&address, 0, sizeof(address));
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)) == -1) {
perror("bind");
close(server_fd);
return 1;
}
if (listen(server_fd, 10) == -1) {
perror("listen");
close(server_fd);
return 1;
}
printf("服务器启动: http://localhost:%d\n", PORT);
printf("按 Ctrl+C 退出\n");
while (running) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int *client_fd_ptr = malloc(sizeof(int));
if (!client_fd_ptr) {
perror("malloc");
continue;
}
*client_fd_ptr = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
if (*client_fd_ptr == -1) {
free(client_fd_ptr);
if (errno == EINTR) continue;
perror("accept");
continue;
}
/* 使用 inet_ntop 替代 inet_ntoa(线程安全) */
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
printf("新连接: %s:%d\n", client_ip, ntohs(client_addr.sin_port));
pthread_t tid;
if (pthread_create(&tid, NULL, handle_client, client_fd_ptr) != 0) {
perror("pthread_create");
close(*client_fd_ptr);
free(client_fd_ptr);
continue;
}
pthread_detach(tid);
}
printf("\n正在关闭服务器...\n");
close(server_fd);
return 0;
}
能在WIndows环境下运行的代码:
c
/*
* 迷你 HTTP 服务器 - Windows 兼容版本
*
* 修改说明:
* 1. 使用 Winsock2 API 替代 POSIX socket API
* 2. 使用 Windows 线程 API 替代 pthread
* 3. 使用 Windows 信号处理替代 POSIX sigaction
* 4. 使用 _stat 替代 stat
* 5. 使用 CreateFile/ReadFile 替代 open/read
*
* 编译命令(MinGW/MSVC):
* gcc -o server.exe server_windows.c -lws2_32
* 或
* cl server_windows.c /link ws2_32.lib
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <windows.h>
#include <ws2tcpip.h>
#include <sys/stat.h>
/* 链接 Winsock 库 */
#pragma comment(lib, "ws2_32.lib")
#define PORT 8080
#define BUFFER_SIZE 4096
#define WWW_DIR "./www"
#define MAX_THREADS 128
/* 全局状态 */
volatile LONG running = 1;
HANDLE server_mutex = NULL;
/* 客户端参数结构 */
typedef struct {
SOCKET client_fd;
struct sockaddr_in client_addr;
} client_info_t;
/* MIME 类型判断 */
const char *get_mime_type(const char *path) {
const char *ext = strrchr(path, '.');
if (!ext) return "application/octet-stream";
if (_stricmp(ext, ".html") == 0) return "text/html";
if (_stricmp(ext, ".css") == 0) return "text/css";
if (_stricmp(ext, ".js") == 0) return "application/javascript";
if (_stricmp(ext, ".png") == 0) return "image/png";
if (_stricmp(ext, ".jpg") == 0) return "image/jpeg";
if (_stricmp(ext, ".jpeg") == 0) return "image/jpeg";
if (_stricmp(ext, ".gif") == 0) return "image/gif";
if (_stricmp(ext, ".txt") == 0) return "text/plain";
return "application/octet-stream";
}
/* 循环发送,确保所有数据都发送出去 */
int send_all(SOCKET fd, const void *buf, size_t len) {
const char *p = buf;
size_t total = 0;
while (total < len) {
int n = send(fd, p + total, (int)(len - total), 0);
if (n == SOCKET_ERROR) {
int err = WSAGetLastError();
if (err == WSAEINTR) continue;
return -1;
}
total += n;
}
return (int)total;
}
void send_response(SOCKET client_fd, int status_code, const char *status_text,
const char *content_type, const void *body, size_t body_len) {
char header[BUFFER_SIZE];
int header_len = snprintf(header, sizeof(header),
"HTTP/1.1 %d %s\r\n"
"Content-Type: %s\r\n"
"Content-Length: %zu\r\n"
"Connection: close\r\n"
"\r\n",
status_code, status_text, content_type, body_len);
if (header_len < 0 || (size_t)header_len >= sizeof(header)) {
const char *err = "HTTP/1.1 500 Internal Server Error\r\n\r\n";
send(client_fd, err, (int)strlen(err), 0);
return;
}
if (send_all(client_fd, header, header_len) == -1) {
fprintf(stderr, "send header failed\n");
return;
}
if (body && body_len > 0) {
if (send_all(client_fd, body, body_len) == -1) {
fprintf(stderr, "send body failed\n");
}
}
}
/* 目录遍历防护 */
int is_path_safe(const char *path) {
if (strstr(path, "..")) {
return 0;
}
if (!path || path[0] != '/') {
return 0;
}
return 1;
}
/* 分块读取并发送文件 */
void serve_file_chunked(SOCKET client_fd, const char *filepath, const char *mime) {
HANDLE hFile = CreateFileA(filepath, GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
const char *msg = "404 Not Found";
send_response(client_fd, 404, "Not Found", "text/plain", msg, strlen(msg));
return;
}
LARGE_INTEGER file_size;
if (!GetFileSizeEx(hFile, &file_size)) {
CloseHandle(hFile);
const char *msg = "500 Internal Server Error";
send_response(client_fd, 500, "Internal Server Error", "text/plain", msg, strlen(msg));
return;
}
/* 发送响应头 */
char header[BUFFER_SIZE];
int header_len = snprintf(header, sizeof(header),
"HTTP/1.1 200 OK\r\n"
"Content-Type: %s\r\n"
"Content-Length: %lld\r\n"
"Connection: close\r\n"
"\r\n",
mime, file_size.QuadPart);
if (header_len < 0 || (size_t)header_len >= sizeof(header)) {
CloseHandle(hFile);
return;
}
if (send_all(client_fd, header, header_len) == -1) {
CloseHandle(hFile);
return;
}
/* 分块读取并发送 */
char buf[BUFFER_SIZE];
DWORD bytes_read;
while (ReadFile(hFile, buf, sizeof(buf), &bytes_read, NULL) && bytes_read > 0) {
if (send_all(client_fd, buf, bytes_read) == -1) {
break;
}
}
CloseHandle(hFile);
}
void serve_file(SOCKET client_fd, const char *path) {
if (!is_path_safe(path)) {
const char *msg = "403 Forbidden";
send_response(client_fd, 403, "Forbidden", "text/plain", msg, strlen(msg));
return;
}
char filepath[512];
int n = snprintf(filepath, sizeof(filepath), "%s%s", WWW_DIR, path);
if (n < 0 || n >= (int)sizeof(filepath)) {
const char *msg = "404 Not Found";
send_response(client_fd, 404, "Not Found", "text/plain", msg, strlen(msg));
return;
}
/* 检查是否是目录 */
struct _stat st;
if (_stat(filepath, &st) == 0 && (st.st_mode & _S_IFDIR)) {
strncat(filepath, "/index.html", sizeof(filepath) - strlen(filepath) - 1);
}
const char *mime = get_mime_type(filepath);
serve_file_chunked(client_fd, filepath, mime);
}
void parse_request(const char *request, char *method, char *path) {
method[0] = '\0';
path[0] = '\0';
const char *p = request;
while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
if (!*p) return;
const char *m_start = p;
while (*p && *p != ' ') p++;
size_t method_len = p - m_start;
if (method_len >= 16) method_len = 15;
memcpy(method, m_start, method_len);
method[method_len] = '\0';
while (*p == ' ') p++;
const char *path_start = p;
while (*p && *p != ' ' && *p != '\r' && *p != '\n') p++;
size_t path_len = p - path_start;
if (path_len >= 256) path_len = 255;
memcpy(path, path_start, path_len);
path[path_len] = '\0';
}
/* 客户端处理函数(Windows 线程入口) */
DWORD WINAPI handle_client(LPVOID arg) {
client_info_t *info = (client_info_t*)arg;
SOCKET client_fd = info->client_fd;
struct sockaddr_in client_addr = info->client_addr;
free(info);
char buffer[BUFFER_SIZE];
int bytes = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
if (bytes <= 0) {
closesocket(client_fd);
return 0;
}
buffer[bytes] = '\0';
char method[16];
char path[256];
parse_request(buffer, method, path);
if (method[0] == '\0' || path[0] == '\0') {
const char *msg = "400 Bad Request";
send_response(client_fd, 400, "Bad Request", "text/plain", msg, strlen(msg));
closesocket(client_fd);
return 0;
}
if (_stricmp(method, "GET") != 0) {
const char *msg = "405 Method Not Allowed";
send_response(client_fd, 405, "Method Not Allowed", "text/plain", msg, strlen(msg));
} else {
if (strcmp(path, "/") == 0) {
strcpy(path, "/index.html");
}
serve_file(client_fd, path);
}
closesocket(client_fd);
return 0;
}
/* Windows 控制台信号处理 */
BOOL WINAPI console_handler(DWORD signal) {
if (signal == CTRL_C_EVENT || signal == CTRL_BREAK_EVENT) {
InterlockedExchange(&running, 0);
return TRUE;
}
return FALSE;
}
int main(void) {
/* 设置控制台信号处理 */
if (!SetConsoleCtrlHandler(console_handler, TRUE)) {
fprintf(stderr, "无法设置信号处理程序\n");
return 1;
}
/* 初始化 Winsock */
WSADATA wsaData;
int wsaResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (wsaResult != 0) {
fprintf(stderr, "WSAStartup 失败: %d\n", wsaResult);
return 1;
}
/* 创建 socket */
SOCKET server_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (server_fd == INVALID_SOCKET) {
fprintf(stderr, "socket 失败: %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
/* 允许端口重用 */
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR,
(const char*)&opt, sizeof(opt)) == SOCKET_ERROR) {
fprintf(stderr, "setsockopt 失败: %d\n", WSAGetLastError());
closesocket(server_fd);
WSACleanup();
return 1;
}
struct sockaddr_in address;
memset(&address, 0, sizeof(address));
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)) == SOCKET_ERROR) {
fprintf(stderr, "bind 失败: %d\n", WSAGetLastError());
closesocket(server_fd);
WSACleanup();
return 1;
}
if (listen(server_fd, 10) == SOCKET_ERROR) {
fprintf(stderr, "listen 失败: %d\n", WSAGetLastError());
closesocket(server_fd);
WSACleanup();
return 1;
}
printf("服务器启动: http://localhost:%d\n", PORT);
printf("按 Ctrl+C 退出\n");
while (InterlockedCompareExchange(&running, 0, 0)) {
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(server_fd, &readfds);
struct timeval tv;
tv.tv_sec = 1;
tv.tv_usec = 0;
int ret = select(0, &readfds, NULL, NULL, &tv);
if (ret == SOCKET_ERROR) {
// select 出错,若 running 已为 0 则退出
if (InterlockedCompareExchange(&running, 0, 0) == 0)
break;
fprintf(stderr, "select 失败: %d\n", WSAGetLastError());
continue;
} else if (ret == 0) {
// 超时,重新检查 running 标志
continue;
}
// 有连接到达
struct sockaddr_in client_addr;
int client_len = sizeof(client_addr);
SOCKET client_socket = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_socket == INVALID_SOCKET) {
int err = WSAGetLastError();
if (err == WSAEINTR)
continue;
if (InterlockedCompareExchange(&running, 0, 0) == 0)
break;
fprintf(stderr, "accept 失败: %d\n", err);
continue;
}
// 获取客户端 IP 并打印
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
printf("新连接: %s:%d\n", client_ip, ntohs(client_addr.sin_port));
client_info_t *info = malloc(sizeof(client_info_t));
if (!info) {
fprintf(stderr, "malloc 失败\n");
closesocket(client_socket);
continue;
}
info->client_fd = client_socket;
info->client_addr = client_addr;
HANDLE hThread = CreateThread(NULL, 0, handle_client, info, 0, NULL);
if (hThread == NULL) {
fprintf(stderr, "CreateThread 失败: %lu\n", GetLastError());
closesocket(client_socket);
free(info);
continue;
}
CloseHandle(hThread);
}
printf("\n正在关闭服务器...\n");
closesocket(server_fd);
WSACleanup();
return 0;
}
八、准备测试网页
在 www/ 目录下创建几个文件:
www/index.html
html
<!DOCTYPE html>
<html>
<head>
<title>欢迎</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<h1>你好,C 语言 HTTP 服务器!</h1>
<p>这是一个用不到 200 行 C 代码写成的迷你服务器。</p>
</body>
</html>
www/style.css
css
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
color: #333;
max-width: 600px;
margin: 50px auto;
text-align: center;
}
h1 { color: #0066cc; }
www/404.html
html
<!DOCTYPE html>
<html>
<head><title>404 Not Found</title></head>
<body>
<h1>404 - 页面未找到</h1>
<p>你请求的资源不存在。</p>
</body>
</html>
九、编译与测试
bash
gcc -pthread -o server server.c
./server
打开浏览器访问 http://localhost:8080,你会看到漂亮的网页!再试一个不存在的路径 http://localhost:8080/nonexist,会显示 404 页面。
按 Ctrl+C,服务器打印关闭信息并退出。
十、可以继续完善的方向
这个迷你 HTTP 服务器已经具备了基本骨架,你可以在此基础上添加:
- POST 请求支持 :解析
Content-Length请求体。 - CGI 动态页面:调用外部程序生成响应。
- 日志记录:记录每个请求的时间和 IP。
- 线程池:避免频繁创建销毁线程。
- 配置解析:从配置文件读取端口、根目录等。
这个项目虽小,但已经融合了你学过的几乎全部核心技能:文件 I/O、字符串处理、内存管理、多线程、信号处理、网络系统调用。
十一、小结
今天,我们用 C 语言从零构建了一个能处理浏览器请求的 HTTP 服务器,把它变成了一个真正的网络服务。你综合运用了:
- Socket 编程 (
socket/bind/listen/accept/send/recv)作为网络通信基础。 - 文件操作 (
fopen/fread/fclose)读取静态资源。 - HTTP 协议的解析与响应构建。
- 多线程并发处理多个客户端。
- 信号处理实现优雅退出。
- 内存管理确保无泄漏。
这是你 C 语言学习路上的一座里程碑。它证明你不再只是"学语法",而是能够构建实际可用的系统。恭喜你!
十二、课后小练习
- 给服务器添加一个简单的访问日志:在控制台打印每个请求的方法、路径和客户端 IP。
- 实现一个简单的目录列表功能:如果请求路径是一个目录且没有默认 index.html,列出该目录下的文件和子目录(返回 HTML 格式)。
- 为服务器增加命令行参数支持:可以指定端口号和根目录(如
./server -p 9090 -d /var/www)。 - (小挑战)将单线程中的
handle_client改造成线程池版本:预先创建固定数量的工作线程,通过队列分发客户端 socket。这可以更好地控制并发量和资源使用。
我们下期见!
💡获取本系列示例代码请访问 GitCode 仓库。