简单web服务器


实现一个简单的web服务器 myhttpd。能够给浏览器提供服务,供用户借助浏览器访问主机中的文件。

1 超文本标记语言 HTML

超文本标记语言时构成网页文档的主要语言。可以说明文字,图形,动画,声音,表格,链接等。在计算机中以 .html , .htm 作为扩展名,可以被浏览器访问。

示例:

<!doctype html>

<html>
    <head>
        <title>404 not fount</title>
    </head>
    <body>
        <p id="top"></p>

        <a href="https://www.w3school.com.cn/" title="HTML学习网站" target="_blank"> HTML学习网站 </HTML></a>

        <div align=center>
            <h1>404 not fount</h1>
        </div>
        <hr size="3" />

        <div align=center>
            <font>ngix 12.14</font>
        </div>

        <ul type="circle">
            <li> 选项1 </li>
            <li> 选项2 </li>
            <li> 选项3 </li>
            <li> 选项4 </li>
            <li> 选项5 </li>
        </ul>

        <ol type="I">
            <li> 选项1 </li>
            <li> 选项2 </li>
            <li> 选项3 </li>
            <li> 选项4 </li>
            <li> 选项5 </li>
        </ol>

        <dl>
            <dt>小标题</dt>
            <dd>解释标题</dd>
        </dl>

        <img src="13.png" alt="图片加载失败" title="壁纸" width="300" height="200" />
        <img src="13.png" alt="图片加载失败" title="壁纸" width="300" height="200" />
        <img src="13.png" alt="图片加载失败" title="壁纸" width="300" height="200" />
        <img src="13.png" alt="图片加载失败" title="壁纸" width="300" height="200" />
        <img src="13.png" alt="图片加载失败" title="壁纸" width="300" height="200" />
        <img src="13.png" alt="图片加载失败" title="壁纸" width="300" height="200" />
        <img src="13.png" alt="图片加载失败" title="壁纸" width="300" height="200" />
        <img src="13.png" alt="图片加载失败" title="壁纸" width="300" height="200" />

        <a href="https://www.bilibili.com/" title="bilibili" target="_blank"> B站 </a>

        <a href="https://www.bilibili.com/" title="bilibili" target="_blank">
            <img src="13.png" alt="图片加载失败" width="300" height="200" />
        </a>

        <a href="https://www.w3school.com.cn/" title="HTML学习网站" target="_blank"> HTML学习网站 </HTML></a>

        <a href="#top"> 回到顶部 </a>
    </body>
</html>

2.HTTP 协议

HTTP:超文本传输协议。互联网最为广泛的一种网络应用层协议。它可以减少网络传输,使浏览器更加高效。
通常HTTP消息包括客户机向服务器的请求消息和服务器向客户机的响应消息。

2.1 请求消息

浏览器----发给----服务器。主旨内容包含 4 部分:

  1. 请求行:说明请求类型,要访问的资源,以及使用的http版本
  2. 请求头:说明服务器要使用的附加信息
  3. 空行:必须!即使没有请求数据
  4. 请求数据:也叫主体,可以添加任意的其他数据。

2.2 响应消息

服务器----发给----浏览器,主旨包括 4 部分:

  1. 状态行:包括http协议版本号,状态码,状态信息
  2. 消息报头:说明客户端要使用一些附加信息
  3. 空行:必须!
  4. 响应正文:服务器返回给客户端的文本信息

2.3 HTTP请求方法

  • GET: 请求指定的页面信息,并返回实体主体
  • POST: 向指定资源提交数据进行处理请求。数据包含在请求体中。post请求可能会导致新的资源建立和/或已有资源的修改。
  • HEAD: 类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头
  • PUT: 从客户端向服务器传送数取代指定的文档内容

2.4 HTTP常用状态码

状态码有三位数字组成,第一个数字代表响应类别,共分五种类别:

  1. 1xx:指示信息–表示请求已连接,继续处理
  2. 2xx:成功–表示请求连接成功
  3. 3xx:重定向–要完成请求必须进行更进一步的操作
  4. 4xx:客户端错误–请求有语法错误或者请求无法实现
  5. 5xx:服务器端错误–服务器未能实现合法请求

