目录
[编写 HTTP 请求的代码 - 验证 http 请求](#编写 HTTP 请求的代码 - 验证 http 请求)
学习任何协议,无非是在学习该协议如何处理以下问题:
Request内部成员属性有什么?Response的应答是什么?协议是如何做序列和反序列化的?如何解决数据包粘包问题?
虽然我们说, 应用层协议是我们程序猿自己定的. 但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用. HTTP(超文本传输协议)就是其中之一。
在互联网世界中,HTTP(HyperText Transfer Protocol,超文本传输协议)是一个至关重要的协议。它定义了客户端(如浏览器)与服务器之间如何通信,以交换或传输超文本(如 HTML 文档)。
HTTP 协议是客户端与服务器之间通信的基础。客户端通过 HTTP 协议向服务器发送请求,服务器收到请求后处理并返回响应。HTTP 协议是一个无连接、无状态的协议,即每次请求都需要建立新的连接,且服务器不会保存客户端的状态信息。
认识URL
URL:统一资源定位器,用于标识互联网的唯一的一个文件
平时我们俗称的 " 网址 " 其实就是说的 URL

urlencode和urldecode
像 / ? : 等这样的字符, 已经被 url 当做特殊意义理解了. 因此这些字符不能随意出现.
比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义. 转义的规则如下:
将需要转码的字符转为 16 进制,然后从右到左,取 4 位(不足 4 位直接处理),每 2 位做一位,前面加上%,编码成%XY 格式
例如:

+" 被转义成了 "%2B"
urldecode 就是 urlencode 的逆过程;
HTTP协议请求与响应格式
HTTP请求

• 首行: [方法] + [url] + [版本]
• Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\r\n 分隔;遇到空行表示 Header 部分结束
• Body: 空行后面的内容都是 Body. Body 允许为空字符串. 如果 Body 存在, 则在Header 中会有一个 Content-Length 属性来标识 Body 的长度;

关于HTTP解决粘包问题

编写HTTP请求的代码**-验证http****请求**
Tcpserver.hpp
cpp
#pragma once
#include "Socket.hpp"
#include "InetAddr.hpp"
#include <memory>
#include <unistd.h>
#include <signal.h>
#include <functional>
// function<返回值(参数)>
using callback_t = std::function<std::string(std::string &)>; // 输入输出型参数
// TcpServer:只负责进行网络通信(IO)
class TcpServer
{
public:
TcpServer(int port, callback_t cb)
: _port(port),
_listensocket(std::make_unique<TcpSocket>()),
_cb(cb)
{
_listensocket->BuildListenSocketMethod(_port);
}
void HandlerRequest(std::shared_ptr<Socket> sockfd, InetAddr addr)
{
// 短服务
std::string inbuffer;
ssize_t n = sockfd->Recv(&inbuffer);
if (n > 0)
{
std::string send_str = _cb(inbuffer);
sockfd->Send(send_str);
}
else if (n == 0)
{
LOG(LoggerLevel::DEBUG) << addr.ToString() << "quit,me too!";
}
else
{
LOG(LoggerLevel::DEBUG) << addr.ToString() << "read error,quit!";
}
sockfd->Close();
}
void Run()
{
signal(SIGCHLD, SIG_IGN);
while (true)
{
InetAddr addr;
auto sockfd = _listensocket->Accept(&addr);
if (sockfd == nullptr)
continue;
LOG(LoggerLevel::DEBUG) << "获取一个新连接:" << addr.ToString() << ",sockfd : " << sockfd->SockFd();
if (fork() == 0)
{
// child
_listensocket->Close();
HandlerRequest(sockfd, addr);
// 子进程退出!否则子进程进入下一个循环,但是_listensocket已经关了,就会报错了
exit(0);
}
// father
sockfd->Close();
}
}
~TcpServer()
{
}
private:
int _port;
// 在构造的时候选择子类
std::unique_ptr<Socket> _listensocket;
callback_t _cb;
};
Socket.hpp
cpp
#ifndef __SOCKET_HPP__
#define __SOCKET_HPP__
#include<iostream>
#include<string>
#include<unistd.h>
#include<cstdio>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<memory>
#include"logger.hpp"
#include"InetAddr.hpp"
enum
{
OK,
CREATE_ERR,
BIND_ERR,
LISTEN_ERR,
ACCEPT_ERR,
};
static int gbacklog =16;
static const int gsockfd=-1;
//设计模式:模块方法模式
//基类定义骨架流程
//子类实现具体步骤
class Socket
{
public:
//Socket *sock=new TcpSocket();
//基类虚构设置为虚函数,目的是保证子类资源能析构,然后再析构基类资源,如果不是虚函数子类析构会被跳过
virtual ~Socket(){}
//定义子类必须实现的抽象接口,do what?(相当于是函数声明,函数定义在子类完成)
//virtual ...=0纯虚函数
//目的:
//1:强制子类实现这个函数(如果该类不需要这个函数就做个空函数)否则编译报错;
//2.使基类变成抽象类,基类不能创建对象,只能通过子类创建
virtual void CreateSocketOrDie()=0;
virtual void BindSocketOrDie(int port)=0;
virtual void ListenSocketOrDie(int backlog)=0;
virtual std::shared_ptr<Socket> Accept(InetAddr *clientaddr)=0;
virtual int SockFd()=0;
virtual void Close()=0;
virtual ssize_t Recv(std::string *out)=0;
virtual ssize_t Send(const std::string &in)=0;
virtual bool Connect(InetAddr &peer)=0;
//other...
public:
//基于抽象接口构建的通用逻辑,How to do?
//方便复用通用流程
void BuildListenSocketMethod(int _port)
{
CreateSocketOrDie();
BindSocketOrDie(_port);
ListenSocketOrDie(gbacklog);
}
void BuildClientSocketMethod()
{
CreateSocketOrDie();
}
// void BuildUdpSocketMethod()
// {
// CreateSocketOrDie();
// BindSocketOrDie();
// }
// void BuildTcpSocketMethod()
// {
// CreateSocketOrDie();
// BindSocketOrDie();
// ListenSocketOrDie();
// }
};
class TcpSocket:public Socket
{
public:
TcpSocket()
:_sockfd(gsockfd)
{
}
TcpSocket(int sockfd):_sockfd(sockfd)
{
}
void CreateSocketOrDie() override
{
_sockfd=socket(AF_INET,SOCK_STREAM,0);
if(_sockfd<0)
{
LOG(LoggerLevel::FATAL)<<"create socker error!";
exit(CREATE_ERR);
}
LOG(LoggerLevel::INFO)<<"create socket success!!!";
}
void BindSocketOrDie(int port) override
{
InetAddr local(port);
if(bind(_sockfd,local.Addr(),local.Length())!=0)
{
LOG(LoggerLevel::FATAL)<<"bind socker error!";
exit(BIND_ERR);
}
LOG(LoggerLevel::INFO)<<"bind socket success!!!";
}
void ListenSocketOrDie(int backlog) override
{
if(listen(_sockfd,backlog)!=0)
{
LOG(LoggerLevel::FATAL)<<"listen socker error!";
exit(LISTEN_ERR);
}
LOG(LoggerLevel::INFO)<<"listen socket success!!!";
}
std::shared_ptr<Socket> Accept(InetAddr *clientaddr) override
{
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int fd=accept(_sockfd,(struct sockaddr*)&peer,&len);
if(fd<0)
{
LOG(LoggerLevel::WARNING)<<"accept socker error!";
return nullptr;
}
LOG(LoggerLevel::INFO)<<"accept socket success!!!";
clientaddr->Init(peer);
return std::make_shared<TcpSocket>(fd);
}
int SockFd() override
{
return _sockfd;
}
void Close() override
{
if(_sockfd>=0)
close(_sockfd);
}
ssize_t Recv(std::string *out) override
{
//只读一次
char buffer[1024];
ssize_t n=recv(_sockfd,buffer,sizeof(buffer)-1,0);
if(n>0)
{
buffer[n]=0;
*out+=buffer;
}
return n;
}
ssize_t Send(const std::string &in) override
{
return send(_sockfd,in.c_str(),in.size(),0);
}
bool Connect(InetAddr &peer) override
{
int n=connect(_sockfd,peer.Addr(),peer.Length());
if(n>=0)return true;
return false;
}
~TcpSocket()
{
}
private:
int _sockfd;
};
// class UdpSocket:public Socket
// {
// };
#endif
Mutex.hpp
cpp
#pragma once
#include <iostream>
#include <mutex>
#include <pthread.h>
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_lock, nullptr);
}
void Lock()
{
pthread_mutex_lock(&_lock);
}
pthread_mutex_t *Get()
{
return &_lock;
}
void Unlock()
{
pthread_mutex_unlock(&_lock);
}
~Mutex()
{
pthread_mutex_destroy(&_lock);
}
private:
pthread_mutex_t _lock;
};
class LockGuard
{
public:
LockGuard(Mutex *_mutex) : _mutexp(_mutex)
{
_mutex->Lock();
}
~LockGuard()
{
_mutexp->Unlock();
}
private:
Mutex *_mutexp;
};
logger.hpp
cpp
#pragma once
#include <iostream>
#include <filesystem>
#include <fstream>
#include <string>
#include <sstream>
#include <memory>
#include <unistd.h>
#include "Mutex.hpp"
enum class LoggerLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
std::string LoggerLevelToString(LoggerLevel level)
{
switch (level)
{
case LoggerLevel::DEBUG:
return "Debug";
case LoggerLevel::INFO:
return "Info";
case LoggerLevel::WARNING:
return "Warning";
case LoggerLevel::ERROR:
return "Error";
case LoggerLevel::FATAL:
return "Fatal";
default:
return "Unknown";
}
}
std::string GetCurrentTime()
{
// 获取时间戳
time_t timep = time(nullptr);
// 把时间戳转化为时间格式
struct tm currtm;
localtime_r(&timep, &currtm);
// 转化为字符串
char buffer[64];
snprintf(buffer, sizeof(buffer), "%4d-%02d-%02d %02d-%02d-%02d",
currtm.tm_year + 1900, currtm.tm_mon + 1, currtm.tm_mday,
currtm.tm_hour, currtm.tm_min, currtm.tm_sec);
return buffer;
}
class LogStrategy
{
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &logmessage) = 0;
};
// 显示器刷新
class ConsoleLogStrategy : public LogStrategy
{
public:
~ConsoleLogStrategy()
{
}
virtual void SyncLog(const std::string &logmessage) override
{
{
LockGuard lockguard(&_lock);
std::cout << logmessage << std::endl;
}
}
private:
Mutex _lock;
};
const std::string default_dir_path_name = "/var/log/";
const std::string default_filename = "test.log";
// 文件刷新
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string dir_path_name = default_dir_path_name,
const std::string filename = default_filename)
: _dir_path_name(dir_path_name), _filename(filename)
{
if (std::filesystem::exists(_dir_path_name))
{
return;
}
try
{
std::filesystem::create_directories(_dir_path_name);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << "\r\n";
}
}
~FileLogStrategy()
{
}
virtual void SyncLog(const std::string &logmessage) override
{
{
LockGuard lock(&_lock);
std::string target = _dir_path_name;
target += '/';
target += _filename;
std::ofstream out(target.c_str(), std::ios::app);
if (!out.is_open())
{
return;
}
out << logmessage << "\n";
out.close();
}
}
private:
std::string _dir_path_name;
std::string _filename;
Mutex _lock;
};
class Logger
{
public:
Logger()
{
}
void EnableConsoleStrategy()
{
_strategy = std::make_unique<ConsoleLogStrategy>();
}
void EnableFileStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
class LogMessage
{
public:
LogMessage(LoggerLevel level, std::string filename, int line, Logger& logger)
: _curr_time(GetCurrentTime()), _level(level), _pid(getpid())
, _filename(filename), _line(line), _logger(logger)
{
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << LoggerLevelToString(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _filename << "] "
<< "[" << _line << "]"
<< " - ";
_loginfo = ss.str();
}
template <typename T>
LogMessage &operator<<(const T &info)
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
~LogMessage()
{
if (_logger._strategy)
{
_logger._strategy->SyncLog(_loginfo);
}
}
private:
std::string _curr_time; // 时间戳
LoggerLevel _level; // 日志等级
pid_t _pid; // 进程pid
std::string _filename; // 文件名
int _line; // 行号
std::string _loginfo; // 一条合并完成的,完整的日志信息
Logger &_logger; // 提供刷新策略的具体做法
};
LogMessage operator()(LoggerLevel level, std::string filename, int line)
{
return LogMessage(level, filename, line, *this);
}
~Logger()
{
}
private:
std::unique_ptr<LogStrategy> _strategy;
};
Logger logger;
#define LOG(level) logger(level, __FILE__, __LINE__)
#define EnableConsoleStrategy() logger.EnableConsoleStrategy()
#define EnableFileStrategy() logger.EnableFileStrategy()
InetAddr.hpp
cpp
#pragma once
// 该类 用于描述客户端套接字信息
// 方便后续用来管理客户端
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define Conv(addr) ((struct sockaddr*)&addr)
class InetAddr
{
private:
void Net2Host()
{
_port = ntohs(_addr.sin_port);
// _ip = inet_ntoa(_addr.sin_addr);
char ipbuffer[64];
inet_ntop(AF_INET,&(_addr.sin_addr),ipbuffer,strlen(ipbuffer));
_ip=ipbuffer;
}
void Host2Net()
{
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_port = htons(_port);
// _addr.sin_addr.s_addr = inet_addr(_ip.c_str());
inet_pton(AF_INET,_ip.c_str(),&(_addr.sin_addr));
}
public:
InetAddr()
{
}
// 网络风格地址构造
InetAddr(const struct sockaddr_in &addr)
: _addr(addr)
{
Net2Host();
}
// 主机地址风格构造
InetAddr(uint16_t port, const std::string &ip = "0.0.0.0")
: _port(port), _ip(ip)
{
Host2Net();
}
void Init(const struct sockaddr_in &addr)
{
_addr=addr;
Net2Host();
}
std::string Ip()
{
return _ip;
}
uint16_t Port()
{
return _port;
}
struct sockaddr* Addr()
{
return Conv(_addr);
}
socklen_t Length()
{
return sizeof(_addr);
}
std::string ToString()
{
return _ip+"-"+std::to_string(_port);
}
bool operator==(const InetAddr&addr)
{
return _ip==addr._ip&&_port==addr._port;
//return _ip==addr._ip;
}
~InetAddr()
{
}
private:
struct sockaddr_in _addr; // 网络风格地址
// 主机风格地址
std::string _ip;
uint16_t _port;
};
Main.cc
cpp
#include"TcpServer.hpp"
#include<memory>
void Usage(std::string proc)
{
std::cerr << "Usage:" << proc << " localport" << std::endl;
}
std::string TestHttp(std::string&requeststr)
{
std::cout<<"###################################"<<std::endl;
std::cout<<requeststr<<std::endl;
std::cout<<"###################################"<<std::endl;
std::string response="HTTP/1.1 200 OK\r\n\r\n";
response+="<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n <meta charset=\"UTF-8\">\r\n <title>Hello World</title>\r\n</head>\r\n<body>\r\n <h1>Hello, World!</h1>\r\n</body>\r\n</html>";
return response;
}
int main(int argc,char* argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(0);
}
EnableConsoleStrategy();
//网络通信模块
uint16_t serverport=std::stoi(argv[1]);
std::unique_ptr<TcpServer> tsock=std::make_unique<TcpServer>(serverport,TestHttp);
tsock->Run();
return 0;
}
Makefile
cpp
httpserver:Main.cc
g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
rm -rf httpserver
结果演示

