目录
- 本节重点
- 理解应用层的作用, 初识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; //请求的资源的url
std::string _http_version; //http协议版本号
std::vector<std::string> _req_head; //请求报头:包括请求行、报头属性信息
std::string _body; //请求正文
std::string _filepath; //文件的路径,从web根目录开始,即wwwroot/url
std::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-lenth
std::string content_lenth = "Content-Lenth: ";
content_lenth + to_string(response_body.size());
//属性2:content-type
std::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 cookie
response_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_client
http_server:HttpServer.cc
g++ -o $@ $^ -std=c++11 -lpthread
http_client:HttpClient.cc
g++ -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网络编程的相关知识哦,我们下期见!!!!!