1、认识URL
虽然说,应用层协议是程序员自己定的,但是已经有大神定义了一些现成的,可供我们直接使用。HTTP协议——超文本传输协议就是其中之一。
在互联网世界中, HTTP(HyperText Transfer Protocol,超文本传输协议)是一个至关重要的协议。它定义了客户端(如浏览器)与服务器之间如何通信,以交换或传输超文本(如 HTML 文档)。
HTTP 协议是客户端与服务器之间通信的基础。客户端通过 HTTP 协议向服务器发送请求,服务器收到请求后处理并返回响应。 HTTP 协议是一个无连接、无状态的协议,即每次请求都需要建立新的连接,且服务器不会保存客户端的状态信息。
平时我们说的网址,其实说的就是URL——Uniform Resource Locator(统一资源定位符):
如图,贴出了一个URL。https表示的是协议。然后news.qq.com表示的是域名,域名会被转换成公网IPIP地址。后面的UTR…表示的是服务器上的一个目标文件,可能是html、css、js。然后从/rain往后这就是路径,意思就是请求服务器上的某个路径下的资源。
请求的资源本质上就是文件。这个路径就是Linux下的路径结构。
前置背景理解:
1、我的数据给服务器,服务器的数据给我,就是在做IO。所以我们上网的所有行为都是在做IO。
比如今天我刷抖音,就是向抖音服务器请求视频资源,抖音服务器将视频资源返回给我的客户端,我就能看到视频。再比如我逛淘宝京东,就是淘宝京东的服务器将图片等等信息返回给我的客户端。这就是服务器的数据给我。再比如我今天登录账号,那就是将我的账户密码上传给服务器,这就是将我的数据给服务器。网络通信本质就是进程间通信,而进程间通信本质就是在做IO。
2、我向服务器请求的图片、视频、音频、文本等都是资源。
3、我要向服务器请求资源,就需要确认在哪台服务器上(网络IP),在什么路径下(系统路径)。所以就需要URL。
而我们进行网络通信是通过IP+端口号来实现的,那URL怎么没有体现端口号呢?这是因为http、https已经是很成熟的应用层协议了,所以它们的端口号都是强关联的。比如http协议端口号就是80,https就是443。
4、url中/…不一定是根目录,其实叫做web根目录,这两者不一定是同一个
urlencode和urldecode:
如图,我们在bing中搜索chaiquan,然后上方的URL就是上图中我给出来的这一串。URL后面是可以跟参数的,上图中?后面的内容就是参数,参数以key=value的形式添加在URL后面,如果有多个参数就以&连接。
所以像/、&、=这些字符都是URL中的特殊字符。上面我们搜索的内容chaiquan就是参数q=chaiquan。那如果我搜索的内容中带有/、&、=这些特殊字符呢?
如图,这时候我们搜索的特意用了一些特殊字符。我们发现URL中的参数value的内容发生了变化,不是我们输入的特殊字符。这是因为这些特殊字符已经被url当作特殊意义理解了,所以这些字符不能随意出现,必须对这些字符进行转义。
转移的规则如下:将需要转码的字符转为 16 进制,然后从右到左,取 4 位(不足 4 位直接处理),每2位做一位,前面加上%,编码成%XY格式。
所以我们输入框输入的内容,在URL中的参数中就被转义了,这个过程就叫做urlencode,那么服务器在收到这个请求,将参数提取出来的时候也要将转义的内容再重新转换成字符,这个过程就是urldecode。
2、HTTP协议的宏观格式
http是应用层协议,是基于TCP协议的。
先来看HTTP协议的格式,先看请求的格式:
第一行表示的是请求行,请求行里面有请求方法,然后空格接着跟着的是url,然后再空格,接着是http协议的版本,紧接着以\r\n结尾。请求行之后就是请求报头,也是一行一行的字符串,他们都是以\r\n分隔开的。请求报头里面是key:,接着空格value,记住冒号之后是有空格的。请求报头之后是空行,单纯的\r\n,然后后面就是请求正文的内容了。
再来看响应的格式,响应的格式类似请求,只不过第一行变成了状态行,状态行的内容是:HTTP版本,然后空格接着是状态码,比如状态码404,接着空格跟状态码描述,比如404的状态码描述就是Not Found,后面跟\r\n。下面就是响应报头,也是key: value的形式,然后每行\r\n分隔开,最后有一行空行,空行只有\r\n,后面就是响应正文了。
那么HTTP如何保证收发的完整性?HTTP是基于TCP协议的,而TCP是面向字节流的。
http协议自己已经做了序列化了,通过\r\n将空行前面的字符串合并成一个大字符串,这个大字符串的分隔符是\r\n,所以我们可以直接将HTTP请求作为一个长字符串给对方发送过去。所以要序列化其实就是将这些字符串合并成一个大字符串就行了。
但是我们反序列化如何知道请求正文呢,因为请求正文是没有\r\n的,首先读取一部分,判断第一个\r\n,那么前面的内容就是请求行的内容,接着继续读取内容,通过\r\n可以拆分出一个一个的key: value,当读取到空串,也就是只有\r\n,没有字符串内容,说明我把前面的内容全都读完了,那么剩下的就是请求正文的内容了。但是我们并不知道请求正文的长度,因此在请求报头里面有一个字段:Content-Length: 数字\r\n,这个字段表示的就是请求正文的长度。根据这个长度就可以将请求正文完整的提取出来。
请求方法有:GET、POST等,URI表示请求服务器的资源路径,HTTP版本如:HTTP/1.0、HTTP/1.1…
状态码如:200——OK,404——Not Found,将来响应正文就是请求的图片、视频、html等等资源。
请求和响应的格式是一样的,因此它们序列化和反序列化的方式也是一样的。
3、实现HttpServer服务器
3.1、使用模板方法模式封装Socket.hpp
#pragma once
#include <iostream>
#include <string>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "InetAddr.hpp"
#include "Log.hpp"
namespace SocketModule
{
using namespace LogModule;
const int gdefaultsockfd = -1;
const int gdefaultbacklog = 8;
class Socket;
using SockPtr = std::shared_ptr<Socket>;
class Socket
{
public:
virtual ~Socket() = default;
virtual void SocketOrDie() = 0;
virtual void SetSocketOpt() = 0;
virtual bool BindOrDie(int port) = 0;
virtual bool ListenOrDie() = 0;
virtual SockPtr Accepter(InetAddr* client) = 0;
virtual void Close() = 0;
virtual int Recv(std::string* out) = 0;
virtual int Send(const std::string& in) = 0;
virtual int Fd() = 0;
// 提供一个创建TCP套接字的固定方法
void BuildTcpSocketMethod(int port)
{
SocketOrDie();
SetSocketOpt();
BindOrDie(port);
ListenOrDie();
}
// void BuildUdpSocketMethod(int port)
// {
// }
};
// class UdpSocket
// {
// };
class TcpSocket
{
public:
TcpSocket()
:_sockfd(gdefaultsockfd)
{
}
TcpSocket(int sockfd)
:_sockfd(sockfd)
{
}
~TcpSocket()
{
}
virtual int Fd() override
{
}
virtual void SocketOrDie() override
{
}
virtual void SetSocketOpt() override
{
}
virtual bool BindOrDie(int port) override
{
}
virtual bool ListenOrDie() override
{
}
virtual SockPtr Accepter(InetAddr* client) override
{
}
virtual void Close() override
{
}
virtual int Recv(std::string* out) override
{
}
virtual int Send(const std::string& in) override
{
}
private:
int _sockfd;
};
}
如图,基类Socket定义出一系列方法,然后提供一个或若干个固定模式的socket方法,派生类TcpSocket继承Socket方法实现出一系列具体方法,将来创建出的对象直接调用父类提供的固定方法,立马将子类实现的具体方法按顺序全部调出来,这种模式叫做模板方法模式,它是一种行为类的设计模式。
也就是说我们把一些固定套路的方法实现细节交给子类,但是这些方法在调用的时候组合是固定的,所以我们把这个调用组合的函数在基类实现。将来定义对象调用这些固定组合的方法,直接调用基类实现的固定套路函数即可。
我们也可以实现UdpSocket然后继承Socket,UdpSocket中没有监听和获取新连接,那么ListenOrDie和Accepter我们在派生类中方法体内为空即可,什么都不实现。
下面实现TcpSocket:
#pragma once
#include <iostream>
#include <string>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "InetAddr.hpp"
#include "Log.hpp"
namespace SocketModule
{
using namespace LogModule;
const int gdefaultsockfd = -1;
const int gdefaultbacklog = 8;
class Socket;
using SockPtr = std::shared_ptr<Socket>;
// 模板方法
class Socket
{
public:
virtual ~Socket() = default;
virtual void SocketOrDie() = 0;
virtual void SetSocketOpt() = 0;
virtual bool BindOrDie(int port) = 0;
virtual bool ListenOrDie() = 0;
virtual SockPtr Accepter(InetAddr* client) = 0;
virtual void Close() = 0;
virtual int Recv(std::string* out) = 0;
virtual int Send(const std::string& in) = 0;
virtual int Fd() = 0;
// 提供一个创建TCP套接字的固定方法
void BuildTcpSocketMethod(int port)
{
SocketOrDie();
SetSocketOpt();
BindOrDie(port);
ListenOrDie();
}
// void BuildUdpSocketMethod(int port)
// {
// }
};
// class UdpSocket : public Socket
// {
// };
class TcpSocket : public Socket
{
public:
TcpSocket()
:_sockfd(gdefaultsockfd)
{
}
TcpSocket(int sockfd)
:_sockfd(sockfd)
{
}
~TcpSocket()
{
}
virtual int Fd() override
{
return _sockfd;
}
virtual void SocketOrDie() override
{
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::ERROR) << "create socket error";
Die(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "create socket success: " << _sockfd;
}
virtual void SetSocketOpt() override
{
}
virtual bool BindOrDie(int port) override
{
if (_sockfd == gdefaultsockfd) return false;
InetAddr server(port);
int n = ::bind(_sockfd, server.NetAddr(), server.NetAddrLen());
if (n < 0)
{
LOG(LogLevel::ERROR) << "bind error";
Die(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success: " << _sockfd;
return true;
}
virtual bool ListenOrDie() override
{
if (_sockfd == gdefaultsockfd) return false;
int n = ::listen(_sockfd, gdefaultbacklog);
if (n < 0)
{
LOG(LogLevel::ERROR) << "listen error";
Die(LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen success: " << _sockfd;
return true;
}
virtual SockPtr Accepter(InetAddr* client) override
{
if (!client) return nullptr;
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newsockfd = ::accept(_sockfd, CONV(&peer), &len);
if (newsockfd < 0)
{
LOG(LogLevel::WARNING) << "accept error";
return nullptr;
}
client->SetAddr(peer, len);
return std::make_shared<TcpSocket>(newsockfd);;
}
virtual void Close() override
{
if (_sockfd != gdefaultsockfd)
{
::close(_sockfd);
}</