Linux篇第二十篇——HTTP协议(认识协议+HTTP协议+HTTPS)

Posted 呆呆兽学编程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux篇第二十篇——HTTP协议(认识协议+HTTP协议+HTTPS)相关的知识,希望对你有一定的参考价值。

⭐️ 本篇博客开始给大家介绍应用层的一个协议——HTTP协议,我会带大家先认识协议,再谈HTTP协议的格式、报头、状态码和方法等相关细节,我还会简单介绍一下在他基础之上扩增一层软件层的协议——HTTPS,它相比HTTP来说,要更安全一些。话不多说,进入今天主要内容~

目录


🌏认识协议

协议是一种“约定”,这种约定是双方都知道的。有了一致的约定,双方才能够正常地进行通信。协议在网络的第一篇博客中也提到过,协议是双方进行通信的基础,在网络通信中存在着各种协议,有了这些协议,网络的通信才能够正常运转。

今天我要用一个例子带大家认识协议——网络计算器。我结合上一篇博客的线程池版本的TCP服务器进行编写这个网络计算器。
大致过程如下:

  • 客户端向服务端发送一个请求数据包
  • 服务端将请求数据包进行解析,并且进行业务处理,然后返回一个响应数据包给客户端
  • 客户端将响应数据包进行解析,得到计算结果

注意: 客户端将请求封装成一个数据包,该过程叫做序列化,服务端将请求数据包进行解析的过程叫做反序列化。目前市面上有json、xml等格式,都可以供程序员进行该操作。

协议定制:

  • 请求数据包用一个结构体进行封装,里面有两个操作数和一个操作符
  • 响应数据包也用一个结构体进行封装,里面有计算结果和状态码

协议的头文件如下:

/**Protocol.hpp**/
#pragma once

struct request

  int _num1;
  int _num2;
  char _op;

  request(int num1, int num2, char op)
    :_num1(num1)
     ,_num2(num2)
     ,_op(op)
  
;

struct response

  int _code;// 0 正常 1 除以0错误  2 操作符选择错误
  int _result;// 结果

  response(int code, int result)
    :_code(code)
     ,_result(result)
  
;

客户端填充请求数据包: 客户端需要让用户输入两个操作数和一个操作符,然后填充好请求数据包(序列化),并且调用send将请求数据包发送过去,然后reccv接受服务端发送过来的响应数据包,并且进行解析(反序列化),分析出状态码和结果即可,代码如下(客户端发起请求部分的代码,服务器创建和上篇博客代码一样):

void Request()

  std::string msg;
  while (1)
    request rq(0, 0, 0);
    std::cout << "Please Enter first num# ";
    std::cin >> rq._num1;
    std::cout << "Please Enter second num# ";
    std::cin >> rq._num2;
    std::cout << "Please Enter(format:num1(+-*/)num2)# ";
    std::cin >> rq._op;
    
    send(_sock, &rq, sizeof(rq), 0);
    response rp(0, 0);
    ssize_t size = recv(_sock, &rp, sizeof(rp), 0);
    if (size <= 0)
      std::cerr << "read error" << std::endl;
      exit(-1);
    
    if (rp._code == 1)
      std::cout << "code: " << rp._code << std::endl;
      //std::cout << "除零错误" << std::endl;
    
    else if (rp._code == 2)
      std::cout << "code: " << rp._code << std::endl;
      //std::cout << "非法操作符" << std::endl;
    
    else
      std::cout << "code: " << rp._code << std::endl;
      std::cout << "result: " << rp._result << std::endl;
    
  

服务端响应: 这里使用了线程池为每个服务端提供服务,这里只需要修改Task中的Run方法即可,也就是修改业务处理的部分,代码如下:

static void Service(std::string ip, int port, int sock)

  while (1)
    request rt(0, 0, 0); 
    ssize_t size = recv(sock, &rt, sizeof(rt), 0);// 阻塞方式读取 
    if (size > 0)
      // 正常读取size字节的数据
      
      std::cout << "[" << ip << "]:[" << port  << "]# "<< rt._num1 << rt._op << rt._num2 << "=?"<< std::endl;
      response rp(0, 0);
      switch(rt._op)
      
        case '+':
          rp._result = rt._num1 + rt._num2; 
          break;
        case '-':
          rp._result = rt._num1 - rt._num2; 
          break;
        case '*':
          rp._result = rt._num1 * rt._num2; 
          break;
        case '/':
          if (rt._num2 == 0)
            rp._code = 1;
          
          else
            rp._result = rt._num1 / rt._num2; 
          
          break;
        default:
          rp._code = 2;
          break;
      
      send(sock, &rp, sizeof(rp), 0);
    
    else if (size == 0)
      // 对端关闭
      std::cout << "[" << ip << "]:[" << port  << "]# close" << std::endl;
      break;
    
    else
      // 出错
      std::cerr << sock << "read error" << std::endl; 
      break;
    
  

  close(sock);
  std::cout << "service done" << std::endl;