3. 示例—web服务器

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <strings.h>
#include <fcntl.h>
#include <error.h>
#include <errno.h>
#include <dirent.h>
#include <ctype.h>

//获取一行 \r\n 结尾的数据
int get_line(int cfd, char *buf, int size) {
    int i = 0;
    char c = '\0';
    int n;

    while ((i < size - 1) && (c != '\n'))
    {
        n = recv(cfd, &c, 1, 0);
        if(n > 0) {
            if(c == '\r') {
                n = recv(cfd, &c, 1, MSG_PEEK);
                if((n > 0) && (c == '\n')) {
                    recv(cfd, &c, 1, 0);
                }
                else {
                    c = '\r';
                }
            }
            buf[i] = c;
            i++;
        }
        else {
            c = '\n';
        }
    }

    buf[i] = '\0';
    if(n == -1) {
        i = n;
    }

    return i;
}

//创建、初始化 socket
int init_listen_fd(int port, int epfd) {

    int ret;

    //创建socket
    int fd = socket(AF_INET, SOCK_STREAM, 0);

    //设置端口复用
    int opt = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in serv;
    memset(&serv, 0, sizeof(serv));
    serv.sin_family = AF_INET;
    serv.sin_port = htons(port);
    serv.sin_addr.s_addr = htonl(INADDR_ANY);

    ret = bind(fd, (struct sockaddr*)&serv, sizeof(serv));
    if(ret < 0) {
        perror("bind error");
        exit(1);
    }

    ret = listen(fd, 128);
    if(ret < 0) {
        perror("listen error");
        exit(1);
    }

    //设置文件描述符非阻塞 ET
    // int flag = fcntl(fd, F_GETFL);
    // flag |= O_NONBLOCK;
    // fcntl(fd, F_SETFL, flag);

    //创建epoll事件
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = fd;

    ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
    if(ret < 0) {
        perror("epoll_ctl error");
        exit(1);
    }

    return fd;
}

//接受新连接
void do_accept(int fd, int epfd) {

    int ret;
    struct sockaddr_in cli;
    socklen_t len = sizeof(cli);

    int cfd = accept(fd, (struct sockaddr*)&cli, &len);
    if(cfd < 0) {
        perror("accept error");
        exit(1);
    }

    printf("client[%d] connected.\n", cfd);

    //设置文件描述符非阻塞 ET
    int flag = fcntl(cfd, F_GETFL);
    flag |= O_NONBLOCK;
    fcntl(cfd, F_SETFL, flag);

    struct epoll_event ev;
    ev.data.fd = cfd;
    ev.events = EPOLLIN | EPOLLET;

    ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
    if(ret < 0) {
        perror("epoll_ctl error");
        exit(1);
    }

    return;
}

//断开连接
void disconnect(int cfd, int epfd) {
    printf("服务器检测到客户端关闭...\n");
    int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL);
    if(ret < 0) {
        perror("epoll_ctl error");
        exit(1);
    }
    close(cfd);
}

//回发应答协议给浏览器
/*
void send_respond(int cfd, int no, char *disp, char *type, int len)
    cfd: 客户端cfd,用于回发数据
    no: 状态码
    disp: 状态描述
    type: 回发文件类型
    len: 文件大小
*/
void send_respond(int cfd, int no, char *disp, const char *type, int len) {

    char buf[1024] = {0};
    //发送状态行
    sprintf(buf, "HTTP/1.1 %d %s\r\n", no, disp);

    //发送消息报头、空行、响应正文
    sprintf(buf + strlen(buf), "Content-Type: %s\r\n", type);
    sprintf(buf + strlen(buf), "Content-Length: %d\r\n", len);
    sprintf(buf + strlen(buf), "\r\n");

    send(cfd, buf, strlen(buf), 0);

}

