文章目录
- OSI七层模型
- http协议
- 域名
- 路径信息
- 请求和响应
- 编写一个httpserver
OSI七层模型
在结束了前面对于序列化反序列化等内容的学习后,重新回到对于OSI模型的部分
如上所示的是对于OSI接口的示意图,在这当中可以看到会话层的概念,会话层的作用主要是建立断开连接管理的,相当于是在网络服务器中在TCP server中获取链接的过程,如果不需要这个链接,就在TCP server中把对应的链接关掉,而下面的表示层中写的是固有的数据格式和网络数据的转换,比如有接受不同形式的信息,文字,图像,声音等等,这个表示层其实就是在前面的部分中定义的协议
这个协议表示的就是固有的数据格式,比如说是序列化和反序列化这些内容,再比如有说网络序列转换为主机序列,或者是在缓冲区中定义了各种视频的名称格式等等,这些都是属于自定义协议的范畴,因此这些内容都是属于是表示层当中的信息
最后是应用层,应用层中有针对于特定应用的协议,比如说有电子邮件远程传输的问题,再比如有传输数据解析的问题,这些其实都是在应用层需要解决的问题
所以说这个OSI的这些层次结构就是已经提前定制好了未来的发展方式,把网络中需要的各个功能和网络中每一层的能力就都定制好了,那在表示层和应用层层相对比较好理解一些,无非就是定制协议的过程,那对于会话层的理解该如何深入理解呢?
对于会话的概念并不陌生,在将Linux终端的时候就有提及过这个session的概念,而当客户端链接到服务端后,如果一直不给服务器发送数据,那么对应的服务端就会一直阻塞在这里,那未来可以设置对应的一些策略,比如说超过5秒钟部进行发送消息,就把对应的链接自动关闭
因此,在定制OSI模型的时候,实际上会话表示应用这三层是不能定义在内核当中的,原因就是无论是这些当中的那一层,最终都必须要根据使用场景的不同,由程序员自己定义出对应的协议信息,然后才能有对应的序列化和反序列化的方法,如果用操作系统来全部实现,自然会引起一定的问题,所以就都交给用户自己来进行实现
http协议
域名
在前面的认知中,对于访问网络服务的概念是,可以使用一个ip地址和端口号来访问唯一一台主机上的唯一的一个进程,所以我们才可以用ip加端口号的方式来进行访问,但是现在的问题是,平时使用访问的都是使用一个网址来进行访问,没有使用端口号,这该如何理解呢?
事实上,的确在技术上用ip地址和端口号就可以实现这样的功能,但是在日常生活中作为用户会直接使用一个域名的内容来进行访问网站,因为域名的名字相比起来更加的好记忆,尤其是对于一些大型的网站都是直接用拼音来进行标注,方便用户来进行访问信息
所以就诞生了域名的概念,域名实际上在背后会被解析为对应的服务,这个技术上叫做是域名解析的概念,通过这个技术就能实现把域名转换为ip地址,因此当浏览器访问了特定的域名的时候,浏览器内部会内置一些地址,就能访问对应的域名解析的服务,这样就能转换为ip地址,我们以下面的这个例子为例
当访问百度一下这个网址的时候,都是直接访问这个www.baidu.com
这个网址,而当在浏览器中访问的时候,其实在背后已经把这个域名转换成ip地址了,那这个怎么看呢?
在命令行中可以使用这样的命令来进行查看
由此我们就可以根据这个信息来进行访问百度了
https协议默认的端口号是80,所以本质上访问的是39.156.66.18:80/
,关于这个问题后续再进行详谈
路径信息
对于每一个网址来说,其实是有固定的规则的:
未来会对带层次的文件路径进行解析,其他的字段不进行额外的讲解
请求和响应
下一步要谈论的主题是,关于http中的请求和响应的问题
http请求
http的请求是有固定格式的,下图画出http请求的具体格式:
下图展示的是http的一个请求:
如图所示的是http的一个请求,第一行表示的是POST方法,后面带有对应的URL网址信息,还有http协议版本,还有换行分割符这些信息,后续的内容都是对应的是请求报头,其中包含有对应的Key和Value信息,在结束之后是一个空行,也是作为一个分割的作用,最后一行的信息表示的是请求的正文,在这里表示的是一个用户名和密码的信息,后续会实现这个内容
这里对于这个\r\n进行一个理解:这两个表示的是,对于请求来说都是按行读取,这样可以读取到完整的一行,读取到完整的一行之后就能对于报头和有效载荷进行分离,进而进行后续的一些操作,那么现在的问题是在对于读取网络请求的时候,如何保证读取到的是一个完整的http请求呢?具体的做法是分析它的长度,并进行提取,这是我们之前的做法,而我们之前的做法本质上是一个自定义协议,而在对应的http协议如何保证?答案是保证不了,但是因为有这个\r和\n的存在,所以可以保证的是可以完整的读取到一个http的报头,之后就能进行解析,而在解析内容中就包含有长度这样的字段,所以说想要实现一个http的请求正确读取是一个非常繁琐的过程
http响应
那http响应该如何理解呢?在http的响应和请求几乎是完全一样的:
所以说此时对于请求和响应已经有了一个基本的视线了,那么下一步就是要进行序列化和反序列化,然后再进行对应的分割就可以,这都是可以解决的问题,所以在未来进行使用http实现网络通信的时候,就是一个发送请求,进行响应,再发送请求,再进行响应的一个过程
编写一个httpserver
那么下一步就直接进行编写一个httpserver来进行测试,这里我就直接用一份已经完备的httpserver来进行测试,主要想法是根据实验现象来理解http背后的细节原理
// socket.hpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"
enum
{
SocketErr = 2,
BindErr,
ListenErr,
};
// TODO
const int backlog = 10;
class Sock
{
public:
Sock()
{
}
~Sock()
{
}
public:
void Socket()
{
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd_ < 0)
{
lg(Fatal, "socker error, %s: %d", strerror(errno), errno);
exit(SocketErr);
}
int opt = 1;
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
}
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(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0)
{
lg(Fatal, "bind error, %s: %d", strerror(errno), errno);
exit(BindErr);
}
}
void Listen()
{
if (listen(sockfd_, backlog) < 0)
{
lg(Fatal, "listen error, %s: %d", strerror(errno), errno);
exit(ListenErr);
}
}
int Accept(std::string *clientip, uint16_t *clientport)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
if(newfd < 0)
{
lg(Warning, "accept error, %s: %d", strerror(errno), errno);
return -1;
}
char ipstr[64];
inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
*clientip = ipstr;
*clientport = ntohs(peer.sin_port);
return newfd;
}
bool Connect(const std::string &ip, const uint16_t &port)
{
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));
int n = connect(sockfd_, (struct sockaddr*)&peer, sizeof(peer));
if(n == -1)
{
std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;
return false;
}
return true;
}
void Close()
{
close(sockfd_);
}
int Fd()
{
return sockfd_;
}
private:
int sockfd_;
};
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <fstream>
#include <vector>
#include <sstream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unordered_map>
#include "Socket.hpp"
#include "Log.hpp"
const std::string wwwroot = "./wwwroot"; // web 根目录
const std::string sep = "\r\n";
const std::string homepage = "index.html";
static const int defaultport = 8082;
class HttpServer;
class ThreadData
{
public:
ThreadData(int fd, HttpServer *s) : sockfd(fd), svr(s)
{
}
public:
int sockfd;
HttpServer *svr;
};
// 定义一个http的请求信息
class HttpRequest
{
public:
// 对信息进行反序列化
void Deserialize(std::string req)
{
while (true)
{
std::size_t pos = req.find(sep);
if (pos == std::string::npos)
break;
std::string temp = req.substr(0, pos);
if (temp.empty())
break;
req_header.push_back(temp);
req.erase(0, pos + sep.size());
}
// 存储请求体的信息
text = req;
}
// .png:image/png
// 对信息进行解析
void Parse()
{
// 解析请求的方法,URL,http版本
std::stringstream ss(req_header[0]);
ss >> method >> url >> http_version;
// 设置默认寻找的路径是wwwroot这个目录
file_path = wwwroot; // ./wwwroot
// 对于URL文件的路径进行一定的拼接
if (url == "/" || url == "/index.html")
{
file_path += "/";
file_path += homepage; // ./wwwroot/index.html
}
else
file_path += url; // /a/b/c/d.html->./wwwroot/a/b/c/d.html
// 查找文件的后缀,默认是html,否则就进行合适的拼接
auto pos = file_path.rfind(".");
if (pos == std::string::npos)
suffix = ".html";
else
suffix = file_path.substr(pos);
}
// 打印调试信息
void DebugPrint()
{
for (auto &line : req_header)
{
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: " << file_path << std::endl;
std::cout << text << std::endl;
}
public:
// 请求信息中至少要包含下面的内容
std::vector<std::string> req_header;
std::string text;
// 解析之后的结果
std::string method;
std::string url;
std::string http_version;
std::string file_path; // ./wwwroot/a/b/c.html 2.png
// 文件后缀
std::string suffix;
};
// http服务端
class HttpServer
{
public:
HttpServer(uint16_t port = defaultport) : port_(port)
{
content_type.insert({".html", "text/html"});
content_type.insert({".png", "image/png"});
}
bool Start()
{
// 进行套接字的创建,绑定,设置监听
listensock_.Socket();
listensock_.Bind(port_);
listensock_.Listen();
for (;;)
{
std::string clientip;
uint16_t clientport;
// 从监听信息中提起对应的ip和端口号信息,当有用户连接的时候会进行接受对应的信息
int sockfd = listensock_.Accept(&clientip, &clientport);
if (sockfd < 0)
continue;
lg(Info, "get a new connect, sockfd: %d", sockfd);
pthread_t tid;
ThreadData *td = new ThreadData(sockfd, this);
// 创建线程去执行对应的handle响应方法
pthread_create(&tid, nullptr, ThreadRun, td);
}
}
// 读取html的内容
static std::string ReadHtmlContent(const 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());
in.close();
return content;
}
// 将后缀名替换为描述信息
std::string SuffixToDesc(const std::string &suffix)
{
auto iter = content_type.find(suffix);
if (iter == content_type.end())
return content_type[".html"];
else
return content_type[suffix];
}
// 处理http的请求信息
void HandlerHttp(int sockfd)
{
char buffer[10240];
// 进行收消息的过程
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0); // bug
if (n > 0)
{
buffer[n] = 0;
std::cout << buffer << std::endl;
// 将接受到的请求信息进行反序列化
HttpRequest req;
req.Deserialize(buffer);
// 对于反序列化后的内容进行解析,解析为当前所需要的信息内容
req.Parse();
// 返回响应的过程
std::string text;
bool ok = true;
text = ReadHtmlContent(req.file_path); // 失败?
if (text.empty())
{
ok = false;
std::string err_html = wwwroot;
err_html += "/";
err_html += "err.html";
text = ReadHtmlContent(err_html);
}
// 设置响应状态行,默认失败是404状态
std::string response_line;
if (ok)
response_line = "HTTP/1.0 200 OK\r\n";
else
response_line = "HTTP/1.0 404 Not Found\r\n";
// 进行字符串的拼接
// response_line = "HTTP/1.0 302 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 += "Content-Type: ";
response_header += SuffixToDesc(req.suffix);
response_header += "\r\n";
response_header += "Set-Cookie: name=haha&&passwd=12345";
response_header += "\r\n";
// response_header += "Location: https://www.qq.com\r\n";
std::string blank_line = "\r\n"; // \n
std::string response = response_line;
response += response_header;
response += blank_line;
response += text;
// 把拼接好的字符串响应再发送回去,这样就完成了一次通信
send(sockfd, response.c_str(), response.size(), 0);
}
close(sockfd);
}
static void *ThreadRun(void *args)
{
// 对该线程进行设置,使得主线程不必关心这个线程
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
// 去执行对应的handle方法
td->svr->HandlerHttp(td->sockfd);
delete td;
return nullptr;
}
~HttpServer()
{
}
private:
Sock listensock_;
uint16_t port_;
std::unordered_map<std::string, std::string> content_type;
};