目录
- 本节重点
- 理解应用层的作用, 初识HTTP协议
- 一、应用层
- 二、HTTP协议
- 2.1 认识URL
- 2.2 urlencode和urldecode
- 2.3 HTTP协议格式
- 2.4 HTTP的方法
- 2.4 HTTP的状态码
- 2.5 HTTP常见的Header属性
- 三、最简单的HTTP服务器
- 3.1 HttpServer.hpp
- 3.2 HttpServer.cc
- 3.3 HttpClient.cc
- 3.4 log.hpp
- 3.5
- 3.6 makefile
- 3.7 wwwroot目录下的资源
- 四、HTTP协议内容一览图
本节重点
理解应用层的作用, 初识HTTP协议
一、应用层
程序员们写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层。
二、HTTP协议
虽然说, 应用层协议是程序员自己定的.
但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用. HTTP(超文本传输协议)就是其中之一。
2.1 认识URL
平时我们俗称的 “网址” 其实就是说的 URL。
2.2 urlencode和urldecode
像 / ? : 等这样的字符, 已经被url当做特殊意义理解了。 因此这些字符不能随意出现.比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义。
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式。
“+” 被转义成了 “%2B”
urldecode就是urlencode的逆过程;
2.3 HTTP协议格式
HTTP请求:
(1)请求行: [请求方法] + [url] + [HTTP版本]。
(2)请求报头(Header): 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;
(3)空行:遇到单单一个\n空行表示Header部分结束.
(4)请求正文: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度;
HTTP响应:
(1)状态行: [HTTP版本] + [状态码] + [状态码描述]
(2)响应报头(Header): 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;
(3)空行:遇到单单一个\n空行表示Header部分结束。
(4)响应正文:空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页面内容就是在body中。
2.4 HTTP的方法
其中最常用的就是GET方法和POST方法。
2.4 HTTP的状态码
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)
2.5 HTTP常见的Header属性
(1)Content-Type: 数据类型(text/html等)
(2)Content-Length: Body的长度
(3)Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
(4)User-Agent: 声明用户的操作系统和浏览器版本信息;
(5)referer: 当前页面是从哪个页面跳转过来的;
(6)location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
(7)Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;
三、最简单的HTTP服务器
实现一个最简单的HTTP服务器, 只在网页上输出 “hello world”; 只要我们按照HTTP协议的要求构造数据, 就很容易能做到;
我们以下的服务器是添加了一点前端的代码的:
3.1 HttpServer.hpp
#pragma once#include <iostream>
#include "Socket.hpp"
#include <functional>
#include <fstream>
#include <vector>
#include <string>
#include <sstream>
#include <unordered_map>// HTTP协议可以以"\r\n"作为行分隔符,也可以是"\n"
const std::string sep = "\r\n";// 传说中的web根目录
const std::string wwwroot = "./wwwroot";// 服务器主页
const std::string homepage = "index.html";const uint16_t DEFAULT_PORT = 8080;class HttpServer;class ThreadData
{
public:ThreadData(int sockfd, HttpServer *svr): _sockfd(sockfd), _svr(svr){}public:int _sockfd;HttpServer *_svr;
};class Request
{
public:// 反序列化void Deserialize(std::string req){while (true){// 用行分隔符"\r\n",分割请求报头中的属性信息auto pos = req.find(sep);if (pos == string::npos){break;}std::string tmp = req.substr(0, pos);if (tmp.empty()){// 说明读取到了空行,即报头已经读完了req.erase(pos, sep.size());break;}_req_head.push_back(tmp);// 没获取一条属性信息记得在原报头中删除req.erase(0, pos + sep.size());}// 取出报头之后得到的剩余的就是请求正文_body = req;}// 解析http协议void Parse(){std::string req_line = _req_head[0];//stringstream默认是以空格作为分隔符的std::stringstream ss(req_line);//必须按顺序ss >> _method >> _url >> _http_version;// 拼接资源的路径_filepath = wwwroot;if (_url == "/" || _url == "/index.html"){_filepath += "/";_filepath += homepage;}else{_filepath += _url;}//从后往前找到'.',从而找出后缀auto pos = _filepath.rfind('.');if (pos == string::npos){_suffix = ".html";}else{_suffix = _filepath.substr(pos);}}//用来调试的void DebugPrint(){for (auto &line : _req_head){std::cout << "--------------------------------" << std::endl;std::cout << line << "\n\n";}std::cout << "method: " << _method << std::endl;std::cout << "url: " << _url << std::endl;std::cout << "http_version: " << _http_version << std::endl;std::cout << "file_path: " << _filepath << std::endl;std::cout << _body << std::endl;}std::string GetFilePath(){return _filepath;}std::string GetFileSuffix(){return _suffix;}private:std::string _method; //请求方法std::string _url; //请求的资源的urlstd::string _http_version; //http协议版本号std::vector<std::string> _req_head; //请求报头:包括请求行、报头属性信息std::string _body; //请求正文std::string _filepath; //文件的路径,从web根目录开始,即wwwroot/urlstd::string _suffix; //url的后缀
};class HttpServer
{
public:HttpServer(const uint16_t &port = DEFAULT_PORT): _port(port){//.html对应的网页是文本类型_content_type[".html"] = "text/html";// _content_type[".html"] = "application/json";//.png对应的网页是图片_content_type[".png"] = "image/png";}~HttpServer(){}void InitHttpServer(){// 1、创建套接字_listen_sock.Socket();// 2、绑定_listen_sock.Bind(_port);// 3、监听_listen_sock.Listen();}static string ReadIndexHtml(const std::string &path){// 有坑?std::string str;//注意,这里一定要用二进制的方式去读取,否则类似于图片的文件就有可能会读取出//错从而在访问的时候看不到图片ifstream in(path, std::ios::binary);if (!in.is_open()){return "";}//对于seekg函数,0表示偏移量,std::ios_base::end是基准,//意思是:设置当前位置相对于最后一个位置的偏移量是0,//说明当前指针的位置就指向文件内容的最后一个位置in.seekg(0, std::ios_base::end);//对于tellg函数,是获取当前读取位置的,因为上面已经设置了//当前指针指向的位置是文件内容的最后一个位置,所以当前位置//的数值就等于文件内容的大小,即一共有len个自己的内容int len = in.tellg();//同上,即设置指针指向文件的开始位置in.seekg(0, std::ios_base::beg);std::string content;content.resize(len);//把内容读取到content中in.read((char *)content.c_str(), len);in.close();return content;}//url的后缀转换成对应的后缀描述,用于构建响应std::string SuffixToDesc(const std::string &suffix){auto ret = _content_type.find(suffix);if (ret == _content_type.end()){//如果从类型中没有找到,那就统一当成是.html后缀//返回对应的文本类型的描述"text/html"return _content_type[".html"];}else{return _content_type[suffix];}}static void *Handler(void *args){//线程分离pthread_detach(pthread_self());ThreadData *ptd = static_cast<ThreadData *>(args);int sockfd = ptd->_sockfd;while (true){char buffer[10240] = {0};ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);if (n > 0){buffer[n] = '\0';std::cout << buffer << std::endl;//解析http协议请求Request req;req.Deserialize(buffer);req.Parse();req.DebugPrint();// 构建http响应std::string response;bool ok = true;//根据请求的url,读取对应的文件内容作为响应的正文std::string response_body = ReadIndexHtml(req.GetFilePath());if (response_body.empty()){ok = false;std::string err_html = wwwroot;err_html += '/';err_html += "err.html";response_body = ReadIndexHtml(err_html);}// 状态行std::string state_line;if (ok){state_line = "HTTP/1.0 200 OK\r\n";}else{state_line = "HTTP/1.0 404 Not Found\r\n";}response += state_line;//做重定向的状态行//response = "HTTP/1.0 302 Found\r\n";// 响应报头std::string response_head;//属性1:content-lenthstd::string content_lenth = "Content-Lenth: ";content_lenth + to_string(response_body.size());//属性2:content-typestd::string content_type = "Content-Type: ";content_type += ptd->_svr->SuffixToDesc(req.GetFileSuffix());response_head += content_lenth;response_head += "\r\n";response_head += content_type;response_head += "\r\n";//属性3:set cookieresponse_head += "Set-Cookie: name=kobe&&passwd=123456";response_head += "\r\n";//属性4:Location,设置重定向时需要访问的网址// response_head += "Location: http://www.qq.com/";// response_head += "\r\n";response += response_head;// 空行response += "\r\n";// 正文response += response_body;ssize_t n = send(sockfd, response.c_str(), response.size(), 0);}else if (n == 0){log(Info, "client quit...,关闭连接:%d", sockfd);close(sockfd);return nullptr;}else if (n < 0){std::cout << "n=" << n << std::endl;log(Error, "recv error,关闭连接:%d", sockfd);close(sockfd);return nullptr;}}log(Info, "服务器退出,关闭连接:%d", sockfd);close(sockfd);return nullptr;}void Start(){while (true){std::string client_ip;uint16_t client_port;int sockfd = _listen_sock.Accept(client_ip, client_port);if (sockfd < 0){continue;}pthread_t tid;ThreadData td(sockfd, this);// 创建线程pthread_create(&tid, nullptr, Handler, (void *)(&td));}}private:Sock _listen_sock;uint16_t _port;std::unordered_map<std::string, std::string> _content_type;
};
3.2 HttpServer.cc
#include "HttpServer.hpp"
#include <memory>void Usage(const std::string& proc)
{std::cout<<"\t\n"<<std::endl;std::cout<<"Usage: "<<proc<<" server_port[>=1024]"<<std::endl<<std::endl;
}int main(int argc, char* argv[])
{if(argc!=2){Usage(argv[0]);exit(1);}uint16_t server_port=(uint16_t)stoi(argv[1]);std::unique_ptr<HttpServer> svr(new HttpServer(server_port));svr->InitHttpServer();svr->Start();return 0;
}
3.3 HttpClient.cc
提示一下:如果直接用HttpClient.cc访问服务器,那么需要按照HTTP协议的标准格式构建报文才能正确地返回,否则服务器会发生段错误的。
#include "HttpServer.hpp"
#include "Socket.hpp"int main()
{Sock sock;sock.Socket();sock.Connect("43.138.156.240",8081);string buffer;while(true){std::cout<<"Please Enter:";std::getline(std::cin,buffer);send(sock.Sockfd(),buffer.c_str(),buffer.size(),0);sleep(1);char tmp[10240]={0};recv(sock.Sockfd(),tmp,sizeof(tmp),0);std::cout<<tmp<<std::endl;}return 0;
}
3.4 log.hpp
#pragma once#include <iostream>
using namespace std;
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string>
#include <time.h>
#include <stdarg.h>// 日志等级
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4#define Screen 1
#define OneFile 2
//向多个文件打印
#define Classfile 3
#define SIZE 1024#define LogFile "log.txt"class Log
{
public:Log(){printMethod = Screen;path = "./log/";}void Enable(int mothod){printMethod = mothod;}string LevelToString(int level){switch (level){case Info:{return "Info";}case Debug:{return "Debug";}case Warning:{return "Warning";}case Error:{return "Error";}case Fatal:{return "Fatal";}default:{return "None";}}}void printlog(int level,const string& logtxt){switch(printMethod){case Screen:{cout<<logtxt<<endl;break;}case OneFile:{PrintOneFile(LogFile,logtxt);break;}case Classfile:{PrintClassfile(level,logtxt);break;}default:{break;}}}void PrintOneFile(const string& logname,const string& logtxt){string _logname=path+logname;int fd=open(_logname.c_str(),O_WRONLY|O_CREAT|O_APPEND,0666);if(fd<0){perror("open fail");return;}write(fd,logtxt.c_str(),logtxt.size());close(fd);}void PrintClassfile(int level,const string& logtxt){string filename=LogFile;filename+='.';filename+=LevelToString(level);PrintOneFile(filename,logtxt);}void operator()(int level,const char* format,...){time_t t=time(nullptr);struct tm* ctime=localtime(&t);char leftbuffer[SIZE];snprintf(leftbuffer,SIZE,"[%s][%d-%d-%d %d:%d:%d]",LevelToString(level).c_str(),ctime->tm_year+1900,ctime->tm_mon+1,ctime->tm_mday,ctime->tm_hour,ctime->tm_min,ctime->tm_sec);va_list s;va_start(s,format);char rightbuffer[SIZE]={0};vsnprintf(rightbuffer,SIZE,format,s);va_end(s);char logtxt[SIZE*2];snprintf(logtxt,sizeof(logtxt),"%s %s",leftbuffer,rightbuffer);printlog(level,logtxt);}~Log(){}private:// 打印方法int printMethod;string path;
};//定义一个全局的log
Log log;
3.5
#pragma once#include <iostream>
using namespace std;
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#include <unistd.h>
#include <strings.h>
#include <cstring>
#include <string>int backlog = 10;enum
{SockErr = 2,BindErr,ListenErr,ConnectErr,
};class Sock
{
public:Sock(): _sockfd(-1){}~Sock(){if(_sockfd>0){close(_sockfd);}}// 创建套接字void Socket(){_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){log(Fatal, "socket failed,errno:%d,errstring:%s", errno, strerror(errno));exit(SockErr);}int opt=1;if(setsockopt(_sockfd,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,&opt,sizeof(opt))<0){log(Warning, "setsockopt failed, sockfd:%d", _sockfd);}log(Info, "setsockopt successed, sockfd:%d", _sockfd);log(Info, "socket successed, sockfd:%d", _sockfd);}// 绑定void Bind(const uint16_t &serverPort){struct sockaddr_in local;bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(serverPort);local.sin_addr.s_addr = INADDR_ANY;if (bind(_sockfd, (struct sockaddr *)(&local), sizeof(local)) < 0){log(Fatal, "bind failed,errno:%d,errstring:%s", errno, strerror(errno));exit(BindErr);}log(Info, "bind successed...");}// 监听void Listen(){if (listen(_sockfd, backlog) < 0){log(Fatal, "set listen state failed,errno:%d,errstring:%s", errno, strerror(errno));exit(ListenErr);}log(Info, "set listen state successed");}//获取连接int Accept(string& clientip,uint16_t& clientport){struct sockaddr_in client;socklen_t len=sizeof(client);bzero(&client,sizeof(client));int sockfd=accept(_sockfd,(struct sockaddr*)(&client),&len);if(sockfd<0){log(Warning, "accept new link failed,errno:%d,errstring:%s", errno, strerror(errno));return -1;}log(Info,"accept a new link...,sockfd:%d",sockfd);clientip=inet_ntoa(client.sin_addr);clientport=(uint16_t)(ntohs(client.sin_port));return sockfd;}// 连接void Connect(const string &serverIp, const uint16_t &serverPort){struct sockaddr_in server;bzero(&server, sizeof(server));server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr(serverIp.c_str());server.sin_port = htons(serverPort);if (connect(_sockfd, (struct sockaddr *)(&server), sizeof(server)) < 0){log(Fatal, "connect server failed,errno:%d,errstring:%s", errno, strerror(errno));exit(ConnectErr);}log(Info, "connect server succeeded...");}void Close(){if(_sockfd>0){close(_sockfd);}}int Sockfd(){return _sockfd;}private:int _sockfd;
};
3.6 makefile
.PHONY:all
all:http_server http_clienthttp_server:HttpServer.ccg++ -o $@ $^ -std=c++11 -lpthread http_client:HttpClient.ccg++ -o $@ $^ -std=c++11 .PHONY:clean
clean:rm -f http_server http_client
3.7 wwwroot目录下的资源
自行创建wwwroot目录并把资源按要求放到目录下:
web根目录下的资源
备注:
此处我们使用 8081 端口号启动了HTTP服务器. 虽然HTTP服务器一般使用80端口,但这只是一个通用的习惯. 并不是说HTTP服务器就不能使用其他的端口号.使用Edge测试我们的服务器时, 可以看到服务器打出的请求中还有一个
GET /favicon.ico HTTP/1.1 这样的请求。favicon.ico是用来设置网页上的小图标的:
可以试试把返回的状态码改成404, 403, 504等, 看浏览器上分别会出现什么样的效果。
四、HTTP协议内容一览图
以上就是今天想要跟大家分享的关于HTTP协议的所有内容了,你学会了吗?如果感觉到有所收获的话,那就点点小心心,再点点关注呗,后期还会持续更新有关Linux网络编程的相关知识哦,我们下期见!!!!!