TCP/IP协议簇之应用层

应用层

我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层.

再谈 “协议”

协议是一种 “约定”. socket api的接口, 在读写数据时, 都是按 “字符串”(字符串描述不准确) 的方式来发送接收的. 如果我们要传输一些"结构化的数据" 怎么办呢?

通过序列化与反序列化实现,我们发送数据至网络要进行序列化,将“结构化的数据”合为一个整体,网络发送数据给另一端接收时,要进行反序列化,将整体数据又转化为“结构化数据”。进行序列化的工具有json,xml

网络版计算器

例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端.
约定方案一:

  • 客户端发送一个形如"1+1"的字符串;
  • 这个字符串中有两个操作数, 都是整形;
  • 两个数字之间会有一个字符是运算符, 运算符只能是 + ;
  • 数字和运算符之间没有空格;

约定方案二:

  • 定义结构体来表示我们需要交互的信息;
  • 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;
  • 这个过程叫做 “序列化” 和 “反序列化”

protocol.hpp

// protocol.hpp 定义通信的结构体
#pragma once

typedef struct request{
  int x;
  int y;
  char op;//操作符

}request_t;

typedef struct response{
  //code为0:运算正常
  //code为1:/的除数为0
  //code为2:%的除数为0
  //code为3:操作符不合法
  int code;//退出码
  int result;//结果
}response_t;

// client.hpp
#pragma once
#include<iostream>
#include<string>
#include<arpa/inet.h>
#include <netinet/in.h>
#include<sys/socket.h>
#include<unistd.h>
#include<sys/wait.h>
#include"protocol.hpp"

class client{
  private:
    std::string _ip;//服务器ip
    int _port;//服务器端口号
    int _sock;

  public:
    //构造函数
    client(std::string ip = "127.0.0.1", int port = 8080)
      :_ip(ip)
      ,_port(port)
    {}

    //初始化
    void init_client()
    { 
      //创建套接字
      _sock = socket(AF_INET, SOCK_STREAM, 0);
      if (_sock < 0)
      {
        std::cerr << "socket error !" << std::endl;
        exit(1);
      }

      //connect
      //服务器信息
      sockaddr_in svr;
      svr.sin_family = AF_INET;
      svr.sin_port = htons(_port);
      svr.sin_addr.s_addr = inet_addr(_ip.c_str());
      
      if (connect(_sock, (sockaddr*)&svr, sizeof(svr)) < 0)
      {
        std::cerr << "connect error !" << std::endl;
        exit(2);
      }

    }


    void start()
    {
      request_t rq;
      response_t rsp;

      std::cout << "请输入第一个操作数: ";
      std::cin >> rq.x;
      std::cout << "请输入操作符: ";
      std::cin >> rq.op;
      std::cout << "请输入第二个操作数: ";
      std::cin >> rq.y;

      //发送给服务器计算
      send(_sock, &rq, sizeof(rq), 0);

      //接收结果
      ssize_t s = recv(_sock, &rsp, sizeof(rsp), 0);
      if (s > 0)
      {
        if (rsp.code != 0)
        {
          std::cout << "输入错误 !" << std::endl;
          std::cout << "code: " << rsp.code << std::endl;
        }
        else 
        {
          std::cout << rq.x << " " << rq.op << " " << rq.y << " = " << rsp.result << std::endl;
      
        }

      }
    }

    //析构
    ~client()
    {
      close(_sock);
    }
};

server.hpp

#pragma once
#include<iostream>
#include<arpa/inet.h>
#include <netinet/in.h>
#include<sys/socket.h>
#include<unistd.h>
#include<sys/wait.h>
#include"protocol.hpp"

class server{
  private:
    int _port;//服务器端口号
    int _lsock;//监听套接字

  public:
    //构造函数
    server(int port = 8080)
      :_port(port)
    {}

    //初始化
    void init_server()
    { 
      //创建套接字
      _lsock = socket(AF_INET, SOCK_STREAM, 0);
      if (_lsock < 0)
      {
        std::cerr << "socket error !" << std::endl;
        exit(1);
      }

      //绑定套接字
      sockaddr_in local;
      local.sin_family = AF_INET;
      local.sin_port = htons(_port);
      local.sin_addr.s_addr = INADDR_ANY;

      if (bind(_lsock, (struct sockaddr*)&local, sizeof(local)) < 0)
      {
        std::cerr << "bind error !" << std::endl;
        exit(2);
      }

      //监听
      if (listen(_lsock, 5) != 0)
      {
        std::cerr << "listen error !" << std::endl;
        exit(3);
      }

    }

    void cal(int sock)
    {
      //短链接完成服务
      request_t rq;
      response_t rsp = {0, 0};
    

      //接收计算信息
      ssize_t s = recv(sock, &rq, sizeof(rq), 0);  
      if (s > 0)
      {
        switch(rq.op)
        {
          case '+':
            rsp.result = rq.x + rq.y;
            break;
          case '-':
            rsp.result = rq.x - rq.y;
            break;
          case '*':
            rsp.result = rq.x * rq.y;
            break;
          case '/':
            if (rq.y == 0)
            {
              rsp.code = 1;
              break;
            }
            rsp.result = rq.x / rq.y;
            break;
          case '%':
            if (rq.y == 0)
            {
              rsp.code = 2;
              break;
            }
            rsp.result = rq.x % rq.y;
            break;
          default:
            rsp.code = 3;
        }  
      }

      //返回结果
      send(sock, &rsp, sizeof(rsp), 0);

      //短链接处理,所以这里就closesock
      close(sock);
    }

    void start()
    {
      //连接客户端
      
      struct sockaddr_in end_point;
      socklen_t len = sizeof(&end_point);

      while (1)
      {

        int sock = accept(_lsock, (struct sockaddr*)&end_point, &len);
        if (sock < 0)
        {
          std::cerr << "accept error !" << std::endl;
          continue;
        }

        //创建子进程进行计算
        if (fork() == 0)
        {
          //子进程
          close(_lsock);
          
          if (fork() > 0)
          {
            //还是子进程
            exit(0);//退出,防止阻塞子进程
          }
          else//孙子进程
          {
            cal(sock);
            exit(0);
          }
        }
        close(sock);
        waitpid(-1, nullptr, 0);
      
      }
    }

    //析构
    ~server()
    {
      close(_lsock);
    }
};

无论我们采用方案一, 还是方案二, 还是其他的方案, 只要保证, 一端发送时构造的数据, 在另一端能够正确的进行解析, 就是ok的,比如输入顺序。这种约定, 就是 应用层协议

我们对上述计算进行抓包:sudo tcpdump -i any -nn tcp port 8080
三次握手,四次挥手

image-20211120172104759

HTTP协议

虽然我们说, 应用层协议是我们程序猿自己定的.
但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用. HTTP(超文本传输协议)就是其中之一.

认识URL

平时我们俗称的 “网址” 其实就是说的 URL

image-20211120131311304

互联网行为

  1. 把服务器的数据拿下来
  2. 把自己的数据传上服务器

urlencode和urldecode

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

例如:

image-20211122200240970

“+” 被转义成了 “%2B”
urldecode就是urlencode的逆过程

urlencode工具

HTTP三大特点

  1. 无连接。前面我们知道,TCP是要连接的,但TCP建立连接和http无关,http直接向服务器发送http request即可

  2. 无状态。我们用一个例子来理解无状态:当我们访问一个网站时,需要登陆,如果我们此次登陆了,下一次再访问时,由于http的无状态特性,我们需要再次输入账号密码才能登陆。这就是http的无状态。而我们实际登陆时,其实是登陆一次后之后再访问就不用输入账号密码了,这是由cookie和session实现的

  3. 简单快速。短链接进行文本(html、img、css、js…)传输,这是早期的http/1.0的传输方式

    http/1.0支持长连接

HTTP协议格式

HTTP构成:

image-20211122205008436

可以通过Fiddler进行抓包。
原理:正常上网时,我们直接通过网络发送请求给服务器即可。而Fiddler抓包是我们先给Filddler进行代理,Fiddler再通过网路发送请求给服务器,并接收响应

HTTP请求

image-20211122204911773

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

HTTP响应

image-20211122205048762

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

HTTP的方法

方法说明支持的HTTP协议版本
GET获取资源1.0、1.1
POST传输实体主体1.0、1.1
PUT传输文件1.0、1.1
HEAD获得报文首部1.0、1.1
DELETE删除文件1.0、1.1
OPTIONS询问支持的方法1.1
TRACE追踪路径1.1
CONNECT要求用隧道协议连接代理1.1
LINK建立和资源之间的联系1.0
UNLINE断开连接关系1.0

其中最常用的就是GET方法和POST方法.
Head不获取正文信息,只获取前三个信息(请求/响应行、报头、空行)

HTTP的状态码

image-20211122205613128

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

HTTP常见Header

  • Content-Type: 数据类型(text/html等)
  • Content-Length: Body的长度
  • Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
  • User-Agent: 声明用户的操作系统和浏览器版本信息;
  • referer: 当前页面是从哪个页面跳转过来的;
  • location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
  • Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;

User-Agent里的历史故事

Cookie的介绍:之前我们了解了http的无状态特性,这样会给用户带来很差的体验,而cookie就是来解决这个问题的。
原理:同样以网站的登录为例来理解,我们在网站上第一次登录时,服务器端就保存了用户名和密码的信息,服务器进行响应时,会在response中包含set-cookie:用户名、密码…的信息,客户端收到后,浏览器就在cookie这个文件中保存了这个信息。下一次进入这个网站,浏览器对网站发送请求时,就会携带这个cookie文件与request一起发送给服务器,服务器进行核对,核对正确后,就不再需要我们手动输入密码了。

cookie的本质是浏览器的一个文件,它分为内存级的和磁盘级的。内存级的cookie只在当前浏览器打开时间内有效,关闭浏览器下次再打开时,我们再次进入网站,还是需要输入账号密码进行登录。磁盘级的cookie则是保存在本地的,这样就不受浏览器打开这一条件限制,可以保存较长的时间。
但实际上,这样也是有风险的,万一我们被黑客入侵,获取到了本地的cookie,他就可以通过这个cookie访问我们同样访问过的信息。

Session就是与Cookie配套使用的。我们第一次登录后,服务器不再将用户名和密码response回来,而是在服务器上用一个session文件保存我们的私密信息,然后生成一个唯一的sid来标识它,之后同样在response里,由set-cookie:sid,此时是将sid返回,本地的cookie中保存sid。下一次访问网站时,浏览器携带cookie发送请求,服务器通过sid找到客户的信息,然后允许客户访问。

cookie和session是相对安全的,但还是有可能会被盗取。此时盗取的只是sid,而不是我们的用户名和密码了。而且他也不知道sid对应的是哪个网站。而要改密码是要原密码的,除非我们密码太简单被别人识破,否则他就不能修改我们的密码然后让我们永远地失去这个账号,除非把后台服务器攻破,拿到密码。

最简单的HTTP服务器

实现一个最简单的HTTP服务器, 只在网页上输出 “hello world”; 只要我们按照HTTP协议的要求构造数据, 就很容易能做到;

#include<iostream>
#include<string.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<signal.h>
#include<string>

#define BAGLOG 5

using namespace std;

class httpserver{
  private:
    int _port;
    int _lsock;

  public:
    //构造
    httpserver(int port)
    :_port(port)
    ,_lsock(-1)
    {}

    //析构
    ~httpserver()
    {
      if (_lsock != -1)
        close(_lsock);
    }

    void initServer()
    {
      signal(SIGCHLD, SIG_IGN);
      //创建套接字
      _lsock = socket(AF_INET, SOCK_STREAM, 0);
      if (_lsock < 0)
      {
        cerr << "socket error !" << endl;
        exit(1);
      }

      //绑定
      struct sockaddr_in local;
      //将local的内容清零
      bzero(&local, sizeof(local));
      local.sin_family = AF_INET;
      local.sin_port = htons(_port);
      local.sin_addr.s_addr = INADDR_ANY;
      if (bind(_lsock, (struct sockaddr*)&local, sizeof(local)) < 0)
      {
        cerr << "bind error !" << endl;
        exit(2);
      }

      //监听
      if (listen(_lsock, BAGLOG) != 0)
      {
        cerr << "listen error !" << endl;
        exit(3);
      }

    }

    void EchoHttp(int sock)
    {
      //短链接执行
      //接收数据
      char request[1024];
      ssize_t s = recv(sock, request, sizeof(request) - 1, 0);
      if ( s > 0 )
      {
        request[s] = 0;
        //打印请求
        cout << request << endl;
        //响应
        //响应行
        string response = "HTTP/1.0 200 OK\r\n";
        //响应报头
        response += "Content-type: text/html\r\n";//发送的类型是html,网页类型
        //响应空行
        response += "\r\n";
        //响应正文
        response += "\
          <html>\
          <head>\
            <title>ysj</title>\
          </head>\
          <body>\
            <h1>Welcome</h1>\
            <p>Hello World !</p>\
          </body>\
        </html>\r\n";
        send(sock, response.c_str(), response.size(), 0); 
      }

      close(sock);

    }


    void start()
    {
      while (1)
      {
        //建立连接
        sockaddr_in peer;
        socklen_t len = sizeof(peer);
        bzero(&peer, len);
        
        int sock = accept(_lsock, (struct sockaddr*)&peer, &len);
        if (sock < 0)
        {
          cerr << "accept error !" << endl;
          continue;
        }
        
        cout << "get a link..." << endl;

        //创建子进程处理任务
        pid_t id = fork();
        if (id == 0)//子进程
        {
          //父进程忽略子进程发送的SIGCHLD信号
          close(_lsock);
          EchoHttp(sock);
          exit(0);
        }

        close(sock);
      }
      
    }
};

编译, 启动服务. 在浏览器中输入 http://[ip]:[port], 就能看到显示的结果 “Hello World”

image-20211125120359262

image-20211125120437918

备注:
此处我们使用 8080 端口号启动了HTTP服务器. 虽然HTTP服务器一般使用80端口,但这只是一个通用的习惯. 并不是说HTTP服务器就不能使用其他的端口号.
使用chrome测试我们的服务器时, 可以看到服务器打出的请求中还有一个 GET /favicon.ico HTTP/1.1 这favicon.ico_百度百科 (baidu.com)

如果我们把状态码设置成404会不会显示经典的Not Found页面?
答案是:不会的,页面的显示是要靠html来设置的,不是由状态码决定的

临时重定向和永久重定向

配合状态码3xx使用。浏览器在接收到重定向响应的时候,会采用该响应提供的新的 URL ,并立即进行加载;大多数情况下,除了会有一小部分性能损失之外,重定向操作对于用户来说是不可见的。

将上面的服务器改造,设置成重定向:
响应报头中设置:location: http://baidu.com\r\n
不需要正文。

 		//响应行
        string response = "HTTP/1.0 302 Found\r\n";
        //响应报头
        response += "Content-type: text/html\r\n";//发送的类型是html,网页类型
        response += "location: https://www.baidu.com\r\n";//发送的类型是html,网页类型
        //响应空行
        response += "\r\n";

输入云服务器公网ip加端口号后,就会跳转到百度了

http与https

https是在http与传输层直接再加了一层SSL/TLS(TLS是SSL的标准化版本):对数据进行加密/解密。
大部分情况下都是使用对称加密。

对称加密与非对称加密

对称加密:只有一个密钥,按同一个密钥进行加密和解密。客户端将数据按照密钥进行加密,服务器按照密钥对数据进行解密。密钥只有客户端和服务器双方知道。即将密钥也发送给服务器,但是这有可能会被黑客劫持,所以单纯的对称加密是不安全的。

于是就有了非对称加密:
非对称加密:通过公钥和私钥的方式来进行加密和解密。通常情况下,公钥用来加密,私钥用来解密。

下图展示了非对称加密发送密钥的过程:
image-20211125104107074

服务器有很多种公钥私钥的算法,客户端与服务器协商使用哪种算法,协商好后,各自会生成一对公钥私钥。服务器将自己的公钥发送给客户端,私钥留给自己,这样,客户端就可以用服务器的公钥对发送的数据加密,而加密的数据只能用服务器的私钥来解密。然后客户端也将自己的公钥发送给服务器,服务器得到客户端的公钥后,就可以将数据按照公钥加密发送给客户端,同样,只有客户端的私钥才能对其解密。

现在我们虽然可以对数据进行加密,但是我们还不清楚对方是否是我们想要沟通的对象,服务端需要申请SSL证书来证明它的身份,要让SSL证书生效就要向CA(Certificate Authority证书授权中心,大家都信任这个机构颁发的证书)申请,证书表明了域名是属于谁的等等信息。

在客户端和服务器完成三次握手后,双方会协商加密方法,然后服务器会发送证书给客户端以示自己的身份。

混合加密

虽然我们可以用非对称加密来进行通信,但每次通信都用非对称加密,加密解密的时间就要花费很多时间,影响效率。所以实际上采用的是两者混合的方式进行加密,即先通过非对称加密获取双方的公钥后,服务器再通过非对称加密方式发送对称密钥,这样客户端收到后,两者就可以按照对称加密的方式进行通信了。

为什么要使用对称加密?

非对称加密:效率比较低
对称加密:效率较高

  • 15
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 24
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WoLannnnn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值