首先我们先看一下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服务器的几个步骤:
- 创建一个socket,绑定到指定的端口进行监听
- 使用epoll或者libevent来监听socket的读事件
- epoll方式:使用epoll_ctl注册socket的EPOLLIN事件,在循环中调用epoll_wait获取活动socket,根据事件类型调用处理函数。(仅限linux)
- libevent方式: 使用event_new注册socket的读事件,event_base_dispatch进入循环,根据活动socket调用回调处理函数。(跨平台)
- 当有新的连接时,接受连接,创建一个新的socket与客户端通讯
- 读取客户端请求,解析HTTP请求的方法、路径等信息
- 根据请求路径调用相应的请求处理函数
- 请求处理函数读取请求数据,生成响应
- 向客户端socket发送HTTP响应
- 关闭与客户端的连接
下面是一个使用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以及网络编程,共勉!