liunx c语言制作 微型web服务器 300行代码

参考:“深入理解计算机系统” 第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;
}



评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值