//发送404错误页面
void send_error(int cfd, int no, char *disp, char *text) {
//void send_error(int cfd, char *text) {
    char buf[4096] = {0};
    int n;
    //发送状态行
    sprintf(buf, "HTTP/1.1 %d %s\r\n", no, disp);

    //发送消息报头、空行、响应正文
    sprintf(buf + strlen(buf), "Content-Type: %s\r\n", "text/html");
    sprintf(buf + strlen(buf), "Content-Length: %d\r\n", -1);
    sprintf(buf + strlen(buf), "Connection: close\r\n");
    sprintf(buf + strlen(buf), "\r\n");

    send(cfd, buf, strlen(buf), 0);
    memset(buf, 0, sizeof(buf));

    int fd = open(text, O_RDONLY);
    if(fd < 0) {
        perror("open error");
        exit(1);
    }

    while((n = read(fd, buf, sizeof(buf))) > 0) {
        send(cfd, buf, strlen(buf), 0);
    }
    // sprintf(buf, "<html><head><title> %d %s <title></head></html>\n", no, disp);
    // sprintf(buf + strlen(buf), "<body bgcolor=\"#cc99cc\"><h4 align=\"center\"> %d %s </h4>\n", no, disp);
    // sprintf(buf + strlen(buf), "%s\n", text);
    // sprintf(buf + strlen(buf), "<hr>\n</body>\n</html>\n");

    // send(cfd, buf, strlen(buf), 0);

    return;
}

//发送文件给浏览器
void send_file(int cfd, const char *filename) {

    int n, ret;
    char buf[4096] = {0};
    int fd = open(filename, O_RDONLY);
    if(fd < 0) {
        //发送404错误页面
        printf("open error on send_file\n");
        //send_error(cfd, 404, "no found", "打开文件失败!");
        send_error(cfd, 404, "no found", "/home/yang/network/web/404.html");
        close(fd);
        return;
    } 

    while((n = read(fd, buf, sizeof(buf))) > 0) {
        ret = send(cfd, buf, n, 0);
        if(ret < 0) {
            printf("errno = %d\n", errno);
            if(errno == EAGAIN) {
                printf("--------------------EAGAIN.\n");
                continue;
            }
            else if(errno == EINTR) {
                printf("---------------------EINTR.\n");
            }
            else {
                perror("send error on send_file.");
                exit(1);
            }
        }
    }

    if(n < 0) {
        perror("read error on send_file");
        exit(1);
    }

    close(fd);
}

//通过文件名判断是哪种文件类型
const char* get_file_type(const char *filename) {
    char *dot = NULL;

    dot = strrchr(filename, '.');
    if(dot == NULL) return "text/plain; charset=utf-8";
    if(strcmp(dot, ".html") == 0 || strcmp(dot, ".htm") == 0) return "text/html; charset=utf-8";
    if(strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0) return "image/jpeg";
    if(strcmp(dot, ".gif") == 0) return "image/gif";
    if(strcmp(dot, ".png") == 0) return "image/png";
    if(strcmp(dot, ".css") == 0) return "text/css";
    if(strcmp(dot, ".au") == 0) return "audio/basic";
    if(strcmp(dot, ".wav") == 0) return "audio/wav";
    if(strcmp(dot, ".avi") == 0) return "video/x-msvideo";
    if(strcmp(dot, ".mov") == 0 || strcmp(dot, ".qt") == 0) return "video/quicktime";
    if(strcmp(dot, ".mpeg") == 0 || strcmp(dot, ".mpe") == 0) return "video/mpeg";
    if(strcmp(dot, ".vrml") == 0 || strcmp(dot, ".wrl") == 0) return "model/vrml";
    if(strcmp(dot, ".midi") == 0 || strcmp(dot, ".mid") == 0) return "audio/midi";
    if(strcmp(dot, ".mp3") == 0) return "audio/mpeg";
    if(strcmp(dot, ".ogg") == 0) return "application/ogg";
    if(strcmp(dot, ".pac") == 0) return "application/x-ns-proxy-autoconfig";

    return "text/plain; charset=utf-8";
}

//十六进制转十进制
int hexit(char c) {
    if(c >= '0' && c <= '9') {
        return c - '0';
    }
    if(c >= 'a' && c <= 'f') {
        return c - 'a' + 10;
    }
    if(c >= 'A' && c <= 'F') {
        return c - 'A' + 10;
    }

    return 0;
}

//编码
void encode_str(char *to, int tosize, const char *from) {
    int tolen;

    for(tolen = 0; *from != '\0' && tolen + 4 < tosize; from++) {
        if(isalnum(*from) || strchr("/_.-~", *from) != (char *)0) {
            *to = *from;
            ++to;
            ++tolen;
        }
        else {
            sprintf(to, "%%%02x", (int)*from & 0xff);
            to += 3;
            tolen += 3;
        }
    }
    *to = '\0';
}
//解码
void decode_str(char *to, char *from) {
    for( ; *from != '\0'; ++to, ++from) {
        if(from[0] == '%' && isxdigit(from[1]) && isxdigit(from[2])) {
            *to = hexit(from[1]) * 16 + hexit(from[2]);
            from += 2;
        }
        else {
            *to = *from;
        }
    }
    *to = '\0';
}

//遍历目录
void send_dir(int cfd, const char *dirname) {
    int i, ret;

    //拼一个html页面
    char buf[4096] = {0};

    sprintf(buf, "<html>\n\t<head>\n\t\t<title>目录名:%s</title>\n\t</head>\n", dirname);
    sprintf(buf + strlen(buf), "<body><h1>当前目录:%s</h1><table>\n", dirname);

    char enstr[1024] = {0};
    char path[1024] = {0};

    struct dirent **ptr;
    int num = scandir(dirname, &ptr, NULL, alphasort);

    for(i = 0; i < num; i++) {
        char *name = ptr[i]->d_name;

        //拼接完整路径
        sprintf(path, "%s/%s", dirname, name);
        printf("path = %s -------------------------------------\n", path);

        struct stat st;
        stat(path, &st);

        //编码
        encode_str(enstr, sizeof(enstr), name);

        if(S_ISDIR(st.st_mode)) { //如果是目录
            sprintf(buf + strlen(buf), "<tr><td><a href=\"%s/\"> %s/ </a></td><td>%ld</td></tr>\n", enstr, name, (long)st.st_size);

        }
        else if(S_ISREG(st.st_mode)) { //如果是文件
            sprintf(buf + strlen(buf), "<tr><td><a href=\"%s\"> %s </a></td><td>%ld</td></tr>\n", enstr, name, (long)st.st_size);
        }

        ret = send(cfd, buf, strlen(buf), 0);
        if(ret < 0) {
            printf("errno = %d\n", errno);
            if(errno == EAGAIN) {
                printf("--------------------EAGAIN.\n");
                continue;
            }
            else if(errno == EINTR) {
                printf("---------------------EINTR.\n");
            }
            else {
                perror("send error on send_file.");
                exit(1);
            }
        }

        memset(buf, 0, sizeof(buf));
    }

    sprintf(buf + strlen(buf), "</table></body></html>\n");
    send(cfd, buf, strlen(buf), 0);

    printf("dir message send OK!!!\n");
}