输入公网ip和端口号



HTTP响应

• 首行: [版本号] + [状态码] + [状态码解释]
• Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\r\n 分隔;遇到空行表示 Header 部分结束。
• Body: 空行后面的内容都是 Body. Body 允许为空字符串. 如果 Body 存在, 则在Header 中会有一个 Content-Length 属性来标识 Body 的长度; 如果服务器返回了一 个 html 页面, 那么 html 页面内容就是在 body 中.

状态码:比如404
状态码描述(给人看的):比如NotFound
或者下图的 200 OK
注:http请求,必须要有应答报文,即便是错误。
基本的应答格式



基于两张图设计http协议

HTTP的方法

最常用的就是GET和POST,因为上网的目的除了获取信息就是上传信息呗。
GET
用途:用于请求 URL 指定的资源。
示例:GET /index.html HTTP/1.1
特性:指定资源经服务器端解析后返回响应内容。
POST
用途:用于传输实体的主体,通常用于提交表单数据。
示例:POST /submit.cgi HTTP/1.1
特性:可以发送大量的数据给服务器,并且数据包含在请求体中。
GET方法和POST方法都可以带参:
------GET方法是通过url传参的。
------POST方法是通过正文传参的。

从GET方法和POST方法的传参形式可以看出,POST方法能传递更多的参数,因为url的长度是有限制的,POST方法通过正文传参就可以携带更多的数据。
这两种方法都是明文传参,都不安全。
此外,使用POST方法传参更加私密,因为POST方法不会将你的参数回显到url当中,此时也就不会被别人轻易看到。不能说POST方法比GET方法更安全,因为POST方法和GET方法实际都不安全,要做到安全只能通过加密来完成。
HTTP状态码
http状态码应该是和相应的状态描述匹配的

