编程实战:自己编写HTTP服务器(系列1:概述和应答)

总目录:专栏介绍与文章目录-CSDN博客

        有些事情可能其实没那么困难,比如写一个可用的web服务器。

        当然我不是说写一个能够和apache或者IIS或者tomcat之类竞争的web服务器,我说的只是给自己的程序添加一个内置的HTTP服务,以便能做一点特别的事情。

        我没打算从HTTP协议讲起,所以你要先学习一下HTTP协议。我直接从代码开始。

        系列索引:

编程实战:自己编写HTTP服务器(系列1:概述和应答)-CSDN博客(本文)

编程实战:自己编写HTTP服务器(系列2:请求)-CSDN博客

编程实战:自己编写HTTP服务器(系列3:处理框架)-CSDN博客

编程实战:自己编写HTTP服务器(系列4:查看文件、下载等一般功能)-CSDN博客

编程实战:自己编写HTTP服务器(系列5:执行后台shell命令)-CSDN博客

编程实战:自己编写HTTP服务器(系列6:调用用户功能)-CSDN博客

编程实战:自己编写HTTP服务器(系列7:用户功能接口)-CSDN博客

编程实战:自己编写HTTP服务器(系列8:流水输出和无刷新动态显示)-CSDN博客

编程实战:自己编写HTTP服务器(系列9:上传文件)-CSDN博客


 

目录

一、概述

二、处理应答

2.1 代码总览

2.2 成员变量

2.3 发送应答 Flash()

 2.4 应答的全部代码

三、处理请求


一、概述

        作为一个TCP服务,首先要基于一个普通的socket服务。socket服务并没有什么特别,不过是bind+accept而已,然后就是普通的recieve和send。

        我本来想贴socket服务代码出来的,一看旧代码晕了,太多私货了。你还是自己写个服务吧。作为技术验证,不需要多线程多进程,最简单的单进程阻塞循环就可以了。

        虽然HTTP2.0挺复杂,但是仅支持1.0也没关系,客户端都能支持。HTTP1.0很简单,一个连接一个请求一个应答,也就是说,服务端收到一串信息,然后返回一串信息,然后就关闭连接。

        所以服务器可以很简单,我写了一个类用来分析请求,又写了一个类处理应答,再加上一点别的处理,一个基本的HTTP服务器就可以工作了。

二、处理应答

2.1 代码总览

        为了能快速见到效果,我们先处理应答。为什么先处理应答呢?有了应答起码能从浏览器知道基本机制已经在运作了,然后再考虑根据请求输出。

        处理应答的类:

        其实相当的简单,SendXXX()系列函数直接发送特定应答。例如返回没有内容的200响应:

		//返回无内容的成功
		bool Send200(CSocket & s)
		{
			m_status_line = "HTTP/1.1 200 OK";
			AddHeaderContentLength();
			return Flush(s);
		}

         实际返回的就是状态行和一个内容长度头标。

2.2 成员变量

	private:
		string m_status_line;//状态行
		vector<pair<string, string > > m_headers;//应答头
		map<string, string > m_cookies;
		string m_body;//消息体
		long m_content_length;//设置的内容长度,未设置为-1
		long m_content_length_sended;//实际发送的内容长度
		bool m_isCgi;

        前四个成员变量直接对应了HTTP应答的四个部分:状态行、头标、cookies、body。当然,cookies也是一个头标,但是是一个很特别的头标,值得特殊处理。

        另外三个是内部用途。与CGI有关的可以无视。

