参考:“深入理解计算机系统” 第663页
web简介:
web服务器其实就是用来响应浏览器(客户端)的请求,他们之间的通讯都遵循着HTTP协议。HTTP请求格式是 <方法><统一资源标识符><HTTP协议版本>,<方法>有GET,POST,PUT等等,其中用得最为广泛得是GET请求,占全世界所有HTTP请求的99%;<统一资源标识符>说白了就是文件的路径,比如index.html。如果你用浏览器输入http://127.0.0.1/index.html,它就会用TCP协议连接上127.0.0.1:80的服务器,发送GET /index.html HTTP/1.1的数据到服务器;服务器收到请求后,就会返回index.html的文件内容,然后断开对浏览器的连接;最后浏览器收到index.html的文件内容后把它显示在屏幕上。既然HTTP协议是建基於TCP的,那我们当然就可以用我们在c语言中熟悉的socket来制作一个属于自己的浏览器。
我们的微型web服务器简介:
不要看它叫微型,就小看它,麻雀虽小但五脏俱全,有错误处理,有文字内容,有图片内容。看到这里一定会有朋友问:你这些都是静态服务,像登入操作的那种动态服务就没了吧?那你就错了,这个小服务器连动态服务都有,你能用c语言或python写动态响应程序/脚本,也就是我们常说的CGI程序,牛逼了吧?事不宜迟,我们马上开始实现一个属于自己的web服务器。
效果图:
要准备的东西:
线程池:下面我们会用到这个线程池模块,是我之前写的,你可以直接复制来用,不用也可以,就把线程池的函数去掉就可以了。不过用法很简单,三个函数init,add_event,destroy。http://blog.csdn.net/sumkee911/article/details/50231891
重定位标准输出:CGI程序是用printf来返回数据的,所以我们必须在调用之前把它的stdout重新定位到客户端socket的描述符上,我之前又写下过这个代码,你们可以参考这里。http://blog.csdn.net/sumkee911/article/details/50238169
我的源码:这个就是我写的微型web服务器,里面有makefile,make一下就能用;还有测试用的html和cgi,都在bin文件夹里。http://download.csdn.net/detail/sumkee911/9367809
这里也放源码(你们从main函数跟着看下去就可以明白。我打了很多注释,而且代码很浅显易明的,请放心。如果实在有函数不懂的话就上百度,我这里就不再详细说明了):
tiny_web_server.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/fcntl.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include "threadpool/threadpool.h"
// 服务器配置
#define PORT 80
#define MAX_LISTEN 128
#define MAX_EPOLL MAX_LISTEN+1
#define THREAD_COUNT 10
#define DEFAULT_PAGE "index.html"
#define SEND_BUF 2048
#define RECV_BUF 2048
#define SERVE_STATIC 0 // 处理静态请求
#define SERVE_DYNAMIC 1 // 处理动态请求
// 服务器根目录
static char g_root[256]="";
// 线程池
static ThreadPool g_pool;
// 多路复用io接口(epoll)
static int g_epoll_fd;
static void del_from_epoll(int efd, int fd);
void get_file_type(char *type, char *filepath) {
if(strstr(filepath,".html")) strcpy(type, "text/html");
else if(strstr(filepath, ".gif")) strcpy(type, "image/gif");
else if(strstr(filepath, ".jpg")) strcpy(type, "image/jpeg");
else if(strstr(filepath, ".png")) strcpy(type, "image/png");
else if(strstr(filepath, ".css")) strcpy(type, "text/css");
else if(strstr(filepath, ".js")) strcpy(type, "application/x-javascript");
else strcpy(type, "text/plain");
}
void get_absolute_path(char *abfilepath, char *rt, char *filepath) {
sprintf(abfilepath, "%s%s", rt, filepath);
}
void serve_error(int fd, char *filepath,const char *errnum,
const char *shortmsg, const char *longmsg) {
char head[SEND_BUF], body[SEND_BUF];
memset(body, 0, sizeof(body));
memset(head, 0 ,sizeof(head));
// 网页错误页面
sprintf(body, "<html><head><title>Tiny Web Server Error</title></head>");
sprintf(body, "%s<body><h1><font color='red'>Tiny Web Server Error</font></h1>", body);
sprintf(body, "%s<p>Error code: %s</p>",body, errnum);
sprintf(body, "%s<p>Cause: %s %s</p></body></html>", body,longmsg, filepath);
// http 头
sprintf(head, "HTTP/1.1 %s %s\r\n", errnum, shortmsg);
sprintf(head, "%sContent-type: text/html\r\n", head);
sprintf(head, "%sContent-length: %d\r\n\r\n", head,(int)strlen(body));
// 发送
send(fd, head, strlen(head), MSG_NOSIGNAL);
send(fd, body, strlen(body), MSG_NOSIGNAL);
}
void serve_static(int fd, char *filepath, long filesize) {
int filefd;
char buf[SEND_BUF], filetype[128],*filemap;
memset(filetype, 0, sizeof(filetype));
memset(buf, 0, sizeof(buf));
// 发送http头
get_file_type(filetype, filepath); // 获取文件类型
sprintf(buf, "HTTP/1.1 200 OK\r\n");
sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
sprintf(buf, "%sContent-length: %ld\r\n",buf, filesize);
sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype);
send(fd, buf, strlen(buf), MSG_NOSIGNAL);
// 发送文件内容
filefd = open(filepath, O_RDONLY);
filemap = (char *)mmap(0, filesize, PROT_READ, MAP_PRIVATE, filefd, 0);
close(filefd);
send(fd, filemap, filesize, MSG_NOSIGNAL);
munmap(filemap,filesize);
}
void serve_dynamic(int fd, char *filepath, char *args) {
// 创建一条进程;设置环境变量QUERY_STRING,将args设置进去; 最后将子进程的标准输出重新定位到客户端的socket描述符中,
// 只要子程序printf,就能把printf得数据传送到客户端。
pid_t pid;
char head[SEND_BUF];
// 发送http头
memset(head, 0, sizeof(head));
sprintf(head, "HTTP/1.1 200 OK\r\n");
sprintf(head, "%sServer: Tiny Web Server\r\n", head);
send(fd, head, strlen(head), MSG_NOSIGNAL);
if((pid=fork()) == 0) {
// 子进程
// 设置环境变量
setenv("QUERY_STRING", args, 1);
// 重定向标准输出, 默认输出fd是1
dup2(fd, 1);
// 启动CGI
execl(filepath, "" , (char*)0);
} else {
// 等待子进程结束
waitpid(pid, 0, 0);
}
}
int serve_type(const char *filepath) {
const char str[] = "/cgi-bin/\0";
if(strncmp(filepath, str, strlen(str)) == 0) {
return SERVE_DYNAMIC;
}
return SERVE_STATIC;
}
void parse_url(char *filepath, char *args, char *url) {
char *file_start, *args_start;
file_start = index(url, '/');
args_start = index(url, '?');
if(args_start != 0) {
memcpy(filepath, file_start, args_start-url);
memcpy(args, args_start+1, strlen(args_start)-1);
} else if(file_start != 0) {
memcpy(filepath, file_start, strlen(file_start));
}
}
void process_command(void *tp_args) {
char data[RECV_BUF], request[16], filepath[256], \
new_abfilepath[256],args[256], url[512];
struct stat filestat;
int res, type;
int fd = *(int*)tp_args;
free(tp_args);
memset(data, 0, sizeof(data));
memset(request, 0, sizeof(request));
memset(filepath, 0, sizeof(filepath));
memset(url, 0, sizeof(url));
memset(args, 0, sizeof(args));
memset(new_abfilepath, 0, sizeof(new_abfilepath));
// 获取请求
res = recv(fd, data, sizeof(data), MSG_NOSIGNAL);
if(res == 0 || res == -1) {
// 删除连接
goto __end;
}
printf("%s\n", data);
// 解析请求
sscanf(data, "%s %s", request, url);
if(strcasecmp("GET", request) != 0) {
// 无法识别请求
serve_error(fd, request, "501", "Not implememted",
"Tiny does not implement this method");
goto __end;
}
// 解析url
parse_url(filepath, args, url);
// 如果文件位置为'/', 就把它设置为默认页面
if(strlen(filepath) == 1) {
// 默认页面
strcat(filepath, DEFAULT_PAGE);
}
get_absolute_path(new_abfilepath, g_root, filepath); // 设置文件的绝对路径
// 获取文件属性
res = stat(new_abfilepath, &filestat);
if(res == -1) {
// 找不到相关文件
serve_error(fd, filepath,"404", "Not found",
"Tiny couldn't find this file");
goto __end;
}
// 判断是静态请求还是动态请求,动态请求也就是我们常说的CGI程序
type = serve_type(filepath);
if(type == SERVE_STATIC) {
if(!(S_ISREG(filestat.st_mode)) || !(S_IRUSR & filestat.st_mode)) {
// 错误,不能读取这文件
serve_error(fd, filepath, "403", "Forbidden",
"Tiny couldn't read this file");
goto __end;
}
// 开始回复静态请求
serve_static(fd, new_abfilepath, filestat.st_size);
} else {
if(!(S_ISREG(filestat.st_mode)) || !(S_IXUSR & filestat.st_mode)) {
// 错误,不能运行这文件
serve_error(fd, filepath, "403", "Forbidden",
"Tiny couldn't run this cgi file");
goto __end;
}
// 开始回复动态请求
serve_dynamic(fd, new_abfilepath, args);
}
// 删除连接
__end:
close(fd);
}
void add_to_epoll(int efd, int fd) {
int res;
struct epoll_event epe;
epe.data.fd = fd;
epe.events = EPOLLIN | EPOLLET;
res = epoll_ctl(efd, EPOLL_CTL_ADD, fd, &epe);
if(res == -1) {
perror("epoll_ctl");
}
}
void del_from_epoll(int efd, int fd) {
int res = epoll_ctl(efd, EPOLL_CTL_DEL, fd, 0);
if(res == -1) {
perror("epoll_ctl");
}
}
void set_nonblock(int fd) {
int tmp = 1;
if(ioctl(fd, FIONBIO, &tmp) == -1) {
perror("ioctl");
}
}
void set_root(char *rt, char *filepath) {
char *end = rindex(filepath, '/');
memcpy(rt, filepath, end-filepath);
strcat(rt, "\0");
}
int main(int, char *argv[]) {
int server,res, blreuse;
struct sockaddr_in addr;
bool blres;
// 设置根目录, 也就是该服务器程序的主目录
set_root(g_root, argv[0]);
// 创建服务器
server = socket(AF_INET, SOCK_STREAM, 0);
if(server == -1) {
perror("socket");
return -1;
}
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 将bind地址设置为可重用
blreuse = 1;
res = setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &blreuse, sizeof(blreuse));
if(res == -1) {
perror("setsockopt");
return -1;
}
res = bind(server, (struct sockaddr*)&addr, sizeof(struct sockaddr_in));
if(res == -1) {
perror("bind");
return -1;
}
listen(server, MAX_LISTEN);
if(res == -1) {
perror("listen");
return -1;
}
// 将服务器设置为非阻塞
set_nonblock(server);
// 创建epoll
g_epoll_fd = epoll_create(MAX_LISTEN);
if(g_epoll_fd == -1) {
perror("epoll_create");
return -1;
}
// 将服务器加入epoll
add_to_epoll(g_epoll_fd, server);
// 初始化线程池, 用来处理url请求
blres = g_pool.init(THREAD_COUNT);
if(blres == false) {
return -1;
}
// 进入接收事件循环
while(1) {
struct epoll_event epes[MAX_EPOLL];
int i, n;
n = epoll_wait(g_epoll_fd, epes, MAX_EPOLL, -1);
for(i=0; i<n; ++i) {
if(epes[i].events & EPOLLERR ||
epes[i].events & EPOLLHUP ||
!(epes[i].events & EPOLLIN)) {
del_from_epoll(g_epoll_fd,epes[i].data.fd);
continue;
} else if(epes[i].data.fd == server) {
// 服务器接收到请求
struct sockaddr_in addr_client;
socklen_t len;
int fd;
len = sizeof(struct sockaddr_in);
while(true) {
fd = accept(server, (struct sockaddr*)&addr_client, &len);
if(fd == -1) {
break;
}
// 将新来得客户加入到epoll
add_to_epoll(g_epoll_fd, fd);
}
} else {
// 接收到来自客户端的数据
int *fd = (int*)malloc(sizeof(int));
*fd = epes[i].data.fd;
// 将客户从epoll中删除
del_from_epoll(g_epoll_fd, epes[i].data.fd);
// 处理url请求
g_pool.add_event(process_command, (void*)fd);
}
}
}
g_pool.destroy();
close(g_epoll_fd);
close(server);
return 0;
}