虽然我们说, 应用层协议是我们程序猿自己定的. 但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用. HTTP(超文本传输协议) 就是其中之一.
认识URL
统一资源定位符(Uniform Resource Locator,缩写:URL),是对资源的引用和访问该资源的方法。俗称网址。
- 主机名:表示IP地址的注册名称(域名)或IP地址,用于识别连接到网络设备的数字标识符
- 端口:跟在域名后面,不是一个URL必须的部分,如果省略端口会使用默认端口。
80是http协议的默认端口,是在输入网站的时候其实浏览器(非IE)已经帮你输入协议了,所以你输入 http://baidu.com,其实是访问 http://baidu.com:80。而8080,一般用与webcahe,完全不一样的两个,比如linux服务器里 apache默认跑80端口,而apache-tomcat默认跑 8080端口,其实端口没有实际意义只是一个接口,主要是看服务的 监听端口。
- 路径:表示服务器上资源的路径,过去这样的路径标记的是服务器上文件的物理路径,但是现在,路径表示的只是一个抽象地址,并不指代任何物理地址.
- 参数:请求里提供的额外参数.这些参数是以键值对的形式,通过
&
符号分隔开来,服务器可以通过这些参数进行相应的个性化处理 - 锚:用于为页面上的标题提供快速链接,如锚点链接
urlencode和urldecode
像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现. 比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义.
转义的规则如下: 将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY 格式
leetcode中文C++ 被转义成了 leetcode%E4%B8%AD%E6%96%87C%2B%2B
HTTP协议
HTTP的方法
创建html文件,输入!并按tab键,可以得到一个简单的模板。
传输正文时中英文会乱码:
在html文件的头标签中加入<meta charset="UTF-8">
添加一个报头 Content-type: text/html
要获取文件信息可以使用stat接口:
//以文件尺寸为例
struct stat st;
int n = stat(path.c_str(), &st);
if(n == 0) size = st.st_size;
else size = -1;
网页教程详情见:w3cschool官网 - 编程狮,随时随地学编程
在Linux中可以使用
wget [域名]
获取网页数据。
一个简单的网站服务器
Util.hpp
#pragma once
#include <iostream>
#include <string>
#include<fstream>
using namespace std;
class Util
{
public:
// XXXX XXX XXX\r\nYYYYY
static std::string getOneLine(std::string &buffer, const std::string &sep)
{
auto pos = buffer.find(sep);
if(pos == std::string::npos) return "";
std::string sub = buffer.substr(0, pos);
buffer.erase(0, sub.size()+sep.size());
return sub;
}
static bool ReadFile(const string& name, string& body)
{
auto pos = name.find(".");
string typeofFile = name.substr(pos);
ifstream fs(name, fstream::in | fstream::binary);
char c;
while(fs.get(c))
{
body += c;
}
fs.close();
return true;
}
};
Protocal.hpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include "Util.hpp"
const std::string sep = "\r\n";
const std::string default_root = "./wwwroot";
const std::string home_page = "index.html";
class HttpRequest
{
public:
HttpRequest(){}
~HttpRequest(){}
void parse()
{
// 1. 从inbuffer中拿到第一行,分隔符\r\n
std::string line = Util::getOneLine(inbuffer, sep);
if(line.empty()) return;
// 2. 从请求行中提取三个字段
// std::cout << "line: " << line << std::endl;
std::stringstream ss(line);
ss >> method >> url >> httpversion;
// 3. 添加web默认路径
path = default_root; // ./wwwroot,
path += url; //./wwwroot/a/b/c.html, ./wwwroot/
if(path[path.size()-1] == '/') path += home_page;
// 4. 获取path对应的资源后缀
// ./wwwroot/index.html
// ./wwwroot/test/a.html
// ./wwwroot/image/1.jpg
auto pos = path.rfind(".");
if (pos == std::string::npos)
suffix = ".html";
else
suffix = path.substr(pos);
}
public:
std::string inbuffer;
// std::string reqline;
// std::vector<std::string> reqheader;
// std::string body;
std::string method;
std::string url;
std::string httpversion;
std::string path;
std::string suffix;
};
class HttpResponse
{
public:
std::string outbuffer;
};
httpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include "Protocol.hpp"
namespace server
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
static const uint16_t gport = 8080;
static const int gbacklog = 5;
using func_t = std::function<bool (const HttpRequest &, HttpResponse &)>;
class HttpServer
{
public:
HttpServer(func_t func, const uint16_t &port = gport) : _func(func), _listensock(-1), _port(port)
{
}
void initServer()
{
// 1. 创建socket文件套接字对象
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
exit(SOCKET_ERR);
}
// 2. bind绑定自己的网络信息
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(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
exit(BIND_ERR);
}
// 3. 设置socket 为监听状态
if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面在填这个坑
{
exit(LISTEN_ERR);
}
}
void HandlerHttp(int sock)
{
// 1. 读到完整的http请求
// 2. 反序列化
// 3. httprequst, httpresponse, _func(req, resp)
// 4. resp序列化
// 5. send
char buffer[4096];
HttpRequest req;
HttpResponse resp;
size_t n = recv(sock, buffer, sizeof(buffer)-1, 0); // 大概率我们直接就能读取到完整的http请求
if(n > 0)
{
buffer[n] = 0;
req.inbuffer = buffer;
req.parse();
_func(req, resp); // req -> resp
send(sock, resp.outbuffer.c_str(), resp.outbuffer.size(), 0);
}
}
void start()
{
for (;;)
{
// 4. server 获取新链接
// sock, 和client进行通信的fd
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
continue;
}
// version 2 多进程版(2)
pid_t id = fork();
if (id == 0) // child
{
close(_listensock);
if(fork()>0) exit(0);
HandlerHttp(sock);
close(sock);
exit(0);
}
close(sock);
// father
waitpid(id, nullptr, 0);
}
}
~HttpServer() {}
private:
int _listensock; // 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的!
uint16_t _port;
func_t _func;
};
} // namespace server
httpServer.cc
#include "httpServer.hpp"
#include <memory>
using namespace std;
using namespace server;
void Usage(std::string proc)
{
cerr << "Usage:\n\t" << proc << " port\r\n\r\n";
}
// 1. 服务器和网页分离,html
// 2. url -> / : web根目录
bool Get(const HttpRequest &req, HttpResponse &resp)
{
// for test
cout << "----------------------http start---------------------------" << 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 << req.inbuffer << std::endl;
cout << "----------------------http end---------------------------" << endl;
std::string respline = "HTTP/1.1 200 OK\r\n";
std::string respheader = "Content-Type: ";
if (req.suffix == ".html")
respheader += "text/html";
else if (req.suffix == ".jpg")
respheader += "application/x-jpg;image/jpeg";
respheader += "\r\n";
std::string respblank = "\r\n";
std::string body;
if(!Util::ReadFile(req.path, body))
{
Util::ReadFile("./wwwroot/404.html", body);
}
resp.outbuffer += respline;
resp.outbuffer += respheader;
resp.outbuffer += respblank;
resp.outbuffer += body;
return true;
}
// ./httpServer 8080
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = atoi(argv[1]);
unique_ptr<HttpServer> httpsvr(new HttpServer(Get, port));
httpsvr->initServer();
httpsvr->start();
return 0;
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>main tittle</title>
</head>
<body>
<h1>每天都刷leetcode</h1>
<img src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw1%2F83b66036-a374-424f-ab45-4a02aaf45a4f%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1688195268&t=19cab953a51d9bd85e9b057dc82d7345" alt="测试图片">
<img src="/image/1.jpg" alt="本地图片">
<img src="/image/2.jpg" alt="石榴花">
<a href="/test/a.html">新闻</a>
<a href="/test/c.html">电商</a>
</body>
</html>
3开头的状态——重定向
重定向流程图:
实现一个简单的临时重定向:
1.将服务器返回的状态行改为
std::string respline = "HTTP/1.1 307 Temporary Redirect\r\n";
2.在响应报头中加入重定向的域名
respheader += "Location: https://www.qq.com/\r\n";
浏览器在访问我们的服务器时会自动跳转到QQ。
表单
<form action="/a/b/c.py" method="POST">
姓名:<br> <input type="text" name="xname"> <br>
密码:<br> <input type="password" name="ypwd"> <br>
<br> <input type="submit" value="登陆">
</form>
使用GET方法和POST方法的区别: