C实现简单web服务器-1(windows)

来源:互联网 发布:斗破沙城翅膀进阶数据 编辑:程序博客网 时间:2024/05/01 08:30

为了了解web服务器工作原理最实在的就是用C自己实现一个了,虽然实现的功能很少,不过却可以加深自己对http/1.1、web服务器的理解。

这里还是简单介绍下http/1.1协议吧:
web服务器的话需要处理接收的请求报文,比如chrome的话请求报文是这样的:(响应报文的话是需要自己构造的,下一篇介绍)
这里写图片描述
首先第一行:GET /favicon.ico HTTP/1.1
常见方法一般有GET、HEAD、POST,post一般用于表单,head是只请求首部,测试网页状态的时候就会用head方法,比如:curl -I www.xxx.com/index.html使用的就是HEAD方法
这里写图片描述
方法后面接url(比如这里就是想要请求/favicon.ico图片了),再后面就是http版本号了

第二行:HOST:localhost
指的就是请求的主机,可以用IP:PORT或域名+PORT指定;如果为空,应该返回400 bad request;像apache的虚拟主机配置(基于域名的主机),同一个ip却可以工作多台web服务器就是根据这个字段来区分是哪台web的请求的。
有三种虚拟主机配置:基于域名,基于ip,基于端口的;
对于http,默认端口是80,如果是基于端口的,用户每次请求还需要输入port,而且很多用户甚至不懂的怎么做,所以这种方法不常用;
基于ip的,现如今ip地址紧缺,使用基于ip也并不是什么好方法;
所以最常用的就是这个基于域名的虚拟主机了(申请一个域名相对与ip来说还是便宜很多的),不过别想太多,我并没有实现这个功能,有兴趣的可以自己去实现,也很简单,对host字段和你的几个虚拟主机进行比较然后转发即可。

第三行:Connection: keep-alive
使用持续连接,http/1.1默认就是持续的,这样的好处在如果你请求一个1.html,该html里面有100张图片时:非持续连接需要请求100+1次,持续连接只需要1次;http使用的是tcp传输协议,每次连接断开需要3次握手和4次挥手,这样就大大降低效率了;而且对于每次的连接,客户和服务器都要分配TCP缓冲区和保持TCP变量,也给服务器加大的负担。
对于apache2.4来说在../apache2/conf/extra/httpd-default.conf里面可以选择开启或关闭持续连接:
这里写图片描述

第5行:User-Agent: …
用户代理(这里即你的浏览器类型),你也可以根据这个来决定要不要让他访问,apache2.4中在http.conf或httpd-vhosts.conf中可以这样设置:
这里写图片描述

第6行:Accept-Encoding…
Accept-Encoding 是浏览器发给服务器,声明浏览器支持的编码类型,从这里可以看出chrome传输是使用gzip压缩传输的

第7行:Accept-Language: …
zh-CN就是支持中文了

下面来看看代码:

// SimpleHTTPServer.cpp// 功能:实现简单的web服务器功能,能同时响应多个浏览器的请求://       1、如果该文件存在,则在浏览器上显示该文件;//       2、如果文件不存在,则返回404-file not found页面//       3、只支持GET、HEAD方法// HTTP1.1 与 1.0不同,默认是持续连接的(keep-alive)#include <Winsock2.h>#include <time.h>#include <stdio.h>#include <string.h>#include <direct.h>     // 目录头文件#pragma comment(lib,"Ws2_32.lib")// http 默认端口是80,如果80端口被占用那么改个端口即可#define DEFAULT_PORT 80 #define BUF_LENGTH 1024#define MIN_BUF 128#define USER_ERROR -1#define SERVER "Server: csr_http1.1\r\n"int file_not_found(SOCKET sAccept);int file_ok(SOCKET sAccept, long flen);int send_file(SOCKET sAccept, FILE *resource);int send_not_found(SOCKET sAccept);DWORD WINAPI SimpleHTTPServer(LPVOID lparam){    SOCKET sAccept = (SOCKET)(LPVOID)lparam;    char recv_buf[BUF_LENGTH];     char method[MIN_BUF];    char url[MIN_BUF];    char path[_MAX_PATH];    int i, j;    // 缓存清0,每次操作前都要记得清缓存,养成习惯;    // 不清空可能出现的现象:输出乱码、换台机器乱码还各不相同    // 原因:不清空会输出遇到 '\0'字符为止,所以前面的不是'\0' 也会一起输出    memset(recv_buf,0,sizeof(recv_buf));    if (recv(sAccept,recv_buf,sizeof(recv_buf),0) == SOCKET_ERROR)   //接收错误    {        printf("recv() Failed:%d\n",WSAGetLastError());        return USER_ERROR;    }           else        printf("recv data from client:%s\n",recv_buf); //接收成功,打印请求报文    //处理接收数据    i = 0; j = 0;    // 取出第一个单词,一般为HEAD、GET、POST    while (!(' ' == recv_buf[j]) && (i < sizeof(method) - 1))    {        method[i] = recv_buf[j];        i++; j++;    }    method[i] = '\0';   // 结束符,这里也是初学者很容易忽视的地方    // 如果不是GET或HEAD方法,则直接断开本次连接    // 如果想做的规范些可以返回浏览器一个501未实现的报头和页面    if (stricmp(method, "GET") && stricmp(method, "HEAD"))    {        closesocket(sAccept); //释放连接套接字,结束与该客户的通信        printf("not get or head method.\nclose ok.\n");        printf("***********************\n\n\n\n");        return USER_ERROR;    }    printf("method: %s\n", method);    // 提取出第二个单词(url文件路径,空格结束),并把'/'改为windows下的路径分隔符'\'    // 这里只考虑静态请求(比如url中出现'?'表示非静态,需要调用CGI脚本,'?'后面的字符串表示参数,多个参数用'+'隔开    // 例如:www.csr.com/cgi_bin/cgi?arg1+arg2 该方法有时也叫查询,早期常用于搜索)    i = 0;    while ((' ' == recv_buf[j]) && (j < sizeof(recv_buf)))        j++;    while (!(' ' == recv_buf[j]) && (i < sizeof(recv_buf) - 1) && (j < sizeof(recv_buf)))    {        if (recv_buf[j] == '/')            url[i] = '\\';        else if(recv_buf[j] == ' ')            break;        else            url[i] = recv_buf[j];        i++; j++;    }    url[i] = '\0';    printf("url: %s\n",url);    // 将请求的url路径转换为本地路径    _getcwd(path,_MAX_PATH);    strcat(path,url);    printf("path: %s\n",path);    // 打开本地路径下的文件,网络传输中用r文本方式打开会出错    FILE *resource = fopen(path,"rb");    // 没有该文件则发送一个简单的404-file not found的html页面,并断开本次连接    if(resource==NULL)    {        file_not_found(sAccept);        // 如果method是GET,则发送自定义的file not found页面        if(0 == stricmp(method, "GET"))            send_not_found(sAccept);        closesocket(sAccept); //释放连接套接字,结束与该客户的通信        printf("file not found.\nclose ok.\n");        printf("***********************\n\n\n\n");        return USER_ERROR;    }    // 求出文件长度,记得重置文件指针到文件头    fseek(resource,0,SEEK_SET);    fseek(resource,0,SEEK_END);    long flen=ftell(resource);    printf("file length: %ld\n", flen);    fseek(resource,0,SEEK_SET);    // 发送200 OK HEAD    file_ok(sAccept, flen);    // 如果是GET方法则发送请求的资源    if(0 == stricmp(method, "GET"))    {        if(0 == send_file(sAccept, resource))            printf("file send ok.\n");        else            printf("file send fail.\n");    }    fclose(resource);    closesocket(sAccept); //释放连接套接字,结束与该客户的通信    printf("close ok.\n");    printf("***********************\n\n\n\n");    return 0;}// 发送404 file_not_found报头int file_not_found(SOCKET sAccept){    char send_buf[MIN_BUF]; //  time_t timep;   //  time(&timep);    sprintf(send_buf, "HTTP/1.1 404 NOT FOUND\r\n");    send(sAccept, send_buf, strlen(send_buf), 0);//  sprintf(send_buf, "Date: %s\r\n", ctime(&timep));//  send(sAccept, send_buf, strlen(send_buf), 0);    sprintf(send_buf, "Connection: keep-alive\r\n");    send(sAccept, send_buf, strlen(send_buf), 0);    sprintf(send_buf, SERVER);    send(sAccept, send_buf, strlen(send_buf), 0);    sprintf(send_buf, "Content-Type: text/html\r\n");    send(sAccept, send_buf, strlen(send_buf), 0);    sprintf(send_buf, "\r\n");    send(sAccept, send_buf, strlen(send_buf), 0);    return 0;}// 发送200 ok报头int file_ok(SOCKET sAccept, long flen){    char send_buf[MIN_BUF]; //  time_t timep;//  time(&timep);    sprintf(send_buf, "HTTP/1.1 200 OK\r\n");    send(sAccept, send_buf, strlen(send_buf), 0);    sprintf(send_buf, "Connection: keep-alive\r\n");    send(sAccept, send_buf, strlen(send_buf), 0);//  sprintf(send_buf, "Date: %s\r\n", ctime(&timep));//  send(sAccept, send_buf, strlen(send_buf), 0);    sprintf(send_buf, SERVER);    send(sAccept, send_buf, strlen(send_buf), 0);    sprintf(send_buf, "Content-Length: %ld\r\n", flen);    send(sAccept, send_buf, strlen(send_buf), 0);    sprintf(send_buf, "Content-Type: text/html\r\n");    send(sAccept, send_buf, strlen(send_buf), 0);    sprintf(send_buf, "\r\n");    send(sAccept, send_buf, strlen(send_buf), 0);    return 0;}// 发送自定义的file_not_found页面int send_not_found(SOCKET sAccept){    char send_buf[MIN_BUF];    sprintf(send_buf, "<HTML><TITLE>Not Found</TITLE>\r\n");    send(sAccept, send_buf, strlen(send_buf), 0);    sprintf(send_buf, "<BODY><h1 align='center'>404</h1><br/><h1 align='center'>file not found.</h1>\r\n");    send(sAccept, send_buf, strlen(send_buf), 0);    sprintf(send_buf, "</BODY></HTML>\r\n");    send(sAccept, send_buf, strlen(send_buf), 0);    return 0;}// 发送请求的资源int send_file(SOCKET sAccept, FILE *resource){    char send_buf[BUF_LENGTH];    while (1)    {        memset(send_buf,0,sizeof(send_buf));       //缓存清0        fgets(send_buf, sizeof(send_buf), resource);    //  printf("send_buf: %s\n",send_buf);        if (SOCKET_ERROR == send(sAccept, send_buf, strlen(send_buf), 0))        {            printf("send() Failed:%d\n",WSAGetLastError());            return USER_ERROR;        }        if(feof(resource))            return 0;    }   }int main(){    WSADATA wsaData;    SOCKET sListen,sAccept;        //服务器监听套接字,连接套接字    int serverport=DEFAULT_PORT;   //服务器端口号    struct sockaddr_in ser,cli;   //服务器地址,客户端地址    int iLen;    printf("-----------------------\n");    printf("Server waiting\n");    printf("-----------------------\n");    //第一步:加载协议栈    if (WSAStartup(MAKEWORD(2,2),&wsaData) !=0)    {        printf("Failed to load Winsock.\n");        return USER_ERROR;    }    //第二步:创建监听套接字,用于监听客户请求    sListen =socket(AF_INET,SOCK_STREAM,0);    if (sListen == INVALID_SOCKET)    {        printf("socket() Failed:%d\n",WSAGetLastError());        return USER_ERROR;    }    //创建服务器地址:IP+端口号    ser.sin_family=AF_INET;    ser.sin_port=htons(serverport);               //服务器端口号    ser.sin_addr.s_addr=htonl(INADDR_ANY);   //服务器IP地址,默认使用本机IP    //第三步:绑定监听套接字和服务器地址    if (bind(sListen,(LPSOCKADDR)&ser,sizeof(ser))==SOCKET_ERROR)    {        printf("blind() Failed:%d\n",WSAGetLastError());        return USER_ERROR;    }    //第五步:通过监听套接字进行监听    if (listen(sListen,5)==SOCKET_ERROR)    {        printf("listen() Failed:%d\n",WSAGetLastError());        return USER_ERROR;    }    while (1)  //循环等待客户的请求    {        //第六步:接受客户端的连接请求,返回与该客户建立的连接套接字        iLen=sizeof(cli);        sAccept=accept(sListen,(struct sockaddr*)&cli,&iLen);        if (sAccept==INVALID_SOCKET)        {            printf("accept() Failed:%d\n",WSAGetLastError());            break;        }        //第七步,创建线程接受浏览器请求        DWORD ThreadID;        CreateThread(NULL,0,SimpleHTTPServer,(LPVOID)sAccept,0,&ThreadID);      }    closesocket(sListen);    WSACleanup();    return 0;}

编译运行后打开浏览器输入:http://localhost/index.html
http默认是80端口,如果端口被占用改成其他的这时就要加上port了:
http://localhost:port/index.html
PS:server.cpp所在的目录就是网站的根目录

这里写图片描述

这里只做了最简单基础的功能,如果想扩充可以试着加上CGI的功能,即动态响应客户端的请求,因为windows下并没有自带perl、php或python脚本所以就没有实现了。
如果不想使用浏览器,下一篇也贴上了一个简单客户端的C代码,可以自己选择使用HEAD或GET方法请求。

部分参考unix的tinyhttpd1.0源码,有兴趣的可以去看看,虽然只有500多行,不过最基础的功能却都齐了。这里提醒一下那是unix的,部分头文件和linux还不完全一样,需要稍加修改才可以在linux下运行,而且版本是1.0的也要注意下,现在1.1版本和1.0还是有区别的

0 0
原创粉丝点击