最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定
向), 504(Bad Gateway)


关于重定向

永久重定向和临时重定向的区别就是,会不会改变客户对地址的认知,对于永久重定向,会直接改变客户访问资源的地址,而临时重定向不会改变访问资源的地址,每次客户访问资源地址固定,只是这次临时重定向导致用户访问资源地址的时候跳转到其他地址。

HTTP状态码301**(永久重定向)和302(临时重定向)都依赖Location选项**。以下
是关于两者依赖 Location 选项的详细说明:
HTTP状态码301**(永久重定向)**:
• 当服务器返回 HTTP 301 状态码时,表示请求的资源已经被永久移动到新的位置。
• 在这种情况下,服务器会在响应中添加一个 Location 头部,用于指定资源的新位置。这个 Location 头部包含了新的 URL 地址,浏览器会自动重定向到该地址。
• 例如,在 HTTP 响应中,可能会看到类似于以下的头部信息:

HTTP状态码302**(临时重定向)**:
• 当服务器返回 HTTP 302 状态码时,表示请求的资源临时被移动到新的位置。
• 同样地,服务器也会在响应中添加一个 Location 头部来指定资源的新位置。浏览器会暂时使用新的 URL 进行后续的请求,但不会缓存这个重定向。
• 例如,在 HTTP 响应中,可能会看到类似于以下的头部信息:

