HTTP协议之编写简单的Web服务器

HTTP协议之编写简单的Web服务器

HTTP即超文本传输协议(Hypertext Transfer Protocol),是Web通信所使用的协议。它是基于TCP/IP实现的协议,所以需要先了解TCP通信,本篇将使用TCP来写一个简单的Web服务器端,它可以响应浏览器的访问。

通信需要服务端和客户端,在这里浏览器就属于客户端,当访问一个网页时,浏览器内部会创建套接字和服务器进行通信。服务器会响应请求返回一些HTML格式的数据给浏览器,浏览器来把这些HTML数据解析成我们看到的漂亮的页面。

当我们在浏览器的地址栏上敲下一个域名地址后,浏览器会先通过默认DNS服务器获取该域名对应的IP地址,然后向服务器发送请求,请求有一定的标准,分为:

  • 请求行
  • 消息头
  • 空行
  • 消息体

现在来随便访问一个网址,这里使用的是firefox浏览器,按Ctrl+Shift+E可以查看网络请求:

HTTP协议之编写简单的Web服务器

左边对应的是浏览器对服务器发出的请求,右边对应的是该条请求相关的信息。

我们先看左边的第一条,这里的请求方式是GET,表示想从服务器获取文件,获取的文件目录是/。然后服务器响应请求,发回了状态码302,表示Found重定向,我们本来访问的是www.bing.com,现在被重定向到了https://cn.bing.com/。这个重定向地址是通过响应头的location指定的,可以在右边看到。

https是加了SSL/TLS的协议,在需要安全的环境下,比如发送银行卡,身份信息等地方都会使用。只使用http这些信息很容易被窃听,SSL/TLS会对请求和响应的信息进行加密解密操作,保证数据安全。

接着同样是一些GET请求用于从服务器获取数据,状态码200 OK表示请求已经成功。还有常见的404 Not Found,表示找不到客户端请求的资源,这种情况就把链接转移到404错误页。现在来把对应的请求整理如下:

