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 文件操作
服务器需要读取本地文件并发送给客户端。本案例使用fopen
、fread
等函数操作文件。
2.5 MIME类型
根据文件扩展名设置正确的Content-Type
,例如:
-
.html
->text/html
-
.jpg
->image/jpeg
-
.png
->image/png
3. 实现思路
3.1 服务器启动流程
-
初始化Winsock库:调用
WSAStartup
初始化网络通信。 -
创建套接字:调用
socket
创建TCP套接字。 -
绑定地址和端口:调用
bind
绑定套接字到本地地址和端口。 -
监听连接:调用
listen
开始监听客户端连接。 -
接受连接:调用
accept
接受客户端连接,并为每个连接创建新线程。
3.2 处理客户端请求
-
读取请求行:从客户端读取HTTP请求的第一行,解析请求方法和URL。
-
解析URL:根据URL确定请求的文件路径。
-
检查文件是否存在:使用
stat
函数检查文件是否存在。 -
发送响应头:根据文件类型设置
Content-Type
,并发送HTTP响应头。 -
发送文件内容:读取文件内容并发送给客户端。
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;
}