【Linux网络编程】 HTTP协议
目录
前言
URL
协议格式
常见的方法
状态码
sessionid
token
总结
HTTP协议是基于TCP的应用层协议,虽然我们说, 应用层协议是我们程序猿自己定的,但是自己定协议也是比较麻烦要解决两个问题:
- 序列化与反序列化
- 数据粘包问题
但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用. HTTP(超文本传输协议);
URL
浏览器访问百度:www.baidu.com可以跳转到百度;使用终端ping 百度,得到百度的ip,使用ip依然跳转到百度;但是发现并没有端口号这是为什么?
原因:使用的是http协议,只要是http协议,那么服务器所使用的端口号都必须是80,https协议,服务器所使用的端口号必须是443,由此根据协议方案名就可以指定端口号是多少,重要的常用的协议,端口号必须是众所周知的,不可以随意修改;
wd就是搜索的关键信息,其余的参数都是浏览器的一些配置信息;如有多组参数就是要 & 符号进行间隔;
网址中的名称比如:“ baidu ” 最终会经过应用层协议——DNS进行域名解析;
DNS域名解析:ip地址是一定要的;浏览器会自动的将域名转为ip地址;转完之后浏览器会那ip地址去访问;
在搜索一些关键信息时,浏览器会将搜索关键字拼接到url上,对于一些特殊的符号(/:#等)会进行编码转换;也就是urlencode和urldecode
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY 格式
"+" 被转义成了 "%2B" urldecode就是urlencode的逆过程;
url在线转码工具 - UrlEncoder.cnhttps://www.urlencoder.cn/url%E5%9C%A8%E7%BA%BF%E8%BD%AC%E7%A0%81%E5%B7%A5%E5%85%B7.html
协议格式
访问百度得到以下信息:
使用telnet工具:
telnet www.baidu.com 80
// ctrl + ]
^]
// 回车
telnet>
GET / HTTP/1.1
// 两个回车
查看响应的http报文,报文是一行一行的,因为http报文都是以 行 为单位的,然而无论有多少行,在tcp看来全部都是一行字符串;
http协议版本:http/1.0、http/1.1、http/2.0、到现在最新的是http/3.0;
- http/1.0:短连接(处理一个请求就关闭)
- http/1.1:是较为主流,长连接(发送很多请求,全都响应并返回);
先看响应的首行:
HTTP/1.1 200 OK
// 其他的响应,比如:
HTTP/1.1 400 Bad Request
他们都符合这样的规则:[http版本] [状态码] [状态码描述]
使用telnet工具发送的请求:
GET / HTTP/1.1
请求方法(GET)、请求路径(/)、协议版本(http/1.1);
HTTP是如何解决数据粘包问题的?
报头和有效载荷的分离:对于请求与响应:通过空行进行分离;
如何做到读取一个完整的报文?
- 对于报头:一直读取,直到读取到空行;
- 对于有效载荷:Content--Length:XXX(正文长度)存在报头中
收到完整报文了,那序列化于发序列化呢?通常HTTP比较常用的协议是JSON、通过现已经由的序列化库进行序列化和发序列化;关于序列化和反序列化后续会进行介绍;
如果请求的 url 资源为 “ / ”,请求的就是默认首页(请求行中的url部分),在设计中可以新建一个文件夹来存放web资源,进行跳转;
上网行为:
- 获取资源
- 上传资源 ---把数据上传到服务器(登陆、注册、搜索等),比如:GET/POST结合html使用(html表单)
GET:用来获取资源,也可以用来传递参数;
POST:上传数据;
比如:
这两种方式有什么不同?
<input type="password">
<input type="text">
password类型,在输入文本后效果:
text类型:
这里只做一些简单的介绍;输入完数据点击提交后,就会将数据发送给指定的文件;
比如我使用get方法传递,那么他就会把参数拼接在URL,然后发送请求:
http://127.0.0.1:8888/dira/dirb/a.html?password=test&password=123456
服务端会收到这个请求,执行相应的处理接口;
使用post方法:把表单的数据添加到了报文的有效载荷中,然后发送请求;
- url传参字节个数有限制 POST方法没有限制
- GET方法私密性较差,POST方法好一些
GET和POST方法都不安全,想要安全,就需要加密解密;
常见的方法
- GET:用于获取资源。可以用来请求网页内容。
- POST:用于传输实体主体,常用于向服务器提交数据,如表单提交。
- PUT:用于传输文件,通常是上传文件到服务器或更新资源。
- HEAD:获取报文首部,不返回实体部分,适用于获取元数据。
- DELETE:用于删除文件或资源。
- OPTIONS:用于查询支持的方法,检测服务器能处理哪些请求。
- TRACE:用于追踪路由(不需要了解)。
- CONNECT:用于请求用隧道协议连接代理,收到请求不处理,把他交给别人处理。
LINK和UNLINK基本已经废弃
GET和POST;可在html的表单中使用;
状态码
最常见的状态码,比如:200(0K),404(Not Found),403(Forbidden),302(Redirect,重定向),504(Bad Gateway);
307:临时重定问,可以用来进行页面的跳转,让用户跳转到目标网页;
301:永久重定向,也就是说使用永久重定向,浏览器会记住这个URL,下次访问旧的URL直接跳转到重定向的URL;
比如:一个网站的域名发生变化(www.a.com->www.b.com),浏览器中搜索出现的还是www.a.com(已经废弃),现在已经更新了新的链接(www.b.com);想在访问www.a.com的同时能跳转到新的链接www.b.com;这时就可以使用301,让搜索引擎更新一下网址 下次爬取时就会到新的url进行爬取;
可以实现一个简易的服务端,负责接收请求,然后重定向到新的网址:
对原生socket接口进行封装:
#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>
#define Convert(addrptr) ((struct sockaddr *)addrptr)
namespace Net_Work
{
enum
{
SocketError = 1,
BindError,
ListenError
};
const static int defaultsockfd = -1;
const int backlog = 5;
// 封装一个基类,Socket接口类
class Socket
{
public:
virtual ~Socket() {}
virtual void CreateSocketOrDie() = 0;
virtual void BindSocketOrDie(uint16_t port) = 0;
virtual void ListenSocketOrDie(int backlog) = 0;
virtual Socket *AcceptConnection(std::string *peerip, uint16_t *peerport) = 0; // 返回文件描述符以及客户端的信息(参数为输出型参数)
virtual bool ConnectServer(std::string &serverip, uint16_t serverport) = 0;
virtual int GetSockFd() = 0;
virtual void SetSockFd(int sockfd) = 0;
virtual void CloseSockFd() = 0;
virtual bool Recv(std::string *buffer, int size) = 0;
virtual void Send(std::string &send_str) = 0;
public:
void BuildListenSocketMethod(uint16_t port, int backlog)
{
// bind监听(服务端) ip地址不需要设置为固定的ip,只需指定端口即可(传端口号)
CreateSocketOrDie();
BindSocketOrDie(port);
ListenSocketOrDie(backlog);
}
bool BuildConnectSocketMethod(std::string &serverip, uint16_t serverport)
{
// 客户端连接时需要服务端的ip地址和端口号
CreateSocketOrDie();
return ConnectServer(serverip, serverport);
}
void BuildNormalSocketMethod(int sockfd)
{
SetSockFd(sockfd);
}
};
class TcpSocket : public Socket
{
public:
TcpSocket(int sockfd = defaultsockfd)
: _sockfd(sockfd)
{
}
~TcpSocket()
{
}
// override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
void CreateSocketOrDie() override
{
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0); // 创建套接字
if (_sockfd < 0)
exit(SocketError);
}
void BindSocketOrDie(uint16_t port) override
{
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;
local.sin_port = htons(port);
int n = ::bind(_sockfd, Convert(&local), sizeof(local));
if (n < 0)
exit(BindError);
}
void ListenSocketOrDie(int backlog) override
{
// backlog:内核允许在等待连接队列中排队的最大连接数
int n = ::listen(_sockfd, backlog);
if (n < 0)
exit(ListenError);
}
Socket *AcceptConnection(std::string *peerip, uint16_t *peerport) override
// 返回文件描述符以及客户端的信息(参数为输出型参数)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newsockfd = accept(_sockfd, Convert(&peer), &len);
if (newsockfd < 0)
return nullptr;
*peerport = ntohs(peer.sin_port);
*peerip = inet_ntoa(peer.sin_addr);
Socket *s = new TcpSocket(newsockfd);
return s;
}
bool ConnectServer(std::string &serverip, uint16_t serverport) override
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(serverip.c_str());
server.sin_port = htons(serverport);
socklen_t len = sizeof(server);
int n = ::connect(_sockfd, Convert(&server), len);
// std::cout << errno << strerror(errno) << std::endl;
if (n == 0)
return true;
else
return false;
}
int GetSockFd() override
{
return _sockfd;
}
void SetSockFd(int sockfd) override
{
_sockfd = sockfd;
}
void CloseSockFd() override
{
if (_sockfd > defaultsockfd)
close(_sockfd);
}
bool Recv(std::string *buffer, int size) override
{
char inbuffer[size];
ssize_t n = recv(_sockfd, inbuffer, size-1, 0);
if(n > 0)
{
inbuffer[n] = 0;
*buffer += inbuffer;//这里读取时是+=不会覆盖原有剩余的报文
return true;
}
//else if(n < 0 || n == 0) return false;
return false;
}
void Send(std::string &send_str) override
{
send(_sockfd, send_str.c_str(), send_str.size(), 0);
}
private:
int _sockfd;
};
}
实现一个简单的TCPServer,接收到请求后直接返回重定向的响应:
#pragma once
#include "Socket.hpp"
#include <pthread.h>
#include <iostream>
#include <functional>
using func_t = std::function<std::string(std::string &request)>;
class TcpServer;
class ThreadData
{
public:
ThreadData(TcpServer *tcp_this, Net_Work::Socket *sockp)
: _this(tcp_this), _sockp(sockp)
{
}
public:
TcpServer *_this;
Net_Work::Socket *_sockp;
};
class TcpServer
{
public:
TcpServer(uint16_t port)
: _port(port), _listensocket(new Net_Work::TcpSocket()) //, _handle_request(handel_request)
{
// 创建套接字,bind、监听
_listensocket->BuildListenSocketMethod(_port, Net_Work::backlog);
}
// 创建线程去执行任务
static void *ThreadRun(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
std::string http_request;
while (true)
{
// 读取数据--不关心数据是什么,只读取,对数据进行处理
//接收成功后进行处理
if (td->_sockp->Recv(&http_request, 1024)) // 一次读取1024自己(可自主设置)
{
// 报文处理
// std::string http_response = td->_this->_handle_request(http_request); // 回调(将接收的数据进行处理
std::string http_response = "HTTP/1.1 307 Temporary Redirect\r\n"
"Location: https://www.qq.com/\r\n"
"\r\n";
// std::string http_response = "HTTP/1.1 301 Moved Permanently\r\n"
// "Location: https://www.baidu.com/\r\n"
// "\r\n";
if (!http_response.empty())
{
// 发送
td->_sockp->Send(http_response);
}
}
}
td->_sockp->CloseSockFd();
delete td->_sockp;
delete td;
return nullptr;
}
void Loop()
{
while (true)
{
std::string peerip;
uint16_t peerport;
// 连接
Net_Work::Socket *newsock = _listensocket->AcceptConnection(&peerip, &peerport); // 返回新的fd
if (newsock == nullptr)
continue;
std::cout << "获取一个新连接, sockfd: " << newsock->GetSockFd() << " client info: " << peerip << ": " << peerport << std::endl;
pthread_t tid;
ThreadData *td = new ThreadData(this, newsock);
// 创建新线程去执行,把新的fd交给新的线程去执行
pthread_create(&tid, nullptr, ThreadRun, td);
}
}
~TcpServer()
{
delete _listensocket;
}
private:
int _port;
Net_Work::Socket *_listensocket;
// public:
// func_t _handle_request; // 初始化时填入,请求服务要做的任务
};
main函数启动:
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cout << "Usage : " << argv[0] << " port" << std::endl;
return 0;
}
uint16_t localport = std::stoi(argv[1]);
// std::unique_ptr<TcpServer> svr(new TcpServer(localport, HandlerHttpRequest));
std::unique_ptr<TcpServer> svr(new TcpServer(localport));
// 执行服务
svr->Loop();
}
原理:
浏览器识别状态码为307,然后又识别到Location字段,他就知道要跳转的网页是是什么,从报头中提取内容跳转到qq.com;
cookie
HTTP是无状态的;什么意思?HTTP 协议对事务处理没有记忆能力,服务器不会在不同请求之间记住客户端的相关信息或状态。每个请求都是独立的,服务器无法自动识别多个请求是否来自同一个客户端以及它们之间的关联;通俗点讲:第二次访问同一个网址时,服务端依然是按照第一次访问时的状态进行响应,常见的情况:网络比较卡,然后一直点刷新,每次刷新就会有一个http请求,服务端会一个一个的对请求进行响应;
但是你会发现一个问题:比如你在网页上登录哔哩哔哩,然后你关闭浏览器,下次再次使用浏览器访问,账号是登录状态;它直接就认识你都账号,为什么?不是无状态吗?
服务端响应时是没有记忆的,但是客户端可以,这一切都是浏览器帮我们记录存储的;
首次访问时,会需要会显示需要登录,在登录页面输入账号密码,发送给服务端进行认证,认证通过后,服务端就会发送响应,响应中有这两个字段:
- Set-Cookie username;
- Set-Cookie password;
浏览器拿到后发现server响应的http报头有Cookie字段,就会对Cookie字段进行保存;下次进行请求时,浏览器会自动将保存的Cookie字段添加到http请求报头中;
Cookie分两种:
- 文件级:浏览器安装时会有安装目录,它会把cookie添加到安装目录的某个文件中
- 内存级:在堆区申请一块空间进行存储 把数据保存到浏览器进程的上下文中
如何分辨时文件级还是内存级?
打开浏览器登陆一次,把浏览器关了(进程退出),再次访问如果需要重新登录,那就是内存级;如果再次登录还是可以识别那就是文件级;
sessionid
比如:你开了一个某视频会员,你的室友想要看电影,这时你可以把你的某视频的cookie信息给他,如果网址预防性不好,允许行同时多人在线,那么他就可以直接用你的账户进行观看;
比如:日常使用电脑时,电脑中了病毒,那病毒主要干什么呢?黑客主要就是搜集你浏览器中所有的cookie文件; 有了cookie数据,他就可以以你的身份进行访问;如果cookie中保存的是敏感的信息(用户名,密码)这就问题更大了账号极有可能会被盗;
有什么解决办法吗?
有的,cookie不直接存储账号密码不就行了;
把用户信息不保存在client端,而是保存在server端,以后访问时认证使用sessionid即可;
那还是存在问题,用户敏感数据没了,但是黑客依然可以通过cookie,以你的身份访问资源,但是这种方式可以减小账号被盗的风险;当然也会有检测方式,判断是否异常登录:
比如:前一秒账户的ip显示在北京,下一秒就到了缅北;那么server端就可以让cookie失效;所以为什么某些程序需要定位权限,他的sessionid很可能与ip地址相绑定 一旦发生很大的变化,就可以让认证失效同时也可以设置cookie的有效时间,来进一步的限制;
token
sessioid在单个服务端使用比较好,如果是分布式系统呢?服务端的主机有很多个,比如你这次访问的和下次访问的可能不是同一台服务器,那sessionid不久不行了吗?如果想要在多台服务器中都可以使用,那就必须让每台服务器都存储sessionid;
这样很浪费空间,有什么办法可以解决吗?有的那就是token;
token由三部分组成:
使用原理:
比如:使用非对称加密方式,验证用户信息的服务器持有一个私钥,其他服务器持有一个公钥;
浏览器发送请求给服务端:
- 服务端将收到的消息(一般是用户信息,账号密码)进行验证,然后生成一个hash值;
- 同时服务端会使用自己的私钥对数据的hash值进行签名;
- 将身份信息,以及签名组成token返回给浏览器;
浏览器接收到token会将token进行保存,下次访问时会带上token,
验证方接收到Token后,首先提取出其中的哈希值和签名。验证方使用相同的哈希算法计算出原始用户信息的哈希值。然后,验证方使用公钥对Token中的签名进行解密,得到一个哈希值。最后,将解密得到的哈希值与自己计算的哈希值进行比较,以确认两者是否相同,从而验证Token的有效性和数据的完整性。
这样服务端就无需存储用户的会话状态了,只需验证token即可;
如果安全要求高的场景可结合hash算法,将消息及token一起hash运算得到哈希值,验证方收到后验证消息是否被篡改;
关于cookie和sessionid,token的关系:cookie中可以存储sessionid以及token;
总结
以上便是本文的全部内容,希望对你有所帮助,感谢阅读!