总结:无论是 HTTP 301 还是 HTTP 302 重定向,都需要依赖 Location 选项来指定资源的新位置。这个 Location 选项是一个标准的 HTTP 响应头部,用于告诉浏览器应该将请求重定向到哪个新的 URL 地址。
HTTP常见Header
• Content-Type: 数据类型(text/html 等)
• Content-Length: Body 的长度
• Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
• User-Agent: 声明用户的操作系统和浏览器版本信息;
• referer: 当前页面是从哪个页面跳转过来的;
• Location: 搭配 3xx 状态码使用, 告诉客户端接下来要去哪里访问;
• Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;
关于connection报头
HTTP 中的 Connection 字段是 HTTP 报文头的一部分,它主要用于控制和管理客户端与服务器之间的连接状态
关于HTTPS协议
早期很多公司刚起步的时候,使用的应用层协议都是HTTP,而HTTP无论是用GET方法还是POST方法传参,都是没有经过任何加密的,因此早期很多的信息都是可以通过抓包工具抓到的。
为了解决这个问题,于是出现了HTTPS协议,HTTPS实际就是在应用层和传输层协议之间加了一层加密层(SSL&TLS),这层加密层本身也是属于应用层的,它会对用户的个人信息进行各种程度的加密。HTTPS在交付数据时先把数据交给加密层,由加密层对数据加密后再交给传输层。
当然,通信双方使用的应用层协议必须是一样的,因此对端的应用层也必须使用HTTPS,当对端的传输层收到数据后,会先将数据交给加密层,由加密层对数据进行解密后再将数据交给应用层。

此时数据只有在用户层(应用层)是没有被加密的,而在应用层往下以及网络当中都是加密的,这就叫做HTTPS。
Cookie和Session
Cookie
HTTP实际上是一种无状态协议,HTTP的每次请求/响应之间是没有任何关系的,但你在使用浏览器的时候发现并不是这样的。
比如当你登录一次CSDN后,就算你把CSDN网站关了甚至是重启电脑,当你再次打开CSDN网站时,CSDN并没有要求你再次输入账号和密码,这实际上是通过cookie技术实现的,点击浏览器当中锁的标志就可以看到对应网站的各种cookie数据。

这些cookie数据实际都是对应的服务器方写的,如果你将对应的某些cookie删除,那么此时可能就需要你重新进行登录认证了,因为你删除的可能正好就是你登录时所设置的cookie信息。
cookie是什么
因为HTTP是一种无状态协议,如果没有cookie的存在,那么每当我们要进行页面请求时都需要重新输入账号和密码进行认证,这样太麻烦了。
比如你是某个视频网站的VIP,这个网站里面的VIP视频有成百上千个,你每次点击一个视频都要重新进行VIP身份认证。而HTTP不支持记录用户状态,那么我们就需要有一种独立技术来帮我们支持,这种技术目前现在已经内置到HTTP协议当中了,叫做cookie。
当我们第一次登录某个网站时,需要输入我们的账号和密码进行身份认证,此时如果服务器经过数据比对后判定你是一个合法的用户,那么为了让你后续在进行某些网页请求时不用重新输入账号和密码,此时服务器就会进行Set-Cookie的设置。(Set-Cookie也是HTTP报头当中的一种属性信息)
当认证通过并在服务端进行Set-Cookie设置后,服务器在对浏览器进行HTTP响应时就会将这个Set-Cookie响应给浏览器。而浏览器收到响应后会自动提取出Set-Cookie的值,将其保存在浏览器的cookie文件当中,此时就相当于我的账号和密码信息保存在本地浏览器的cookie文件当中。

从第一次登录认证之后,浏览器再向该网站发起的HTTP请求当中就会自动包含一个cookie字段,其中携带的就是我第一次的认证信息,此后对端服务器需要对你进行认证时就会直接提取出HTTP请求当中的cookie字段,而不会重新让你输入账号和密码了。
也就是在第一次认证登录后,后续所有的认证都变成了自动认证,这就叫做cookie技术。

内存级别&&文件级别 的 Cookie
cookie就是在浏览器当中的一个小文件,文件里记录的就是用户的私有信息。cookie文件可以分为两种,一种是内存级别的cookie文件,另一种是文件级别的cookie文件。
------将浏览器关掉后再打开,访问之前登录过的网站,如果需要你重新输入账号和密码,说明你之前登录时浏览器当中保存的cookie信息是内存级别的。
------将浏览器关掉甚至将电脑重启再打开,访问之前登录过的网站,如果不需要你重新输入账户和密码,说明你之前登录时浏览器当中保存的cookie信息是文件级别的。
Session
HTTP Session是服务器⽤来跟踪⽤⼾与服务器交互期间⽤⼾状态的机制。由于HTTP协议是⽆状态的(每个请求都是独⽴的),因此服务器需要通过Session来记住⽤⼾的信息。
Session ID
当⽤⼾⾸次访问⽹站时,服务器会为⽤⼾创建⼀个唯⼀的 Session ID ,并通过 Cookie 将其发送到客⼾端。
客⼾端在之后的请求中会携带这个 Session ID ,服务器通过 Session ID 来识别⽤⼾:服务器识别到HTTP请求当中包含了SessionID,就会提取出这个SessionID,然后再到对应的集合当中进行对比,对比成功就说明这个用户是曾经登录过的,从⽽获取⽤⼾的会话信息,此时也就自动就认证成功了,然后就会正常处理你发来的请求,这就是我们当前主流的工作方式。
服务器通常会将 Session 信息存储在内存、数据库或缓存中。

- 在引入SessionID之前,用户登录的账号信息都是保存在浏览器内部的,此时的账号信息是由客户端去维护的。
- 而引入SessionID后,用户登录的账号信息是有服务器去维护的,在浏览器内部保存的只是SessionID。
此时虽然SessionID也可能被非法用户盗取,但服务器也可以使用各种各样的策略来保证用户账号的安全。
1.IP是有归类的,可以通过IP地址来判断登录用户所在的地址范围。如果一个账号在短时间内登录地址发送了巨大变化,此时服务器就会立马识别到这个账号发生异常了,进而在服务器当中清除对应的SessionID的值。这时当你或那个非法用户想要访问服务器时,就都需要重新输入账号和密码进行身份认证,而只有你是知道自己的密码的,当你重新认证登录后服务器就可以将另一方识别为非法用户,进而对该非法用户进行对应的黑名单/白名单认证。
2.当操作者想要进行某些高权限的操作时,会要求操作者再次输入账号和密码信息,再次确认身份。就算你的账号被非法用户盗取了,但非法用户在改你密码时需要输入旧密码,这是非法用户在短时间内无法做到的,因为它并不知道你的密码。这也就是为什么账号被盗后还可以找回来的原因,因为非法用户无法在短时间内修改你的账号密码,此时你就可以通过追回的方式让当前的SessionID失效,让使用该账号的用户进行重新登录认证。
3.SessionID也有过期策略,比如SessionID是一个小时内是有效的。所以即便你的SessionID被非法用户盗取了,也仅仅是在一个小时内有效,而且在功能上受约束,所以不会造成太大的影响。
此篇完。
2025.11.09课设前夕