欢迎光临
我们一直在努力

【linux】网络基础(四)HTTP协议,域名,url,认识http的请求和响应,Fiddler Classic,Postman,httpserver服务器的实现,recv

小编个人主页详情<—请点击 小编个人gitee代码仓库<—请点击 linux系统编程专栏<—请点击 linux网络编程专栏<—请点击 倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己! 在这里插入图片描述


目录

    • 前言
    • 一、两个预备知识
      • 域名
      • url
    • 二、http的请求和响应
      • http请求
      • http响应
    • 三、三个工具
      • telnet
      • Fiddler Classic
      • Postman
    • 四、httpserver服务器
      • HttpServer.hpp
        • 基础框架
        • Start
        • ThreadRun
      • HttpServer.cc
    • 五、测试
    • 六、源代码
      • makefile
      • HttpServer.hpp
      • HttpServer.cc
      • Sock.hpp
      • Log.hpp
    • 总结

前言

【linux】网络基础(三)TCP服务端网络版本计算器的优化,Json的使用,服务器守护进程化daemon,重谈OSI七层模型——书接上文 详情请点击<——,本文会在上文的基础上进行讲解,所以对上文不了解的读者友友请点击前方的蓝字链接进行学习 本文由小编为大家介绍——【linux】网络基础(四)HTTP协议