struct Task

  int _port;
  std::string _ip;
  int _sock;

  Task(int port, std::string ip, int sock)
    :_port(port)
    ,_ip(ip)
     ,_sock(sock)
  
  void Run()
  
      Service(_ip, _port, _sock);
  
;

代码运行效果如下:

正常运算:

错误处理: 状态码为1,表示发生除0错误

🌏HTTP协议

🌲介绍

HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,本质是基于TCP协议来进行文本设置完成协议通信。HTTP协议支持客户端——服务端模式,也就是请求与响应模式,且客户端需要以浏览器的方式访问服务端。

上网的大部分行为就是进行进程间通信,获取信息和发送信息:

  1. 把服务器资源拿到本地(下载资源、刷视频等)
  2. 把本地资源推送到服务器(搜索信息)

🌲URL、URI和URN

  • URI: 统一资源标识符(Uniform Resource Identifier),用来标识资源的唯一性
  • URN: 统一资源名称(Uniform Resource Name),用名字标识资源
  • URL: 统一资源定位符(Uniform Resource Locator),给互联网上的每一个文件资源都贴上这样一个唯一标签,并且包含了资源位置信息和访问方式浏览器可以通过URL中的文件位置信息找到对应的资源文件。

结构:

  • 协议方案名: 发起请求用到的协议
  • 登录信息: 登录认证是用的的信息,通常被忽略
  • 服务器地址: 访问资源所在的服务器的地址,也就是域名(字符串风格的)
  • 端口号: 服务器绑定的端口号
  • 文件路径: 访问资源在目标服务器上的位置信息
  • 查询字符串: 查询信息
  • 片段标识符: 对某些资源信息的描述与补充

三者关系与区别:

  • URL是URI的一种,URL是URI的一种具体表现,包含了资源的位置信息和获取资源的方式
  • URN是URI的一种,用特定命名字标识资源,但不包含访问方式。

🌲urlencode和urldecode

urlcode是一种编码方式,urldecode是一种解码方式。在客户端向服务器发起http请求时,为了方便http服务器识别,会将请求进行urlcode编码,同样地httpserver也会对这些字符进行urldecode解码。
编码规则:

  1. 数字,字母和连字符不作处理
  2. 中文字符和特殊字符会进行编码
  3. 要转码的字符会被转为16进制,从右至左取4位,每两位为以为,前面加上%,编码成%XY格式

代码实现如下:

string UrlEncode(const string& szToEncode)

    string src = szToEncode;
    char hex[] = "0123456789ABCDEF";
    string dst;
 
    for (size_t i = 0; i < src.size(); ++i)
    
        unsigned char cc = src[i];
        if (isascii(cc))
        
            if (cc == ' ')
            
                dst += "%20";
            
            else
                dst += cc;
        
        else
        
            unsigned char c = static_cast<unsigned char>(src[i]);
            dst += '%';
            dst += hex[c / 16];
            dst += hex[c % 16];
        
    
    return dst;

string UrlDecode(const string& szToDecode)

    string result;
    int hex = 0;
    for (size_t i = 0; i < szToDecode.length(); ++i)
    
        switch (szToDecode[i])
        
        case '+':
            result += ' ';
            break;
        case '%':
            if (isxdigit(szToDecode[i + 1]) && isxdigit(szToDecode[i + 2]))
            
                string hexStr = szToDecode.substr(i + 1, 2);
                hex = strtol(hexStr.c_str(), 0, 16);
                //字母和数字[0-9a-zA-Z]、一些特殊符号[$-_.+!*'(),] 、以及某些保留字[$&+,/:;=?@]
                //可以不经过编码直接用于URL
                if (!((hex >= 48 && hex <= 57) || //0-9
                    (hex >=97 && hex <= 122) ||   //a-z
                    (hex >=65 && hex <= 90) ||    //A-Z
                    //一些特殊符号及保留字[$-_.+!*'(),]  [$&+,/:;=?@]
                    hex == 0x21 || hex == 0x24 || hex == 0x26 || hex == 0x27 || hex == 0x28 || hex == 0x29
                    || hex == 0x2a || hex == 0x2b|| hex == 0x2c || hex == 0x2d || hex == 0x2e || hex == 0x2f
                    || hex == 0x3A || hex == 0x3B|| hex == 0x3D || hex == 0x3f || hex == 0x40 || hex == 0x5f
                    ))
                
                    result += char(hex);
                    i += 2;
                
                else result += '%';
            else 
                result += '%';
            
            break;
        default:
            result += szToDecode[i];
            break;
        
    
    return result;

