目录
五、http会话保持(客户端Cookie,服务器session)
本文http协议详细代码可参照博主gitee。
一、http协议
1、http协议的介绍
http属于应用层协议。虽然应用层协议是程序员自己定的,但实际上, 已经有前辈们定义了一些现成的, 又非常好用的应用层协议, 供我们参考使用。 http(超文本传输协议) 就是其中之一,这个协议就是用户从服务器拿视频、图片等文件资源的一种协议。(这些资源存放在服务器的磁盘上)
2、URL的组成
平时我们俗称的 "网址" 其实就是说的 URL。
ip会标识一台网络主机,看到"/"就知道这台网络主机用的是Linux系统。使用url就可以通过浏览器请求这台网络主机的服务器,从指定的文件路径下找到用户请求的文件返回给用户。
3、urlencode和urldecode
像 / + : ?等字符, 已经被url特殊处理了。比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义.
转义的规则如下:取出字符的ASCII码,转成16进制,然后前面加上百分号即可。编码成%XY格式。服务器收到url请求,将会对%XY进行解码,该过程称为decode,如果哪天需要解码了,网上搜一下就行,这不是很重要的东西。
"+" 被转义成了 "%2B"。urldecode就是urlencode的逆过程。
二、http的请求方法、状态码及状态码描述、常见的响应报头
1、http请求方法
请求方法 | 说明 | 支持的http协议版本 |
GET | 获取资源 | 1.0/1.1 |
POST | 传输实体主体 | 1.0/1.1 |
其他方法不常用,略。 |
GET和POST方法提交参数的区别
客户端进行数据提交时,是通过前端的from表单提交的,浏览器会将from表单中的内容转换为GET/POST方法。
注意:如果用的是GET方法,需要对url进行额外的处理,例如/test.py?key1=value1&key2=value2,需要拆解出其中的路径(_path),即"test.py"。问号右侧则是参数(_parm)。
if(req._path=="test.py")
{
//建立进程间通信,pipe
//fork创建子进程,execl("/bin/python",test.py)进行进程程序替换
//父进程,将req._parm通过管道写给某些后端语言,例如py、java、php等
//....
return true;
}
2、http状态码及状态码描述
以307状态码为例,临时重定向至CSDN首页:
std::string respLine="HTTP/1.1 307 Temporary Redirect\r\n";//重定向,配合"Location: "使用
//响应报头
std::string respHeader=suffdeixDesc(req._suffix);//将后缀转换为对应的响应报头
if(req._size>0)
{
respHeader+="Content-Length: ";
respHeader+=std::to_string(req._size);
respHeader+="\r\n";
}
respHeader+="Location: https://www.csdn.net/\r\n";
3、http常见的响应报头
三、http协议客户端和服务器的通信过程
以个人云服务器作服务器,浏览器作客户端,服务器打印客户端请求如图:
这个http请求可以看到请求的设备信息,如果在手机端浏览器搜索“微信下载”,浏览器将会返回手机版的微信下载网页,若在电脑浏览器搜索,将会返回电脑版的微信下载网页。
以个人云服务器作服务器,浏览器作客户端,服务器的响应格式:
Content-Type: text/html\r\n用于指示所传输的数据是HTML格式的文本。注意响应报头位置不要打成test了,这些都是设定好的,如果输错了,浏览器请求服务器时,会变成下载html文件!同样的,图片的响应报头写错了,当客户端请求图片时,同样会变成下载逻辑。
1、如何保证请求和响应被应用层完整的读取了?
一个用户看到的网页结果,可能由多个资源组合而成,所以要获取一张完整的网页效果,浏览器会发起多次http请求。所以需要根据客户端的请求信息,在服务器设置好不同的状态行和响应报头。
2、请求和响应如何做到序列化和反序列化
3、客户端和服务器通信的全过程
void HandlerHttp(int sock)
{
//1、读取完整的http请求
char buffer[4096];
HttpRequest req;
HttpResponse resp;
size_t n=recv(sock,buffer,sizeof(buffer)-1,0);//读取套接字中的内容
if(n>0)
{
buffer[n]=0;//添加'\0'
req._inBuffer=buffer;//获得序列化请求
//2、对客户端请求进行反序列化
//从客户端请求中解析获得请求行(请求方法、请求url、http版本)、请求资源的后缀、请求资源的大小
req.Parse();//对请求进行解析
//3、回调_func方法,由http请求获得http响应 _func(req,resp)
//4、对http响应resp进行序列化
_func(req,resp);
//funcs[req._path](req,resp);
send(sock,resp._outBuffer.c_str(),resp._outBuffer.size(),0);//5、send发送
}
}
步骤2反序列化:解析请求行,从url中获取请求的资源路径并计算资源的大小等。
void Parse()//从客户端请求中解析获得请求行(请求方法、请求url、http版本)、请求资源的后缀、请求资源的大小
{
//1、从请求结构体中的_inbuffer中拿到请求行(第一行),分隔符\r\n
std::string line=Util::getOneLine(_inBuffer,sep);
if(line.empty()){return;}
//2、从请求行中获取三个字段:请求方法、请求url、请求版本
//std::cout<<"line:"<<line<<std::endl;
std::stringstream ss(line);
ss>>_method>>_url>>_httpVersion;//以空格为分割读取其中的字段
//2.1如果是GET方法,需要对_url进行额外处理
///search?name=jiang&pwd=123通过?对GET方法进行左右分离(POST本来就是分离的没有问号)
//左边是PATH,右边是_parm
//3、添加web默认路径
_path=defaultRoot;//客户端所有请求路径前都会被加上./wwwroot前导目录字符串
_path+=_url;//如果客户端请求a/b/c.html,则会请求./wwwroot/a/b/c.html
if(_path[_path.size()-1]=='/')//如果请求的是web根目录(./wwwroot/),就让它访问设定好的默认首页的路径(./wwwroot/index.html)
{
_path+=homePage;
}
//4、获取path对应的资源后缀
//./wwwroot/index.html
//./wwwroot/test/a.html
//./wwwroot/test/b.html
//./wwwroot/image/dog.jpg
auto pos=_path.rfind(".");
if(pos==std::string::npos){_suffix=".html";}//实在找不到,先给个html
else{_suffix=_path.substr(pos);}
//5、得到响应正文的大小(客户端请求资源的大小)
struct stat st;
int n=stat(_path.c_str(),&st);
if(0==n)
{
_size=st.st_size;
}
else{_size=-1;}
}
步骤3、4,由客户端请求转换为客户端响应(设置一下不同响应方法的状态行、响应报头、响应正文等信息,这里的响应正文是html和图片)
bool Get(const HttpRequest& req,HttpResponse& resp)
{
// if(req._path=="test.py")
// {
// //建立进程间通信,pipe
// //fork创建子进程,execl("/bin/python",test.py)进行进程程序替换
// //父进程,将req._parm通过管道写给某些后端语言,例如py、java、php等
// //....
// return true;
// }
// if(req._path=="/search")
// {
// //如果PATH是"/search",可以在这里写具体的C++方法,提供相应的服务
// //....
// return true;
// }
//打印客户端的请求的一些信息
std::cout<<"-----------httpStart------------"<<std::endl;
std::cout<<req._inBuffer<<std::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;
std::cout<<"suffix:"<<req._suffix<<std::endl;
std::cout<<"size:"<<req._size<<"字节"<<std::endl;
std::cout<<"-----------httpEnd--------------"<<std::endl;
//服务器的状态行、响应报头、空行、响应正文等信息
//状态行
std::string respLine="HTTP/1.1 200 OK\r\n";
//std::string respLine="HTTP/1.1 307 Temporary Redirect\r\n";//重定向,配合"Location: "使用
//响应报头
std::string respHeader=suffdeixDesc(req._suffix);//将后缀转换为对应的响应报头
if(req._size>0)
{
respHeader+="Content-Length: ";
respHeader+=std::to_string(req._size+1);//为了和后面的body.size()大小匹配
respHeader+="\r\n";
}
//respHeader+="Location: https://www.csdn.net/\r\n";
//写入Cookie
respHeader+="Set-Cookie: name=12345abcde;Max-Age=120\r\n";//设置Cookie响应报头
//往后,每次http请求都会自动携带曾经设置的所有Cookie,帮助服务器的鉴权行为
//空行
std::string respBlack="\r\n";
std::string body;
body.resize(req._size+1);
if(!Util::readFile(req._path,(char*)body.c_str(),req._size))//将_path路径下的文件内容读到buffer里
{
Util::readFile(html_404,(char*)body.c_str(),req._size);//访问的资源不存在,body的路径就是404html的路径
respLine="HTTP/1.1 404 Not Found\r\n";//同时,状态行修改为404
}
std::cout<<"-----------httpStart------------"<<std::endl;
std::cout<<(respLine+respHeader+respBlack)<<std::endl;
std::cout<<"-----------httpEnd--------------"<<std::endl;
resp._outBuffer=respLine+respHeader+respBlack+body;
return true;
}
4、获取响应正文的长度
STAT(2)
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);
struct stat {
dev_t st_dev; // 包含文件的设备ID
ino_t st_ino; // inode号
mode_t st_mode; // 文件保护模式
nlink_t st_nlink; // 硬链接数
uid_t st_uid; // 文件所有者的用户ID
gid_t st_gid; // 文件所有者的组ID
dev_t st_rdev; // 设备ID(如果是特殊文件)
off_t st_size; // 总大小,以字节为单位
blksize_t st_blksize; // 文件系统I/O的块大小
blkcnt_t st_blocks; // 分配的512B块数
time_t st_atime; // 上次访问时间
time_t st_mtime; // 上次修改时间
time_t st_ctime; // 上次状态更改时间
};
得到_path路径下的文件大小,单位字节:
struct stat st;
int n=stat(_path.c_str(),&st);
if(0==n)
{
_size=st.st_size;
}
else{_size=-1;}
四、http长连接
http请求是基于tcp协议的,而tcp是需要进行连接的。对于一个网页,可能包含多种元素,则需要发起多次连接。为了减少连接次数,需要客户端和服务器均支持长链接,建立一条连接,传输一份大的资源通过一条连接完成。
Connection: keep-alive
Connection: close
如果报头的"Connection: "显示是"keep-alive",则代表支持长连接。
五、http会话保持(客户端Cookie,服务器session)
例如我们打开哔哩哔哩的首页进行登录操作,哪怕用户马上把浏览器关了,短期内再次访问哔哩哔哩是不需要用户重复登录操作的。同样的,当我们在哔哩哔哩进行页面跳转时,http协议并不会记录用户信息,但是曾经的用户登录信息在页面跳转时并不会丢失。浏览器仍会记住上一次登录的信息。这就是会话保持。
其实http请求是无状态的,每次请求并不会记录它曾经请求了什么。所以会话保持不是http协议天然具备的特点,而是浏览器为了满足用户的使用需求,做了相应的工作。
用户在第一次输入账号和密码时,浏览器会进行保存(Cookie),近期再次访问同一个网站,浏览器会自动将用户信息推送给服务器。像哔哩哔哩中某些需要大会员才能观看的视频,服务器都会先获取用户信息进行身份判断,用的就是浏览器缓存的信息。这样只要用户首次输入密码,一段时间内将不用再做登录操作了。
但是本地的Cookie如果被不法分子拿到,那不是危险了,所以信息的保存是在服务器上完成的,服务器会对每个用户创建一份独有的session id,并将其返回给浏览器,浏览器存到Cookie的其实是session id。但这样只能保证原始的账号密码不会被泄漏,黑客盗取了用户的session id后仍可以非法登录,只能靠服务端的安全策略保障安全,例如账号被异地登录了,服务端察觉后只要让session id失效即可,这样异地登录将会使用户重新验证账号密码或手机或人脸信息(尽可能确保是本人),一定程度上保障了信息的安全。
服务器向客户端返回Cookie:
//写入Cookie
respHeader+="Set-Cookie: name=12345abcde; Max-Age=120\r\n";//设置Cookie响应报头,有效期2分钟
//往后,每次http请求都会自动携带曾经设置的所有Cookie,帮助服务器的鉴权行为————http会话保持
六、http相关工具
1、postman——能够模拟客户端浏览器的行为。
2、fiddler——一个本地抓包工具,作为http调试使用。(能够明文抓到本地的POST方法请求正文!)
所以http协议并不安全,想要安全还得看https协议......