自顶向下理解网络协议(应用层_HTTP协议)

Posted 楠c

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自顶向下理解网络协议(应用层_HTTP协议)相关的知识,希望对你有一定的参考价值。

1. 简单了解Tcp的三次握手与四次挥手

客户端主动发起请求,服务器被动接收信息。

而客户端是发起链接,建立链接的过程叫做三次握手。

客户端询问服务器是否可以和你建立链接。

  • 向服务器发送携带SYN标志位的数据报

服务器回应客户端,可以建立链接,什么时候建立呢?

  • 向客户端发送携带SYN+ACK报文

客户端回应服务器,就现在。

  • 向服务器发送ACK报文

三次握手完成,然后链接被建立,操作系统需要维护链接,那么还要创建对应的数据结构。对应的接口是connect系统调用,也就是说connect触发了三次握手。

主动发起链接的客户端,但是客户端,服务器都可能主动断开链接。这个过程叫4次挥手。

例如,客户端主动断开。
向服务器发送FIN标志位报文,服务器向客户端发送ACK确认,至此客户端到服务器这条通到被关闭。服务器发送FIN标志位报文,客户端发送ACK确认,然后服务器到客户端这条通道被关闭。

四次挥手完成,首先关闭打开的链接,然后操作系统会清理刚才创建的数据结构。对应的接口是close,当谁调用close即就触发了四次挥手。

Tcp双方通信,地位对等。
TCP是全双工即服务器可以向客户端收发数据,服务器也可以收发数据。
管道是一种半双工,他只能单向传输。即一个发数据,一个接受数据。

在这里插入图片描述

2. TCP协议在内核中的数据结构

在创建网络sock套接字时,以Tcp为例
操作系统会创建出struct socket结构体他是文件系统层面的,与进程相关连起来。
Tcp协议,同时还会有具体的tcp文件描述符。那么怎么将它和文件相关连起来呢。

在这里插入图片描述

通过sock_map_fd函数创建新文件,将他和sk与tcp_sock关联起来。
在这里插入图片描述

归纳一下,创建一个套接字,在内核分配了这些数据结构
在这里插入图片描述

创建一个套接字文件描述符时,先创建了newfile,然后与socket关联起来,socket里面的file回指到newfile中,sk指向一个具体的tcp_sock。

所以在read/recv的时候,传入文件描述符,读取到socket中的sk,从他的接收队列里面读取到了数据。

3. HTTP协议

应用层充满了协议,协议是一种 “约定”。 socket api的接口, 在读写数据时, 都是按一连串的比特位来发送接收的. 如果我们要传输一些"结构化的数据" 怎么办呢?比如说结构体数据。
网络发送采用的是序列化方式,可以简单的理解成将它们全部揉在一起。多变一,而接收的时候采用反序列化,也就是一变多。

3.1 网络版计算器

我们所写的protocol也就是一种协议。我们是在应用层编写的,使用的是传输层tcp协议的接口。我只管用,并不关心它的底层细节是怎么样的。上层永远是使用下层的接口。

3.1.1 自定义协议

Protocol.hpp:

#ifndef __PROTOCOL_HPP__
#define __PROTOCOL_HPP__

#include<iostream>

typedef struct request{
     int x;
     int y;
     char op;

}request_t;

typedef struct reponse{
  int code;   
  int result; 

}reponse_t;

#endif 

3.1.2 服务器

Server.hpp:

#ifndef _SERVER_HPP_
#define _SERVER_HPP_ 
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
#include<cstdlib>
#include<cstring>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/wait.h>
#include"protocol.hpp"
using namespace std;

