Linux - 第16节 - 网络基础(应用层二)

目录

1.HTTP协议

1.1.HTTP简介

1.2.认识URL

1.3.urlencode和urldecode

1.4.HTTP协议格式

1.4.1.HTTP请求协议格式

1.4.2.HTTP响应协议格式

1.4.3.验证HTTP请求协议格式

1.4.4.验证HTTP响应协议格式

1.5.服务端HTTP响应代码实现

1.6.HTTP的方法

1.6.1.客户端上传数据代码示例

1.6.2.GET方法和POST方法

1.7.HTTP的状态码

1.7.1.HTTP的状态码介绍

1.7.2.Redirection重定向状态码

1.7.3.Redirection重定向状态码代码使用

1.8.HTTP常见的Header请求/响应报头

1.8.1.HTTP常见的Header请求/响应报头介绍

1.8.2.Cookie

1.8.3.Session


1.HTTP协议

我们在套接字部分编写的代码和应用层一中编写的网络计算器代码都是在应用层工作的,是应用层代码,因此应用层代码包括:

(1)基本系统socket套接字系列接口的使用。

(2)定制协议(例如网络计算器Protocol.hpp文件中的代码)

(3)编写业务(例如网络计算器severTcp文件中的calculator接口)

注:定制协议和编写业务是两回事,可以进行解耦。

网络计算器场景下我们使用socket套接字系列接口、定制协议、编写业务的过程较为麻烦,考虑的也不一定全面。一些常用的场景已经有程序员自定义过成熟的协议,这些协议慢慢成为了应用层特定协议的标准,可以被我们直接使用。

应用层代码的定制协议部分常见协议标准有http、https、smtp、ftp、DNS等,我们这里以http、https为主进行讲解。

注:这些常见协议标准主要完成了应用层代码的“基本系统socket套接字系列接口的使用”部分和“定制协议”部分功能。

1.1.HTTP简介

HTTP(Hyper Text Transfer Protocol)协议又叫做超文本传输协议,是一个简单的请求-响应协议,HTTP通常运行在TCP之上。

在编写网络通信代码时,我们可以自己进行协议的定制,但实际有很多优秀的工程师早就已经写出了许多非常成熟的应用层协议,其中最典型的就是HTTP协议。

1.2.认识URL

URL(Uniform Resource Lacator)叫做统一资源定位符,也就是我们通常所说的网址,是因特网的万维网服务程序上用于指定信息位置的表示方法。

一个URL大致由如下几部分构成:

协议方案名:

http://表示的是协议名称,表示请求时需要使用的协议,通常使用的是HTTP协议或安全协议HTTPS。HTTPS是以安全为目标的HTTP通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性。

常见的应用层协议:

\cdot DNS(Domain Name System)协议:域名系统。
\cdot FTP(File Transfer Protocol)协议:文件传输协议。
\cdot TELNET(Telnet)协议:远程终端协议。
\cdot HTTP(Hyper Text Transfer Protocol)协议:超文本传输协议。
\cdot HTTPS(Hyper Text Transfer Protocol over SecureSocket Layer)协议:安全数据传输协议。
\cdot SMTP(Simple Mail Transfer Protocol)协议:电子邮件传输协议。
\cdot POP3(Post Office Protocol - Version 3)协议:邮件读取协议。
\cdot SNMP(Simple Network Management Protocol)协议:简单网络管理协议。
\cdot TFTP(Trivial File Transfer Protocol)协议:简单文件传输协议。

登录信息:

usr:pass表示的是登录认证信息,包括登录用户的用户名和密码。虽然登录认证信息可以在URL中体现出来,但绝大多数URL的这个字段都是被省略的,因为登录信息可以通过其他方式(网页扫码登录、网页账号密码登录等)交付给服务器。

服务器地址:
www.example.jp表示的是服务器地址,也叫做域名,比如www.alibaba.com,www.qq.com,www.baidu.com。

需要注意的是,我们用IP地址标识公网内的一台主机,但IP地址本身并不适合给用户看。比如说我们可以通过ping命令,分别获得www.baidu.com和www.qq.com这两个域名解析后的IP地址。

如果用户看到的是这两个IP地址,那么用户在访问这个网站之前并不知道这两个网站到底是干什么的,但如果用户看到的是www.baidu.com和www.qq.com这两个域名,那么用户至少知道这两个网站分别对应的是哪家公司,因此域名具有更好的自描述性。 

实际我们可以认为域名和IP地址是等价的,在计算机当中使用的时候既可以使用域名,也可以使用IP地址。但URL呈现出来是可以让用户看到的,因此URL当中是以域名的形式表示服务器地址的。

注:域名最终会被转化为IP地址。

服务器端口号:

80表示的是服务器端口号。HTTP协议和套接字编程一样都是位于应用层的,在进行套接字编程时我们需要给服务器绑定对应的IP和端口,而这里的应用层协议也同样需要有明确的端口号。

常见协议对应的端口号:

协议名称 对应端口号
HTTP 80
HTTPS 443
SSH 22

当我们使用某种协议时,该协议实际就是在为我们提供服务,现在这些常用的服务与端口号之间的对应关系都是明确的,所以我们在使用某种协议时实际是不需要指明该协议对应的端口号的,浏览器会自动添加对应的端口号,因此在URL当中,服务器的端口号一般也是被省略的。

注:

1.客户端访问服务器时必须要有域名(IP地址)和端口号,因为网络通信的本质是socket通信,socket通信需要IP地址+端口号。

2.我们前面讲过做测试时,我们不要绑定1024之前的端口号,因为0-1023端口号是给这些标准协议使用的。

带层次的文件路径:
/dir/index.htm表示的是要访问的资源所在的路径。访问服务器的目的是获取服务器上的某种资源,通过前面的域名和端口已经能够找到对应的服务器进程了,此时要做的就是指明该资源所在的路径。

比如我们打开浏览器输入百度的域名后,此时浏览器就帮我们获取到了百度的网页首页,如下图一所示。当我们发起网页请求时,本质是访问服务器的某个.html网页文件,从.html文件中获取了如下图二三这样的网页信息到本地(图二三分别展示了网页端和Linux端两种获取百度网页首页文件信息的方法),然后浏览器对这张网页信息进行解释,最后就呈现出了对应的网页,我们可以将这种资源称为网页资源。