//处理客户端http请求--文件操作,判断文件是否存在 回发
void http_request(int cfd, const char *buf) {

    //拆分请求行
    //将http请求的首行进行拆分 get /xxx http/1.1
    //int sscanf(const char *str, const char *format, ...);
    char method[16], path[256], protocol[16];
    sscanf(buf, "%[^ ] %[^ ] %[^ ]", method, path, protocol);
    printf("method = [%s]\t path = [%s] \t protocol = [%s]\n", method, path, protocol);

    //解码 将不能识别的中文乱码-> 中文
    decode_str(path, path);

    char *filename = path + 1; //因为path为 /xxx 所以文件名要地址加 1

    if(strcmp(path, "/") == 0) { //如果未指定访问资源
        filename = "./"; //filename的值为当前目录
    }

    struct stat sbuf;
    //判断文件是否存在
    //int stat(const char *pathname, struct stat *buf);
    int ret = stat(filename, &sbuf);
    if(ret < 0) {
        //不存在 回发浏览器 404 错误页面
        printf("-----It's not a file.-----\n");
        //send_error(cfd, 404, "no found", "文件不存在!");
        send_error(cfd, 404, "no found", "/home/yang/network/web/404.html");
        return;
    }

    //判断是什么文件类型
    //S_ISREG(m)
    if(S_ISREG(sbuf.st_mode)) { //普通文件
        printf("-----It's a file.-----\n");
        //回发http协议应答
        //void send_respond(int cfd, int no, char *disp, char *type, int len)
        send_respond(cfd, 200, "OK", get_file_type(filename), sbuf.st_size);
        //发送文件信息
        send_file(cfd, filename);
    }
    else if(S_ISDIR(sbuf.st_mode)) { //目录
        //回发http协议应答
        //void send_respond(int cfd, int no, char *disp, char *type, int len)
        send_respond(cfd, 200, "OK", get_file_type(".html"), -1);
        //发送目录信息
        send_dir(cfd, filename);
    }

}

//接收数据
void do_read(int cfd, int epfd) {

    //读取一行http请求,拆分,获取get 文件名 协议号
    char buf[1024] = {0};
    int n;
    n = get_line(cfd, buf, 1024);
    if(n < 0) {
        perror("get_line error");
        exit(1);
    }
    else if(n == 0) { //客户端关闭连接
        disconnect(cfd, epfd);
    }
    else {
        //将http请求的首行进行拆分 get /xxx http/1.1
        //int sscanf(const char *str, const char *format, ...);
        //char method[16], path[256], protocol[16];
        //sscanf(buf, "%[^ ] %[^ ] %[^ ]", method, path, protocol);
        //printf("method = [%s]\t path = [%s] \t protocol = [%s]\n", method, path, protocol);

        printf("---------------请求头-------------\n");
        printf("请求行数据: %s", buf);
        //读完缓冲区还剩余的数据
        while(1) {
            char sbuf[1024];
            n = get_line(cfd, sbuf, sizeof(sbuf));
            if(n < 0) {
                break;
            }
            else if(n == 1 && sbuf[0] == '\n') {
                break;
            }
            //printf("%d-----%s", n, sbuf);
        }
        printf("----------------------------------\n");
    }

    //判断是否为get请求
        //int strncasecmp(const char *s1, const char *s2, size_t n); 比较 s1 s2 前n个字符是否相等
        if(strncasecmp(buf, "GET", 3) == 0) { //相同-是get请求

            //char *filename = path + 1; //因为path为 /xxx 所以文件名要地址加 1

            http_request(cfd, buf);

            //关闭套接字,将cfd从树上摘下
            disconnect(cfd, epfd);
        }

}


//启动epoll
void epoll_run(int port) {

    int i;
    struct epoll_event evs[1024];

    int epfd = epoll_create(1024);
    if(epfd < 0) {
        perror("epoll_create error\n");
        exit(1);
    }

    int lfd = init_listen_fd(port, epfd);

    //循环监听事件
    while(1) {
        int ret = epoll_wait(epfd, evs, 1024, 0);
        if(ret < 0) {
            perror("epoll_wait error");
            exit(1);
        }

        for(i = 0; i < ret; i++) {
            struct epoll_event *ev = &evs[i];
            if(!(ev->events & EPOLLIN)) {
                continue;
            }

            if(ev->data.fd == lfd) { //接受连接请求
                do_accept(lfd, epfd);
            }
            else { //接收数据
                do_read(ev->data.fd, epfd);
            }
        }
    }
}

int main(int argc, char *argv[]) {

    if(argc < 3) {
        printf("./Server port path");
    }

    //获取输入的端口
    int port = atoi(argv[1]);

    //改变进程工作目录
    int ret = chdir(argv[2]);
    if(ret < 0) {
        perror("chdir error\n");
        exit(1);
    }

    //启动监听
    epoll_run(port);
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值