class server{
  private:
    int port;
    int lsock;
  public:
  server(int _p)
    :port(_p)
     ,lsock(-1)
  {}
  void initServer()
  {
    lsock=socket(AF_INET,SOCK_STREAM,0);
    if(lsock < 0)
    {
          cerr<<"socket error"<<endl;
          exit(1);
    }
    struct sockaddr_in local;
    local.sin_family=AF_INET;
    local.sin_port=htons(port);
    local.sin_addr.s_addr=INADDR_ANY;
    //创建套接字就是创建那批数据结构,而bind就是往结构里面填充ip与端口号
    if(bind(lsock,(struct sockaddr*)&local,sizeof(local))<0)
    {
    cerr<<"bind err"<<endl;
    exit(2);
    }
    if(listen(lsock,5)<0)
    {
      cerr<<"listen error"<<endl;
      
    }
  }
  void cal(int sock)
  {
    //短链接来完成计算,执行一来一回,断开。
    
    //计算结果返还回去
    reponse_t rp{4,0};
    //收到请求开始计算
    request_t rq;
    ssize_t s=recv(sock,&rq,sizeof(rq),0);
    if(s > 0)
    {
      rp.code=0;
      switch( rq.op )
      {
        case '+':
          rp.result=rq.x + rq.y;
          break;
        case '-':
          rp.result=rq.x - rq.y;
          break;
        case '*':
          rp.result=rq.x * rq.y;
          break;
        case '/':
          if(rq.y!=0)
          {
          rp.result=rq.x/rq.y;
          }
          else 
          {
            //除0
            rp.code=1;
          }
          break;
        case '%':
          if(rq.y!=0)
          {
            rp.result=rq.x % rq.y;
          }
          else{
            //模0
            rp.code=2;
          }
          break;
        default:
          rp.code=3;
          break;


      }
    }
    send(sock,&rp,sizeof(rp),0);
    close(sock);
  }
    void start()
    {
     struct sockaddr_in peer;
     for(;;)
     {
       socklen_t len=sizeof(peer);
       int sock=accept(lsock,(struct sockaddr*)&peer,&len);
       if(sock < 0)
       {
         cerr<<"accept error"<<endl;
         continue;
       }
       if(fork()==0)
       {
         if(fork() > 0)
         {
           //子进程直接退出
           exit(0);
         }
         //孙子执行任务,虽然他成为孤儿进程,但是会被1号进程所领养
         close(lsock);
         cal(sock);
         exit(0);
       }
       close(sock);
       //子进程直接退出,我就等待他
       waitpid(-1,nullptr,0);

     }
    }
  
  ~server()
{
  close(lsock);
}

};
#endif 

Server.cc

#include"Server.hpp"

void Menu(string str)
{
  cout<<"Usage: "<<endl;
  cout<<str<<"server port"<<endl;
}
int main(int argc,char* argv[])
{
  if(argc!=2)
  {
   Menu(argv[0]);
   exit(1);
  }

  server *cp=new server(atoi(argv[1]));
  cp->initServer();
  cp->start();

  delete cp;
  return 0;
}

3.1.3 客户端

Client.hpp

#ifndef _CLIENT_HPP_
#define _CLIENT_HPP_
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
#include<cstdlib>
#include<cstring>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/wait.h>
#include"protocol.hpp"
using namespace std;

class client{
  private:
    string ip;
    int port;
    int sock;
  public:
  client(string _ip,int _p)
    :ip(_ip)
    ,port(_p)
     ,sock(-1)
  {}
  void initClient()
  {
    sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock < 0)
    {
          cerr<<"socket errot"<<endl;
          exit(0);
    }
    
   }
  
    void start()
    {

      struct sockaddr_in server;
      server.sin_family=AF_INET;
      server.sin_port=htons(port);
      server.sin_addr.s_addr=inet_addr(ip.c_str());
      if(connect(sock,(struct sockaddr*)&server,sizeof(server))<0)
      {
       cerr<<"connect error"<<endl;
       exit(2);
      }
      reponse_t rp;
      request_t rq;

      cout<<"please enter data1:";
      cin>>rq.x;
      cout<<"please enter data2:";
      cin>>rq.y;
      cout<<"op:";
      cin>>rq.op;
      
      send(sock,&rq,sizeof(rq),0);
      recv(sock,&rp,sizeof(rp),0);
      
      cout<<"code:"<<rp.code<<endl;
      cout<<"result:"<<rp.result<<endl;
    }

  
  ~client()
{
  close(sock);
}

};
#endif 