此外我们还会向服务器请求视频、音频、网页、图片等资源。HTTP之所以叫做超文本传输协议,而不叫做文本传输协议,就是因为有很多资源实际并不是普通的文本资源。

因此在URL当中就有这样一个字段,用于表示要访问的资源所在的路径。此外我们可以看到,这里的路径分隔符是/,而不是\,这也就证明了实际很多服务都是部署在Linux上的。
注:

1.http协议的功能就是向特定的服务器申请特定的“资源”获取到本地,进行展示或某种使用。

2.如果我们在客户端主机(硬件)的客户端进程client(软件)没有获取资源的时候,资源在哪里呢?资源就在对应的网络服务器进程sever(软件)所在的服务器(硬件)上。

3.客户端向服务端请求的所有视频、音频、网页、图片等资源都是文件,这些资源文件在Linux服务器上,服务器对应进程在服务器上打开并读取对应文件,发送给请求的客户端进程。

服务器对应进程要打开并读取对应客户端请求的文件,前提条件是能够找到这个文件,Linux服务器是通过路径找到某个文件的,因此URL里面要有带层次的文件路径字段。

4.带层次的文件路径中,开头的/不一定代表根目录,访问服务器的某个文件资源可以以绝对路径的方式访问也可以以相对路径的方式访问,开头的/一般都不是根目录,而是接在某个特定路径下进行访问。

例如:客户端请求时,URL中带层次的文件路径为/dxf/test/index.htm,而对应服务器内部的特定路径为/home/qyn,则服务器对应要查找的文件路径就应为/home/qyn/dxf/test/index.htm。

查询字符串:
uid=1表示的是请求时提供的额外的参数,这些参数以KV键值对的形式存在,每个查询字符串通过&符号分隔开的。

比如我们在百度上面搜索HTTP,此时可以看到URL中有很多参数,而在这众多的参数当中有一个参数wd(word),表示的就是我们搜索时的搜索关键字wd=HTTP。

因此双方在进行网络通信时,是能够通过URL进行用户数据传送的。 

片段标识符:

ch1表示的是片段标识符,是对资源的部分补充。

我们在看组图的时候,URL当中就会出现片段标识符。

1.3.urlencode和urldecode

像 / ? : 等这样的字符, 已经被url当做特殊意义理解了。因此这些字符不能随意出现。

比如,某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义。

转义的规则如下:

将需要转码的字符(字符本质是整数)转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式。

如下图所示,“+” 被转义成了 “%2B”。

注:

1.除了特殊字符,汉字也会被编码转义,如下图所示。

2.客户端发送请求前需要进行urlencode编码过程,服务端收到消息后需要进行urldecode解码过程,编码(解码)过程既可以在序列化(反序列化)之前也可以在序列化(反序列化)之后,一般推荐在序列化之后(反序列化之前)进行urlencode编码(urldecode解码)过程。

3.想要知道特殊符号或汉字进行编码后的结果,可以搜素URL在线编码解码器,如下图所示。

4.urldecode就是urlencode的逆过程。

1.4.HTTP协议格式

应用层常见的协议有HTTP和HTTPS,传输层常见的协议有TCP,网络层常见的协议是IP,数据链路层对应就是MAC帧了。其中下三层是由操作系统或者驱动帮我们完成的,它们主要负责的是通信细节。如果应用层不考虑下三层,在应用层自己的心目当中,它就可以认为自己是在和对方的应用层在直接进行数据交互。

下三层负责的是通信细节,而应用层负责的是如何使用传输过来的数据,两台主机在进行通信的时候,应用层的数据能够成功交给对端应用层,因为网络协议栈的下三层已经负责完成了这样的通信细节,而如何使用传输过来的数据就需要我们去定制协议,这里最典型的就是HTTP协议。

HTTP是基于请求和响应的应用层服务,作为客户端,你可以向服务器发起request,服务器收到这个request后,会对这个request做数据分析,得出你想要访问什么资源,然后服务器再构建response,完成这一次HTTP的请求。这种基于request&response这样的工作方式,我们称之为cs或bs模式,其中c表示client,s表示server,b表示browser。

由于HTTP是基于请求和响应的应用层访问,因此我们必须要知道HTTP对应的请求格式和响应格式,这就是学习HTTP的重点。

1.4.1.HTTP请求协议格式

HTTP请求协议格式如下:

HTTP请求由以下四部分组成:

\cdot 请求行:[请求方法]+[url](一般是去掉域名和端口只包含路径的url)+[http版本](客户端请求的http版本)
\cdot 请求报头:请求的属性,这些属性都是以key: value的形式按行陈列的(key: value中key和value之间是以冒号+空格隔开的)。
\cdot 空行:遇到空行表示请求报头结束。
\cdot 请求正文:请求正文允许为空字符串,如果请求正文存在,则在请求报头中会有一个Content-Length属性来标识请求正文的长度。
其中,前面三部分是一般是HTTP协议自带的,是由HTTP协议自行设置的,而请求正文一般是用户的相关信息或数据,如果用户在请求时没有信息要上传给服务器,此时请求正文就为空字符串。

注:请求行后面有\r\n与请求报头隔开,请求报头每个key: alue属性后面也都有\r\n,请求报头后面还会再多加一个\r\n与请求正文隔开。

问题:如何将HTTP请求的报头与有效载荷进行分离?
答:当应用层收到一个HTTP请求时,它必须想办法将HTTP的报头与有效载荷进行分离。对于HTTP请求来讲,这里的请求行和请求报头就是HTTP的报头信息,而这里的请求正文实际就是HTTP的有效载荷。

我们可以根据HTTP请求当中的空行\r\n来进行分离,当服务器收到一个HTTP请求后,就可以按行进行读取,如果读取到两个空行\r\n\r\n则说明已经将报头读取完毕,实际HTTP请求当中的两个空行\r\n\r\n就是用来分离报头和有效载荷的。

如果将HTTP请求想象成一个大的线性结构,此时每行的内容都是用\n隔开的,因此在读取过程中,如果连续读取到了两个\r\n,就说明已经将报头读取完毕了,后面剩下的就是有效载荷了。

1.4.2.HTTP响应协议格式

HTTP响应协议格式如下:

HTTP响应由以下四部分组成:

\cdot 状态行:[http版本](服务端相应的http版本)+[状态码]+[状态码描述]
\cdot 响应报头:响应的属性,这些属性都是以key: value的形式按行陈列的。
\cdot 空行:遇到空行表示响应报头结束。
\cdot 响应正文:响应正文允许为空字符串,如果响应正文存在,则响应报头中会有一个Content-Length属性来标识响应正文的长度。比如服务器返回了一个html页面,那么这个html页面的内容就是在响应正文当中的。

注:

1.我们常见的404其实就是请求资源不存在对应的状态码。

2.HTTP响应的报头与有效载荷进行分离的方式与HTTP请求相同。

问题:HTTP如何保证请求和响应报文的报头部分和有效载荷部分被全部读取呢?

答:

\cdot 读取完整报头:按行读取,直到读取到空行。

\cdot 读取完整有效载荷:将报头读完后,报头的请求报头和响应报头部分包含有效载荷长度,根据有效载荷长度进行读取即可将有效载荷读取完。

1.4.3.验证HTTP请求协议格式

创建server.hpp文件,如下图一所示,创建serverTcp.cc文件,如下图二所示,创建Makefile文件,如下图三所示。使用make命令生成serverTcp可执行程序,使用./udpServer 8081命令运行serverTcp可执行程序,打开浏览器输入123.60.25.237:8081,向服务端发起请求,如下图四所示。

这里URL我们只写了服务器地址和服务器端口号,协议方案名http没有写是因为不写就会默认http协议,登录信息、查询字符串、片段标识符本身可以不写,带层次的文件路径不写就会默认服务器特定的路径。

这里客户端是浏览器,浏览器是多线程的,浏览器短期内得不到响应会重复发送http请求,因此这里服务端收到并打印了多个请求。

下图二中标明了请求行、请求报头、空行对应的部分,因为没有请求正文,所以这里没有标明。

 

server.hpp文件:

#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <cstdio>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
#include <cerrno>
#include <cassert>

using namespace std;

void handlerHttpRequest(int sock)
{
    cout << "+++++++++++++++++++++++++++++++++++++++++++++++++++" << endl;
    char buffer[10240];
    ssize_t s = read(sock, buffer, sizeof buffer);
    if(s > 0) cout << buffer;
   close(sock);
}

class ServerTcp
{
public:
    ServerTcp(uint16_t port, const std::string &ip = "")
        : port_(port),
          ip_(ip),
          listenSock_(-1)
    {
        quit_ = false;
    }
    ~ServerTcp()
    {
        if (listenSock_ >= 0)
            close(listenSock_);
    }

public:
    void init()
    {
        // 1. 创建socket
        listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
        if (listenSock_ < 0)
        {
            exit(1);
        }
        // 2. bind绑定
        // 2.1 填充服务器信息
        struct sockaddr_in local; // 用户栈
        memset(&local, 0, sizeof local);
        local.sin_family = PF_INET;
        local.sin_port = htons(port_);
        ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
        // 2.2 本地socket信息,写入sock_对应的内核区域
        if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
        {
            exit(2);
        }

        // 3. 监听socket,为何要监听呢?tcp是面向连接的!
        if (listen(listenSock_, 5 /*后面再说*/) < 0)
        {
            exit(3);
        }
        // 运行别人来连接你了
    }
    void loop()
    {
        signal(SIGCHLD, SIG_IGN); // only Linux
        while (!quit_)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);

            int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
            if (quit_)
                break;
            if (serviceSock < 0)
            {
                // 获取链接失败
                cerr << "accept error ...." << endl;
                continue;
            }
            // 5.1 v1 版本 -- 多进程版本 -- 父进程打开的文件会被子进程继承吗?会的
            pid_t id = fork();
            assert(id != -1);
            if(id == 0)
            {
                close(listenSock_); //建议
                if(fork() > 0) exit(0);
                //孙子进程
                handlerHttpRequest(serviceSock);
                exit(0); // 进入僵尸
            }
            close(serviceSock);
            wait(nullptr);
        }
    }

    bool quitServer()
    {
        quit_ = true;
        return true;
    }

private:
    // sock
    int listenSock_;
    // port
    uint16_t port_;
    // ip
    std::string ip_;
    // 安全退出
    bool quit_;
};

serverTcp.cc文件:

#include "server.hpp"

static void Usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " port" << std::endl;
    std::cerr << "example:\n\t" << proc << " 8080\n"
              << std::endl;
}

// ./ServerTcp local_port local_ip
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = atoi(argv[1]);

    ServerTcp svr(port);
    svr.init();
    svr.loop();
    return 0;
}

Makefile文件:

serverTcpd:serverTcp.cc
	g++ -o $@ $^ -std=c++11 -lpthread

.PHONY:clean
clean:
	rm -f serverTcpd

注:上图二服务端收到的报文中请求报头部分,User-Agent是了客户端的属性信息,Accept是客户端可以接收的资源。

1.4.4.验证HTTP响应协议格式

如下图所示,使用telnet www.baidu.com 80命令,如果显示^]表示登陆成功,然后输入ctrl+]进入telnet命令行并回车,使用GET / HTTP/1.1命令(http请求时,命令字母的大小是忽略的)(GET后面的字段表示请求的资源路径,这里我们只写一个/)获取百度服务器的响应报文。

注:上图客户端收到的报文中响应报头部分,Content-Length是相应正文部分的长度。

1.5.服务端HTTP响应代码实现

创建server.hpp文件,如下图一所示,创建serverTcp.cc文件,如下图二所示,创建Makefile文件,如下图三所示。在本路径下创建wwwroot文件夹,在wwwroot文件夹中创建index.html文件,写入下图四所示的代码,文件关系如下图五所示。使用make命令生成serverTcp可执行程序,使用./udpServer 8081命令运行serverTcp可执行程序,打开浏览器输入123.60.25.237:8081,向服务端发起请求,如下图六所示,使用telnet 127.0.0.1 8081命令,如果显示^]表示登陆成功,然后输入ctrl+]进入telnet命令行并回车,使用GET / http/1.0(字母大小写均可)命令,如下图七所示。

server.hpp文件:

#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <cstdio>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
#include <cerrno>
#include <cassert>