一、两个预备知识

  • 应用层协议是我们自己定制的,在之前的文章中小编已经带领大家定制了自定义协议并且实现了网络版计算器,那么本文小编带领大家认识一下成熟的,好用的应用层协议:HTTP协议((hypertext transport protocol超文本传输协议)
  • 下面小编先学习两个预备知识,域名和url然后再来学习HTTP协议更容易上手
  • 域名

  • 我们使用浏览器进入百度通常要输入网址www.baidu.com,并且按下回车之后,前面会自动补齐https,即访问百度使用的是 https://www.baidu.com/ 通常我们把这个叫做域名,前面以https开头那么代表的是使用的https协议,所以浏览器如果我们不显示的输入协议,那么浏览器默认使用的协议大多是https协议,少部分是http
  • 那么我们知道我们访问百度主页本质上访问的还是百度的服务器,既然是访问百度的服务器那么就需要两个东西必不可少,没错,服务器的IP地址和端口号port,但是为什么我们在 https://www.baidu.com/ 这个域名中并没有见到服务器的IP地址和端口号port呢?下面小编带领大家探究一下 在这里插入图片描述
  • 首先我们在搜索中输入cmd,打开命令提示符,即终端 在这里插入图片描述
  • 然后输入百度的网址www.baidu.com,此时会看到如上字段,那么此时我们最关心的是以点分十进制分隔的一个IP地址 39.156.70.239 在这里插入图片描述
  • 所以我们直接拿这个 39.156.70.239 在浏览器上搜索,按下回车 在这里插入图片描述
  • 此时我们神奇的进入到了百度主页,即此时我们使用的是 http://39.156.70.239/ 访问的百度主页,并且在前面默认浏览器帮我们补齐了http,所以可以说明,其实域名本身就可以经过域名解析,解析成IP地址,小编,我已经成功的见到了IP地址,可是端口号呢?为什么我们没有看见百度的端口号呢?
  • 其实诸如百度等常见的端口号已经被内置在了浏览器中,当我们使用百度的域名去访问百度主页的时候,浏览器会自动的根据域名然后经过域名解析解析成IP地址然后去自动匹配内置在浏览器中对应的端口号
  • 那么https协议使用的默认端口号是443,http协议使用的默认端口号是80,即如果我们想要使用https协议那么可以给浏览器指明443,如果我们想要使用http协议那么就可以给浏览器知名80,所以呢?我们该如何指明呢?那么通过 IP地址:端口号 即可 在这里插入图片描述 在这里插入图片描述
  • 例如我们使用冒号分隔IP地址和端口号,指明使用80端口号 39.156.70.239:80,所以就会自动匹配http协议 在这里插入图片描述 在这里插入图片描述
  • 例如我们使用冒号分隔IP地址和端口号,指明使用443端口号 39.156.70.239:443,所以就会自动匹配https协议
  • url

  • 相信喜欢打王者荣耀的读者友友,都听说过听安,那么听安经常会说,打出那六个英文字母,YGking(意义代表的是元歌王),那么小编使用浏览器搜索一下YGking 在这里插入图片描述 在这里插入图片描述
  • 接下来我们点击第一个哔哩哔哩的视频,然后点击右上方王者荣耀听安的主页 在这里插入图片描述
  • 所以我们就拿到了如上界面,即王者荣耀听安的主页,而我们关注的不是YGking,不是元歌单挑,也不是王者荣耀听安,而是是哔哩哔哩王者荣耀听安对应的浏览器中最上方的网址
  • // 王者荣耀听安主页的网址
    https://space.bilibili.com/430044260?spm_id_from=333.788.upinfo.head.click

  • 所以从此以后,如果你想跟你的好朋友分享王者荣耀听安,那么只需要将王者荣耀听安主页的网址发给他,然后你好朋友只要拿着这个网址就可以唯一的访问到王者荣耀听安的主页,通常的我们将这个网址叫做url(uniform resource locator统一资源定位符)
  • 所以有了url,那么所有网络上的资源(图片,音频等),都可以使用唯一的一个"字符串"标识,并且可以获取到,那么下面我们来详细认识一下url的格式 在这里插入图片描述
  • 那么第一个字段通常是https://或者http://格式,是进行协议的选择然后冒号两个斜杠进行分隔,第二个字段是登录信息,目前几乎不使用了,可以省略,所以我们不关心,然后分隔符@也可省略,第三个字段是服务器的地址,即虽然填写的是一个域名,但是我们要知道这个是可以被经过域名解析然后转化为服务的IP地址的,然后分隔符是冒号,通常冒号也可省略,接下来就是服务器的端口号通常也可以省略,因为浏览器已经将很多的常用服务器对应的端口号已经内置了,接下来是 / 其中这个 / 我们称之为web根目录,这个目录有可能是根目录同样也有可能是采用的相对路径的根目录,那么 /dir/index.htm 我们称之为带层次的文件路径,其实就是要访问的资源的存放路径
  • 我们知道,我们要上网,那么我们的网络行为无非就两种 (一)把别人的东西拿下来,例如,刷抖音,将抖音服务器上的短视频资源拿到本地抖音客户端 (二)把自己的东西传上去,例如,我们登录王者荣耀,那么首先就要发起登录请求,这个登录请求,就是将自己的用户信息等字段传上去
  • 所以说对于用户来讲,那么本次网络行为就有可能是把自己的东西传上去,所以就要使用到接下来的分隔符?紧接着就是kv风格的以等号=分隔的字符串,为什么是kv?例如user=tingan&password=1433223,那么前面的这就是将用户名和密码传上去,如果有多个需要上传的字段,那么中间采用与符号&进行分隔,当然如果用户不需要把自己的东西传上去那么?k=v就可以省略
  • 那么最后一个字段就是#ch1,即片段标识符,例如这个网页上有循环显示的图片片段,最后一个字段就是标识循环到了第几个图片片段
  • 此时url介绍完毕,同时我们还注意到,url中采用了很多分隔的字符://?&#=@所以如果用户去搜索查询这些分隔符会不会影响到url呢?下面小编搜索一下 在这里插入图片描述
  • https://cn.bing.com/search?q=%3A%2F%2F%3F%26%23%3D%40&qs=n&form=QBRE&sp=-1&lq=0&pq=%3A%2F%2F%3F%26%23%3D%40&sc=12-8&sk=&cvid=C39C5BAD36D548818AF5B0A87C56BA0B

  • 所以此时没有影响到url,查询成功,反馈没有对应的结果,并且我们观察上面的网址有很多的%形式的字符,那么这是什么意思呢?为什么没有影响到url呢?因为我们的字符被编码了
  • 少量的情况,提交或者获取的数据本身可能包含和url的特殊字符冲突的字符,这时候就要求BS双方(B代表的是browser浏览器,S代表的是server服务器)进行Encode编码和Decode解码
  • 那么如何理解Encode呢?其实既然是特殊字符,那么就有应该在ASCII码表中存在,ASCII码表上有256个字符,那么256是2的8次方,所以我们使用8个比特位就完全可以将特殊字符进行表示
  • 那么url进行转码的规则规定,需要将转码的字符转换成16进制,需要转码的字符可以使用8个比特位进行表示(不够的话左侧补零),那么4个比特位可以转换成一个16进制数,所以8个比特位中的右侧四个比特位转换为16进制数作为y,8个比特位中的左侧的四个比特位转换为16进制数作为x,所以8个比特位的数就被转换成了16进制数xy,那么规定在xy前加上%此时完成编码,即%xy,那么为什么要转换成16进制?因为有可能需要转换的字符是汉字,汉字同样也需要经过编码后才能发送
  • 其实我们不需要关心Encode编码和Decode解码,因为网上有现成的转换工具,我们可以直接拿来使用
  • https://cn.bing.com/search?q=%3A%2F%2F%3F%26%23%3D%40&qs=n&form=QBRE&sp=-1&lq=0&pq=%3A%2F%2F%3F%26%23%3D%40&sc=12-8&sk=&cvid=C39C5BAD36D548818AF5B0A87C56BA0B

  • 所以我们就在网络上直接搜索url编码在线转换即可 详情请点击<——,将查询的://?&#=@对应编码后的%3A%2F%2F%3F%26%23%3D%40进行解码转换查看,如下无误 在这里插入图片描述

  • 同样的,我们也可以进行编码,如下无误 在这里插入图片描述

  • 二、http的请求和响应

  • 所以下面小编就将http的请求和响应的格式分别画出来,我们现在理论层面见一见http的请求和响应的样子 在这里插入图片描述
  • http请求

    在这里插入图片描述

  • 所以对于http的请求,一共分为四部分,依次是请求行,请求报头,空行,请求正文,其中请求行和请求报头的结尾以及空行必须以\\r\\n结尾,其中\\r是回车,回到当前行的开头,\\n是竖直换行,所以合起来就是回到下一行的开头
  • 那么在请求行中的各个字段依次是Method代表请求的方法,大约95%的请求都是GET获取,POST发送,那么第二个字段是URL,即url统一资源定位符,这个之前小编文章开头的第二个补充知识url已经进行了讲解,那么第三个字段是当前http的版本,版本可以是1.0 1.1 2.0,主流版本一般是1.1,那么进行写入的时候需要加斜杠/,即HTTP/1.1这种格式,那么末尾是以\\r\\n结尾,并且请求行各个字段需要以空格分隔
  • 请求报头中包含的是本次请求的属性以及属性对应值,即是一个kv结构,那么形式是Key: Value注意这里的分隔符是冒号空格,请求报头中的请求属性有多个,那么每一个Key: Value的结尾都需要添加\\r\\n,同样的请求报头中还包含一个字段Content-Length: XXX,其中Content-Length的属性意思是请求正文的大小字节数,XXX表示请求正文的实际的大小字节数
  • 请求正文即我们要上传的内容,如果我们没有要上传的内容的话,请求正文可以省略,问题来了,如何区分请求报头和请求正文 在这里插入图片描述
  • 很简单,http规定请求报头和请求正文中间要包含一个空行\\r\\n,此时读取一个请求报文就可以按照如下规则进行读取,首先按行读取请求报文,那么按行读取第一行就是请求行,请求报文中的各个字段是以空格进行间隔结尾是\\r\\n,第二行及其往后按行读取的就是请求报头,那么按行读取,按行读取,按行读取,直到读取到一个空行\\r\\n,即这一行仅仅包含\\r\\n,那么就代表请求报头已经读取完整,往下就按照字节进行读取,继续向下按照字节读取,可是我怎么知道要读取的请求正文有多少字节呢?
  • 别忘了,请求报头中有一个属性字段是Content-Length: XXX,其中的就XXX表示了请求正文的实际的大小字节数,所以我们就可以按照XXX,从空行向后读取XXX个字节,那么读取上来的我们就当作请求正文
  • 所以此时我们就可以将http请求完美的进行读取,此时我们在宏观上看待整个http请求报文,虽然请求行和请求报文以及空行中包含了\\r\\n,打印出来的话是呈现按行分隔,但是我们也要知道,诸如\\r\\n实际在存储的时候,也是一个一个的字符,一个字符就是一个字节,所以请求报文整体实际在存储的时候也是一个一个字节的进行存储的
  • http响应

  • 那么在见识到了http请求之后,实际上http的响应和http的请求大部分格式都完全相同,所以我们学习了http请求之后,很快的就能上手http响应 在这里插入图片描述
  • 那么http响应也包含四部分,分别是状态行,响应报头,空行,响应正文,那么也是同样的要求,要求状态行,响应报头,空行全部都要以\\r\\n结尾
  • 那么我们主要讲解状态行,状态行的字段中第一个字段是HTTP Version即http的版本,同样的,版本可以是1.0 1.1 2.0,主流版本一般是1.1,那么进行写入的时候需要加斜杠/,即HTTP/1.1这种格式,第二个字段是状态码,例如200,404等,第三个字段是状态码的字符串形式的描述,那么第二个字段和第三个字段相信大多数的读者友友都曾经见到过,即对应右图的界面,状态行的各个字段同样需要空格进行分隔,并且状态行的结尾是\\r\\n
  • 三、三个工具

    telnet

  • 在之前的文章中,小编已经讲解了telnet的使用 在第三点进行的讲解,详情请点击<——
  • 那么小编在文章开头已经在windows的终端cmd上ping出了百度的IP地址 39.156.70.239,那么下面小编使用linux上的telnet我们自己构建http请求报文,然后进行访问,那么百度的端口号是80,在telnet上访问的格式是telnet IP地址 端口号
  • telnet 39.156.70.239 80

    在这里插入图片描述在这里插入图片描述

  • 那么观察到如上左图现象即为连接成功,所以接下来小编先ctrl + ]然后再按下回车,就可以进入telnet的输入,即对应如上右图,所以我们构建发送一个http请求,请求中首先是请求方法我们选择GET获取,然后输入空格,然后就是要进行url的输入了,但是这里我们仅仅是想要获取百度的主页,所以这里我们并不需要输入任何的url那么这里我们简单的输入一个斜杠/即可,然后再输入空格,然后就该选择HTTP的版本,即输入HTTP/1.1,接下来按下回车换行,所以此时就是在请求行的结尾的回车换行,那么由于我们仅仅是发送一个获取请求,所以并不需要带携带任何的请求属性以及请求正文,所以这里我们再次按下回车换行,那么此时就是表示我们输入的回车换行即表示输入了一个空行
  • GET / HTTP/1.1

    在这里插入图片描述在这里插入图片描述

  • 如上,左图输入后按下第一个回车,现象如上的右图,没有反应,这是因为我们这是在输入请求行的回车换行,那么如下继续按下第二个回车才有反应,代表我们此时输入了第二个回车换行,表示输入了空行
  • 在这里插入图片描述

  • 所以此时我们观察上图,此时接收到了百度服务器发来的http响应,那么响应报文的状态行是HTTP/1.1 200 OK,果然第一个字段是版本http的版本HTTP/1.1,第二个字段是状态码200,第三个字段是OK表示状态良好,并且这些字段以空格作为分隔符,并且观察上图确实打印的时候进行了回车换行,说明状态行的结尾是回车换行
  • 并且我们还看到了上图中响应报头的Content-Length: 29506字段,即表示响应正文的字节数,无误,那么接下来我们继续向下翻
  • 在这里插入图片描述

  • 翻找到了"百度一下,你就知道",这不就是如下左上角百度的标题,无误 在这里插入图片描述 在这里插入图片描述
  • 那么我们翻到末尾,那么最后一行就是响应正文,倒数第二行是空行,所以说我们可以得出,响应正文和响应报头之间确实有空行进行间隔
  • Fiddler Classic

  • Fiddler Classic是一款抓包工具,你电脑上所有访问网络的行为,即从网络中获取数据,将数据上传到网络中的行为都会被Fiddler Classic抓到并显示到如下界面上,刚刚小编在浏览器上使用百度进行了搜索,所以就被Fiddler Classic进行了抓包,并且将抓取到的进行显示,那么我们分别点击右侧上面和下面界面的Raw,右侧的上半部分即http请求,左侧的下半部分即http响应 在这里插入图片描述
  • 所以如下即http请求,第一行就是请求行,分别是请求方法,url,http的版本,如下无误 在这里插入图片描述
  • 所以如下即http响应,第一行就是状态行,分别是http的版本,状态码,状态字符串版本的描述,如下无误 在这里插入图片描述
  • 可是为什么Fiddler Classic可以将网络的请求与响应的结果都进行抓包呢?原理是什么呢? 在这里插入图片描述
  • 因为Fiddler Classic一旦启动就变成了进程,它运行起来之后,它就变成了一个管理层,本主机上所有的请求,包括http请求要发送到网络上都需要经过我Fiddler Classic,并且远端服务器响应,包括http响应要从网络上发送到本主机上,那么也要经过Fiddler Classic,既然Fiddler Classic可以看得见请求与响应,所以自然它可以得知http的请求与响应的内容了
  • Postman

    在这里插入图片描述 在这里插入图片描述

  • Postman是一款用于管理发送的请求的工具,那么我们的http请求就可以使用Postman进行发送,下面我们先点击上图中央偏左的+加号,然后点击中央的GET可以选择发送请求的方法,观察第二个就是POST发送方法,这里我们默认使用GET获取方法即可,所以我们就可以在上图输入要访问的url,那么就是这里我们访问百度的主页,即www.baidu.com,然后点击send发送 在这里插入图片描述
  • 所以此时我们就看到了上图现象,即此时我们http请求访问百度成功 在这里插入图片描述
  • 那么我们点击Preview,然后就可以看到postman已经将百度的主界面的预览进行了加载 在这里插入图片描述
  • 那么我们点击Headers可以看到对于http响应的响应报头的Key: Value进行了划分,如上无误
  • 四、httpserver服务器

  • 那么下面就由小编带领大家编写一个简单的httpserver服务器,然后我们直接拿浏览器作为客户端发起http请求,然后在进行测试,并且在Fiddler Classic期望观察到测试结果
  • HttpServer.hpp

    基础框架
  • 那么作为一个服务器,http底层是使用的TCP协议,所以这里我们就使用TCP协议对应的网络套接字接口,那么其实在之前的文章,小编已经对这些网络套接字接口进行封装为了Sock.hpp,这里包含了之后就可以直接使用,在第三点的TCP服务器中的Sock.hpp进行的讲解,详情请点击<——
  • 并且我们还要使用日志打印信息,所以这里我们将日志的头文件Log.hpp包含就可以直接使用了详情请点击<——
  • 那么服务端需要知道要绑定的端口号,默认为8080,所以就要有int16_t类型的成员变量端口号port_,以及用于监听的网络文件描述符listensock_,那么对于这个listensock_我们定义为Sock类型的即可,因为Sock有封装的文件描述符,所以在构造函数这里接收端口号,对成员变量端口号进行初始化即可
  • 这里我们期望当服务器收到一个请求的时候,拿到了sockfd之后,创建线程进行响应处理,所以这里我们还要将线程引进来,那么就要编写线程函数ThreadRun, 由于线程函数是类内成员函数,第一个参数是this指针,类型和pthread_create需要的线程函数的参数类型不匹配,所以要将线程函数ThreadRun使用static修饰成为没有this指针的静态成员函数
  • 但是静态线程函数ThreadRun需要sockfd,并且静态线程函数ThreadRun有可能会有调用类内的普通的成员变量或者普通的成员函数的需求,所以还需要传参this指针,所以就需要定义一个线程数据ThreadData,将这两个字段设置为公有的成员变量,并且在构造函数中对这两个字段进行初始化
  • #pragma once

    #include <iostream>
    #include <pthread.h>
    #include "Log.hpp"
    #include "Socket.hpp"

    static const int defaultport = 8080;
    class HttpServer;

    class ThreadData
    {
    public:
    ThreadData(int fd, HttpServer* s)
    : sockfd(fd)
    , svr(s)
    {}
    public:
    int sockfd;
    HttpServer* svr;
    };

    class HttpServer
    {
    public:
    HttpServer(uint16_t port = defaultport)
    : port_(port)
    {}

    void Start();
    static void* ThreadRun(void* args);

    ~HttpServer()
    {}
    private:
    uint16_t port_;
    Sock listensock_;
    };

    Start
  • 那么这里小编就压缩一下,将初始化,启动以及释放的工作全部放在Start中
  • 首先常规套路调用接口创建套接字,然后绑定,监听即可,然后服务器一旦启动要基于死循环所以这里for( ; ; )死循环即可,那么就要调用Accept接收请求,然后如果接收失败,那么continue继续接收,走到下一步就接收成功了,此时得到了Accept的返回值sockfd用于对客户端做出响应
  • 那么接收成功就使用日志打印信息即可,然后new一个线程数据对象将sockfd和this指针初始化,然后定义线程tid,然后创建线程,传参即可,如果服务器退出,那么调用Close关闭监听文件描述符即可
  • void Start()
    {
    listensock_.Socket();
    listensock_.Bind(port_);
    listensock_.Listen();

    for(;;)
    {
    std::string clientip;
    uint16_t clientport;
    int sockfd = listensock_.Accept(&clientip, &clientport);
    if(sockfd < 0)
    continue;
    lg(Info, "get a new connect, sockfd: %d, %s:%d",sockfd, clientip.c_str(), clientport);
    ThreadData* td = new ThreadData(sockfd, this);

    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRun, td);
    }

    listensock_.Close();
    }

    ThreadRun
  • 那么我们在静态成员函数这个线程函数ThreadRun中,首先服务器只负责接收连接请求,所以服务器不会等待我这个新线程,即服务器不关心新线程的退出信息以及退出状态,那么我们就让新线程执行pthread_dethch线程分离即可,当新线程退出的时候,由系统释放新线程的资源即可
  • 那么接下来进行类型转换,获取线程数据对象的指针td
  • 然后定义一个缓冲区,buffer用于接收http请求的报文,尽量定义大一点 在这里插入图片描述
  • 那么下面我们使用recv替代read,recv的前三个参数和read一样,recv的最后一个参数设置为0作用和read一样,阻塞式等待,即recv可以通过设置最后一个参数来决定是否是阻塞式接收,那么recv返回值也和read一样,所以我们使用n接收recv的返回值
  • 当n大于0的时候,那么就表示recv读取客户端的http请求成功,那么我们约定将http请求当作字符串来看,所以我们在缓冲区的n位置上设置为’\\0’,那么服务器简单的打印一下buffer即可,即本文的服务端的响应仅仅是简单的打印一下消息,并不做任何其它处理
  • 那么响应完成使用close关闭文件描述符,然后delete释放td,最后返回nullptr即可
  • static void* ThreadRun(void* args)
    {
    pthread_detach(pthread_self());
    ThreadData* td = static_cast<ThreadData*>(args);
    char buffer[10240];
    ssize_t n = recv(td->sockfd, buffer, sizeof(buffer) 1, 0);
    if(n > 0)
    {
    buffer[n] = 0;
    std::cout << buffer << std::endl;
    }

    close(td->sockfd);
    delete td;

    return nullptr;
    }

    HttpServer.cc

  • 所以我们在HttpServer.cc中进行主要的调用逻辑,那么我们期望调用服务器的时候在命令行中接收服务器要绑定的端口号,所以这里我们给main函数带参,然后获取端口号即可,接下来使用智能指针unique_ptr管理new的服务器,然后调用Start启动服务器即可
  • #include <iostream>
    #include <memory>
    #include <unistd.h>
    #include "HttpServer.hpp"

    int main(int args, char* argv[])
    {
    if(args != 2)
    {
    exit(1);
    }
    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<HttpServer> svr(new HttpServer(port));
    svr->Start();

    return 0;
    }

    五、测试

  • 所以我们将服务器启动,并且我们将浏览器当作客户端观察服务器是否可以接收并且打印http请求,那么我们启动服务器,然后拿出浏览器输入小编云服务器的IP地址和httpserver服务器绑定的端口号8080,如下无误 在这里插入图片描述
  • 并且观察Fiddler Classic也确实抓包成功 在这里插入图片描述
  • 那么我们如下观察打印的http请求,符合http请求的格式,无误,测试成功 在这里插入图片描述
  • 六、源代码

    makefile

    httpserver:HttpServer.cc
    g++ -o $@ $^ -std=c++11 -pthread

    .PHONY:clean
    clean:
    rm -f httpserver

    HttpServer.hpp

    #pragma once

    #include <iostream>
    #include <pthread.h>
    #include "Log.hpp"
    #include "Socket.hpp"

    static const int defaultport = 8080;

    class HttpServer;

    class ThreadData
    {
    public:
    ThreadData(int fd, HttpServer* s)
    : sockfd(fd)
    , svr(s)
    {}
    public:
    int sockfd;
    HttpServer* svr;
    };

    class HttpServer
    {
    public:
    HttpServer(uint16_t port = defaultport)
    : port_(port)
    {}

    void Start()
    {
    listensock_.Socket();
    listensock_.Bind(port_);
    listensock_.Listen();

    for(;;)
    {
    std::string clientip;
    uint16_t clientport;
    int sockfd = listensock_.Accept(&clientip, &clientport);
    if(sockfd < 0)
    continue;
    lg(Info, "get a new connect, sockfd: %d, %s:%d",sockfd, clientip.c_str(), clientport);
    ThreadData* td = new ThreadData(sockfd, this);

    pthread_t tid;
    pthread_create(&tid, nullptr, ThreadRun, td);
    }

    listensock_.Close();
    }

    static void* ThreadRun(void* args)
    {
    pthread_detach(pthread_self());
    ThreadData* td = static_cast<ThreadData*>(args);
    char buffer[10240];
    ssize_t n = recv(td->sockfd, buffer, sizeof(buffer) 1, 0);
    if(n > 0)
    {
    buffer[n] = 0;
    std::cout << buffer << std::endl;
    }

    close(td->sockfd);
    delete td;

    return nullptr;
    }

    ~HttpServer()
    {}
    private:
    uint16_t port_;
    Sock listensock_;
    };

    HttpServer.cc

    #include <iostream>
    #include <memory>
    #include <unistd.h>
    #include "HttpServer.hpp"

    int main(int args, char* argv[])
    {
    if(args != 2)
    {
    exit(1);
    }
    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<HttpServer> svr(new HttpServer(port));
    svr->Start();

    return 0;
    }

    Sock.hpp

    #pragma once

    #include <iostream>
    #include <string>
    #include <cstring>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include "Log.hpp"

    const int backlog = 10;

    enum{
    SocketErr = 1,
    BindErr,
    ListenErr,
    };

    class Sock
    {
    public:
    Sock()
    {}

    void Socket()
    {
    sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd_ < 0)
    {
    lg(Fatal, "socket error, %s : %d", strerror(errno), errno);
    exit(SocketErr);
    }
    }

    void Bind(uint16_t port)
    {
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    local.sin_addr.s_addr = INADDR_ANY;
    socklen_t len = sizeof(local);

    if(bind(sockfd_, (struct sockaddr*)&local, len) < 0)
    {
    lg(Fatal, "bind error, %s : %d", strerror(errno), errno);
    exit(BindErr);
    }
    }

    void Listen()
    {
    if(listen(sockfd_, backlog) < 0)
    {
    lg(Fatal, "listen error, %s : %d", strerror(errno), errno);
    exit(ListenErr);
    }
    }

    int Accept(std::string* clientip, uint16_t* clientport)
    {
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);

    int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
    if(newfd < 0)
    {
    lg(Warning, "accept error, %s : %d", strerror(errno), errno);
    return 1;
    }

    char ipstr[128];
    inet_ntop(AF_INET, &(peer.sin_addr), ipstr, sizeof(ipstr));
    *clientip = ipstr;
    *clientport = ntohs(peer.sin_port);

    return newfd;
    }

    bool Connect(const std::string& serverip, uint16_t serverport)
    {
    struct sockaddr_in peer;
    memset(&peer, 0, sizeof(peer));
    peer.sin_family = AF_INET;
    peer.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &(peer.sin_addr));
    socklen_t len = sizeof(peer);

    int n = connect(sockfd_, (struct sockaddr*)&peer, len);
    if(n == 1)
    {
    std::cerr << "connect to " << serverip << ':' << serverport << "error" << std::endl;
    return false;
    }

    return true;
    }

    void Close()
    {
    if(sockfd_ > 0)
    {
    close(sockfd_);
    }
    }

    int Fd()
    {
    return sockfd_;
    }

    ~Sock()
    {}
    private:
    int sockfd_;
    };

    Log.hpp

    #pragma once

    #include <iostream>
    #include <string>
    #include <ctime>
    #include <cstdio>
    #include <cstdarg>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>

    #define SIZE 1024

    #define Info 0
    #define Debug 1
    #define Warning 2
    #define Error 3
    #define Fatal 4

    #define Screen 1 //输出到屏幕上
    #define Onefile 2 //输出到一个文件中
    #define Classfile 3 //根据事件等级输出到不同的文件中

    #define LogFile "log.txt" //日志名称

    class Log
    {
    public:
    Log()
    {
    printMethod = Screen;
    path = "./log/";
    }

    void Enable(int method) //改变日志打印方式
    {
    printMethod = method;
    }

    ~Log()
    {}

    std::string levelToString(int level)
    {
    switch(level)
    {
    case Info:
    return "Info";
    case Debug:
    return "Debug";
    case Warning:
    return "Warning";
    case Error:
    return "Error";
    case Fatal:
    return "Fatal";
    default:
    return "";
    }
    }

    void operator()(int level, const char* format, ...)
    {
    //默认部分 = 日志等级 + 日志时间
    time_t t = time(nullptr);
    struct tm* ctime = localtime(&t);
    char leftbuffer[SIZE];
    snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
    ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
    ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

    va_list s;
    va_start(s, format);
    char rightbuffer[SIZE];
    vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
    va_end(s);

    char logtxt[2 * SIZE];
    snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);

    printLog(level, logtxt);
    }

    void printLog(int level, const std::string& logtxt)
    {
    switch(printMethod)
    {
    case Screen:
    std::cout << logtxt << std::endl;
    break;
    case Onefile:
    printOneFile(LogFile, logtxt);
    break;
    case Classfile:
    printClassFile(level, logtxt);
    break;
    default:
    break;
    }
    }

    void printOneFile(const std::string& logname, const std::string& logtxt)
    {
    std::string _logname = path + logname;
    int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
    if(fd < 0)
    return;
    write(fd, logtxt.c_str(), logtxt.size());
    close(fd);
    }

    void printClassFile(int level, const std::string& logtxt)
    {
    std::string filename = LogFile;
    filename += ".";
    filename += levelToString(level);

    printOneFile(filename, logtxt);
    }

    private:
    int printMethod;
    std::string path;
    };

    Log lg;


    总结

    以上就是今天的博客内容啦,希望对读者朋友们有帮助 水滴石穿,坚持就是胜利,读者朋友们可以点个关注 点赞收藏加关注,找到小编不迷路!

    赞(0)
    未经允许不得转载:171主机测评 » 【linux】网络基础(四)HTTP协议,域名,url,认识http的请求和响应,Fiddler Classic,Postman,httpserver服务器的实现,recv
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址