Client.cc

#include"Client.hpp"

void Menu(string str)
{
  cout<<"Usage: "<<endl;
  cout<<str<<"server ip and  port"<<endl;
}
int main(int argc,char* argv[])
{
  if(argc!=3)
  {
   Menu(argv[0]);
   exit(1);
  }

  client *cp=new client(argv[1],atoi(argv[2]));
  cp->initClient();
  cp->start();

  delete cp;
  return 0;
}

3.1.4 实验现象

Server端正在提供服务。
在这里插入图片描述

Client端由于是短链接,所以他发出信息,接收到回来的消息就退出了。
在这里插入图片描述

4. TcpDump抓包工具

tcpdump,可以抓取tcp,udp,可以抓icmp,基本传输层的时候都可以抓,
在这里插入图片描述
先启动服务器,在启动客户端,开始三次握手,准备连接(未发数据)
在这里插入图片描述
客服端发数据,服务器接收并处理数据,然后服务器先断开。
在这里插入图片描述
像刚才的protocol.hpp中就可以认为我们做的某种协议规定,基于应用层的自定义协议。一个个结构体,send将他们看做一个个二进制流,recv将他们有看做一个个结构体,所以也完成了序列化与反序列化。但是结构体有一个隐患,由于客户端和服务器的主机不同,结构体内存对齐数,主机位数,指针大小,都不一样。

5. HTTP协议

他是应用层的超文本传输协议

5.1 认识URL

url就是俗称的网址。

现在用的大多数https,对数据有所加密,端口为443。
在这里插入图片描述

  1. http,超文本传输协议,端口为80。
  2. 登录信息,一般省略。
  3. 服务器地址即域名
  4. 端口号不能省略,但是浏览器知道你是什么协议,你的名字已经和一个端口号强绑定了。
  5. 即服务器的资源
  6. 查询字符串就是后面就是所带参数,最经典的就是搜索引擎
  7. 片段标识符,就像网站中嵌入图片的编号

服务器资源里,第一个’/’,他叫做web根目录,但不能等同于根目录,他是任意文件夹的根目录。即就是把这个文件(数据)从服务器拿到本地,让浏览器解释。

5.2 urlencode与urldecode

像’?’ ’ / ',url中的符号,像这样的字符,他被url采用特殊含义去理解,因此这些字符是不能随意出现的,假如哪个参数中不得已要带,我们要对他进行转义操作。
在这里插入图片描述
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式。

还有专门的urlencode工具。可以进行在线编码。

urlencode就是编码。
urldecode就是解码。

5.3 HTTP基本特征

  1. 无链接
  2. 无状态
    不记录用户任何的信息,发送request,收到reponse进行解释
  3. 简单快速
    http1.0,短链接进行文本传输。http1.1,长连接。

Tcp已经在传输层成功的建立了链接,不需要http多此一举。
Tcp有状态,三次握手后,建立链接,进入establish。断开链接,四次挥手时time_wait,close_wait等。也不需要http多此一举。

也就是说,http只管,客户端发送一个request,服务器收到然后返回一个response,客户端进行解释,就完成了他的工作。但是这时又有个疑惑,他只管这些,那我们登录某个网站之后,在一段时间内,是不需要再次重新登录的,这又是怎么回事呢?其实是两个东西在起作用cookie和session。

5.4 HTTP协议格式

tcp是面向字节流的,那么上层怎么知道他把request数据发送完,或者将response数据解析完了呢?

先来看看他们的结构
request:
在这里插入图片描述

  1. 请求行
    请求方法:大多数为post和get
    url:即资源所在的路径
    version:http版本
    最后以/r/n结束,说明他是一行数据
  2. 请求报头
    由一个个key,val组成,每个key,val都以/r/n结束。
    例子Content-Length:256,就是一个key:val,说明正文部分有256个字节
  3. 空行
    与正文的分割点,就是有效载荷。
  4. 请求正文
    上传的某些数据

