网络 :HTTP
一、预备知识
虽然我们说, 应用层协议是我们程序猿自己定的。 但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用. HTTP(超文本传输协议)就是其中之一.
1.1 认识URL
平时我们俗称的 “网址” 其实就是说的 URL
1.2 encode和urldecode
像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出。但是,某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义。如下:
转义的规则如下:
- 将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式。
- 例如:“+” 被转义成了 “%2B”
urldecode就是urlencode的逆过程;
二、HTTP协议格式
HTTP 协议由 Request
请求 和 Response
响应 两部分组成
2.1.请求(Request)格式
从宏观角度来看,HTTP 请求 分为这几部分:
- 请求行,包括请求方法(GET / POST)、URL、协议版本(http/1.0 http/1.1 http/2.0)
- 请求报头,表示请求的详细细节,由多组 k: v 结构所组成
- 空行,区分报头和有效载荷
- 有效载荷(可以没有)
在 HTTP 协议中是使用 \r\n 作为 分隔符 的。
2.2 响应(Response)格式
从宏观角度来看,HTTP 响应 分为这几部分:
- 状态行,协议版本、状态码、状态码描述
- 响应报头,表示响应的详细细节,由多组 k: v 结构所组成
- 空行,区分报头和有效载荷
- 有效载荷,即客户端请求的资源
三、HTTP协议内容
3.1 HTTP的请求方法
请求方法有很多种,但是 GET
和 POST
是最常用的方法。
都是发送一个请求给服务端,两者的区别是什么呢?
- Get 请求是 HTTP 协议中的一种请求方法,通常用于从服务器获取资源。使用 Get 请求时,参数会附加在 URL 的末尾,多个参数之间用 & 符号分隔。
- Post 请求是 HTTP 协议中的一种请求方法,通常用于向服务器提交数据,或者创建新的资源。使用 Post 请求时,数据会放在请求体(body)中传输,而不是暴露在 URL 里。
3.2 HTTP的状态码
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden),302(Redirect, 重定向), 504(Bad Gateway)。
重定向状态码
当浏览器(客户端)访问的目标网站地址发生改变时,浏览器会返回 3xx 重定向错误码,用于引导浏览器访问正确的网址,常见的重定向状态码如下:
- 永久重定向:301、308
- 临时重定向:302、303、307
- 其他重定向:304
最具有代表性的重定向状态码为 301 和 302
3.3 协议版本
- http/1.0是短连接:一次请求响应一个资源,关闭连接。
- http/1.1是长连接:建立一个TCP连接,可以发送和返回多个http的request和response。
报文信息中有一个Connection: keep-alive的信息,表示的是服务端和客户端都是长连接。
3.4 HTTP请求报头属性
- Content-Type: 数据类型(text/html等)
- Content-Length: Body的长度
- Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
- User-Agent: 声明用户的操作系统和浏览器版本信息;
- referer: 当前页面是从哪个页面跳转过来的;
- location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
- Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;
Cookie属性
在HTTP协议中,Cookie是一种由服务器发送到客户端浏览器的小型文本数据,用于在用户访问网站时存储状态信息。它允许服务器识别用户会话、保存用户偏好或跟踪用户行为,从而提供个性化的Web体验。Cookie通过HTTP头(如Set-Cookie)传输,并在后续请求中由浏览器自动发送回服务器。
cookie有文件级和用户级。当cookie文件放到文件级中的时候,就是放到磁盘中了,所以下次打开浏览器依旧可以免登录了。而设置了自动消除时间,cookie就会自动消亡了。内存级的则浏览器不会免登录。
http协议是无状态的,http对登录用户的会话保持功能,就比如我们访问浏览器登录一次以后隔10分钟再访问一次不用登录了。
这种情况cookie被盗取并且个人信息泄露。
session
那么这就存在安全问题,其他人只要得到我们的Cookie就可以登录我们的账号了??
为了应对这种问题而有了一个session方法。
这种做法是将客户端信息统一放到服务端去进行管理,这样子个人信息泄露的风险比较小,但仍旧避免不了cookie被盗取的风险。而为了避免风险,那么就需要服务端那边对session id进行管理来减少风险。
当我们第一次登录网站后,服务器会将与用户相关的信息打包成一个session文件,并且为这个文件分配一个独有的session ID,将这个session ID返回到Cookie中。
当用户再次登录网站后则通过这个session ID来确认用户的身份,即使如此还是存在安全问题的,其他人拿到这个ID也可以用你的身份登录网站,但是他没办法直接拿到用户的个人信息,不过还是存在风险。
服务器可以制定安全策略,识别是否为异常登录
- IP比对:识别登录用户的IP在短时间内是否发生了改变
- 行为检测:识别用户是否存在异常信息,比如QQ突然大面积发生消息、添加好友
当服务器判定异常登录后,就会释放服务器中存储的 session id,这就意味着原本的 session id 失效了,需要重新输入密码登录
- 如果是用户,重新使用 账号&密码 登录后,获取服务器重新生成的 session id 即可
- 其他人则无法登录,因为没有 账号&密码
四、一个简易的http服务器
Log.hpp 日志
#pragma once
#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
// PrintMethod
#define Screen 1
#define Onefile 2
#define Muchfile 3
// leve,指的是日志等级,等级不同处理的方式也不同
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define LogFile "log.txt"
class Log
{
public:
Log()
{
path = "./log/";
_PrintMethod = Screen;
}
// 用户指定打印方式
void AppontPrint(int PrintMethod)
{
_PrintMethod = PrintMethod;
}
std::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 std::string &logtxt)
{
switch (_PrintMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Muchfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
void printOneFile(const std::string &logname, const std::string &logtxt)
{
std::string filename = path + logname;
int fd = open(filename.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
if(fd < 0)
{
return;
}
int n = write(fd,logtxt.c_str(),logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string &logtxt)
{
std::string filename = LogFile;
filename += ".";
filename += levelToString(level); // "logtxt.Info/Fatal"
printOneFile(filename,logtxt);
}
void operator()(int level, const char *format, ...)
{
// 自定义部分
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
char leftbuffer[1024];
snprintf(leftbuffer, sizeof(leftbuffer), "[%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[1024];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
char logtxt[2024];
snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);
printLog(level, logtxt);
}
~Log()
{
}
private:
std::string path; // 将路径信息打印到某个路径文件下
int _PrintMethod; // 打印的方法(打印到屏幕或文件或多个文件等)
};
Socket.hpp 封装套接字接口
#pragma Once
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include "Log.hpp"
Log lg;
enum
{
SOCK_ERR = 1,
BIND_ERR,
LISTEN_ERR,
S
};
class Sock
{
public:
void Socket()
{
socketfd = socket(AF_INET, SOCK_STREAM, 0);
if (socketfd < 0)
{
lg(Fatal, "socket errno : %d ,%s", errno, strerror(errno));
exit(SOCK_ERR);
}
}
void Bind(uint16_t &port)
{
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(socketfd, (const struct sockaddr *)&local, sizeof(local)) < 0)
{
lg(Fatal, "bind errno : %d ,%s", errno, strerror(errno));
exit(BIND_ERR);
}
}
void Listen()
{
int n = listen(socketfd, 10);
if (n < 0)
{
lg(Fatal, "listen errno : %d ,%s", errno, strerror(errno));
exit(LISTEN_ERR);
}
}
int Accept(std::string *clientip, uint16_t *clientport)
{
struct sockaddr_in remote;
socklen_t len = sizeof(remote);
int newfd = accept(socketfd, (struct sockaddr *)&remote, &len);
if (newfd < 0)
{
lg(Warning, "accept errno : %d ,%s", errno, strerror(errno));
return -1;
}
char buffip[64];
inet_ntop(AF_INET, &remote.sin_addr, buffip, sizeof(buffip));
*clientip = buffip;
*clientport = ntohs(remote.sin_port);
return newfd;
}
bool Connect(std::string &serverip, uint16_t &serverport)
{
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);
int n = connect(socketfd, (const struct sockaddr *)&server, sizeof(server));
if (n < 0)
{
lg(Warning, "connect errno : %d ,%s", errno, strerror(errno));
return false;
}
return true;
}
int Getfd()
{
return socketfd;
}
void Close()
{
close(socketfd);
}
private:
int socketfd;
};
HttpServer.cc
#include"HttpServer.hpp"
#include <memory>`在这里插入代码片`
#include <iostream>
int main()
{
uint16_t port = 9999;
std::unique_ptr<HttpServer> svr(new HttpServer(port));
svr->start();
return 0;
}
HttpServer.hpp
#include "Socket.hpp"
#include <pthread.h>
#include <vector>
#include <sstream>
#include <fstream>
static const int defaultport = 8080;
const std::string sep = "\r\n";
const std::string wwwroot = "./wwwroot"; // web 根目录
class HttpServer;
// 用于给线程函数传参
class ThreadData
{
public:
ThreadData(int sockfd, HttpServer *svr) : sockfd_(sockfd), svr_(svr)
{
}
public:
int sockfd_;
HttpServer *svr_;
};
//用于对http请求内容做反序列
class HttpRequest
{
public:
void Deserialize(std::string req)
{
while (true)
{
std::size_t pos = req.find(sep); // 将每一行的信息都插入到vector中
if (pos == std::string::npos)
break;
std::string tmp = req.substr(0, pos);
if (tmp.empty())
break;
req_header.push_back(tmp);
req.erase(0, pos + sep.size()); // 一行中也有 '\r\n' ,所以需要加上这个长度
}
}
// 解析请求行
void Parse()
{
std::stringstream ss(req_header[0]);
ss >> method >> url >> http_version;
file_path = wwwroot; // 所有的文件都在 wwwroot中
if (url == "/" || url == "/index.html")
{
// 当用户不指明访问路径的时候,给用户默认访问./wwwroot/index.html
file_path += "/index.html";
}
else
{
file_path += url;
}
}
public:
std::vector<std::string> req_header; // 将用户发送的报文放进到vector中
std::string method; // 请求方法
std::string url; // 路径
std::string http_version; // http版本
std::string file_path; // http服务器所有的访问路径
};
class HttpServer
{
public:
HttpServer(uint16_t port = defaultport)
: port_(port)
{
}
void start()
{
listensock_.Socket();
listensock_.Bind(port_);
listensock_.Listen();
while (1)
{
// 建立连接
std::string clientip;
uint16_t clientport;
int socketfd = listensock_.Accept(&clientip, &clientport);
if (socketfd < 0)
{ // 重新连接
continue;
}
lg(Info, "get a new connect, sockfd: %d", socketfd);
// 建立连接成功后,对用户发来的请求做处理
// 创建线程来处理
pthread_t tid;
ThreadData *td = new ThreadData(socketfd, this);
pthread_create(&tid, nullptr, ThreadRun, td);
}
}
// 线程处理用户请求
static void *ThreadRun(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
// 处理细节另设函数
td->svr_->HandlerHttp(td->sockfd_);
delete td;
return nullptr;
}
// 处理细节
static void HandlerHttp(int sockfd)
{
// 接受信息recv,向用户发送信息send
char buffer[10240];
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0; // 当作字符串来读取
std::cout << buffer << std::endl;
HttpRequest res;
// 反序列化
res.Deserialize(buffer);
res.Parse();
// 处理用户请求的内容
std::string text;
bool path_right = true;
text = HandlerContent(res.file_path);
if (text.empty()) //如果用户访问路径根本不存在,则返回一个错误界面
{
path_right = false;
std::string err_html = wwwroot;
err_html += "/";
err_html += "err.html";
text = HandlerContent(err_html);
}
std::string response_line; //res首行 ,状态行
if (path_right)
response_line = "HTTP/1.0 200 OK\r\n";
else
response_line = "HTTP/1.0 404 Not Found\r\n";
std::string response_header = "Content-Length: "; //相应报文
response_header += std::to_string(text.size()); // Content-Length: 11
response_header += "\r\n";
response_header += "Set-Cookie: username=R_L&&passwd=123";
response_header += "\r\n";
std::string blank_line = "\r\n"; //空白行
// 将得到的结果返回给用户
std::string response = response_line;
response += response_header;
response += blank_line;
response += text;
send(sockfd, response.c_str(), response.size(), 0);
// respond
}
// 处理完后关掉sockfd
close(sockfd);
}
// 处理用户请求的内容
static std::string HandlerContent(std::string htmlpath)
{
// 以二进制的方式读数据
std::ifstream in(htmlpath, std::ios::binary);
if (!in.is_open())
return "";
in.seekg(0, std::ios_base::end);
auto len = in.tellg();
in.seekg(0, std::ios_base::beg);
std::string content;
content.resize(len);
in.read((char *)content.c_str(), content.size());
// std::string content;
// std::string line;
// while(std::getline(in, line))
//{
// content += line;
// }
in.close();
return content;
}
private:
Sock listensock_; // 套接字相关接口
uint16_t port_; // 端口号
};
在HttpServer类中调调用start()函数 开始整个服务器的运行,通过线程的方式来处理用户发来的http请求,由于是在类中实现线程函数,所以线程函数ThreadRun是static的,而为了传递socketfd(因为线程函数需要fd来实现接受http请求和发送响应)到线程函数中,所以我创造了一个类ThreadData专门来传递socketfd。
而在线程函数处理用户请求的内容中我创造了一个HttpRequest类来处理,该类通过http请求分离请求行中的信息,并且给URL路径加上web路径。