在线工具: https://tool.chinaz.com/tools/urlencode.aspx

🌲HTTP协议格式

🍯请求协议格式

我们可以编写一个简单的基于TCP协议的服务器,并且通过以浏览器作为客户端,对服务器发起请求,并把请求部分打印下来,代码如下:

#include <iostream>
#include <unistd.h>
#include <fstream>
#include <cstring>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <string>

using namespace std;

int main()

  // 创建套接字
  int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
  if (listen_sock < 0)
    cerr << "socket creat fail" << endl;
    exit(1);
  
  cout << "socket creat succes, socket: " << listen_sock << endl;

  // 绑定
  struct sockaddr_in local;

  memset(&local, 0, sizeof(local));

  local.sin_family = AF_INET;
  local.sin_port = htons(8081);
  local.sin_addr.s_addr = INADDR_ANY;

  
  if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
    cerr << "bind fail" << endl;
    exit(2);
  
  cout << "bind success" << endl;

  // 将套接字设置为监听状态
  if (listen(listen_sock, 5) < 0)
    cerr << "listen fail" << endl;
    exit(3);
  
  
  struct sockaddr_in peer;
  socklen_t len = sizeof(peer);
  while (1)
    // 获取连接
    int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
    if (sock < 0)
      cerr << "accept error" << endl;
      continue;
    

    // 创建子进程
    if (fork() == 0)
      close(listen_sock);
      if (fork() > 0)
        exit(-1);
      
      else
        // 孙子进程
        while (1)
          char buf[1024];
          ssize_t size = recv(sock, buf, sizeof(buf), 0);
          if (size >  0)
            buf[size] = 0;
            cout << "#################################### http begin ############################################" << endl;
            cout << buf << endl;
            cout << "#################################### http   end ############################################" << endl;
            
          
          else if (size == 0)
            cout << "close" << endl;
            break;
          
          else
            cerr << "recv error" << endl;
          
        
        close(sock);
        exit(0);
      
    

    close(sock);
    waitpid(-1, nullptr, 0);
  
  return 0;

下面是通过浏览器对我们的服务器发起请求:

请求内容:

上面是一次请求的内容,大体分为四个部分:请求行、请求报头、空行和请求正文

  • 请求行: 请求方法+请求url+http协议版本
  • 请求报头: 请求相关属性信息,以key:value的形式显示,且用空行分隔每一个属性信息
  • 空行: 分隔报头和报文
  • 请求正文: 允许为空,且如果请求方法为post,请求报头中会有Content-Length属性字段来标识请求正文的长度

回答几个小问题:

1、HTTP协议如何保证报头和有效载荷分离?

通过空行来分离报头和有效载荷,空行之后都是有效载荷的内容,且一般还会有Content-Length属性字段来标识正文的长度

2、服务端如何保证自己读取报头完毕?

循环读取,直到读到空行,就说明报头信息读取完毕

🍯响应协议格式

响应的格式如下:

  • 响应行: 版本号+状态码+状态码解释
  • 响应报头: 响应相关属性信息,以key:value的形式显示,且用空行分隔每一个属性信息
  • 空行: 分隔报头和报文
  • 响应正文: html、JSON、XML等格式的文本

为了更好地让大家看到响应的效果,这里再上面的代码的基础上增加一点响应的部分,其中包含两个字段:Content-Type(正文格式,数据类型)Content-Length(正文长度) 两个字段
这里我们正文返回一个html格式的表单,具体如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>注册页面</title>
</head>
	<body>
	    <!-- 表单域 form 表单域action url 地址 method 提交方式 name 表单名称-->
	    <form method = "POST" name="注册表" action=以上是关于Linux篇第二十篇——HTTP协议(认识协议+HTTP协议+HTTPS)的主要内容,如果未能解决你的问题,请参考以下文章

Linux篇第二十篇——HTTP协议(认识协议+HTTP协议+HTTPS)

Linux从青铜到王者第二十篇:Linux网络基础第三篇之IP协议

Python全栈开发之路 第二十篇:Django框架

Linux篇第十七篇——网络基础(概念+协议的认识+OSI七层模型+TCP/IP五层模型+网络传输的流程)

Linux篇第十七篇——网络基础(概念+协议的认识+OSI七层模型+TCP/IP五层模型+网络传输的流程)

Linux篇第十七篇——网络基础(概念+协议的认识+OSI七层模型+TCP/IP五层模型+网络传输的流程)