HTTP基础知识
一、网址的构成
1.协议
告诉浏览器用户用什么规则访问。
2.域名
域名通过域名解析转变为ip地址,对于对外提供服务的Web服务器默认使用80(HTTP)和443(HTTPS)端口,如果没写端口号,访问http://会自动补上:80。
3.URL统一资源定位符
所有网络上的资源都可以被唯一的一个字符串标识,也就是URL(统一资源定位符)
4.文件路径
浏览器不可能把所有网站的所有资源都存起来,一个网站的资源几种存放在服务器的指定的目录下(web根目录),而网址中显示的文件路径是客户端当前访问资源对应的路径。
5.冲突字符的处理
网络行为包括获取数据与传输数据,而传输数据如果采用的是时可能因为
域名通过域名解析变为ip地址
二、HTTP协议格式
输入网址按下enter本质是向服务器发送了请求,服务器处理后发送对应的网页资源,这才实现了网页页面的跳转。下面详细介绍HTTP请求和响应的协议格式。
1.请求协议
HTTP请求协议格式如上图所示,行间使用"\r\n"进行分隔,而行内使用空格分开(附:属性键值对的“Key:”后面也要加一空格)下面对各部分进行说明。
(1)请求行
请求方法一般用GET和POST这两种方法,后文会展开来讲,空一格后写URL,接着是协议版本,客户端和服务器的版本可能不同导致支持的功能有所差异(后文解释),所以需要带上。
(2)请求属性
请求属性以键值对格式填写,实际上也的确使用键值对做查找匹配。下面为常见属性:
(3)空行
空出一行用于区分报头和报文。服务端读到空行时说明报头已完整读取,而报头里包含内容请求正文长度这一属性,这两个特点共同保证报文的完整性。因此,一个请求至少要有请求行和空行。
(4)请求正文
即用户要上传的数据。
2.响应协议
响应协议和请求协议类似,仅状态行有所不同,也有协议版本,而状态码和状态码描述用于表述响应状态(如404 Not Found)
3.协议细节
(1)http方法
GET方法
使用把要上传的数据放到url里面
POST方法
提交的参数是放在请求正文内的,
两种方法的使用时机
get方法的参数个数有限制 另外参数在搜索的时候会回显到网址框那里不安全
post方法相对安全,但不加密也是不安全的,本质是不直接泄漏用户的信息。
(2)表单
无论采用上述哪一种方法,用户的数据都是通过前端的表单提交的,如下面代码:
<form action="/" method="get"> name: <input type="text" name="name"><br> passwd: <input type="text" name="passwd"><br> <input type="submit" value="提交"> </form>效果如下图(emm乱码那是提交按键):
输入并提交后浏览器上传数据如下:
http://182.254.173.195:8080/?name=dasdsa&passwd=dadasdsasdasda表单还可以有其它作用,比如在上文表单代码示例中,如果action指向一个可执行程序时,服务器收到请求后,会创建子进程执行该程序,并将表单数据通过环境变量或标准输入传递给它,实现动态处理(如用户注册、数据录入)。
(3)状态码
响应报头中的状态码向客户端说明处理状态,类似于错误码。如常见的404就是代表无法找到页面的状态码。
下面讲下3开头的状态码,也就是重定向相关的状态码。
301(永久重定向):资源永久迁移到新 URL,浏览器会缓存跳转结果,下次直接访问新地址(适用于域名更换、资源永久搬家)。
302(临时重定向):资源暂时跳转,浏览器不会缓存,每次仍然访问原 URL(适用于临时维护、活动页跳转)。
(4)常见的报头
Content-Type: 数据类型(text/html等)
Content-Length: Body的长度
Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
User-Agent: 声明用户的操作系统和浏览器版本信息,可用于反爬。
referer: 当前页面是从哪个页面跳转过来的;可配合浏览器的前进和后退页面的功能实现
location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;
(5)长短连接
一个网页页面通常包含多个元素,而每一个元素就是一个资源。如果服务器和浏览器对每个资源都做请求和响应会非常耗费时间。
一次请求响应一个资源,随后关闭连接,就是短链接,http1.0默认使用短链接,如果要使用长连接则Connection属性要设置为keep-alive。
建立链接后一次性发送或返回多个http的请求或响应的过程就是长链接。http1.1默认使用长链接,也可使用短连接。
之所以一开始要交换版本信息,就是为了防止通信双方中有一方只支持 HTTP/1.0连接自动降级为短连接模式。
(6)Cookie(缓存)
http本身默认是无状态的,也就是没有缓存。
在网页注册账户后,浏览器会帮忙保存cookie,也就是你的账号密码,此后用户登录该网页时,浏览器向网页服务器发送请求会自动带上账密,从而跳过到了登陆环节。
cookie有文件级别也有内存级别。顾名思义内存级cookie是保存在浏览器的内存中的,关闭浏览器后下次再次打开并登录网页需要重新输入。而文件级别的cookie可将cookie放在浏览器文件夹中长期保留。
在服务端的响应字段中加入Set-Cookie属性从而设立一个cookie,见下面参考代码:
response+= "Set-Cookie: ussrname=zzs&&passwd=12345\r\n";//缓存设置(7)cookie的安全问题
1.可能被盗取
cookie被外部拿到本身不可避免,但sessionid由服务器统一管理,可随时销毁,所以异地登录可要求认证,也就是重新设置sessionid。
2.个人私有信息泄漏
登陆账号也是上传数据的过程,也就是密码可能出现在URL中。
应对方法:不再直接将密码作为cookie而是由服务器生成的sessionid传给浏览器,下次通过id匹配登录,服务器可将sessionid交由redis数据库管理。黑客虽然仍可拿取cookie,但这时候回显的只是id,真正的密码由网站管理,使得私人信息不被泄漏。
HTTP的实现
实现目的
结合上述HTTP知识,利用TCP实现一HTTP服务器,并编写简单网页,打通前后端隔阂,理解HTTP的应用。
实现思路
和TCP的封装及应用类似,首先利用socket接口建立连接,然后基于TCP协议进行数据传输。而对于HTTP而言,难点在于服务端需要遵守协议规范,具有正确读取并解析请求、并返回正确响应的功能,此外还涉及到网页代码和文件操作相关知识,这里不对这两点以及服务器的运作方式多做深挖,直接采用多线程的方式接收请求,线程函数负责处理请求并返回响应。
反序列化
对请求做反序列化,和以往需要拆解字符串本身不同,请求本身已规范使用"\r\n"作为分隔符,这里的任务是把各行拆开放入数组,并对请求行做拆解赋值给成员变量。
具体代码如下:
std::string linesep = "\r\n";//行分隔符 std::string wordsep = "\0";//行内分隔符 struct thread_info//线程信息 { HttpServer *hsp;//服务器指针,传this要用 int fd; }; class Request { public: Request() {} void deserialization(std::string target)//对请求做反序列化 { // 按行读取 std::string line; while (true) { size_t pos = target.find(linesep); std::string temp = target.substr(0, pos); if (temp.empty()) { break;//空行被找到 } header.push_back(temp); target.erase(0, pos + linesep.size());//将缓冲区内对应的字符删除,避免影响后续存储的请求。 } // 找到空行后剩下的就是请求正文,直接赋值 text = target; } void Parse() { std::stringstream ss(header[0]); //流式切割,获取请求头信息,url最为重要,用于定位用户需要访问的资源路径 ss >> method >> url >> http_version; } std::string GetUrl() { return url; } private: //用于存放请求头和请求报头内容的数组 std::vector<std::string> header; //请求正文 std::string text; //请求行信息 std::string method; std::string url; std::string http_version; };负责处理请求的线程函数
这里直接省略完整代码,从线程函数开始讲解,完整代码见本章末。
接收到的参数是封装好的线程信息,包括服务器指针和sockfd.
struct thread_info//线程信息 { HttpServer *hsp;//服务器指针,传this要用 int fd; };Run方法负责接收请求和发送响应,而处理并构建响应由handler方法处理。
static void *Run(void *argv) { // 接收 处理并得到响应(handler) 发送 thread_info *info = (thread_info *)argv; pthread_detach(pthread_self()); char buf[10240] = {0}; int n = recv(info->fd, buf, sizeof(buf) - 1, 0); // 接收http请求 if (n > 0) { std::string request = buf; std::string sendmessage = info->hsp->handler(request); send(info->fd, sendmessage.c_str(), sendmessage.size(), 0); delete info; } else if (n == 0) { LOG(INFO, "Client exit"); delete info; } else { LOG(FATAL, "recv fail"); delete info; } return nullptr; }handler负责拆解请求并判断用户输入的路径,根据路径获取对应的资源(Getcontent方法实现)用于填充响应正文,至于响应头,主要是检查资源类型补充的Content_Type属性,响应正文的长度Content-Length,以及缓存设置Set-Cookie,然后返回整个响应。content_type对应的unordermap直接在构造函数中硬编码。
std::string Getcontent(std::string &sourcepath) { // 图片文件需要用读取为二进制方式读取 std::ifstream in(sourcepath, std::ios::binary); if (!in.is_open()) { return ""; // 文件打开失败,返回空字符串 } in.seekg(0, std::ios::end); size_t size = in.tellg(); in.seekg(0, std::ios::beg); std::string content(size, '\0'); std::cout<<"size:"<<size<<std::endl; in.read(&content[0], size); in.close(); return content; } std::string CheckType(std::string &url){ size_t pos=url.rfind('.'); //如果没有找到默认是网页资源 if (pos == std::string::npos) { return content_type_[".html"]; } std::string type=url.substr(pos); auto it =content_type_.find(type); if(it==content_type_.end()){ return content_type_[".html"]; } else{ return content_type_[type]; } } std::string handler(const std::string &recvmessage) { // 负责处理请求,判断路径,获取资源(Getcontent方法实现),返回响应 //构建请求类,做反序列化 Request rq; rq.deserialization(recvmessage); rq.Parse();//获取请求行各参数信息 rq.DebugPrint();//博客中省略debugprint // 对资源路径做判断 //注意一般用户不会吧web根目录写上来,服务器在这里直接帮忙添加 std::string sourcepath; std::string url = rq.GetUrl(); bool redirect = false; if (url == "/" + homepage || url == "/")//如果用户给的是/或本来就是首页,直接把首页返回给浏览器(不可能把整个根目录下所有资源给用户) { sourcepath = wwwroot + "/" + homepage;//首页路径 } else if (url == "/oldpakage.html")//若该网址搬迁,自动跳转到新网站中 { sourcepath = wwwroot + "/" + "newpakage.html"; redirect = true; } else { sourcepath = wwwroot + url;//正常资源路径正常处理 } std::string content = Getcontent(sourcepath);//获得响应正文 std::string response_line;// if (content.empty()) { //如果内容为空,说明文件路径错误,给用户返回404页面 std::string errpath = wwwroot + '/' + "notfound.html"; content = Getcontent(errpath); response_line = "HTTP/1.0 404 Notfound\r\n";//响应行直接硬编码,后文相同 } else if (redirect)//如果前文发现用户要访问旧网址,响应行给302状态码和描述 { response_line = "HTTP/1.0 302 Redirect\r\n"; } else { response_line = "HTTP/1.0 200 OK\r\n"; } std::cout << "content:" << content << std::endl; //检查用户要访问的资源类型并构建Content_Type属性 std::string ContentType="Content_Type: "+CheckType(url)+"\r\n"; std::string blank_line = "\r\n"; //组装响应 std::string response; response += response_line; response+=ContentType; response += "Content-Length: " + std::to_string(content.size()) + "\r\n"; response += "Connection: close\r\n"; response+= "Set-Cookie: ussrname=zzs&&passwd=12345\r\n";//缓存设置 //检查重定向 if (redirect) { response += "Location: /newpakage\r\n"; } response += blank_line; response += content; return response; }html文件这里省略,如果要查看完整代码和文件详见:
HTTP的封装与应用https://gitee.com/zhang-zheshun/linux-learning-repository/tree/master/net_learning/HTTP