学习目标:⭐理解应用层的作用,理解协议,理解序列化和反序列化,并且实现网络版计算器⭐HTTP协议。⭐手写一个简单的http协议。
应用层
我们写的一个个解决实际问题, 满足我们日常需求的网络程序, 都是在应用层。
协议/序列化与反序列化
协议是一种约定,在使用socket api接口的的时候,比如网络套接字的博文中写的示例代码,都是按字符串的方式来接收的,但是如果传输的是结构体的数据,就需要考虑以下问题:
结构体的大小是需要内存对齐的,但是每一台机器都是不一样的,有的可能是32位,有的是64位,未来可能会有别的形式,如果直接将这个结构体数据直接传输到目的计算机,就会造成很多问题。
序列化:将结构体数据转化成长字符串。字符串便于网络传输。
反序列化:将传过来的字符串,按照协议,一一对应将数据填入结构体中。也就是将字符串“转换成”结构体数据。
使用TCP协议,实现网络版计算器
代码思路:
服务器:首先对套接字的接口进行封装。然后定制协议跟响应格式。在服务端中,首先创建监听套接字,接着是绑定监听等一系列的操作后,使得服务器处于监听状态,让客户端可以与服务器建立连接。接着创建用于通信的套接字,通过线程分离的方式进行通信。在分离的线程中,线程主要完成的任务是:读取请求、分析请求并计算结果,最后通过把结果写回,写给客户端。
客户端:首先创建套接字,然后使用套接字、ip和端口号与服务器建立连接。连接建立后,客户输入数据(此时在服务器中就会进行读取请求、分析请求和计算结果,然后把结果写回),然后读回数据,最后打印出来。
定制协议:在协议中,有3个变量,数字x、数字y和运算符op。在结果中,有2两个变量,一个是计算结果,一个是判断结果是否合法。在协议当中,需要定制序列化和反序列化。
准备jsoncpp库
在此之前,我们需要在云服务器上下载jsoncpp库,并且简单演示如何操作。
首先,在云服务器上安装jsoncpp库,用于序列化与反序列化。
sudo yum install -y jsoncpp-devel
序列化代码演示:
先写一个结构体,并用结构体创建一个结构体数据。在Json中,Value类是一种kv式的容器,可以将结构体数据装载起来。装载起来后,使用FastWriter类或者是StyledWriter类创建的对象,使用对象的方法write进行序列化。
#include<iostream> #include<string> #include<jsoncpp/json/json.h>
//序列化
typedef struct request
{
int x;
int y;
char op;//"+-*/%"
}request_t;int main()
{
request_t req = {10,20,'*'};//结构体数据
//创建json对象,这个对象可以承装任何对象
//kv式的序列化方案
//这一步:将需要序列化的数据先装载到json的对象中
Json::Value root;
root["datax"] = req.x;
root["datay"] = req.y;
root["dataop"] = req.op;//写入,并将其序列化 //Wirte有两种: //一种是FastWriter,一种是StyledWriter Json::StyledWriter writer; std::string json_string = writer.write(root); std::cout<<json_string<<std::endl; return 0;
}
使用 StyledWriter
使用FastWriter
反序列化代码演示:
代码思路:Json中的Reader类,将字符串装载到Value类的对象中,然后赋值给我们准备好的结构体对象就可以了。
int main()
{
//反序列化
std::string json_string = R"({"datax":10,"datay":20,"operator":42})";
//读取
Json::Reader reader;
//用来装载字符串的值
Json::Value root;
reader.parse(json_string,root);
request_t req;
req.x = root["datax"].asInt();
req.y = root["datay"].asInt();
req.op = (char)root["operator"].asUInt();
std::cout<<req.x<<" "<<req.op<<" "<<req.y<<std::endl;return 0;
}
实现网络版计算器
定制协议代码:
#pragma once
#include<iostream>
#include<string>
#include<jsoncpp/json/json.h>
using namespace std;//定制协议
//定制协议的过程,目前就是定制结构化数据的过程
//请求格式
typedef struct request
{
int x;
int y;
char op;//"+-*/%"
}request_t;//响应格式
typedef struct response
{
int code;//server运算完毕的计算状态,规定:code为0的时候成功,code为-1,除0了
int result;//计算结果
}response_t;//对请求格式request定制序列化和反序列化
//序列化
std::string SerializeRequest(const request_t& req)
{
//装载
Json::Value root;
root["datax"] = req.x;
root["datay"] = req.y;
root["operator"] = req.op;//序列化 Json::FastWriter writer; std::string json_string = writer.write(root); return json_string;
}
//反序列化
void DeserializeRequest(const std::string &json_string,request_t& out)
{
Json::Reader reader;
Json::Value root;
//将字符串装载到root中
reader.parse(json_string,root);
out.x = root["datax"].asInt();
out.y= root["datay"].asInt();
out.op = (char)root["operator"].asInt();
}//对响应格式response定制序列化和反序列化
//序列化
std::string SerializeRespond(const response_t& resp)
{
//装载
Json::Value root;
root["code"] = resp.code;
root["result"] = resp.result;//序列化 Json::FastWriter writer; std::string json_string = writer.write(root); return json_string;
}
//反序列化
void DeserializeRespond(const std::string &json_string,response_t& out)
{
Json::Reader reader;
Json::Value root;
//将字符串装载到root中
reader.parse(json_string,root);
out.code = root["code"].asInt();
out.result = root["result"].asInt();
}
套接字的封装
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>using namespace std;
class Sock
{
public:
static int Socket()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
cerr << "socket error" << endl;
exit(2);
}
return sock;
}static void Bind(int sock, 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(sock, (struct sockaddr *)&local, sizeof(local)) < 0) { cerr << "bind error!" << endl; exit(3); } } static void Listen(int sock) { if (listen(sock, 5) < 0) { cerr << "listen error !" << endl; exit(4); } } static int Accept(int sock) { struct sockaddr_in peer; socklen_t len = sizeof(peer); int fd = accept(sock, (struct sockaddr *)&peer, &len); if(fd >= 0){ return fd; } return -1; } static void Connect(int sock, std::string ip, uint16_t port) { struct sockaddr_in server; memset(&server, 0, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(port); server.sin_addr.s_addr = inet_addr(ip.c_str()); if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0) { cout << "Connect Success!" << endl; } else { cout << "Connect Failed!" << endl; exit(5); } }
};
服务端代码思路及代码:
服务端通过套接字将来自客户端传输来的请求信息进行反序列化,然后通过计算得出结果填入响应的结构体对象中,然后对结构体数据进行序列化传输回给客户端,完成通信。
#include <pthread.h>
#include "Protocol.hpp"
#include "Sock.hpp"static void Usage(string proc)
{
cout << "Usage: " << proc << " port" << endl;
exit(1);
}void* HandlerRequest(void *args)
{
int sock = *(int *)args;
delete (int *)args;pthread_detach(pthread_self()); // 业务逻辑, 做一个短服务 // request -> 分析处理 -> 构建response -> sent(response)->close(sock) // 1. 读取请求 char buffer[1024]; request_t req; ssize_t s = read(sock,buffer,sizeof(buffer)-1); if(s > 0) { buffer[s] = 0; std::cout<<"get a new request: "<<buffer<<std::endl; std::string str = buffer; //反序列化 //将读取到的字符串,反序列化为结构体数据 DeserializeRequest(str,req); //读取到了完整的请求,待定 //请求格式为:req.x , req.y, req.op //2. 分析请求 && 3. 计算结果 //4. 构建响应,并进行返回 response_t resp = {0, 0}; switch (req.op) { case '+': resp.result = req.x + req.y; break; case '-': resp.result = req.x - req.y; break; case '*': resp.result = req.x * req.y; break; case '/': if (req.y == 0) resp.code = -1; //代表除0 else resp.result = req.x / req.y; break; case '%': if (req.y == 0) resp.code = -2; //代表模0 else resp.result = req.x % req.y; break; default: resp.code = -3; //代表请求方法异常 break; } cout << "request: " << req.x << req.op << req.y << endl; //计算完成后,将结构体数据进行序列化,传输回给客户端 std::string send_string = SerializeRespond(resp); write(sock,send_string.c_str(),send_string.size()); std::cout<<"服务结束"<<send_string<<std::endl; // 5. 关闭链接 close(sock); }
}
// ./CalServer port
int main(int argc, char *argv[])
{
if (argc != 2)
Usage(argv[0]);
uint16_t port = atoi(argv[1]);int listen_sock = Sock::Socket(); Sock::Bind(listen_sock, port); Sock::Listen(listen_sock); for (;;) { int sock = Sock::Accept(listen_sock); if (sock >= 0) { cout << "get a new client..." << endl; int *pram = new int(sock); pthread_t tid; pthread_create(&tid, nullptr, HandlerRequest, pram); } } return 0;
}
客户端代码思路及代码:
客户端先将用于请求的数据填入请求结构体对象中,然后将其序列化并通过套接字传输给服务端,然后再通过套接字将服务端返回来的结果接收,反序列化,完成通信。
#include "Protocol.hpp"
#include "Sock.hpp"void Usage(string proc)
{
cout << "Usage: " << proc << " server_ip server_port" << endl;
}int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
int sock = Sock::Socket();
Sock::Connect(sock, argv[1], atoi(argv[2]));// 业务逻辑 request_t req; memset(&req, 0, sizeof(req)); cout << "Please Enter Data One# "; cin >> req.x; cout << "Please Enter Data Two# "; cin >> req.y; cout << "Please Enter opreator# "; cin >> req.op; //发送请求,把其序列化 std::string json_string = SerializeRequest(req); ssize_t s = write(sock, json_string.c_str(), json_string.size()); //把服务端返回来的结果接收,并且反序列化 char buffer[1024]; s = read(sock,buffer,sizeof(buffer)-1); if(s > 0) { response_t resp; buffer[s] = 0; std::string str = buffer; DeserializeRespond(str,resp); cout << "code[0:success]: " << resp.code << endl; cout << "result: " << resp.result << std::endl; } return 0;
}
重新看待TCP/IP应用层
上面实现的网络版计算器,本质就是一个应用层的网络服务。它包含了请求和响应的格式,即协议,有业务逻辑,也有网络通信的实现,也有序列化和反序列化。在OSI模型中,应用层作用是针对特定的协议,表示层的作用是格式转化(序列化和反序列化),会话层的作用是管理网络通信!而在TCP/IP模型中,应用层就已经将这三部分包含起来,成为一个整体了!
HTTP协议
在本文中学习HTTP协议的顺序流程:
①首先认识什么是HTTP协议。②了解如何定位到在网络上的唯一的网络资源,从而引入并学习URL,进而引入并简单学习urlencode和urldecode在URL中的作用。③简单认识和学习HTTP协议格式,从而再引入并学习HTTP获取资源的方法(如何将前端的资源输送到后端后台)、HTTP的状态码(也就是我们见得最多的404那种)和HTTP的header(也就是HTTP格式中的报头部分),期间会用代码例子来演示。④最后实现一个超简单的HTTP服务器。
HTTP协议是什么
在上面的网络计算器的例子中,它的应用层协议是我们自己指定的,而在现实中,已经有大佬定义了现成的,非常好用的应用层协议,而HTTP(超文本传输协议)就是其中之一!
因此,http协议,本质上跟我们在网络计算器中的协议没有什么区别,都是应用层协议!
URL
确定唯一的网络资源
我们看到的东西,比如图片、视频、音频、html、js、css、标签、文档等等这些都称之为资源。要确定网络上唯一的资源,我们可以联想到如何确定网络中的唯一一台主机,那就是通过主机的公网IP地址+端口号来确定唯一的一台主机。而在网络中,资源是需要放在某台服务器上的,一般的服务器后台是Linux系统做的,因此这些资源一定存在某台Linux服务器中!而对于Linux系统,是以文件的方式保存资源的,而对于文件来说,是通过路径来标识文件的!因此,我们可以通过IP+路径的方式来确定网络唯一的网络资源!而这里的IP,不是公网IP,是以域名方式呈现的,路径可以通过目录名+/确定。
什么是URL
我们平时说的网址,就是URL。来看一个比较简单的URL。
urlencode和urldecode
当我们在某度的搜索栏上查询“C++”的时候,其URL是这样的:
可以看到,在显示结果的网址URL中,出现了wd=C%2B%2B这样的字符,其实它表示的便是C++的意思。这就涉及到了字符转义的点了。
为什么需要转义?
因为像 / ? : +等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现.比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义。
转义的规则:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式。那么"+" 就被转义成了 "%2B"。做这一步转义的就是urlencode做的,而urldecode就是urlencode的逆过程,将C%2B%2B转成C++。
HTTP协议格式
简单认识HTTP格式
我们来简单认识一些HTTP协议格式:
HTTP协议格式一般都是按照行(\n)为单位进行构建请求和响应的,而格式一般有3或4部分(跟我们上课的教材中,把空行也算进去了,因为空行有用):请求行或响应行、报头、空行和有效载荷(有时候没有,就是3行)。
第一部分:请求行或响应行
⭐请求行中保存的是请求方法、url和http的版本,最后带一个\n。 :①请求方法一般是GET方法,也有POST方法,这两种方法是最常用的。②这里保存的url一般是去掉域名之后的内容。③http的版本有http/1.0 和 http/1.1,一般是1.1版本。
⭐响应行中保存的是http的版本、状态码和状态码描述,最后带一个\n。
:①状态码:比如说我们见得最多是就是404,也就是网页访问错误的时候的状态码。②而状态码描述就是对状态码的解释,状态码表示什么意思。
第二部分:报头
报头中是以kv式的方式保存报头信息,并且有很多行,每一行最后都带有\n。
第三部分的空行和第四部分的有效载荷并没有什么可以单独拿出来说明的。接下来我们来看看http是如何进行解包和封装的,这就涉及到了空行了。
http在封装中,将所有的行的字符串看做成一个大的长的整体的字符串装起来,并发送出去。这也是http发送请求响应的方式。在解包中,用空行将长字符串一分为二!
接下来,我们使用代码,化理论为实践,看看HTTP的请求和响应。
HTTP请求示例代码
recv和send接口
recv方法:从套接字中接收的数据读到buf中。send方法:将buf中的数据写入到套接字中。这两个方法都是TCP使用的。
recv和read方法:recv方法的前三个参数与read方法的三个参数是一样的,是向文件中读取数据到某个空间中。区别是recv的第四个参数,这个参数我们直接设为0就🆗。
send和write方法也是如此,send方法的前三个参数跟write的三个参数是一样的,是将buf中的数据弄到文件中。
示例代码:
#include "Sock.hpp" #include <pthread.h>
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " port" << std::endl;
}void *HandlerHttpRequest(void *args)
{
//Http协议,如果自己写的话,本质是,我们要根据协议内容,来进行文本分析!int sock = *(int*)args; delete (int*)args; pthread_detach(pthread_self());
#define SIZE 1024*10
char buffer[SIZE]; memset(buffer, 0 , sizeof(buffer)); ssize_t s = recv(sock, buffer, sizeof(buffer), 0); if(s > 0) { buffer[s] = 0; std::cout << buffer; //查看http的请求格式! for test //响应行 std::string http_response = "http/1.0 200 OK\n"; http_response += "Content-Type: text/plain\n"; //text/plain,正文是普通的文本 http_response += "\n"; //传说中的空行 //有效载荷 http_response += "hello bit, hello liunx,hello internet!"; send(sock, http_response.c_str(), http_response.size(), 0); } close(sock); return nullptr;
}
int main(int argc, char *argv[])
{
if( argc != 2 )
{
Usage(argv[0]);
exit(1);
}uint16_t port = atoi(argv[1]); int listen_sock = Sock::Socket(); Sock::Bind(listen_sock, port); Sock::Listen(listen_sock); for( ; ; ) { int sock = Sock::Accept(listen_sock); if(sock > 0) { pthread_t tid; int *parm = new int(sock); pthread_create(&tid, nullptr, HandlerHttpRequest, parm); } }
}
此时,我们使用我们的公网IP地址和端口号,在网页中打开,就会显示一下请求的HTTP格式:
分析报头信息(一)
①Content-Length
在上述代码中,有一处是不合理的,那就是每次读取HTTP请求格式的时候,我们都是使用代码中的空间大小为1024*10的buffer。其不合理之处便是我们每次读取这个字节大小的信息,不能保证每次读取都是一个完整的格式信息,或许读少了,或许是读多了,把下一个请求格式的一部分也读取了过来。因此,在HTTP中,为了完整地读取格式,并且不会多读,在HTTP的格式中,有一个叫做Content-Length的自描述字段,和发挥空行的作用!
Content-Length字段就显示了当前格式的有效载荷的长度。而读到了空行,就证明已经把报头部分读完。
通过web根目录访问资源
在请求行中,我们看到反斜杠“/”,这个是web根目录的意思。对于web根目录,我们在打开网页,使用这个根目录的时候,一般会默认打开官网首页。接下来我们使用代码简单实现一下这个操作:
在源代码文件所处的文件夹中,创建一个新的文件夹wwwroot,在wwwroot文件夹中,创建一个html文件,使用html写一个简单的网页index.html,而这个index.html便是这个网站的首页:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h3>hello linux!</h3>
<h3>hello linux!</h3>
<h3>hello linux!</h3>
<h3>hello linux!</h3>
<h3>hello linux!</h3>
<h3>hello linux!</h3>
<h3>hello linux!</h3>
</body>
</html>
在写入请求的正文部分中,将网页文件打开,然后将其内容按行读取到字符串中,最后交给响应的正文即可。
#include "Sock.hpp"
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fstream>//web路径
#define WWWORT "./wwwroot/"
#define HOME_PAGE "index.html"void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " port" << std::endl;
}void *HandlerHttpRequest(void *args)
{
//Http协议,如果自己写的话,本质是,我们要根据协议内容,来进行文本分析!int sock = *(int*)args; delete (int*)args; pthread_detach(pthread_self());
#define SIZE 1024*10
char buffer[SIZE]; memset(buffer, 0 , sizeof(buffer)); ssize_t s = recv(sock, buffer, sizeof(buffer), 0); if(s > 0) { buffer[s] = 0; std::cout << buffer; //查看http的请求格式! for test //无论发起什么请求,都把首页返回 //网页文件的路径 std::string html_file = WWWORT; html_file+=HOME_PAGE; struct stat st; stat(html_file.c_str(),&st); //构建响应 //返回的时候,不仅仅返回网页的正文信息,还会返回HTTP的请求 std::string http_response = "http/1.0 200 OK\n"; //报头信息 //正文部分的数据类型 http_response+="Content-Type: text/html; charset=utf8\n"; //文件的长度,通过stat函数获取文件的属性 http_response+="Content-Length: "; http_response+=std::to_string(st.st_size); http_response+="\n"; //添加空行 http_response+="\n"; //正文,即有效载荷 //先打开这个网页文件 std::ifstream in(html_file); if(!in.is_open()) { std::cerr<<"open html file err"<<std::endl; } else { std::cout<<"read html begin"<<std::endl; //将文件的内容按行读到line字符串中,然后复制给content字符串 std::string content; std::string line; while(std::getline(in,line)) { content+=line; } //然后将正文交给正文部分 http_response+=content; in.close(); std::cout<<http_response<<std::endl; //最后发出去 send(sock, http_response.c_str(), http_response.size(), 0); std::cout<<"read html end"<<std::endl; } } close(sock); return nullptr;
}
int main(int argc, char *argv[])
{
if( argc != 2 )
{
Usage(argv[0]);
exit(1);
}uint16_t port = atoi(argv[1]); int listen_sock = Sock::Socket(); Sock::Bind(listen_sock, port); Sock::Listen(listen_sock); for( ; ; ) { int sock = Sock::Accept(listen_sock); if(sock > 0) { pthread_t tid; int *parm = new int(sock); pthread_create(&tid, nullptr, HandlerHttpRequest, parm); } } return 0;
}
HTTP方法
GET和POST方法介绍
HTTP方法中,很多方法都不能对外提供的,而且其中对于我们来说,最重要的是GET和POST两种方法,GET和POST的作用其实都是获取资源,但两者也有区别,接下来我们通过代码来验证一下这两种方法。
验证GET方法:
使用上面访问网页首页的代码,然后改写index.html,使其可以带上输入框输入姓名和密码,这些不重要,重要的是,在GET方法中,数据从前端到达后端究竟是怎么样的?
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h5>hello linux!我是首页哦!</h5>
<h5>hello 我是表单!</h5><!--方法为GET,/a/b/handler_from并不存在,也不处理--> <form action="/a/b/handler_from" method="GET"> 姓名:<input type="text" name="name"><br/> 密码:<input type="password" name="passwd"><br/> <input type = "submit" value="登陆"> </form> </body>
</html>
从网页中可以看到,数据显示在了网址的输入框中了。而从发送的请求当中,我们可以看到,数据被拼接到了请求行中,以问号?做分隔符,用&隔开显示。
因此,GET方法结论:GET方法提交参数是通过url的方式进行提交的。
验证POST方法
将方法改为POST方法后,来看结果:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h5>hello linux!我是首页哦!</h5>
<h5>hello 我是表单!</h5><!--方法为POST,/a/b/handler_from并不存在,也不处理--> <form action="/a/b/handler_from" method="POST"> 姓名:<input type="text" name="name"><br/> 密码:<input type="password" name="passwd"><br/> <input type = "submit" value="登陆"> </form> </body>
</html>
POST方法结论:POST方法是通过正文提交参数的。
GET和POST方法总结
概念问题:
GET方法叫做获取,是最常用的方法,它是提交参数的方式是通过URL来进行参数拼接从而提交给服务端。
POST方法叫做推送,也是很常用的方法,它提交参数的方式是通过正文提交的,其中Content-Length便是表示参数的长度。
两者区别:
提交参数的位置不同,POST方法比较私密(但不能说安全),不会回显到浏览器的url输入框中。GET方法不私秘,因为它会把参数回显到url中,被盗取的风险比较大。
GET方法是通过URL传参的,而URL是由大小限制的,和具体的浏览器有关。POST是通过正文传参的,一般没有大小限制。
如何选择两者其中之一
如果提交的参数很少,并且不敏感,那么可以选择使用GET方法,否则就使用POST方法。
HTTP状态码
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)。
3XX重定向状态码
重定向的意思我们可以理解为网页的自动跳转。
3XX重定向状态码有两个重要的状态码:301永久重定向和302或307临时重定向。
301永久重定向:即我们打开了一个网页,但是这个网页已经是上古版本的网页了,而为了用户的方便使用,即使老用户不知道新页面的网址,只要打开了老页面,就会自动跳转到新页面中。这个便是永久重定向。
302或307临时重定向:当我们要访问某种资源的时候,首先会跳转到登录页面,而登录之后,又会跳转回到我们需要的那个页面。这就叫做临时重定向。
对于重定向,我们就需要用到报头信息中的Location搭配着使用。
location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问。
301永久重定向:状态码描述为Permanently moved。
void *HandlerHttpRequest(void *args)
{
//Http协议,如果自己写的话,本质是,我们要根据协议内容,来进行文本分析!int sock = *(int*)args; delete (int*)args; pthread_detach(pthread_self());
#define SIZE 1024*10
char buffer[SIZE]; memset(buffer, 0 , sizeof(buffer)); ssize_t s = recv(sock, buffer, sizeof(buffer), 0); if(s > 0) { buffer[s] = 0; std::cout << buffer; //查看http的请求格式! for test //重定向 std::string response = "http/1.1 301 Permanently moved\n"; //location response+="Location: https://www.qq.com/\n"; //空行 response+='\n'; send(sock, response.c_str(), response.size(), 0); } close(sock); return nullptr;
}
此时打开我们的网址后,就会自动跳转到腾讯的首页。
HTTP常见Header(分析报头信息二)
①Connection
一般而言,一个网页是由许多元素组成的,而http/1.0采用的网络请求方案是短链接
......