【Linux】http 协议
目录
一、http协议
(一)http 协议的概念
(二)URL的组成
(三)urlencode 和 urldecode
二、http 的协议格式
(一)http 请求方法
(二)http 响应状态码
(三)http 常见的响应报头
三、http 协议客户端和服务器通信过程
(一)应用层如何保证将请求或响应完整读取完了?
(二)http如何进行序列化和反序列化
(三)通信全过程
1、启动服务器
2、工具类
2、分析请求报文
3、发送响应报文
四、http 会话保持
一、http协议
(一)http 协议的概念
上节我们提到可以自定协议,但是面对一些复杂的场景单靠自定协议是很难满足需求的。实际上已经有一些现成且好用的应用层协议,http 超文本传输协议就是其中之一。
(二)URL的组成
URL即我们平时常称的网址。
ip会标识一台网络主机,我们通过在浏览器中输入网址就可以访问该服务器,从指定的文件路径下找到用户请求的文件返回给用户。
(三)urlencode 和 urldecode
对于URL而言,在浏览器中其并不是完全明文显式的,例如:
我们可以看到当我们搜索 C++ 时, URL中不是显式的C++,而是 C%2B%2B 的形式,这是因为在http协议对于URL中的特殊字符,必须将其进行转义。
转义规则:
一个字节是八个比特位,从中间划分左右各4位(不足4位直接处理),将左右4位分别转为16进制,将两位组合在一起后前面加上%便编码成%XY格式。
二、http 的协议格式
(一)http 请求方法
本文只介绍两种常用的请求方法:GET和POST。
方法 | 说明 | 支持的HTTP协议版本 |
GET | 获得资源 | 1.0、1.1 |
POST | 传输实体主题 | 1.0、1.1 |
虽然它们都是客户端向服务器发出的请求方法,但是二则略有不同。
针对同一份前端表单form,当不同的请求方法时,浏览器会将执行不同的动作:
1、GET方法通过URL传递参数。例如上例中,当我们使用GET方法处理表单时,浏览器会将我们的表单内容拼接为URL发送给服务器,http://ip::port/XXX?key=value。使用URL进行传参注定了参数不能过大,当传输数据过大时使用GET方法请求就不合适了;
2、 POST方法则是通过http请求正文传递参数的,对于正文数据大小没有限制,适合传递一些大型文件。
3、POST方法相较于GET方法更加私密,GET方法通过URL传递参数,所以传递的数据可以被直接看到。虽然POST方法比GET方法更私密,但二者实际都是不安全的,如果需要加密安全的话得使用https协议。
(二)http 响应状态码
https协议的状态码:
类别 | 原因短语 | |
1XX | Informational(信息状态码) | 接收的请求正在处理 |
2XX | Success(成功状态码) | 请求正常处理完毕 |
3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 |
5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 |
常见错误码:
2XX:
- 200 OK:请求成功
- 201 Created:请求已经被实现,资源已经被创建。
- 204 No Content:请求成功,但响应报文不含实体的主体部分。
3XX:
- 301 Moved Permanently:永久性重定向
- 302 Found:临时性重定向
- 307 Temporary Redirect:临时性重定向
4XX:
- 400 Bad Request:请求报文存在语法错误
- 401 Unauthorized:未经授权,需要身份验证
- 403 Forbidden:服务器拒绝请求
- 404 Not Found:服务器无法找到请求的资源
5XX:
- 500 Internal Server Error:服务器内部错误
- 502 Bad Gateway:网关错误
- 503 Service Unavailable:服务器暂时无法处理请求
- 504 Gateway Timeout:网关超时
(三)http 常见的响应报头
- Content-Type:指定响应体的MIME类型,例如text/html表示HTML文本,image/jpeg表示JPEG图片等。
- Content-Length:指定响应体的长度,单位为字节
- Host:客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上
- User-Agent:声明用户的操作系统和浏览器版本信息
- referer:当前页面是从哪个页面跳转过来的
- location:搭配3xx状态码使用, 告诉客户端接下来要去哪里访问
- cookie:用于在客户端存储少量信息. 通常用于实现会话的功能
三、http 协议客户端和服务器通信过程
(一)应用层如何保证将请求或响应完整读取完了?
首先对于请求行、请求报头、状态行和响应报头都是按照 \r\n 为行分隔符,因此可以读取完整的一行。而报头与正文之间也存在着一个分隔符,因此可以区分请求报头是否读取完整。而请求报头中存在着 Content-Length 字段,其记录了正文的长度,因此应用层可以完整地读取请求正文或响应正文,从而保证了应用层可以完整地读取一个完整的请求或响应。
(二)http如何进行序列化和反序列化
上节我们自定义协议的时候是需要对请求和响应进行序列化和反序列化的,那么 http 协议是如何进行需要序列化和反序列化呢?实际 http 协议本身无需用户关注序列化和反序列化,直接发送即可,而对于正文部分,如果有需要用户可以自定义序列化和反序列化方案。
(三)通信全过程
1、启动服务器
void httpHandler(int fd, func_t func)
{
// 读取数据
char buffer[1024];
ssize_t n = recv(fd, buffer, sizeof(buffer) - 1, 0);
if (n >= 0)
{
buffer[n] = 0;
Request req;
Response resp;
req.parse(buffer);
func(req, resp);
send(fd, resp.outbuffer.c_str(), resp.outbuffer.size(), 0);
}
}
void start(func_t func)
{
while (1)
{
signal(SIGCHLD, SIG_IGN);
sockaddr_in addr;
socklen_t len = sizeof(addr);
int socket = accept(_fd, (struct sockaddr *)&addr, &len);
if (socket == -1)
{
cerr << "accept failure : " << strerror(errno) << endl;
continue;
}
pid_t pid = fork();
if (pid == 0)
{
close(_fd);
httpHandler(socket, func);
close(socket);
exit(0);
}
close(socket);
}
}
2、工具类
#include <iostream>
#include <string>
#include <fstream>
using namespace std;
class Util
{
public:
static string getLine(string &buffer, const string &sep)
{
auto index = buffer.find(sep);
if (index == string::npos)
return "";
string ret = buffer.substr(0, index);
buffer.erase(0, index);
return ret;
}
static bool readFile(const string &text, char *buffer, int size)
{
fstream in(text, ios_base::binary | ios_base::in);
if (!in.is_open())
return false;
in.read(buffer, size);
in.close();
return true;
}
};
2、分析请求报文
class Request
{
public:
Request() {}
~Request() {}
void parse(const string &in)
{
inbuffer = in;
string firstLine = Util::getLine(inbuffer, SEP);
stringstream s(firstLine);
s >> method >> url >> httpversion;
path = DEFAULT_PATH;
path += url;
if (path[path.size() - 1] == '/')
path += HOME_PAGE;
else
path += ".html";
struct stat st;
int ret = stat(path.c_str(), &st);
if (ret == 0)
size = st.st_size;
else
size = -1;
}
public:
string inbuffer;
string method;
string url;
string path;
string httpversion;
string suffix;
string parm;
int size;
};
3、发送响应报文
void Get(const Request &req, Response &resp)
{
cout << "----------------------http recv start---------------------------" << endl;
cout << req.inbuffer << std::endl;
std::cout << "method: " << req.method << std::endl;
std::cout << "url: " << req.url << std::endl;
std::cout << "httpversion: " << req.httpversion << std::endl;
std::cout << "path: " << req.path << std::endl;
cout << "----------------------http recv end---------------------------" << endl;
cout << "----------------------http send start---------------------------" << endl;
resp.outbuffer += "HTTP/1.1 200 OK\r\n";
// resp.outbuffer += "HTTP/1.1 302 Found\r\n";
resp.outbuffer += "Content-Type: text/html\r\n";
// resp.outbuffer += "Location:https://blog.csdn.net/Sweet_0115?spm=1000.2115.3001.5343\r\n";
resp.outbuffer += "\r\n";
string body;
body.resize(req.size + 1);
if (!Util::readFile(req.path, (char *)body.c_str(), body.size()))
!Util::readFile("./wwwroot/404.html", (char *)body.c_str(), body.size());
cout << "test : -----------" << req.path << endl;
// cout << body << endl;
resp.outbuffer += body;
cout << resp.outbuffer << endl;
cout << "----------------------http send end---------------------------" << endl;
}
四、http 会话保持
当我们打开CSDN登录后,即使我们关闭了浏览器,短期内我们再打开CSDN仍然不需要重复登录。同样的,我们在CSDN内进行网页跳转时,也不需要再重复登录。http 协议是无状态的,那么浏览器是怎么做到网页跳转时用户不需要重复登录呢?
这实际就是会话保持,http请求是无状态的,也就是以上功能并不是 http 提供的,而是session和cookie提供的。
当用户首次登录时,浏览器会将用户的账号和密码保存在cookie文件里,当用户近期再次访问该网站或在网站内进行跳转时,浏览器会自动将cookie文件里的数据推送给服务器,从而不需要用户再次登录。例如当我们使用软件观看一些会员视频时,利用以上机制可以鉴权进行身份判断。
但以上方式实际很不安全,例如一些不法分子可以通过劫持 http请求 从而获取到用户的账号和密码,所以以上机制是很危险的。
实际当用户登录后,服务器会为用户建立会话(session),其会保存用户的信息,同时会返回给浏览器 session id。当用户近期再次访问网站或再网站内进行跳转时,浏览器则向服务器发送 session id,而服务器则通过该 session id 进行身份鉴权判断。
但实际以上机制仍存在风险,不法分子仍然可以通过劫持 http 请求获取 session id,通过该id仍然可以向服务器伪造请求,但相比于第一种方案,至少用户的账户密码信息没有丢失。上述方案也可以配合别的机制,例如短信验证或人脸识别保障信息安全,例如当账号突然被异地登录(用户信息泄漏),服务器检测后使 session id 失效并令用户重新进行验证登录,一定程度上保护了用户的信息安全。
http请求并不安全,如果有安全防护需求必须得使用https协议。