2.3 发送应答 Flash()

        Flash函数根据成员变量发送应答:

		bool Flush(CSocket & s)
		{
			if (m_isCgi)return CgiFlush();
			string str;
			vector<pair<string, string > >::size_type i;
			//仅在状态行不为空的情况下发送应答头
			if (m_status_line.size() != 0)
			{
				str = m_status_line + "\r\n";
				for (i = 0; i < m_headers.size(); ++i)
				{
					str += m_headers[i].first + ": " + m_headers[i].second + "\r\n";
				}
				map<string, string >::const_iterator it;
				for (it = m_cookies.begin(); it != m_cookies.end(); ++it)
				{
					str += "Set-Cookie: ";
					str += it->first + "=" + it->second + "\r\n";
				}
				str += "\r\n";
			}
			str += m_body;
			m_content_length_sended += m_body.size();
			m_status_line = "";
			m_headers.clear();
			m_body = "";
			DEBUG_LOG << "发送应答" << endl << str << ENDI;
			if (!s.Send(str))
			{
				s.Close();
				return false;
			}
			return true;
		}

        看了这个这个函数应该对应答有了充分了解了,很简单的嘛。

 2.4 应答的全部代码

        真的没什么好多说的,代码都是一目了然的。

	//HTTP应答
	//AddHeaderXXXX系列函数添加应答头,必须在Flush之前使用
	class CHttpRespond
	{
	private:
		string m_status_line;//状态行
		vector<pair<string, string > > m_headers;//应答头
		map<string, string > m_cookies;
		string m_body;//消息体
		long m_content_length;//设置的内容长度,未设置为-1
		long m_content_length_sended;//实际发送的内容长度
		bool m_isCgi;
	public:
		//判断是否可以保持连接,依据是发送了内容长度并且实际发送长度匹配
		bool isCanKeepAlive()const
		{
			return m_content_length >= 0 && m_content_length == m_content_length_sended;
		}
		string GetBody()const { return m_body; }
		//返回无内容的成功
		bool Send200(CSocket & s)
		{
			m_status_line = "HTTP/1.1 200 OK";
			AddHeaderContentLength();
			return Flush(s);
		}
		//重定向
		bool Send302(CSocket & s, string const & location)
		{
			m_status_line = "HTTP/1.1 302 Found";
			AddHeader("Location", location);
			AddHeaderContentLength();
			return Flush(s);
		}
		//错误的请求
		bool Send400(CSocket & s)
		{
			m_status_line = "HTTP/1.1 400 BadRequest";
			AddHeaderContentLength();
			return Flush(s);
		}
		//需要认证
		bool Send401(CSocket & s, string const & realm)
		{
			m_status_line = "HTTP/1.1 401 Unauthorized";
			string str = "Basic realm=\"" + realm + "\"";
			AddHeader("WWW-Authenticate", str);
			AddHeaderContentLength();
			return Flush(s);
		}
		//禁止访问
		bool Send403(CSocket & s)
		{
			m_status_line = "HTTP/1.1 403 Forbidden";
			AddHeaderContentLength();
			return Flush(s);
		}
		//没有找到
		bool Send404(CSocket & s)
		{
			m_status_line = "HTTP/1.1 404 NotFound";
			AddHeaderContentLength();
			return Flush(s);
		}
		//内部错误
		bool Send500(CSocket & s)
		{
			m_status_line = "HTTP/1.1 500 Internal Server Error";
			AddHeaderContentLength();
			return Flush(s);
		}
		//内部错误,返回第一行为错误码然后是错误信息,错误信息为全局错误信息
		bool Send500WithMessage(CSocket & s)
		{
			m_status_line = "HTTP/1.1 200 Internal Server Error";
			AddHeaderContentTypeByFilename("*.txt");

			char buf[256];
			sprintf(buf, "%ld\n", G_GET_ERROR);
			AppendBody(buf);

			AppendBody(G_ERROR_MESSAGE().str());
			G_CLEAR_ERROR;

			AddHeaderContentLength();
			return Flush(s);
		}
		void Clear()
		{
			m_status_line = "";
			m_body = "";
			m_content_length = -1;
			m_content_length_sended = 0;
			m_headers.clear();
			m_cookies.clear();
			m_isCgi = false;
		}
		void Init(bool _cgi = false)//初始化应答,设置通用部分
		{
			Clear();
			m_isCgi = _cgi;
			m_status_line = "HTTP/1.1 200 OK";
			AddHeader("Server", "WWW-Server/1.0 by ZB++");
			//AddCookie("user","unknown");
			//AddCookie("id","unknown");
		}
		void AddHeader(string const & header, string const & value)//添加应答头,除了"Server"其它都可以用,但有一些可以更方便地使用AddHeaderXXXX
		{
			if ("Server" != header)m_headers.push_back(pair<string, string >(header, value));
		}
		void AddHeader(string const & header, long value)
		{
			char buf[256];
			sprintf(buf, "%ld", value);
			AddHeader(header, buf);
		}
		void AddCookie(string const & name, string const & value)
		{
			m_cookies[name] = value;
		}
		void AppendBody(long n)//添加消息体
		{
			char buf[64];
			sprintf(buf, "%ld", n);
			m_body += buf;
		}
		void AppendBody(string const & str)//添加消息体
		{
			m_body += str;
			//LOG<<"HTML 输出:"<<endl<<str<<ENDI;
		}
		void AppendBody(char const * str)//添加消息体
		{
			m_body += str;
			//LOG<<"HTML 输出:"<<endl<<str<<ENDI;
		}
		void AppendBodyHtmlStart(char const * title = NULL)//添加HTML文档头部,直到<body>
		{
			m_body += "<!DOCTYPE HTML>\r\n"
				"<html>\r\n"
				"<head>\r\n"
				"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\r\n"
				"<link href=\"/css/css.css\" rel=\"stylesheet\" type=\"text/css\"/>\r\n"
				;
			if (NULL != title)
			{
				m_body += "<title>";
				m_body += title;
				m_body += "</title>\r\n";
			}
			m_body += "</head>\r\n<body>\r\n";
		}
		void AppendBodyHtmlEnd()//添加HTML文档结束,从</body>开始
		{
			m_body += "</body>\r\n</html>\r\n";
		}
		void AppendBodyHtmlScroll()//添加HTML滚动
		{
			m_body += "<script language=\"JavaScript\">window.scrollBy(0,document.body.scrollHeight);</script>\r\n";
		}
		void AppendBodyHtmlCopyright()//添加HTML版权
		{
			m_body += "\r\n<BR><CODE>C++ WEB服务类库 包含嵌入式WEB服务器、CGI、HTML、XML支持</CODE>";
			m_body += "\r\n<BR><CODE>Powered by Embeded Web Server , Copyright &copy; " COPY_RIGHT " 我 ,All rights reserved.</CODE>";
			m_body += "\r\n<BR><CODE>本站点由嵌入式WEB服务器支持 , 版权所有 &copy; " COPY_RIGHT " 我 ,保留所有权利.</CODE>";
		}
		bool Flush(CSocket & s)
		{
			if (m_isCgi)return CgiFlush();
			string str;
			vector<pair<string, string > >::size_type i;
			//仅在状态行不为空的情况下发送应答头
			if (m_status_line.size() != 0)
			{
				str = m_status_line + "\r\n";
				for (i = 0; i < m_headers.size(); ++i)
				{
					str += m_headers[i].first + ": " + m_headers[i].second + "\r\n";
				}
				map<string, string >::const_iterator it;
				for (it = m_cookies.begin(); it != m_cookies.end(); ++it)
				{
					str += "Set-Cookie: ";
					str += it->first + "=" + it->second + "\r\n";
				}
				str += "\r\n";
			}
			str += m_body;
			m_content_length_sended += m_body.size();
			m_status_line = "";
			m_headers.clear();
			m_body = "";
			DEBUG_LOG << "发送应答" << endl << str << ENDI;
			if (!s.Send(str))
			{
				s.Close();
				return false;
			}
			return true;
		}
		bool CgiFlush()
		{
			string str;
			string::size_type i;
			if (m_status_line.size() != 0)
			{
				//cgi不发送状态行,直接发送头标
				m_status_line = "";
				for (i = 0; i < m_headers.size(); ++i)
				{
					str += m_headers[i].first + ": " + m_headers[i].second + "\r\n";
				}
				m_headers.clear();
				map<string, string >::const_iterator it;
				for (it = m_cookies.begin(); it != m_cookies.end(); ++it)
				{
					str += "Set-Cookie: ";
					str += it->first + "=" + it->second + "\r\n";
				}
				m_cookies.clear();
				str += "\r\n";//应答头结束标记
			}
			str += m_body;
			m_body = "";
			thelog << "发送应答" << endl << str << endi;
			cout << str << flush;
			return true;
		}

		void AddHeaderContentTypeByFilename(char const * filename)//添加内容类型到应答头,如果已经存在则替换,根据文件名后缀判断
		{
			for (size_t i = 0; i < m_headers.size(); ++i)
			{
				if (m_headers[i].first != "Content-Type")continue;
				m_headers[i].second = CMIMEType::GetMIMEType(filename);
				return;
			}
			m_headers.push_back(pair<string, string >("Content-Type", CMIMEType::GetMIMEType(filename)));
		}
		bool AddHeaderContentLength()//添加消息体长度到应答头,若要使用此功能必须在唯一的Flush之前调用(因为第一个Flush将发出应答头,以后无法再更改应答头并且无法获得正确的消息体长度)
		{
			if (m_content_length < 0)
			{
				m_content_length = m_body.size();
				char buf[256];
				sprintf(buf, "%ld", m_content_length);
				m_headers.push_back(pair<string, string >("Content-Length", buf));
				return true;
			}
			else
			{
				LOG << "错误:已经设置过Content-Length" << ENDE;
				LOG << this->m_status_line << ENDE;
				LOG << this->m_content_length << ENDE;
				LOG << this->m_body << ENDE;
				return false;
			}
		}
		bool AddHeaderContentLength(long len)//添加消息体长度到应答头
		{
			if (m_content_length < 0)
			{
				m_content_length = len;
				char buf[256];
				sprintf(buf, "%ld", m_content_length);
				m_headers.push_back(pair<string, string >("Content-Length", buf));
				return true;
			}
			else
			{
				LOG << "错误:已经设置过Content-Length" << ENDE;
				return false;
			}
		}
		void AddHeaderNoCache()//添加禁止缓存到应答头
		{
			AddHeader("Pragma", "no-cache");
			AddHeader("Cache-Control", "no-cache, must-revalidate");
			AddHeader("Expires", "0");
		}
		void AddHeaderExpires(time_t t1, long seconds)//添加缓存控制到应答头
		{
			AddHeader("Expires", CHttpDate::HttpDate(t1, seconds));
		}
	};

三、处理请求

        编程实战:自己编写HTTP服务器(系列2:请求)-CSDN博客

(这里是结束,但不是整个系列的结束)

  • 26
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值