目录
应用层
再谈 "协议"
网络版计算器
HTTP协议
认识URL
urlencode和urldecode
HTTP协议格式
HTTP请求
HTTP响应
HTTP的方法
HTTP的状态码
HTTP常见Header
拓展知识(了解)
长链接
http周边会话保持
基本工具(http)
应用层
程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层
再谈 "协议"
协议是一种 "约定". socket api的接口, 在读写数据时, 都是按 "字符串" 的方式来发送接收的. 如果我们要传输一些"结构化的数据" 怎么办呢?
这个"结构化的数据"是什么呢?比如我们在用聊天软件聊天时,发送的消息只有文字吗?当然不是,我们发送的数据中会包含:头像,昵称,时间和内容等,它们是一个整体。
"结构化的数据"在发送到网络中的时候,先序列化在发送,收到的数据一定是序列字节流,要先进行反序列化,然后才能使用。
网络版计算器
实现一个服务器版的计算器. 我们需要客户端把要计算的两个数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端。
在原有的tcp网络通信的基础上,添加了几个组件:
- 处理计算任务
- 读取一个完整的请求。因为tcp是面向字节流的,所以怎样读取一个完整的请求也需要我们自己处理;
- 自定义协议报头,定制接口用于添加和删除报头;
- 将结构化的数据进行序列化和反序列化;(自己实现和使用工具)
calServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
#include "log.hpp"
#include "Protocol.hpp"
namespace Server
{
// using namespace std;
static const uint16_t gport = 8080;
static const int gbacklog = 5;
// const Request& req 输入性参数 | Response& resp 输出型参数
typedef function<bool(const Request& req,Response& resp)> func_t;
void handlerEnter(int sock,func_t func)
{
string inbuffer;
while(true)
{
// 1.读取数据 "content_len"\r\n"x op y"\r\n
// 1.1保证读到的是完整的请求
string req_text,req_str;
// 1.2 到这 req_text里面一定是一个完整的请求:"content_len"\r\n"x op y"\r\n
if(!recvPackage(sock,inbuffer,&req_text)) //读取一个完整的请求
return;
// cout << "带报头的请求:\n" << req_text << endl;
if(!deLength(req_text,&req_str)) //去掉报头
return;
// cout << "去掉报头的正文:" << req_str << endl;
// 2.反序列化
// 2.1得到一个结构化的对象
Request req;
if(!req.deserialize(req_str))
return;
// 3.进行处理
// 3.1得到一个结构化的响应
Response resp;
func(req,resp); //req的处理结果,全部放到了resp中
// 4.对响应的Response 进行序列化
// 4.1得到一个 "字符串"
string resp_str;
resp.serialize(&resp_str);
// cout << "计算完成,对响应进行序列化:" << resp_str << endl;
// 5.发送响应
// 5.1 构建一个完整的报文
string send_string = enLength(resp_str);
// cout << "构建完整的报文:\n" << send_string << endl;
send(sock,send_string.c_str(),send_string.size(),0); //发送
}
}
class calServer
{
public:
calServer(const uint16_t& port = gport)
:_listensock(-1),_port(port)
{}
void initServer()
{
//1.创建socket文件套接字对象
_listensock = socket(AF_INET,SOCK_STREAM,0);
if(_listensock < 0)
{
logMessage(FATAL,"create socket error");
exit(SOCKET_ERR);
}
logMessage(NORMAL,"create socket success: %d",_listensock);
// 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)
{
logMessage(FATAL,"bind socket error");
exit(BIND_ERR);
}
logMessage(NORMAL,"bind socket success");
//3. 设置socket为监听状态
if(listen(_listensock,gbacklog) < 0)
{
logMessage(FATAL,"listen socket error");
exit(LISTEN_ERR);
}
logMessage(NORMAL,"listen socket success");
}
void start(func_t func)
{
for( ; ; ) //死循环
{
//4. Server获取新链接
//sock 是用于和client进行通信的
struct sockaddr_in peer;
socklen_t peer_len = sizeof(peer);
int sock = accept(_listensock,(struct sockaddr*)&peer,&peer_len);
if(sock < 0)
{
logMessage(ERROR,"accept error,next");
continue;
}
logMessage(NORMAL,"accept a new link success sock:%d",sock);
//5. 未来通信就用这个sock,面向字节流的,后面全是文件操作
// //version 2.1 多进程版
pid_t id = fork();
if(id == 0) //子进程
{
close(_listensock);
if(fork() > 0) exit(0); //让孙子进程执行代码,子进程退出被父进程回收,孙子进程会变成孤儿进程
// serviceIO(sock);
handlerEnter(sock,func);
close(sock);
exit(0);
}
//父进程
pid_t ret = waitpid(id,nullptr,0);
close(sock);
if(ret > 0)
{
logMessage(NORMAL,"wait child success");
}
// //------------------------------------------------------------------------------------
// //version 2.2 多进程版 信号方式
// signal(SIGCHLD,SIG_IGN); //信号忽略,忽略对子进程的管理
// pid_t id = fork();
// if(id == 0) //子进程
// {
// close(_listensock);
// serviceIO(sock);
// close(sock);
// exit(0);
// }
// close(sock);
}
}
~calServer()
{}
private:
int _listensock; //不是用来数据通信的,它是监听链接是否到来的,用于获取新链接的
uint16_t _port;
} ;
}//namespace end Server
calServer.cc
#include "calServer.hpp"
#include <memory>
using namespace Server;
// 提示 运行格式
static void Usage(string proc)
{
cout<<"Usage:\n\t" << proc <<" local_port\n\n";
}
// 计算客户端发来的任务
// req:里面是一个处理好的完整的请求对象
// resp:根据req进行业务处理,填充resp,不用管理任何读取和写入,序列化和反序列化等细节,在这之前已经处理好了
bool cal(const Request& req,Response& resp)
{
// req 里已经有结构化的数据了,直接使用即可
resp._exitcode = OK;
resp._result = OK;
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._exitcode = DEV_ZERO;
else
resp._result = req._x / req._y;
}
break;
case '%':
{
if(req._y == 0)
resp._exitcode = MOD_ZERO;
else
resp._result = req._x % req._y;
}
break;
default:
resp._exitcode = OP_ERROR;
break;
}
return true;
}
// ./calServer local_port
int main(int argc,char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
unique_ptr<calServer> tsvr(new calServer(port));
tsvr->initServer();
tsvr->start(cal);
return 0;
}
Protocol.hpp
#pragma once
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>
#define SEP " "
#define SEP_LEN strlen(SEP) //不能使用sizeof
#define LINE_SEP "\r\n"
#define LINE_SEP_LIN strlen(LINE_SEP) //不能使用sizeof
enum{ OK = 0,DEV_ZERO,MOD_ZERO,OP_ERROR};
//定制协议,添加报头
// "x op y" -> "content_len"\r\n"x op y"\r\n
string enLength(const string& text)
{
string send_str = to_string(text.size());
send_str += LINE_SEP;
send_str += text;
send_str += LINE_SEP;
return send_str;
}
//定制协议,去掉报头
// "content_len"\r\n"exitcode result"\r\n -> "exitcode result"
bool deLength(const string& package,string* text)
{
auto pos = package.find(LINE_SEP);
if(pos == string::npos)
return false;
string text_len_str = package.substr(0,pos);
int text_len = stoi(text_len_str);
*text = package.substr(pos+LINE_SEP_LIN,text_len);
return true;
}
// 读取一个完整的请求
// "content_len"\r\n"x op y"\r\n
bool recvPackage(int sock,string& inbuffer,string* text)
{
char buffer[1024];
while(true)
{
ssize_t n = recv(sock,buffer,sizeof(buffer)-1,0);
if(n > 0)
{
buffer[n] = 0;
inbuffer += buffer;
// 分析处理
auto pos = inbuffer.find(LINE_SEP);
if(pos == string::npos) //一个请求都不完整,继续读取
continue;
string text_len_str = inbuffer.substr(0,pos);
int text_len = stoi(text_len_str);
// text_len_str.size() + 2*LINE_SEP_LIN +text_len <= inbuffer.size() 证明至少有一个完整的请求
int total_len = text_len_str.size() + 2*LINE_SEP_LIN +text_len;
if(inbuffer.size() < total_len)//一个请求都不完整,继续读取
continue;
// cout << "inbuffer处理前: \n" << inbuffer << endl;
// 至少有一个完整的请求
*text = inbuffer.substr(0,total_len);
inbuffer.erase(0,total_len);
// cout << "inbuffer处理后: \n" << inbuffer << endl;
break;
}
else return false;
}
return true;
}
//由于是阻塞式读取,所以这种方法暂时用不了
// bool recvRequestAll(int sock,vector<string>* out)
// {
// string line;
// while(recvRequest(sock,&line))
// out->push_back(line);
// }
class Request
{
public:
Request()
:_x(0),_y(0),_op(0)
{}
Request(int x,int y,char op)
:_x(x),_y(y),_op(op)
{}
//序列化,通过条件编译可以随意切换,是用自己写的,还是使用工具
bool serialize(string* out)
{
#ifdef MYSELF
//结构化数据 -> "x op y"
*out = "";
string x_str = to_string(_x);
string y_str = to_string(_y);
*out = x_str;
*out += SEP;
*out += _op;
*out += SEP;
*out += y_str;
#else //使用Json工具
Json::Value root;
root["first"] = _x;
root["second"] = _y;
root["oper"] = _op;
Json::FastWriter writer;
*out = writer.write(root);
#endif
return true;
}
//反序列化
bool deserialize(const string& in)
{
//"x op y" -> 结构化数据
#ifdef MYSELF
auto left = in.find(SEP);
auto right = in.rfind(SEP);
if(left == string::npos || right == string:: npos || left == right)
return false;
if(right- (left + SEP_LEN) != 1)
return false;
string x_str = in.substr(0,left);
string y_str = in.substr(right+SEP_LEN);
if(x_str.empty() || y_str.empty())
return false;
_x = stoi(x_str);
_y = stoi(y_str);
_op = in[left+SEP_LEN];
#else
Json::Value root;
Json::Reader reader;
reader.parse(in,root);
_x = root["first"].asInt();
_y = root["second"].asInt();
_op = root["oper"].asInt();
#endif
return true;
}
public:
// "x op y"
int _x;
int _y;
char _op;
};
class Response
{
public:
Response()
:_exitcode(0),_result(0)
{}
Response(const int exitcode,int result)
:_exitcode(exitcode),_result(result)
{}
//自己写
//序列化
bool serialize(string* out)
{
#ifdef MYSELF
*out = "";
string ec_str = to_string(_exitcode);
string re_str = to_string(_result);
*out = ec_str;
*out += SEP;
*out += re_str;
#else
Json::Value root;
root["exitcode"] = _exitcode;
root["result"] = _result;
Json::FastWriter writer;
*out = writer.write(root);
#endif
return true;
}
//反序列化
bool deserialize(const string& in)
{
#ifdef MYSELF
// "exitcode result"
auto pos = in.find(SEP);
if(pos == string::npos)
return false;
string ec_str = in.substr(0,pos);
string re_str = in.substr(pos + SEP_LEN);
if(ec_str.empty() || re_str.empty())
return false;
_exitcode = stoi(ec_str);
_result = stoi(re_str);
#else
Json::Value root;
Json::Reader reader;
reader.parse(in,root);
_exitcode = root["exitcode"].asInt();
_result = root["result"].asInt();
#endif
return true;
}
public:
int _exitcode; // 0:表示计算成功 !0:表示计算失败,具体的是多少会有一个标准
int _result; //计算结果
};
calClient.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "log.hpp" //日志信息
#include "Protocol.hpp"
#define NUM 1024
namespace Client
{
// using namespace std;
class calClient
{
public:
calClient(const string& serverip,const uint16_t serverport)
:_sock(-1),_serverip(serverip),_serverport(serverport)
{}
void ininClinet()
{
//1.创建socket
_sock = socket(AF_INET,SOCK_STREAM,0);
if(_sock < 0)
{
logMessage(FATAL,"socket create error");//错误日志信息
exit(2);
}
//2.tcp客户端也要bind,但是不用我们显示的bind,OS会帮我们bind的
}
void start()
{
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(_serverport);
server.sin_addr.s_addr = inet_addr(_serverip.c_str());
//向服务器发起链接请求
if(connect(_sock,(struct sockaddr*)&server,sizeof(server)) != 0)
{
logMessage(ERROR,"socket connect error");
}
else
{
string msg;
string inbuffer;
while(true)
{
cout << "Mycal>> ";
getline(cin,msg); //cin>> 1+1
//准备计算任务,
Request req = ParseLine(msg); //计算任务结构化
string content;
if(!req.serialize(&content)) continue; //"1 + 1" 序列化
string send_string = enLength(content); //"content_len"\r\n"x op y"\r\n 添加报头
// 发送计算任务
send(_sock,send_string.c_str(),send_string.size(),0);
// 获取计算结果
string package,text;
if(!recvPackage(_sock,inbuffer,&package)) // 获取一个完整的响应"content_len"\r\n"exitcode result"\r\n
continue;
if(!deLength(package,&text)) //"exitcode result" 去报头
continue;
Response resp;
resp.deserialize(text); //反序列化
// 输出计算结果
cout << "exitcode: " <<resp._exitcode << endl;
cout << "result: " <<resp._result << endl << endl;
}
}
}
// 简易版本的状态机,用于拆分输入的计算字符串
Request ParseLine(const string& line)
{
// "1+1" "10*10" "1234/0"
int status = 0;
int i = 0,cnt = line.size();
string x,y;
char op;
while(i < cnt)
{
switch(status)
{
case 0:
{
if(!isdigit(line[i]))
{
op = line[i];
status = 1;
}
else
x.push_back(line[i++]);
}
break;
case 1:
{
i++;
status = 2;
}
break;
case 2:
y.push_back(line[i++]);
break;
}
}
return Request(stoi(x),stoi(y),op);
}
~calClient()
{
if(_sock >= 0) close(_sock);//可写,可不写
}
private:
int _sock;
string _serverip;
uint16_t _serverport;
};
}//namespace Client end
calClient.cc
#include "calClient.hpp"
#include <memory>
using namespace Client;
// 提示 运行格式
static void Usage(string proc)
{
cout<<"Usage:\n\t" << proc <<" server_ip server_port\n\n";
}
// ./calClient server_ip_ip server_port
int main(int argc,char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
unique_ptr<calClient> tpcl(new calClient(serverip, serverport));
tpcl->ininClinet();
tpcl->start();
return 0;
}
还有一些日志信息和执行指令请看完整代码:lesson13/1_ · 晚风不及你的笑/MyCodeStorehouse - 码云 - 开源中国 (gitee.com)
操作演示:
只要能够保证, 客户端发送时构造的数据, 在服务端能够正确的进行解析, 就是ok的。 这种约定, 就是 应用层协议。
HTTP协议
虽然我们说, 应用层协议是我们程序猿自己定的。但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用:HTTP(超文本传输协议)就是其中之一。
认识URL
平时我们俗称的 "网址" 其实就是说的 URL。
我们的上网行为无非两种:1.把网络上的资源拉到本地做显示或使用,2.把本地资源上传或提交到网络中。
http的作用就是找到服务器IP地址、通过端口号和文件路径、把对应的资源返回给客户端。
说白了http就是一个文件传输的协议,它能从服务器上拿到对应的 “资源”,像日常上网中,你看到的图片、视频、文字、音频等这些都属于对应的资源,一切你在网络中看到的都是资源,我们把这些资源看作是资源文件,它存储在服务器的磁盘上。有了对应的路径结构我们就能在网络中从服务器上拿到该资源,又因为文件资源种类特别多,http都能搞定,所以http叫超文本传输协议。
urlencode和urldecode
像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现.比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义。
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式
urldecode就是urlencode的逆过程;
编码解码工具:url解码在线,在线url编码解码工具,urldecode在线解码-站长工具 (senlt.cn)
HTTP协议格式
HTTP请求
- 首行: [方法] + [url] + [版本]
- Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
- Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度;
HTTP响应
- 首行: [版本号] + [状态码] + [状态码解释]
- Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
- Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页面内容就是在body中.
这里有两个问题需要回答:
1. 怎么保证应用层读取的是一个完整的请求或响应呢?
首先不管是请求还是响应,空行之前的内容都是以行为单位的,可以设计接口读取完整的一行,再循环读取请求行和请求报头,一直读取到空行才结束。那么正文怎么读呢?总不能还按行读取吧。我们只要能保证把报头读完,在报头中有一个属性Content-Length标识正文的长度,通过解析出来的长度,在读取正文即可,这样整个求情或响应就读取完毕了。
2. 请求和响应是怎么做到序列化和反序列化的?
是http自己实现的,第一行的内容+请求/响应报头,只需要按照\r\n的方式将字符串由第一行到第n行即可。正文不需要处理。
HTTP的方法
其中最常用的就是GET方法和POST方法。GET通过url传递参数,POST通过http请求的正文提交参数,POST方法通过正文提交参数,所以一般用户看不到,私密性会更好,但是这里私密性不代表安全性。无论GET和POST这两种方法都不安全,要谈安全必须加密,要安全就得用https。
HTTP的状态码
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(BadGateway)
HTTP常见Header
- Content-Type: 数据类型(text/html等)
- Content-Length: Body的长度
- Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
- User-Agent: 声明用户的操作系统和浏览器版本信息;
- referer: 当前页面是从哪个页面跳转过来的;
- Location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
- Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;
最简单的HTTP服务器
实现一个最简单的HTTP服务器, 只在网页上输出 "hello world"; 只要我们按照HTTP协议的要求构造数据, 就很容易能做到;
HttpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
namespace Server
{
// using namespace std;
enum { USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR};
static const uint16_t gport = 8080;
static const int gbacklog = 5;
using func_t = 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)
{
exit(LISTEN_ERR);
}
}
void handlerHttp(int sock)
{
// 1. 读取一个完整的http请求
// 2. 序列化
// 3. 回调
// 4. resp 序列化
// 5. 发送
char buffer[4096];
HttpRequest req;
HttpResponse resp;
ssize_t n = recv(sock,buffer,sizeof(buffer)-1,0); //大概率能直接读到一个完整的请求
if(n > 0)
{
buffer[n] = 0;
req.inbuffer = buffer;
_func(req,resp); //req -> resp
send(sock,resp.outbuffer.c_str(),resp.outbuffer.size(),0);
}
}
void start()
{
for( ; ; ) //死循环
{
//4. Server获取新链接
//sock 是用于和client进行通信的
struct sockaddr_in peer;
socklen_t peer_len = sizeof(peer);
int sock = accept(_listensock,(struct sockaddr*)&peer,&peer_len);
if(sock < 0)
{
continue;
}
//5. 未来通信就用这个sock,面向字节流的,后面全是文件操作
// //version 2.1 多进程版
pid_t id = fork();
if(id == 0) //子进程
{
close(_listensock);
if(fork() > 0) exit(0); //让孙子进程执行代码,子进程退出被父进程回收,孙子进程会变成孤儿进程
handlerHttp(sock);
close(sock);
exit(0);
}
close(sock);
//父进程
waitpid(id,nullptr,0);
}
}
~HttpServer()
{}
private:
int _listensock; //不是用来数据通信的,它是监听链接是否到来的,用于获取新链接的
uint16_t _port;
func_t _func;
} ;
}//namespace end Server
HttpServer.cc
#include "HttpServer.hpp"
#include <memory>
using namespace Server;
static void Usage(string argv)
{
cerr << "Usage: \n\t" << argv <<" port \n\n" << endl;
}
bool Get(const HttpRequest& req, HttpResponse& resp)
{
cout<< "----------------------http start------------------------------------" << endl;
cout << req.inbuffer << endl;//请求
cout<< "----------------------http end------------------------------------" << endl;
//响应
string respline = "Http/1.1 200 OK\r\n";
string respheader = "Content-Type:text/html\r\n";
string respblank = "\r\n";
string body = "<html lang=\"en\"><head><meta charset=\"UTF-8\"><title>for test</title><h1>hello world</h1></head><body><p>你好呀</p></body></html>";
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(1);
}
uint16_t port = atoi(argv[1]);
unique_ptr<HttpServer> httpsvr(new HttpServer(Get,port));
httpsvr->initServer();
httpsvr->start();
return 0;
}
Protocol.hpp
#pragma once
#include <iostream>
#include <string>
const string sep = "\r\n";
using namespace std;
class HttpRequest
{
public:
HttpRequest(){}
~HttpRequest(){}
public:
string inbuffer;
};
class HttpResponse
{
public:
string outbuffer;
};
编译, 启动服务. 在浏览器中输入 [ip]:[port], 就能看到显示的结果 "Hello World"
此处我们使用 8080 端口号启动了HTTP服务器. 虽然HTTP服务器一般使用80端口,但这只是一个通用的习惯. 并不是说HTTP服务器就不能使用其他的端口号.
拓展知识(了解)
长链接
我们看到的网页,实际上可能由多种元素组成,文字、图片、视频等,那么一张网页就需要多次http请求。
http网页中可能包含多个元素,由于http是基于tcp,tcp是面向连接的,如果频繁发起http请求,就会出现频繁创建链接的问题。解决这个问题需要客户端和服务器都支持长连接,建立好一条链接,获取大份资源的时候,通过类似串行化的方式在一条链接上完成。
http周边会话保持
会话保持严格意义来讲不是http天然具备的,是后面使用的时候发现需要的。比如,某个视频网页上登陆自己的账号,多开几个网页,或者关闭网页后重新开启,我们发现网页自动帮我们登录了账号。
http协议是无状态的,也就是说同一个图片资源,我们多次刷新,http每一次都会提交一个请求,这样会导致访问变慢。因为用户查看新的网页是常规操作,如果发生网页跳转,新的页面也就无法识别用户了,但是为了让用户一次登陆,多次访问不受限制,有一个会话保持功能,是浏览器实现的,也就是它会将我们输入的账号密码保存起来,往后我们只要访问同一个网站,浏览器会自动推送历史保留信息。这种我们称为cookie,cookie分为文件级别和内存级别。
基本工具(http)
piostman 不是抓包工具,主要是用来模拟客户端(浏览器)行为的,相当于自己就是一个客户端。
fiddler 是一个抓包工具,是用来抓http的,且只能是抓本地的。主要用于调试,他相当于一个中间代理,它抓到客户端的请求,经过处理再次发给服务器,服务器也会把内容先返回给它,再由它返回给客户端。