#define CRLF "\r\n"
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define HOME_PAGE "index.html"
#define ROOT_PATH "wwwroot"

using namespace std;

std::string getPath(std::string http_request)
{
    std::size_t pos = http_request.find(CRLF);
    if(pos == std::string::npos) return "";
    std::string request_line = http_request.substr(0, pos);
    //GET /a/b/c http/1.1
    std::size_t first = request_line.find(SPACE);
    if(pos == std::string::npos) return "";
    std::size_t second = request_line.rfind(SPACE);
    if(pos == std::string::npos) return "";

    std::string path = request_line.substr(first+SPACE_LEN, second - (first+SPACE_LEN));
    if(path.size() == 1 && path[0] == '/') path += HOME_PAGE;
    return path;
}

std::string readFile(const std::string &recource)
{
    std::ifstream in(recource, std::ifstream::binary);
    if(!in.is_open()) return "404";
    std::string content;
    std::string line;
    while(std::getline(in, line)) content += line;
    in.close();
    return content;
}
void handlerHttpRequest(int sock)
{
    char buffer[10240];
    ssize_t s = read(sock, buffer, sizeof buffer);
    if(s > 0) cout << buffer;
    std::string path = getPath(buffer);

    std::string recource = ROOT_PATH;
    recource += path;
    std::cout << recource << std::endl;

    std::string html = readFile(recource);

    //开始响应
    std::string response;
    response = "HTTP/1.0 200 OK\r\n";
    response += "Content-Type: text/html\r\n";
    response += ("Content-Length: " + std::to_string(html.size()) + "\r\n");
    response += "\r\n";
    response += html;

    send(sock, response.c_str(), response.size(), 0);
}

class ServerTcp
{
public:
    ServerTcp(uint16_t port, const std::string &ip = "")
        : port_(port),
          ip_(ip),
          listenSock_(-1)
    {
        quit_ = false;
    }
    ~ServerTcp()
    {
        if (listenSock_ >= 0)
            close(listenSock_);
    }

public:
    void init()
    {
        // 1. 创建socket
        listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
        if (listenSock_ < 0)
        {
            exit(1);
        }
        // 2. bind绑定
        // 2.1 填充服务器信息
        struct sockaddr_in local; // 用户栈
        memset(&local, 0, sizeof local);
        local.sin_family = PF_INET;
        local.sin_port = htons(port_);
        ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
        // 2.2 本地socket信息,写入sock_对应的内核区域
        if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
        {
            exit(2);
        }

        // 3. 监听socket,为何要监听呢?tcp是面向连接的!
        if (listen(listenSock_, 5 /*后面再说*/) < 0)
        {
            exit(3);
        }
        // 运行别人来连接你了
    }
    void loop()
    {
        signal(SIGCHLD, SIG_IGN); // only Linux
        while (!quit_)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);

            int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
            if (quit_)
                break;
            if (serviceSock < 0)
            {
                // 获取链接失败
                cerr << "accept error ...." << endl;
                continue;
            }
            // 5.1 v1 版本 -- 多进程版本 -- 父进程打开的文件会被子进程继承吗?会的
            pid_t id = fork();
            assert(id != -1);
            if(id == 0)
            {
                close(listenSock_); //建议
                if(fork() > 0) exit(0);
                //孙子进程
                handlerHttpRequest(serviceSock);
                exit(0); // 进入僵尸
            }
            close(serviceSock);
            wait(nullptr);
        }
    }

    bool quitServer()
    {
        quit_ = true;
        return true;
    }

private:
    // sock
    int listenSock_;
    // port
    uint16_t port_;
    // ip
    std::string ip_;
    // 安全退出
    bool quit_;
};

serverTcp.cc文件:

#include "server.hpp"

static void Usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " port" << std::endl;
    std::cerr << "example:\n\t" << proc << " 8080\n"
              << std::endl;
}

// ./ServerTcp local_port local_ip
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = atoi(argv[1]);

    ServerTcp svr(port);
    svr.init();
    svr.loop();
    return 0;
}

Makefile文件:

serverTcpd:serverTcp.cc
	g++ -o $@ $^ -std=c++11 -lpthread

.PHONY:clean
clean:
	rm -f serverTcpd

wwwroot文件夹中的index.html文件:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>测试</title>
</head>
<body>
    <h3>hello my server!</h3>
    <p>我终于测试完了我的代码</p>
</body>
</html>

注:

1.发送消息向套接字写入内容,既可以用write接口,也可以使用send接口,send接口声明如下图所示。

参数sockfd是要写入的套接字,参数buf是要发送的数据,参数len是发送数据的大小,flags设为0与write功能相同。send接口返回实际写入的字节数。

 使用send函数需要包含<sys/types.h><sys/socket.h>头文件。

2.在服务器响应报文的响应正文部分,我们可以在内容的首尾加上<html>,给内容的前后加上<h1>作为大标题,将内容字体变大。

给响应正文部分加上<html>和<h1>,主流的浏览器就可以识别应用了,但是我们最好还是在响应报头部分说明响应正文的类型等属性,例如"Content-type:text/html"描述响应正文的类型为html文本类型。

3.一般服务器响应报文的响应正文内容都是从文件中读取的,服务端根据客户端要请求的文件路径获取对应的资源进行响应,因此这里我们创建文件index.html,将响应正文内容放在该文件中。

客户端在发送请求时,URL中,带层次的文件路径部分就是要访问的文件路径,telnet请求行中,第二个字段就是要访问的文件路径。

这里我们创建一个wwwroot文件夹,在wwwroot文件夹中保存响应正文内容所在的index.html文件。前面我们讲过,在URL中,带层次的文件路径的首个/一般都不是根目录,我们这里设置服务端的特定文件路径为wwwroot,然后通过getPath接口获取URL中带层次的文件路径,将URL中带层次的文件路径拼接在特定文件路径后面得到完整的文件路径,服务器根据完整的文件路径,读取对应文件资源响并应给客户端。

实际在各网站中,如果客户端发送的请求中,URL带层次的文件路径只是一个/,那么服务端会把网站的.html首页资源响应给客户端,我们这里选择直接将index.html文件资源返回给客户端。