请求行:GET/HTTP/2.0消息头:Accept:text/html,application/xhtml+xm…plication/xml;q=0.9,*/*;q=0.8Accept-Encoding:gzip,deflate,brAccept-Language:zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2Connection:keep-aliveCookie:_EDGE_V=1;MUID=3802FC7ABA5D6F…ndefined;_UR=OMD=13190641321Host:cn.bing.comUpgrade-Insecure-Requests:1User-Agent:Mozilla/5.0(WindowsNT10.0;…)Gecko/20100101Firefox/64.0空行:消息体:

请求行为一行数据,其中包含着请求方式,请求文件,HTTP版本。

这里的请求方式为GET。文件为/,表示根目录,这种情况一般都会被重定向到别的页面上,一般好多服务器都会自动检查有没有index.html/index.php/…等等,发现有就转到这些页面。最后是使用的HTTP版本,为2.0版本。

主要来看消息头。

  • Accept:表示客户端可以处理的文件类型
  • Accept-Encoding:表示客户端能理解的内容的编码方式
  • Accept-Language:表示支持的语言,比如此处支持中文,英文等等
  • Connection:这个决定着当前事务完成后,是否会关闭网络连接。此处指定为keep-alive,表示持久,不会关闭。指定close会关闭。
  • Cookie:由于服务器响应客户端的请求后,需要断开连接去接收下一个请求,所以服务端无法记住客户端的状态,哪怕下一次再连接服务器也无法辩认为原先访问的。所以就有了Cookie,当客户端连接后服务器可以通过Set-Cookie给客户端设置一些信息,第二次连接时这个Cookie就包含的有服务器给此客户端设置的Cookie值,这样服务器就能识别客户端了。例如那些密码记录,购物车清单啊,都有用到。
  • Host:表示主机域名
  • Upgrade-Insercure-Request:表示客户端优先选择加密及带有身份验证的响应,并且它可以成功处理 upgrade-insecure-requests CSP 指令。
  • user-Agent:用户代理,包含着我们访问所使用的应用类型,操作系统,版本号等等信息。

空行是为了区分前面的信息和消息体的,消息体是在POST请求时提交给服务器的,此处没有POST请求,所以也就没有消息体了。

现在来看响应消息的格式:

  • 状态行
  • 消息头
  • 空行
  • 消息体

把响应的内容整理如下:

状态行:HTTP/2.0200OK消息头:cache-control:private,max-age=0content-encoding:brcontent-type:text/html;charset=utf-8date:Sun,30Dec201811:11:10GMTp3p:CP="NONUNICOMNAVSTALOCCURaDEVaPSAaPSDaOURIND"set-cookie:SNRHOP=I=&TS=;domain=.bing.com;path=/strict-transport-security:max-age=31536000;includeSubDomains;preloadvary:Accept-EncodingX-Firefox-Spdy:h2x-msedge-ref:RefA:7F437C20D4F049CB86705CA…6RefC:2018-12-30T11:11:10Z空行:消息体:<html>....</html>

第一行用于表示状态行,为HTTP版本 + 状态码。

在消息头中设置了内容编码,类型,日期,cookie等等信息,这里就不细说了。服务器需要向客户端返回数据,这里的数据就在消息体中进行发送的,这里返回了主页的html数据。

现在对这些基本知识有了了解,就可以来完成一个简单的Web服务器。

先创建一个WebServer类,作为网络服务器:

#pragmaonce#include<string>#include<WinSock2.h>#pragmacomment(lib,"ws2_32.lib")classWebServer{public:WebServer();~WebServer();voidRespondRequest();//响应请求private:voidInitSock();//初始化SocketvoidRequestHandler();//处理请求的线程voidSendData(conststd::string&ct,conststd::string&fileName);//发送数据voidSendNotFound();//找不到指定文件时,发送404NotFound页面private:WSADATAm_wsaData;SOCKETm_servSock,m_clntSock;SOCKADDR_INm_servAddr,m_clntAddr;};

关于TCP的东西就不再多说了,相信大家已经很熟悉了。这里只需开放一个接口供用户调用,其它由我们内部使用。

在实现文件中,还需要包含以下文件:

#include"WebServer.h"#include<iostream>#include<cassert>//断言#include<future>//线程用#include<fstream>//文件操作用#pragmawarning(disable:4996)//忽略一些警告constintCONTENT_SIZE=2048;//内容大小constintBUF_SIZE=100;//缓冲区大小

先来看InitSock()函数,在这里就是TCP的一些绑定,监听操作:

voidWebServer::InitSock(){//初始化Winsock库  int ret = WSAStartup(MAKEWORD(2, 2), &m_wsaData);assert(ret==0);//创建服务器套接字m_servSock=socket(PF_INET,SOCK_STREAM,0);//设置地址信息memset(&m_servAddr,0,sizeof(m_servAddr));m_servAddr.sin_family=AF_INET;m_servAddr.sin_addr.S_un.S_addr=htonl(INADDR_ANY);m_servAddr.sin_port=htons(8000);//绑定地址信息  ret = bind(m_servSock, (SOCKADDR *)&m_servAddr, sizeof(m_servAddr));assert(ret !=SOCKET_ERROR);  //监听  ret = listen(m_servSock, 5);assert(ret!=SOCKET_ERROR);}

初始化信息在构造函数中调用:

WebServer::WebServer(){InitSock();}

同时需要在析构函数中关闭释放:

WebServer::~WebServer(){closesocket(m_servSock);WSACleanup();}

大家可能发现这里只关闭了服务器套接字,这是因为前面说过,HTTP是无状态的,它响应了一个客户端后就会断开来响应别的请求,所以这里不需要关闭,而是在响应的函数中关闭。

现在来看响应请求的函数RespondRequest():

voidWebServer::RespondRequest(){for(;;){intnClntAddrSize=sizeof(m_clntAddr);//接收客户端连接m_clntSock=accept(m_servSock,(SOCKADDR*)&m_clntAddr,&nClntAddrSize);//获取客户端信息char*szClntAddr=inet_ntoa(m_clntAddr.sin_addr);intnClntPort=ntohs(m_clntAddr.sin_port);//打印信息charszBuf[BUF_SIZE]={0};snprintf(szBuf,BUF_SIZE,"ConnectionRequest:%s%d",szClntAddr,nClntPort);std::cout<<szBuf<<std::endl;//开启线程,发送数据autofu=std::async(std::launch::async,std::bind(&WebServer::RequestHandler,this));}}

这里一直循环着来接收客户端的请求,当有客户端连接后,读取客户端的地址和端口关打印出来。关于发送数据专门开启一个线程交给RequestHandler处理,因为类成员函数其实都隐含了一个this指针,所以还得传入this,这里的std::bind会把成员函数和this绑定起来,返回一个函数对象。

接着来看处理数据的线程函数:

voidWebServer::RequestHandler(){charszContent[CONTENT_SIZE]={0};//接收到的内容recv(m_clntSock,szContent,CONTENT_SIZE,0);//接收//打印接收到的内容std::cout<<"-------------------\n"<<szContent<<std::endl;std::stringcontent(szContent);//检测是否使用的是HTTP协议intprotocol=content.find("HTTP/");if(protocol==std::string::npos){std::cerr<<"ErrorRequest!"<<std::endl;closesocket(m_clntSock);return;}intloc=content.find_first_of("/");std::stringmethod=content.substr(0,loc);//解析请求方式std::stringfileName=content.substr(loc+1,protocol-loc-1);//解析请求文件名std::stringcontentType;loc=fileName.find_first_of(".")+1;std::stringtype=fileName.substr(loc);//解析类型if(type.compare("html")==0||type.compare("htm")==0)contentType="text/html";//html格式elsecontentType="text/plain";//纯文本格式SendData(contentType,fileName);//发送数据}

可以先测试下能否收到客户端的请求,前面指定的端口为8000,所以使用浏览器访问localhost:8000,接收到的数据如图:

HTTP协议之编写简单的Web服务器

当成功等到这些信息后,需要解析出它的请求方式,请求文件名,通过请求文件名再判断出请求类型,若是html,请求类型则为text/html。若不是,设置为text/plain。当给服务器返回时若是text/plain,服务器就会把收到的内容当作文本显示出来,若是html,则会是解析出显示。

再来看发送数据之前,首先来新建两个HTML文件,一个为正常请求的文件index.html,一个为失踪网页404.html,并将它们放到工程目录下。

<!--index.html--><html><head><title>MyWebServer</title></head><bodystyle="background:#0f0;"><divstyle="text-align:center;font-size:50px;margin-top:300px">RequestSuccessful!</div></body></html><!-- 404.html --><html><head><title>NotFound</title></head><bodystyle="background:#f00;"><divstyle="text-align:center;font-size:100px;margin-top:300px">404!</div><divstyle="text-align:center;font-size:50px;">Oops!Thatpagecan'tbefound.</div></body></html>

然后继续来看发送数据:

voidWebServer::SendData(conststd::string&ct,conststd::string&fileName){std::stringprotocol="HTTP/1.0200OK\r\n";//状态行std::stringservName="Server:MyWebServer\r\n";//服务器名称std::stringcontentLen="Content-length:2048\r\n";//内容长度std::stringcontentType="Content-type:"+ct+"\r\n\r\n";//内容类型//打开请求的文件std::ifstreamsendFile(fileName);if(!sendFile.is_open()){//打开失败,说明当前路径无此文件,发送NotFoundSendNotFound();return;}//发送状态行,消息头send(m_clntSock,protocol.c_str(),protocol.size(),0);send(m_clntSock,servName.c_str(),servName.size(),0);send(m_clntSock,contentLen.c_str(),contentLen.size(),0);send(m_clntSock,contentType.c_str(),contentType.size(),0);//发送消息体charbuf[CONTENT_SIZE]={0};sendFile.getline(buf,CONTENT_SIZE);for(;!sendFile.eof();){send(m_clntSock,buf,strlen(buf),0);sendFile.getline(buf,CONTENT_SIZE);}sendFile.close();closesocket(m_clntSock);//由HTTP协议响应后断开连接}

开始先组织一下状态行和消息头,因为客户端请求为HTTP/1.0,所以我们也使用HTTP/1.0,状态设置为200 OK,表示成功。接着设置了服务器名,返回的内容长度,内容类型,需要注意的是在内容类型后有两个\r\n,后一个代表着空行,若是忘记了,客户端就识别不了你返回的消息体了。

接着,在目录中读取请求文件,若是读取失败,则表示请求文件不存在,那就返回404了。若存在,便将文件读取出来发送给客户端,这些内容就是消息体。

最后,需要断开与客户端的连接,继续接收其它请求,所以说HTTP协议是无状态的协议。

最后剩一个404页面,操作和发送数据就没什么两样,便不细说:

voidWebServer::SendNotFound(){std::stringprotocol="HTTP/1.0404NotFound\r\n";std::stringservName="Server:MyWebServer\r\n";std::stringcontentLen="Content-length:2048\r\n";std::stringcontentType="Content-type:text/html\r\n\r\n";std::ifstreamsendFile("404.html");if(!sendFile.is_open()){std::cout<<"Notfound404.html"<<std::endl;return;}//发送状态行,消息头send(m_clntSock,protocol.c_str(),protocol.size(),0);send(m_clntSock,servName.c_str(),servName.size(),0);send(m_clntSock,contentLen.c_str(),contentLen.size(),0);send(m_clntSock,contentType.c_str(),contentType.size(),0);charbuf[CONTENT_SIZE]={0};sendFile.getline(buf,CONTENT_SIZE);for(;!sendFile.eof();){//发送文件send(m_clntSock,buf,strlen(buf),0);sendFile.getline(buf,CONTENT_SIZE);}sendFile.close();closesocket(m_clntSock);}

不同之处在于状态行得设置为404 Not Found。

现在在main中调用:

#include"WebServer.h"intmain(){WebServerwebServ;webServ.RespondRequest();return0;}

运行程序,当浏览器访问localhost:8000/index.html时:


HTTP协议之编写简单的Web服务器

当访问其它任何页面时:

HTTP协议之编写简单的Web服务器

当然,懂了HTTP协议后,我们也可以作为客户端去访问各种网页,关于这个后面准备写一个C++的网络爬虫。不过之前准备先把SMTP,select,重叠IO,IOCP,boost::asio,regex等等写了再去写,所以就都是明年的事了。:)

您可能还会对下面的文章感兴趣: