一 HTTP前置知识
这篇博客会有点长,但对我来说非常有意义,这是我从一无所知到理解网络的重大突破,在前两个月我对网络非常恐惧,还十分不理解什么是网络,什么是协议。接下来先介绍几个概念。
1 流量
我们把数据给别人,用上行流量,拿到别人的数据,用下行流量。为什么流量还分上下行呢?你想,我们下载的视频资源都是在别人的服务器上的,这里显然是要和对方建立网络链接,我们还要发数据给别人,这里也要建立网络连接,所以如果我们作为网络的提供者,我们该如何衡量一个用户使用网络的情况来收取费用呢,那就是计算下载资源的大小与上传资源的大小,所以提出了下行流量和上行流量共同来描述用户的使用网络的情况,实际上我们刷视频,逛网站,看文章用的都是下行流量。
2 网址
现实生活中我们使用的网址(又叫url)由域名+协议构成,可是之前客户端访问服务端不是要服务端ip和端口吗,我们只有一个网址,哪里有拿到百度的ip和端口号,那怎么访问百度服务器呢,实际上我们使用的网址会被解析成对应的ip和端口,域名会被解析为对应的ip地址。
那端口号呢? 网址前面的https协议已经告诉了我们答案。http和https一样是一个成熟的应用层协议,这个协议在服务端有固定端口。
这个我一开始看这句话也是很头大,看不懂一点,首先什么是应用层协议我当时都不知道,更别说理解应用层协议和端口号绑定这句话了。如果你们看过我前面序列化反序列的博客中的代码,就会发现我们序列化反序列化的基础就是一个协议,这个协议就是应用层协议,证据就是序列化反序列化的时候我们已经拿到数据了开始用协议来解析报文了,那此时数据就应该已经经过了传输层,所以就只能是应用层协议了。
所以应用层协议就在我们自己写的代码上,一个软件分客户端和服务端代码的编写,所以协议存在于客户端和服务端代码上,两份代码在各自主机上跑起来就形成了两个进程,只有这两个进程能看懂这个协议对应的报文,只有两个吗,是的,没必要加多了,难道你还要产生多个服务端进程接收网络响应吗。
这两个进程都要绑定端口,此时服务端进程就不能随便调配,因为服务端每次变更端口都要通知客户端,会很麻烦,不通知客户端,客户端就还是拿着旧端口来通信,就把数据发给其它进程了,其它进程用的是不同的应用层协议,会解析错误,所以服务端端口往往是固定不变的,所以应用层协议也就和端口号绑定了。
剩下的/js...表示要访问资源的路径。/表示web根目录,不一定是linux根目录,后面实现我们完善响应的时候就知道了,我们我们会默认在访问路径前添加一个目录。
由此看出资源大致像某个文件的路径,所以服务端大概率是linux系统。?的右侧则是一些参数,后面我们会提到我们如何发起一个请求,并且带上参数。所以一个网址就可以定位网络中的唯一的进程,唯一的资源,故url被称为资源定位符。
网址中偶尔有些%7%7B这种奇奇怪怪的数字,这个主要是为了解决搜索时传的特殊字符,例如传了:,//这些分隔符,会干扰服务端解析网址,怎么解决?用url encode编码,将特殊字符转成某个数字,也就是转义化,再发送给服务端。
二 http宏观理解
首先http是个应用层协议,就一定有序列化和反序列化。
1 请求报文
一般分四行
第一行理解:请求方法:get/post, URL:这里面表示请求的资源,一般用目录,也就是说我们访问服务器的资源,是指定目录,由服务器去搜索目录然后把资源给你的,如果我们不写这个目录,这个目录一般会是'/',到了后面我们就知道这个'/'不会是根目录。
协议版本: 好像没什么用,作用后提,我们在响应报文中也能看到这个字段,请求报头内部每个字段是由kv结构构成的,内部字段后提。然后就是一个空行。
空行之后就是有效载荷,这个内部是用户提交的参数,我们后面写完服务器会在Post请求中演示用户如何提交参数。
从上述宏观结构来看,如何做序列化和反序列化,我们只要一行一行读取就可以将报头内容分离,读到空行时,我们就认为报头结束了,如何区分空行呢,我认为我们只要某一行字符数小于4,那这行肯定就是空行,那序列化就是把每一行合并成一个大字符串,可是反序列化的时候如何得知有效载荷长度呢?后面讲属性字段再提,在第五大点中的添加属性字段小节中会再提及。
2 响应报文
大致结构都是一样的,都分四行,因为大家都遵循一个http协议,这一点我在写完序列化反序列化代码和博客时颇有感悟,正是那份序列化反序列代码演示,让我大致理解应用层协议是什么。
第一行 协议版本这不就来了吗,状态码是什么呢? 例如我们之前访问某些网站出现的404,那个就是状态码。
状态码描述表示对状态码的解释。序列化和反序列同理,都是一行行累加和读取。
服务器由请求报文知道客户端协议版本,这样就可以根据版本来给客户端提供不同版本的功能,这就是双方报文为什么要带上协议版本的原因。可是突然学习这么多报文字段,谁记得住呢?记不住一点,接下来我们就要模拟一个服务端,来抓取请求报文和响应报文来看看。
三 代码实现
客户端不用写?浏览器就是客户端?我们只要给浏览器网址,它就能构建一个请求,这一点我一开始也是不理解,第二天就突然想通了,浏览器不就是个软件吗,你能用浏览器搜索出那么多视频,文章,难道这些都存在你手机上吗,当然不是,是从服务器发送过来的,谁让服务器发的,那不就是浏览器吗,后面第四点构建请求我们就能理解一个网址在构建请求中的角色。
那如何构建响应呢? 先复用先前写的err.hpp和套接字接口封装sock.hpp以及日志输出log.hpp,这三份代码在我前面写的博客都复用过,这就是封装的好处。
log.hpp
//错误等级
enum ErrorLevel
{Info = 1,Warning,Fatal,Debug
};
//接收输出的文件
enum PMethod
{Screen = 1,//输出到屏幕OneFile ,//输出到一个文件上ClassFile//分类输出到多个文件中
};class Log
{
public:Log(int method = Screen):printmethod(method){;}string leveltostring(int level){switch (level){case Info:return "Info";case Warning:return "Warning";case Fatal:return "Fatal";case Debug:return "Debug";default: return "None"; }}//日志信息//枚举常量版本void operator()(int level, const char *format, ...){char leftbuffer[SIZE];time_t t = time(NULL);struct tm * ltime = localtime(&t);//默认部分 事件等级和时间snprintf(leftbuffer,sizeof(leftbuffer),"%s [%d %d %d %d:%d]",leveltostring(level).c_str(),ltime->tm_year+1900,ltime->tm_mon+1,ltime->tm_mday,ltime->tm_hour,ltime->tm_min);//可变部分char rightbuffer[SIZE];va_list s;va_start(s,format);vsnprintf(rightbuffer,sizeof(rightbuffer),format,s);char Logbuffer[SIZE*2];snprintf(Logbuffer,sizeof(Logbuffer),"%s %s",leftbuffer,rightbuffer);LogPrint(level,Logbuffer);}void PrintOnefile( const char *filename,string& lbuffer){lbuffer+='\n';int fd = open(filename, O_CREAT|O_APPEND|O_WRONLY,0666);if(fd < 0)return;write(fd,lbuffer.c_str(),lbuffer.size()); }void PrintClassFile(int level,string& lbuffer){string filename = Logname;//将不同错误信息分流到对应的文件filename += ".";filename += leveltostring(level);PrintOnefile(filename.c_str(),lbuffer);}void LogPrint(int level,string lbuffer){switch(printmethod){case Screen://输出到屏幕cout<<lbuffer<<endl;break;case OneFile: //输出到一个文件上PrintOnefile(Logname,lbuffer);break;case ClassFile:PrintClassFile(level,lbuffer);break;}}
private:int printmethod;
};
套接字接口封装
class Sock
{
public:const int backlog = 12;void Socket(){socket_ = socket(AF_INET, SOCK_STREAM, 0);if (socket_ < 0){log_(ErrorLevel::Fatal, "create socket error:%d :%s ", errno, strerror(errno));exit(SOCKET_ERR);}}//输入const&//输出*//输入输出&void Bind(const uint16_t& port){// 绑定端口号和ip地址struct sockaddr_in sock;bzero(&sock, sizeof(sock));sock.sin_addr.s_addr = INADDR_ANY; //设置ip地址 表示所有的ip的地址sock.sin_port = htons(port);sock.sin_family = AF_INET;if (bind(socket_, (sockaddr *)(&sock), sizeof(sock)) < 0){log_(ErrorLevel::Fatal, "bind error:%d errstring:%s ", errno, strerror(errno));exit(BIND_ERR);}}void Listen(){// 开始监听if (listen(socket_, backlog)) // 返回0,监听成功{log_(ErrorLevel::Fatal, "listen err");exit(LISTEN_ERR);}}int Connect(const string &ip,const u_int16_t& port){struct sockaddr_in sock;sock.sin_addr.s_addr = inet_addr(ip.c_str());sock.sin_port = htons(port);sock.sin_family = AF_INET;int len = sizeof(sock);// 开始连接int timenum = 5;return connect(socket_, (sockaddr *)&sock, len);//由外部控制重连}int Accept(string* ip,uint16_t* port) // 名字不能为accept{// 获取链接struct sockaddr_in sock; // 头文件<netinet/in.h>bzero(&sock, sizeof(sock));socklen_t len = sizeof(sock);int socket = accept(socket_, (sockaddr *)&sock, &len);if (socket < 0){log_(ErrorLevel::Info, "accept err");exit(SOCKET_ERR);}else{*ip = inet_ntoa(sock.sin_addr);*port = ntohs(sock.sin_port);}return socket;}~Sock(){close(socket_);}int socket_;Log log_;
};
接下来我们要封装出一个HttpServer类来实现对请求的响应,这个类内提供两个接口,一个是初始化服务器,还是就是启动服务器。
./server + port server端要指定端口号,这一点在套接字编程中就反复强调了
//./server + port
int main(int argc, char *argv[])
{if (argc != 2){exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);std::unique_ptr<Httpserver> http(new Httpserver(HttpHead, port));http->Init(); HttpHead是一个请求处理函数,由内部读取到请求后调用http->start(); 后面再看实现return 0;
}
初始化服务端
启动服务端接收链接。
然后创建线程去处理请求。
可是我们应该给threadRoutine函数传什么参数呢,那就肯定要刚刚Accept返回的套接字信息以及客户端ip端口号了,方便我们查看是哪个客户端发来的请求,this指针后面有用,具体看内部实现就明白了。
ThreadData类,因为线程执行函数只有一个参数void*,所以要想传多个类型的数据,只能封装成类,这是线程的知识,linux的知识真是一环扣一环,少了一点都不好往下推进。
开始读取,我们认为一次就可以读到一个请求数据报,懒得保证读完一个完整请求了,因为我们后面测试的时候是一个一个请求的发送,不会出现多个报文粘合的情况。
static void *threadRoutine(void *arg){ThreadData *td = static_cast<ThreadData *>(arg);td->hs_->HandlerRequest(td->socket_);td->hs_->log_(ErrorLevel::Debug, "Quit...");close(td->socket_);delete td;return nullptr;}
我们封装一个HandlerRequest接口来获取请求,可不可以直接在threadRoutine内实现呢,都可以但不太好,因为后续如果我们想更新线程的处理函数,那就得改整个threadRoutine的实现,如果封装成一个函数调用,改动就少很多了。threadRoutine静态成员函数调用HandlerRequest这个成员函数,此时就要用到this指针来调用了。
我们在这个函数内获取请求,但是请求处理是由外部传给server类的函数执行的。
void HandlerRequest(int socket){// 1 读取数据while(true){char buffer[4096] = {0};int n = recv(socket, buffer, sizeof(buffer) - 1, 0);if (n > 0){cout<<"n: "<<n<<endl;std::string Response = func_(buffer);send(socket,Response.c_str(),Response.size(),0);}else{cout<<"n: "<<n<<endl;log_(ErrorLevel::Debug, "Quit...");sleep(1);}}}
这个处理请求函数就是我们一开始传入的,暂时什么都不做,就是简单打印,下面测试构建请求来测试。
四 构建请求和响应
1 请求报文解析
开始向我们写的服务端发请求,发请求和很多种做法,一种是用浏览器,此时要带上服务端的公网ip+端口号。
因为我们还没有给响应,所以页面展示如下。
但是我们可以在服务端看到很多请求,我们可以看到第一行就是我们先前说的请求行,包括请求方法,请求资源目录以及协议版本,有意思的是我们只是给了网址和端口号,没给目录,所以这个资源目录一般是'/',还是要到后面实现才能理解'/'表示哪个资源的目录,再次提醒,不是看到'/'就认为是根目录了。
此时我们再给浏览器传个目录
然后我们在接收报文中就看见了这个目录,所以说网址是会被拆分到请求报文中发送过去的,所以我们给网址就是在构建请求报文,如果没网址,就无法向服务端发起请求,就访问不了资源。
报头其它内容如上图,我们发现确实都是kv结构的,Host表示服务端的ip和端口。Connection:表示长短链接,这个得博客最后才提到了,几分钟就能说清楚。http报文中还会有个Cache-Control是缓存控制,默认为零,我们客户端请求一次资源a,如果过了很短时间后我们又访问这个资源,要不要再次下载呢? 有时候是不需要的,所以服务端为了减少客户端访问资源的次数,就有了缓存,客户端会保存访问资源,下次发请求给服务端,服务端发现你之前请求过了,就让你去本地找资源了,而缓存控制就是靠这个字段,max-age=123表示123秒后资源失效,这个字段对前端路线意义比较大。
有时候我们在百度搜一个软件,这个软件明明有安卓版和ios版,可是我如果用安卓手机去搜,安卓下载方式就会显示给我们,用苹果手机去搜,ios下载就会显示给我们,服务端如何认识我们的电脑系统和手机系统。因为发请求时带了主机的设备信息(USer-Agent)。
此时我们是Get的http请求,所以正文中啥也没有,在Post我们就会在正文看到数据了。
我们关闭服务端,立即启动往往会绑定失败,原因后提,报错是说这个端口已经被使用了。
2 响应报文解析
当我们大致了解了请求报文,就来看看响应报文吧。我们抓取一下百度的报文下来。telnet + 百度网址 端口,然后ctrl + ]即可输入请求,换行就可以收到响应了。
但是不是时时都能收到的,有点看运气,因为百度设置了反爬机制,有时候获取不了资源。
空行和正文
一般抓取的网页也很难看,因为这是被压缩了的,没了缩进和换行,当然浏览器可以识别。
五 完善响应
1 响应制作
刚刚那个是百度的响应,现在我们要自己模拟一个响应,让浏览器做解释,此时就是完善我们的HttpHead函数了,也就是发一个响应报文回去。我们来个最简易的报文,只有版本号状态码;状态码描述以及body保存的报文载荷,这样的报文可以被识别,但是不规范,因为数据类型没说,然后中间的属性字段也没有。
有时候浏览器解析响应如下。这就是一个乞丐版的网页,确实挺好玩的,我之所以学习计算机,也是被这种能创造事物的能力所吸引。
有时候可能都不会显示,我们后面还要完善报文的,那个时候再试试浏览器有没有反应。我们还可以用telnet去获取自己服务端的响应。
2 添加属性字段
在http宏观理解中我们已经说了服务器和客户端能区分报头和有效载荷,但是我们怎么知道载荷的长度呢?报头属性中记录了。
请求和响应都会有这么个属性字段:Content-Length来保存有效载荷的长度。可是刚刚响应没有添加报头属性啊?浏览器如何知道载荷长度呢,简单理解就是我们只发了一次响应,暂时还不容易出现浏览器少收多收的情况。
添加报头属性:载荷长度
有时候添加报头属性后浏览器就不解释成网页了呢? 还需要一个属性表示拉取资源的种类,这个可能是底层的实现,如果我们添加了属性字段,就要顺便说说资源的种类,不然默认为文本。
不同的资源有着不同的content_type(左边是content-type,右边是资源后缀)。
下面的这个表示返回的是网页资源。
把资源类型一加,网页又回来了。
3 从文件中读取资源
此时我们已经大致了解了网页资源其实是一种前端代码,我们编写网页显然不能把网页硬编码到响应中,所以我们就要用文件保存起来。
不然每次修改网页就要修改代码。我们要把上面的资源写入文件中,而这些文件都有路径,这就是为什么我们请求资源的时候要带路径,就是因为响应载荷要从请求的文件中读取资源,然后发送给客户端。
我的服务器将资源都放在该目录下,例如网页资源。
在util.hpp文件中实现一个获取指定路径文件全部资源的接口,返回给响应,再发给客户端。我们显然是要用于HttpHead函数中来读取资源构建响应。
// 从指定文件中读取资源static bool ReadOnefile(const std::string &path, std::string *fileContent){struct stat st;int ret = stat(path.c_str(), &st);if(ret < 0)//这里非常关键,因为后面会发起个./wwwroot/favicon.ico,如果文件不存在,后面打开就会出错,所以我们要直接返回,不然return false;(*fileContent).resize(st.st_size);int fd = open(path.c_str(), O_RDONLY);if(fd < 0)return false;int n = read(fd, (char *)fileContent->c_str(), st.st_size);close(fd);if (n < 0){std::cout << "Read err" << std::endl;return false;}return true;}
./wwwroot/favicon.ico是浏览器发起的第二次请求,当我们请求一个网页时,会发起两次请求,一次获取页面,还有一次是获取这个图标,每个网站都是会有下面这个图标的。
我们这个路径是定死的,但是实际上我们应该是去分析请求报头得来的,等一下要做的就是去分析请求报头中的信息。
我们先跑一下,此时还是可以正常显示。
4 解析请求
对请求做分析,解析出url,然后在url目录下找对应文件资源返回。我们由前面提到的请求报文字段设计出如下类来保存。
class Request
{
public:void Print(){log_(ErrorLevel::Debug, "method: %s url: %s httpversion: %s ", method_.c_str(), url_.c_str(), httpversion_.c_str());log_(ErrorLevel::Debug, " path_: %s suffix_: %s", path_.c_str(), suffix_.c_str());for (auto e : body_){cout << e;}}Log log_;std::string method_;std::string url_;std::string path_;std::string httpversion_;std::vector<std::string> body_;std::string suffix_;
};
我们大致可以看到有method_请求方法,url_请求资源,httpversion_协议版本,body这几个成员,suffix_和path_后提。之前我们在下面这个函数中是直接打印,现在我们要开始解析message字符串。
所以就要实现反序列化,为了验证解析效果,我们还打印显示了。我们已经知道如何对这个字符串做解析了,就是一直读取分隔符,所以就有了下面这个接口。
static std::string ReadOneline(std::string &Request,const std::string sep){int pos = 0;std::string ret;int nextpos = Request.find(sep.c_str(), pos);//截取一行数据返回if(nextpos == -1)return ret;ret = Request.substr(pos, nextpos - pos + sep.size());Request.erase(0,nextpos + sep.size());//删除被读完的数据return ret; }
我们先读取第一行,然后用Phrase接口获取请求方法,url以及协议版本。
void Phrase(const std::string& firstline,std::string* method,std::string* url,std::string*httpversion)
{stringstream ss(firstline);ss >> *method >> *url >> *httpversion;
}
Request deserialize(std::string message)
{int pos = 0;Request rq;std::string firstline = util::ReadOneline(message, SEP);Phrase(firstline, &(rq.method_), &(rq.url_), &rq.httpversion_);if (rq.url_ == "/"){rq.path_ = defaultdirector;} else 注意url是请求报文中的字段,不是资源的真正路径,path_才是资源的真正路径。{rq.path_ = curdirector; //"./wwwroot"rq.path_ += rq.url_; // /file1.html}int nextpos = rq.path_.rfind(".", rq.path_.size() - 1);rq.suffix_ = rq.path_.substr(nextpos, -1);while (message.size()){rq.body_.push_back(util::ReadOneline(message, SEP));}rq.body_.push_back(util::ReadOneline(message, SEP));message.erase(0, -1);return rq;
}
当发现请求资源路径是/,我们就让它获取默认资源路径,就像是我们每次搜索百度,都是先出来默认搜索界面一样,如果是请求某个资源,我们就给一个前缀目录。
下面就是解析的最后几步,我们不断地根据分隔符读取请求报文的属性字段,由于我们本次实现不对属性做处理,所以是直接保存,有效载荷也在body_中。
还有个suffix_是干什么的呢? 这个是请求资源后缀,我们响应的时候最好要指明资源类型,我们可以根据用户请求的资源来判断这个类型,例如请求/1.jpg,那我们就知道要返回的是图片资源。
5 构建响应
我们用ReadOnefile接口读取文件资源,然后添加到报文正文中。
// 2 构建响应std::string Response;if (util::ReadOnefile(rq.path_, &body)){Response = "HTTP/1.0 200 OK" + SEP;Response += "Content-Length: " + std::to_string(body.size()) + SEP;Response += GetContentType(rq.suffix_);Response += SEP;Response += body;}else // 读取文件失败,返回404页面{util::ReadOnefile(page_404.c_str(), &body);Response = "HTTP/1.404 NoFound" + SEP;Response += "Content-Length: " + std::to_string(body.size()) + SEP;Response += GetContentType(".html");Response += SEP;Response += body;}
在解释GetContentType之前我们先多弄几个资源,再搞个文件写上hello file2。
后面我们的http会请求多种类型资源,需要我们分析suffix来指定响应返回资源类型。
当我们已经能返回一个网页响应后,接下来我们就支持一下网页跳转和图片显示。
六 网页跳转
1 图片下载
如何构建图片资源,先下载图片到image目录中
wget获取远程资源的命令,后面跟着是图片下载地址,我们直接在网上点击一张图片就会出现下载地址。
浏览器扫描时发现还要个图片就会发起一次http请求。获取失败后会输出后面的文字,可是下面只有路径啊,我们是先访问了下面这个html文件,这个已经告诉了浏览器服务端的ip和端口。
如果图片未显示,可以看看是不是响应制作有问题,还有就是图片大小最好小于2mb,如果出现乱码,在原先网页基础上加上下面的标签代码解决。
2 跳转实现
我们在网站上随便点击一个链接,我们就跳转到了新的页面,这其实又发起了一次http请求,甚至可以回退,如下,我们弄一个链接,可以跳转至百度。
我们先访问/file2.html,然后关联到我们/file1.html。
点击file1就可以去到新页面。
当我们去到了新页面还想回到首页,我们可以让新页面关联起我们的首页。
网页原理是如此的有趣,本质就是发起http请求。
3 请求方法探究
请求方法主要用get和post方法,get是获取资源,post是传输资源,不过get也可以传输资源后面提。那如何让浏览器客户端发起post和get方法,我们好像只给了ip和端口啊,显然我们默认是get方法,因为每次给浏览器网址,我们都是从服务端获取了大把的文章,而非我们把文章发布上去。
我的程序一开始没有对方法做甄别,所以GET和POST都是访问某个目录去获取资源。我们先在资源中写一个表单,可以指定Get和Post方法,会在页面形成一个输入框,输入的会被当做参数再发起一次http请求。
action后面是某个路径,这个是我们要访问的资源,后面的method当然就是访问方法了。下面的input就是输入窗口,password表示是密码,text表示输入的是文本,不同的类型回显到显示器时不同的,如下。
可以理解name就是变量名,value就像数据,因为我们不能直接把数据丢过去,我们还得加个说明。只要我们按了提交就会变成一个http的请求,就下来就分析这个http请求。
ip和端口还是复用原来请求的,访问路径是/a/b/c.exe,这不就是我们写的action吗,后面紧跟着myname和password。
再来看看请求报文。
GET不仅仅可以获取静态网页资源,还可以提交参数,如上图,浏览器不就将参数放到url中了吗。这不就是在提交资源了吗。
如果是改成post,那我们的参数就会变成有效载荷。不会添加到url路径中。
4 get post的应用
Get方法不私密,因为从网址就容易泄露了,我们输入密码用黑点回显,就是为了更私密,当然私密不一定安全,(例如post提交比较私密了,但是也不安全),但是不私密一定是不安全的。
那什么时候用post,什么是用get呢,其实我们已经有答案了,get是把数据放到路径中,而路径长度是有限的,而post则是将数据放到有效载荷中,所以如果要提交大数据,可以用post,小数据且不私密可以用get。所以所有的登录注册支付等行为,有些核心的数据都要用post方法传输,有些不重要的也可以用get方法。
为什么post是不安全的,因为如果我们的主机被劫持了,浏览器的请求要先发给黑客,黑客再转成给服务器,此时我们的密码就被别人获取了。所以我们的数据应该要被加密,所以就有了https。
七 报文字段收尾
1 http状态码
状态码分类如下。
先提个问题,服务端不存在某个资源,客户端去访问?属于客户端错误还是服务端错误?
属于客户端错误,因为服务端能提供的服务是有限的,而用户的需求是无限的,所以总有些东西我们无法在网页找到某些资源,此时就会出现一个404错误页面,所以当客户端访问服务器资源路径不存在时,我们就返回一个404页面。下面是页面代码,网上随便找一个演示一下即可。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>404 Not Found</title>
<style>
body {
text-align: center;
padding: 150px;
}
h1 {
font-size: 50px;
}
body {
font-size: 20px;
}
a {
color: #008080;
text-decoration: none;
}
a:hover {
color: #005F5F;
text-decoration: underline;
}
</style>
</head>
<body>
<div>
<h1>404</h1>
<p>页面未找到<br></p>
<p>
您请求的页面可能已经被删除、更名或者您输入的网址有误。<br>
请尝试使用以下链接或者自行搜索:<br><br>
<a href="https://www.baidu.com">百度一下></a>
</p>
</div>
</body>
</html>
显示如下。
服务器错误一般是自己程序运行错误,而且也不会返回5开头,因为容易被发现并复现漏洞,家丑不可外扬。所以状态码好像大家不是特别遵守,简单理解就是状态码返回多少并不影响客户使用,所以就没有形成标准共识。
比较有意思的就是3开头的状态码,接下来介绍完这个状态码就可以了。
接下来就主要解释一下临时重定向和永久重定向的区别,首先什么叫重定向呢,概念:是由于资源换地方了,要告诉浏览器去新的路径请求服务。
永久重定向,告诉浏览器永久变更资源路径,以后用户都只会去新路径请求服务。那为什么是永久的,我第一次访问知道路径变了,后面那不就直接去新路径吗,这就重定向了一次,怎么叫永久呢,因为这个永久是站在服务器角度的,可能一直有老用户来访问,我要一直告诉他们换地方了,而临时重定向则是站在用户角度,临时变了位置,每次用户访问还是会去老路径看看回来了没。
临时重定向应用场景:有时候我们点一些小广告偶尔会跳到京东,拼多多,下次点击我们就不会跳转了,此时也算一种临时重定向,表单那里是页面跳转的一种方式,和临时重定向实现不一样。
永久重定向的应用场景:一个网站不仅我们的浏览器会访问,搜索引擎也会访问,因为我们用户一般都是通过搜索引擎去搜索的,由于有些网站在被淘汰,或者换新,所以为了让用户能在搜索引擎搜到新网站的信息,网站路径变更时就要弄一个永久重定向,让搜索引擎在下次爬取的时候能爬到新网站的路径,免得把用户搞到其它地方去了。
2 无状态的http
是什么意思呢,也就是说每次http客户端和服务端的交互是独立的,服务端不会记得你上次访问了什么,客户端也不会记录上次访问的资源的信息,但是有些浏览器会进行缓存最近访问的资源,这个不考虑。诶,什么意思,http不就是浏览器吗,http只是一个协议,而浏览器支持多个协议,说明会对不同的协议做不同的处理,http客户端服务端实现一般是不缓存先前的访问信息的。
但是这个功能很有用,特别是提高用户效率,例如访问网站的某个视频资源,第一次我们没有登录,不能访问,然后我们登录了,结果访问下一个资源的时候又要登录,因为双方都没记录你的用户身份,要重新验证,那可能你看一个视频就要登录一次,那用户体验简直绝了,所以用户需要能够保持登录功能,现实中是客户端保存了我们第一次输入的用户信息,每次请求都发给服务端让用户端识别,我们看起来是一直保持登录状态,其实底层还是要一次次验证身份信息。
那只能让客户端或者服务端保存了我们的密码和用户名,免得使用者每次请求都要输入密码。
方案一 cookie
原理:在本地保存了用户信息,本地内存还是文件? 文件,一定是文件,不会是浏览器这个进程保存在内存,因为你关闭了浏览器下次进去还是不用登录。
第一次我们访问vip资源,服务端发现我们没有登录,就要我们登录,此时我们就输入密码,又发了一次请求个服务端,随后认证通过,浏览器就收到服务端发来的set-cookie,并保存到内存或者文件。 后续http构建请求会将cooke信息带上,就不用用户输入了。
但是cooke信息容易被盗取,例如当我们用别人的热点,流量时,如果这个是黑客开的网络,此时我们的cookie就会被盗取下来,我们只能改密码才能防止黑客登录。
如何解决?此时就有了新方案,session id。
服务端不把不把密码发送过来,而是把收到的密码用户名转为session id发送给客户端,之后客户端访问就用这个session id了,server端就认证这个session了,这个可以解决用户个人信息泄露的问题,但是黑客还可以盗取session id来用用户身份去访问server端,和先前有什么区别吗?有的,服务端如果识别出黑客了就可以解决黑客了,直接释放客户的session id,需要重新登录,先前客户端服务端每次用密码来验证,密码被盗取后就泄露了,就只能让用户改密码,而用方案2也就是泄露session id,用户可以不改密码,而是再次登录向服务端弄一个session id就好了。
如何识别冒认客户? 服务端发现客户的ip快速变更,也就是异地登录,此时就识别出来了。底层用这个sesion id做key,方便哈希map快速查找用户名和密码做对比,当然公司一般用redies。
当然上面还是有很多安全隐患,因为黑客如果在登录时就截取了呢? 不谈加密,是无法保证安全的,这就要引出https了。
3 长短连接
http底层是tcp,tcp每次访问资源都要重新建立链接,那一个网页上有多个资源,难道每次请求,都要断开连接,然后四次握手建立链接,不是的,我们是基于一条链接,将多个请求塞入,然后结果也是一次性返回,这就是长链接,那为什么tcp每次访问资源都要链接,主要是为了保证可靠性。