4.这里我们将服务器响应报文的响应正文部分存储在index.html文件中,index.html文件内容是前端代码,第一行的<!DOCTYPE html>标签是为了告知浏览器本文档所使用的HTML规范,<html>和</html>是起始标签和结束标签,<meta charset="utf-8">标签表明编码类型,<title>和</title>标签之间填写标题内容,<h1>和<h1>之间填写标题内容(h后面的数字表示标题的大小,数字越大标题越小,我们这里将h1改为h3),<p></p>之间填写段落内容。

1.6.HTTP的方法

HTTP常见的方法如下:

其中最常用的就是GET方法和POST方法。

1.6.1.客户端上传数据代码示例

客户端上传数据代码示例:

server.hpp文件、serverTcp.cc文件、Makefile与1.5小节服务端HTTP响应代码对应文件相同,将wwwroot文件夹中的index.html文件进行修改,如下图一所示。使用make命令生成serverTcp可执行程序,使用./udpServer 8081命令运行serverTcp可执行程序,打开浏览器输入123.60.25.237:8081,向服务端发起请求,如下图二所示。

wwwroot文件夹中的index.html文件:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>104 期测试</title>
</head>
<body>
    <h3>hello my server!</h3>
    <p>我终于测试完了我的代码</p>
    <form action="/a/b/c.html" method="get">
        Username: <input type="text" name="user"><br>
        Password: <input type="password" name="passwd"><br>
        <input type="submit" value="Submit">
    </form>
</body>
</html>

注:

1.在index.html文件中,我们新增一个表单标签,action表示表单提交后要做出反应的网页,这里我们没有其他的网页因此先随便传个字符串,method表示http方法,br标签表示换行,<form...>和</form>之间是表单内容,input标签对应浏览器的输入框,type表示输入框的输入类型,text代表文本类型,password表示密码类型,name表示输入框的名字。

当我们在客户端网页中输入完用户密码并点击submit提示符后,index.html文件中的action部分会作为请求URL中带层次的文件路径,Username和Password部分的输入框名字name和输入内容会作为KV键值对与URL中查询字符串KV键值对相对应,如下图所示。

在http中,get方法会以明文的方式将用户对应的参数信息拼接在URL中。

2. 如果将http方法改为post,那么只会将index.html文件中的action部分拼接在URL中,Username和Password部分的输入框名字name和输入内容不再拼接在URL中,而是在请求正文部分。

在http中,post方法会以明文的方式将用户对应的参数信息拼接在请求正文中来进行提交。

1.6.2.GET方法和POST方法

GET方法一般用于获取某种资源信息,而POST方法一般用于将数据上传给服务器。但实际我们上传数据时也有可能使用GET方法,比如百度提交数据时实际使用的就是GET方法。

GET方法和POST方法都可以带参:

\cdot GET方法是通过url传参的。 

\cdot POST方法是通过正文传参的。
从GET方法和POST方法的传参形式可以看出,POST方法能传递更多的参数,因为url的长度是有限制的,POST方法通过正文传参就可以携带更多的数据。 

此外,使用POST方法传参更加私密,因为POST方法不会将你的参数回显到url当中,此时也就不会被别人轻易看到。不能说POST方法比GET方法更安全,因为POST方法和GET方法实际都不安全,二者都是以明文的方式传参的,要做到安全只能通过加密来完成。

1.7.HTTP的状态码

1.7.1.HTTP的状态码介绍

HTTP的状态码如下:

最常见的状态码,比如200(OK),404(Not Found),403(Forbidden请求权限不够),302(Redirect),504(Bad Gateway)。

1.7.2.Redirection重定向状态码

重定向概念:

重定向就是通过各种方法将各种网络请求重新定个方向转到其它位置,此时这个服务器相当于提供了一个引路的服务。

例如:客户端向服务端发起一个请求request,服务端构建一个response进行响应,在response中状态码设为301/302,响应报头部分会有Location字段(后面会讲),该字段的value部分是一个新的URL网址,告诉客户端接下来要去这里访问。客户端浏览器接收到响应response后,就会跳转到这个新的URL网址,给对应新的服务端发送请求,这就是重定向。

Redirection(重定向状态码):

重定向又可分为临时重定向和永久重定向,其中状态码301表示的就是永久重定向,而状态码302和307表示的是临时重定向。

临时重定向和永久重定向本质是影响客户端的标签,决定客户端是否需要更新目标地址。如果某个网站是永久重定向,那么第一次访问该网站时由浏览器帮你进行重定向,但后续再访问该网站时就不需要浏览器再进行重定向了,此时你访问的直接就是重定向后的网站(客户端浏览器记住了重定向后的网址,如果客户端访问了重定向前的网址,浏览器直接默认去访问重定向后的那个网址)。而如果某个网站是临时重定向,那么每次访问该网站时如果需要进行重定向工作,都需要浏览器先向重定向前服务端发送请求,然后收到响应后再帮我们完成重定向,跳转到响应对应的目标网站。

注:临时重定向和永久重定向的选取取决于对应服务器资源是临时不可用还是永久不可用,如果因为更新等原因临时不可用就用临时重定向,如果因为更换资源路径等原因永久不可用就用永久重定向。

1.7.3.Redirection重定向状态码代码使用

创建server.hpp文件,如下图一所示,创建serverTcp.cc文件,如下图二所示,创建Makefile文件,如下图三所示。使用make命令生成serverTcp可执行程序,使用./udpServer 8081命令运行serverTcp可执行程序,打开浏览器输入123.60.25.237:8081,向服务端发起请求,如下图四所示,使用telnet 127.0.0.1 8081命令,如果显示^]表示登陆成功,然后输入ctrl+]进入telnet命令行并回车,使用GET / http/1.0(字母大小写均可)命令,如下图五所示。

server.hpp文件:

#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <cstdio>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
#include <cerrno>
#include <cassert>

#define CRLF "\r\n"
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define HOME_PAGE "index.html"
#define ROOT_PATH "wwwroot"

using namespace std;

std::string getPath(std::string http_request)
{
    std::size_t pos = http_request.find(CRLF);
    if(pos == std::string::npos) return "";
    std::string request_line = http_request.substr(0, pos);
    //GET /a/b/c http/1.1
    std::size_t first = request_line.find(SPACE);
    if(pos == std::string::npos) return "";
    std::size_t second = request_line.rfind(SPACE);
    if(pos == std::string::npos) return "";

    std::string path = request_line.substr(first+SPACE_LEN, second - (first+SPACE_LEN));
    if(path.size() == 1 && path[0] == '/') path += HOME_PAGE;
    return path;
}

std::string readFile(const std::string &recource)
{
    std::ifstream in(recource, std::ifstream::binary);
    if(!in.is_open()) return "404";
    std::string content;
    std::string line;
    while(std::getline(in, line)) content += line;
    in.close();
    return content;
}
void handlerHttpRequest(int sock)
{
    char buffer[10240];
    ssize_t s = read(sock, buffer, sizeof buffer);
    if(s > 0) cout << buffer;
    std::string response = "HTTP/1.1 302 Temporarily Moved\r\n";
    //std::string response = "HTTP/1.1 301 Permanently Moved\r\n";
    response += "Location: https://www.qq.com/\r\n"; 
    response += "\r\n";
    send(sock, response.c_str(), response.size(), 0);
}

class ServerTcp
{
public:
    ServerTcp(uint16_t port, const std::string &ip = "")
        : port_(port),
          ip_(ip),
          listenSock_(-1)
    {
        quit_ = false;
    }
    ~ServerTcp()
    {
        if (listenSock_ >= 0)
            close(listenSock_);
    }

public:
    void init()
    {
        // 1. 创建socket
        listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
        if (listenSock_ < 0)
        {
            exit(1);
        }
        // 2. bind绑定
        // 2.1 填充服务器信息
        struct sockaddr_in local; // 用户栈
        memset(&local, 0, sizeof local);
        local.sin_family = PF_INET;
        local.sin_port = htons(port_);
        ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
        // 2.2 本地socket信息,写入sock_对应的内核区域
        if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
        {
            exit(2);
        }

        // 3. 监听socket,为何要监听呢?tcp是面向连接的!
        if (listen(listenSock_, 5 /*后面再说*/) < 0)
        {
            exit(3);
        }
        // 运行别人来连接你了
    }
    void loop()
    {
        signal(SIGCHLD, SIG_IGN); // only Linux
        while (!quit_)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);

            int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
            if (quit_)
                break;
            if (serviceSock < 0)
            {
                // 获取链接失败
                cerr << "accept error ...." << endl;
                continue;
            }
            // 5.1 v1 版本 -- 多进程版本 -- 父进程打开的文件会被子进程继承吗?会的
            pid_t id = fork();
            assert(id != -1);
            if(id == 0)
            {
                close(listenSock_); //建议
                if(fork() > 0) exit(0);
                //孙子进程
                handlerHttpRequest(serviceSock);
                exit(0); // 进入僵尸
            }
            close(serviceSock);
            wait(nullptr);
        }
    }

    bool quitServer()
    {
        quit_ = true;
        return true;
    }

private:
    // sock
    int listenSock_;
    // port
    uint16_t port_;
    // ip
    std::string ip_;
    // 安全退出
    bool quit_;
};

serverTcp.cc文件:

#include "server.hpp"

static void Usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " port" << std::endl;
    std::cerr << "example:\n\t" << proc << " 8080\n"
              << std::endl;
}

// ./ServerTcp local_port local_ip
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = atoi(argv[1]);

    ServerTcp svr(port);
    svr.init();
    svr.loop();
    return 0;
}

Makefile文件:

serverTcpd:serverTcp.cc
	g++ -o $@ $^ -std=c++11 -lpthread

.PHONY:clean
clean:
	rm -f serverTcpd

注:状态码302为临时重定向,对应的状态描述为Temporarily Moved,状态码301为永久重定向,对应的状态描述为Permanently Moved。响应报头部分设置Location关键字,对应value为要重定向URL网址。

1.8.HTTP常见的Header请求/响应报头

1.8.1.HTTP常见的Header请求/响应报头介绍

HTTP常见的Header如下:

Header请求/响应报头 功能
Content-Type 数据类型(text/html等)
Content-Length 正文的长度
Host 客户端告知服务器,所请求的资源是在哪个主机的哪个端口上
User-Agent 声明用户的操作系统和浏览器的版本信息
Referer 当前页面是哪个页面跳转过来的
Location 搭配3XX状态码使用,告诉客户端接下来要去哪里访问
Connection 链接的方案(长连接/短链接)
Cookie 用于在客户端存储少量信息,通常用于实现会话(session)的功能

Host:

\cdot Host字段表明了客户端要访问的服务的IP和端口,比如当浏览器访问我们的服务器时,浏览器发来的HTTP请求当中的Host字段填的就是我们的IP和端口。但客户端不就是要访问服务器吗?为什么客户端还要告诉服务器它要访问的服务对应的IP和端口?

\cdot 因为有些服务器实际提供的是一种代理服务,也就是代替客户端向其他服务器发起请求,然后将请求得到的结果再返回给客户端。在这种情况下客户端就必须告诉代理服务器它要访问的服务对应的IP和端口,此时Host提供的信息就有效了。

User-Agent:

\cdot User-Agent代表的是客户端对应的操作系统和浏览器的版本信息。
\cdot 比如当我们用电脑下载某些软件时,它会自动向我们展示与我们操作系统相匹配的版本,这实际就是因为我们在向目标网站发起请求的时候,User-Agent字段当中包含了我们的主机信息,此时该网站就会向你推送相匹配的软件版本。

Connection:

\cdot HTTP/1.0是通过request&response的方式来进行请求和响应的,HTTP/1.0常见的工作方式就是客户端和服务器先建立链接,然后客户端发起请求给服务器,服务器再对该请求进行响应,然后立马端口连接。
\cdot 但如果一个连接建立后客户端和服务器只进行一次交互,就将连接关闭,就太浪费资源了,因此现在主流的HTTP/1.1是支持长连接的。所谓的长连接就是建立连接后,客户端可以不断的向服务器一次写入多个HTTP请求,而服务器在上层依次读取这些请求就行了,此时一条连接就可以传送大量的请求和响应,这就是长连接。
如果HTTP请求或响应报头当中的Connect字段对应的值是Keep-Alive,就代表支持长连接,如果Connect字段对应的值是closed,就代表只支持短连接。

注:

1.用户所看到的完整网页内容,背后可能是无数次的http请求,http底层主流采用的是tcp协议,如果网页的每一次请求都采用短链接,那么n次http请求底层要进行3n次的握手和4n次的挥手,这样就大大降低了网页获取的速度,所以短链接实际效率较低。

2.http协议其实是无连接的。http协议和tcp协议是处于不同层的协议,因此两个协议毫无关系,tcp协议是面向连接的与http协议并无关系,http协议底层使用的是tcp协议只是用了tcp协议的能力,利用tcp协议将信道建立好来完成http的工作,http的工作中并不会建立连接。

3.当http采用长连接的方式建立好连接后,给服务端连续发送了多个请求,服务端如果不按照客户端请求的顺序进行响应,有可能会出现问题,所以在一条连接下,服务端会按照客户端在该连接下请求的顺序进行响应。

1.8.2.Cookie

HTTP实际上是一种无状态协议,HTTP的每次请求/响应之间是没有任何关系的,但你在使用浏览器的时候发现并不是这样的。

比如当你登录一次CSDN后,就算你把CSDN网站关了甚至是重启电脑,当你再次打开CSDN网站时,CSDN并没有要求你再次输入账号和密码,这实际上是通过cookie技术实现的,点击浏览器当中锁的标志就可以看到对应网站的各种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技术。

创建server.hpp文件,如下图一所示,创建serverTcp.cc文件,如下图二所示,创建Makefile文件,如下图三所示,在本路径下创建wwwroot文件夹,在wwwroot文件夹中创建index.html文件,写入下图四所示的代码。使用make命令生成serverTcp可执行程序,使用./udpServer 8080命令运行serverTcp可执行程序,打开浏览器输入123.60.25.237:8080,向服务端发起请求,服务端响应,客户端浏览器根据响应中的Set-Cookie值进行cookie设置,如下图五所示,浏览器对cookie设置后,客户端浏览器往后再向对应服务端发送请求时,请求报头部分都会携带cookie字段,如下图六所示。

server.hpp文件:

#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <cstdio>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
#include <cerrno>
#include <cassert>

#define CRLF "\r\n"
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define HOME_PAGE "index.html"
#define ROOT_PATH "wwwroot"

using namespace std;

std::string getPath(std::string http_request)
{
    std::size_t pos = http_request.find(CRLF);
    if(pos == std::string::npos) return "";
    std::string request_line = http_request.substr(0, pos);
    //GET /a/b/c http/1.1
    std::size_t first = request_line.find(SPACE);
    if(pos == std::string::npos) return "";
    std::size_t second = request_line.rfind(SPACE);
    if(pos == std::string::npos) return "";

    std::string path = request_line.substr(first+SPACE_LEN, second - (first+SPACE_LEN));
    if(path.size() == 1 && path[0] == '/') path += HOME_PAGE;
    return path;
}

std::string readFile(const std::string &recource)
{
    std::ifstream in(recource, std::ifstream::binary);
    if(!in.is_open()) return "404";
    std::string content;
    std::string line;
    while(std::getline(in, line)) content += line;
    in.close();
    return content;
}
void handlerHttpRequest(int sock)
{
    char buffer[10240];
    ssize_t s = read(sock, buffer, sizeof buffer);
    if(s > 0) cout << buffer;

    std::string path = getPath(buffer);

    std::string recource = ROOT_PATH;
    recource += path;
    std::cout << recource << std::endl;

    std::string html = readFile(recource);
    std::size_t pos = recource.rfind(".");
    std::string suffix = recource.substr(pos);
    cout << suffix << endl;

    //开始响应
    std::string response;
    response = "HTTP/1.0 200 OK\r\n";
    if(suffix == ".jpg") response += "Content-Type: image/jpeg\r\n";
    else response += "Content-Type: text/html\r\n";
    response += ("Content-Length: " + std::to_string(html.size()) + "\r\n");
    response += "Set-Cookie: this is my cookie content;\r\n";
    response += "\r\n";
    response += html;

    send(sock, response.c_str(), response.size(), 0);
}

class ServerTcp
{
public:
    ServerTcp(uint16_t port, const std::string &ip = "")
        : port_(port),
          ip_(ip),
          listenSock_(-1)
    {
        quit_ = false;
    }
    ~ServerTcp()
    {
        if (listenSock_ >= 0)
            close(listenSock_);
    }

public:
    void init()
    {
        // 1. 创建socket
        listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
        if (listenSock_ < 0)
        {
            exit(1);
        }
        // 2. bind绑定
        // 2.1 填充服务器信息
        struct sockaddr_in local; // 用户栈
        memset(&local, 0, sizeof local);
        local.sin_family = PF_INET;
        local.sin_port = htons(port_);
        ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
        // 2.2 本地socket信息,写入sock_对应的内核区域
        if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
        {
            exit(2);
        }

        // 3. 监听socket,为何要监听呢?tcp是面向连接的!
        if (listen(listenSock_, 5 /*后面再说*/) < 0)
        {
            exit(3);
        }
        // 运行别人来连接你了
    }
    void loop()
    {
        signal(SIGCHLD, SIG_IGN); // only Linux
        while (!quit_)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);

            int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
            if (quit_)
                break;
            if (serviceSock < 0)
            {
                // 获取链接失败
                cerr << "accept error ...." << endl;
                continue;
            }
            // 5.1 v1 版本 -- 多进程版本 -- 父进程打开的文件会被子进程继承吗?会的
            pid_t id = fork();
            assert(id != -1);
            if(id == 0)
            {
                close(listenSock_); //建议
                if(fork() > 0) exit(0);
                //孙子进程
                handlerHttpRequest(serviceSock);
                exit(0); // 进入僵尸
            }
            close(serviceSock);
            wait(nullptr);
        }
    }

    bool quitServer()
    {
        quit_ = true;
        return true;
    }

private:
    // sock
    int listenSock_;
    // port
    uint16_t port_;
    // ip
    std::string ip_;
    // 安全退出
    bool quit_;
};

serverTcp.cc文件:

#include "server.hpp"

static void Usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " port" << std::endl;
    std::cerr << "example:\n\t" << proc << " 8080\n"
              << std::endl;
}

// ./ServerTcp local_port local_ip
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = atoi(argv[1]);

    ServerTcp svr(port);
    svr.init();
    svr.loop();
    return 0;
}

Makefile文件:

serverTcpd:serverTcp.cc
	g++ -o $@ $^ -std=c++11 -lpthread

.PHONY:clean
clean:
	rm -f serverTcpd

wwwroot文件夹中的index.html文件:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>104 期测试</title>
</head>
<body>
    <h3>hello my server!</h3>
    <p>我终于测试完了我的代码</p>
    <form action="/a/b/c.html" method="get">
        Username: <input type="text" name="user"><br>
        Password: <input type="password" name="passwd"><br>
        <input type="submit" value="Submit">
    </form>
</body>
</html>

1.8.3.Session

内存级别和文件级别:

cookie就是在浏览器当中的一个小文件,文件里记录的就是用户的私有信息。cookie文件可以分为两种,一种是内存级别的cookie文件,另一种是文件级别的cookie文件。

\cdot 将浏览器关掉后再打开,访问之前登录过的网站,如果需要你重新输入账号和密码,说明你之前登录时浏览器当中保存的cookie信息是内存级别的。
\cdot 将浏览器关掉甚至将电脑重启再打开,访问之前登录过的网站,如果不需要你重新输入账户和密码,说明你之前登录时浏览器当中保存的cookie信息是文件级别的。

cookie被盗:

\cdot 如果你浏览器当中保存的cookie信息被非法用户盗取了,那么此时这个非法用户就可以用你的cookie信息,以你的身份去访问你曾经访问过的网站,我们将这种现象称为cookie被盗取了。
\cdot 比如你不小心点了某个链接,这个链接可能就是一个下载程序,当你点击之后它就会通过某种方式把程序下载到你本地,并且自动执行该程序,该程序会扫描你的浏览器当中的cookie目录,把所有的cookie信息通过网络的方式传送给恶意方,当恶意方拿到你的cookie信息后就可以拷贝到它的浏览器对应的cookie目录当中,然后以你的身份访问你曾经访问过的网站。

SessionID:

\cdot 单纯的使用cookie是非常不安全的,因为此时cookie文件当中就保存的是你的私密信息,一旦cookie文件泄漏你的隐私信息也就泄漏。

\cdot 所以当前主流的服务器还引入了SessionID这样的概念,当我们第一次登录某个网站输入账号和密码后,服务器认证成功后会在服务器中形成一个session文件,用户的临时私密信息保存在这个session文件中,每个用户对应的session文件名为SessionID,每个用户对应的session文件名即SessionID是唯一的。服务端生成的SessionID与用户信息是不相关的,系统会将所有登录用户的SessionID值统一维护起来。

\cdot 此时当认证通过后服务端在对浏览器进行HTTP响应时,就会将这个生成的SessionID值响应给浏览器。浏览器收到响应后会自动提取出SessionID的值,将其保存在浏览器的cookie文件当中。后续访问该服务器时,对应的HTTP请求当中就会自动携带上这个SessionID。

\cdot 而服务器识别到HTTP请求当中包含了SessionID,就会提取出这个SessionID,然后再到对应的集合当中进行对比,对比成功就说明这个用户是曾经登录过的,此时也就自动就认证成功了,然后就会正常处理你发来的请求,这就是我们当前主流的工作方式。

安全是相对的:

引入SessionID之后,浏览器当中的cookie文件保存的是SessionID,此时这个cookie文件同样可能被盗取。此时用户的账号和密码虽然不会泄漏了,但用户对应的SessionID是会泄漏的,非法用户仍然可以盗取我的SessionID去访问我曾经访问过的服务器,相当于还是存在刚才的问题。

\cdot 之前的工作方式就相当于把账号和密码信息在浏览器当中再保存一份,每次请求时都自动将账号和密码的信息携带上,但是账号和密码一直在网当中发送太不安全了。
\cdot 因此现在的工作方式是,服务器只有在第一次认证的时候需要在网络中传输账号和密码,此后在网络上发送的都是SessionID。
这种方法虽然没有真正解决安全问题,但这种方法是相对安全的。互联网上是不存在绝对安全这样的概念的,任何安全都是相对的,就算你将发送到网络当中的信息进行加密,也有可能被别人破解。

不过在安全领域有一个准则:如果破解某个信息的成本已经远远大于破解之后获得的收益(说明做这个事是赔本的),那么就可以说这个信息是安全的。

引入SessionID后的好处:

\cdot 在引入SessionID之前,用户登录的账号信息都是保存在浏览器内部的,此时的账号信息是由客户端去维护的。
\cdot 而引入SessionID后,用户登录的账号信息是有服务器去维护的,在浏览器内部保存的只是SessionID。
此时虽然SessionID可能被非法用户盗取,但服务器也可以使用各种各样的策略来保证用户账号的安全。

\cdot IP是有归类的,可以通过IP地址来判断登录用户所在的地址范围。如果一个账号在短时间内登录地址发送了巨大变化,此时服务器就会立马识别到这个账号发生异常了,进而在服务器当中清除对应的SessionID的值。这时当你或那个非法用户想要访问服务器时,就都需要重新输入账号和密码进行身份认证,而只有你是知道自己的密码的,当你重新认证登录后服务器就可以将另一方识别为非法用户,进而对该非法用户进行对应的黑名单/白名单认证。

\cdot 当操作者想要进行某些高权限的操作时,会要求操作者再次输入账号和密码信息,再次确认身份。就算你的账号被非法用户盗取了,但非法用户在改你密码时需要输入旧密码,这是非法用户在短时间内无法做到的,因为它并不知道你的密码。这也就是为什么账号被盗后还可以找回来的原因,因为非法用户无法在短时间内修改你的账号密码,此时你就可以通过追回的方式让当前的SessionID失效,让使用该账号的用户进行重新登录认证。

\cdot SessionID也有过期策略,比如SessionID是一个小时内是有效的。所以即便你的SessionID被非法用户盗取了,也仅仅是在一个小时内有效,而且在功能上受约束,所以不会造成太大的影响。

任何事情都有两面性,如果不是这些非法用户的存在,现在的服务器肯定是漏洞百出,只有双方不断进行对抗双方才能不断进步。

猜你喜欢

转载自blog.csdn.net/qq_45113223/article/details/130778488