也就是说,当读到空行,就说明我们将http协议属性读取完毕,接下来就是他的正文。从空行开始,由于报头部分已经读取到正文的长度,所以直接读取对应的val就完成了。

response:
在这里插入图片描述

  • 响应行
    http版本
    状态码
    其余与request完全类似。

request与response构成一次短链接
在这里插入图片描述

6. HTTP抓包工具

所使用的工具叫Fiddler,其原理比较简单。通过这张图即可表明在这里插入图片描述

抓取的request

在这里插入图片描述
抓取的response
在这里插入图片描述
他们都可以和刚才画的两张简图对应起来。

7. Telent工具

之前提到过这个工具,百度服务器的80端口是对外提供服务的,所以直接连接.
在这里插入图片描述
写一行请求行,回车发出去。
不写报头,在回车一次。
就发出去了空行。由于是get方法就不发正文了。
在这里插入图片描述
其他不必多说,和简图可以对应,最主要的是这个正文长度。底下很多很多,Content就记录了他的字节
在这里插入图片描述
在浏览器查看后
在这里插入图片描述
html被浏览器解释过后,就成了看到的这样。

当在任意登陆窗口,登录账号和密码。post传参数据在正文中,get方法传参数据在url中。

使用fiddler抓包,竟然可以看到正文中的账号密码,没有加密
request:
在这里插入图片描述
response立刻返回:
在这里插入图片描述

所以get,post方法的区别:
get通过url传参,post通过正文传参。get方法不安全,post方法也不是很安全,但是post相对于get更加私密一些。
url是有长度的,正文部分理论是没有长度限制的,因为有个Content——length标识,但是电脑资源有限,那么他也肯定是有限的。
在这里插入图片描述
HTTP状态码
在这里插入图片描述
4xx,其实是客户端出错,因为他是“不合理的请求”。

8. HTTP常见报头

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

9. wget工具简单爬取网页

使用wget工具
在这里插入图片描述
在这里插入图片描述
服务器会有反爬机制,判断user-agent是否存在,有说明你是浏览器用户,没有就说明机器在访问。即使伪装user-agent也会被发现规律性来防止。虽然永远是进攻的人占取主动权,但千万不要乱爬,否则会被带走的。

10. cookie 和 session

http虽然是无状态,但是有cookie保存信息。
比如看视频看了第一个视频,继续看第二个,不需要登录,就是因为cookie保存了信息,提升用户体验。
在这里插入图片描述
原理

在这里插入图片描述
那么他是内存级文件,还是硬盘级文件呢?
内存级就是浏览器关闭,重新打开就需要再次输入。
硬盘级就是即使关机,重新打开依旧不需要输入。
显然是硬盘级。

但是很不安全,假如电脑中毒,直接copy我的cookie文件,就可以不用登陆,访问我曾经的资源。
所以他又引出了一个概念,session。
在这里插入图片描述
虽然这时依旧有可能泄漏,但他最多可以和我访问一个资源,他是看不见我的用户名,密码等敏感信息,因为此时我的一切都在服务器保存着!存在本地客户端的只有一个sid。相对比较安全。
假如此时他想要更改你的qq账号密码,需要窃取腾讯服务器,几率机会为零,除非你的账号密码过于简单,就算由于简单被盗取更改,还可以查看ip来查看你的常驻ip。常驻ip还一样,那就只能申诉了。

以上是关于自顶向下理解网络协议(应用层_HTTP协议)的主要内容,如果未能解决你的问题,请参考以下文章

Day3: Python学习笔记之计算机基础——网络片

因特网协议栈和七层OSI

因特网协议栈和七层OSI

网络通信之应用层协议--Linux

网络编程 应用层协议 -HTTP

网络编程 应用层协议 -HTTP