• LinuxC/C++ 实现HTTP Request


    LinuxC/C++ 实现HTTP Request



    http请求报文

    想实现http request就得先了解http请求报文的格式. http 请求报文由请求行请求头空行请求体组成.

    请求报文

    • 请求方法:通常有GET和POST.
    • URL:域名 + 路由.
    • 版本号:目前最流行的是HTTP/1.1,最新的是HTTP/2.0.

    请求头

    以键值对的形式定义了很多属性,例如HTTP/1.1有两种连接方式:长连接(keep-alive)和短连接,默认是长连接,如果要改成短连接,则需要把Connection属性修改为:Connection: close.

    空行

    表示响应头的结束标记.

    请求体

    服务器返回给客户端的具体数据,可能有各种不同的格式,其中最常见的格式: html. Body允许为空字符串. 如果Body有内容,则在Header中会有一个Content-Length属性来标识Body的长度, 如果服务器返回了一个html页面,,那么html页面内容就是在body中.

    在这里插入图片描述

    另外还需要注意报文中的换行符.

    select I/O复用

    HTTP/1.1默认采用的连接方式是keep-alive,属于阻塞式I/O. 但是我们要使用非阻塞的connect技术来实现,那么就涉及到客户端程序要同时处理多个socket的情况了,这里我们使用select系统调用来对这些socket进行监听.

    select系统调用的用途是:在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常等事件.

    select API

    #include <sys/select.h>
    
    int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
    
    • 1
    • 2
    • 3
    • nfds参数指定被监听的文件描述符的总数. 它通常被设置为select监听的所有文件描述符中的最大值加1,因为文件描述符是从0开始计数的.

    • readfdswritefdsexceptfds参数分别指向可读、可写和异常等事件对应的文件描述符集合. 应用程序调用select函数时,通过这3个参数传入自已感兴趣的文件描述符. select调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪. 这3个参数是fd_set结构指针类型,里面存的是监听的socket文件描述符.

    • timecout参数用来设置select函数的超时时间. 它是一个timeval结构类型的指针,采用指针参数是因为内核将修改它以告诉应用程序select等待了多久. timeval结构体的定义如下:

      struct timeval {
      	long tv_sec;	// 秒数
      	long tv_usec;	// 微秒数
      	
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5

      select 给我们提供了一个微秒级的定时方式,如果给timeout变量tv.sec成员和tv_usec成员都传递0,则select将立即返回;如果给timeout传递NULL,则seleet将一直阻塞,直到某个文件描述符就绪.

    sclect成功时返回就绪(可读、可写和异常)文件描述符的总数. 如果在超时时间内没有任何文件描述符就绪,select 将返回0, select 失败时返回-1并设置errno. 如果在select等待期间,程序接收到信号,则select立即返回-1,并设置errno为EINTR.

    具体实现

    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    
    #include <fcntl.h>
    #include <netdb.h>
    
    // 使用HTTP/1.1
    #define HTTP_VERSION    "HTTP/1.1"
    // 使用短连接
    #define CONNETION_TYPE  "Connection: close\r\n"
    
    // 缓冲区大小
    #define BUFFER_SIZE     4096
    
    // 自定义函数,将域名转化为ip地址
    char *host_to_ip(const char *hostname) {
        struct hostent *host_entry = gethostbyname(hostname);
    
        // unsigned in --> char *
        if (host_entry) {
        // inet_ntoa函数将网络字节序转化为点分十进制字符串
            return inet_ntoa(*(struct in_addr *)*host_entry->h_addr_list);
        }
    
        return NULL;
    }
    
    int http_create_socket(char *ip) {
    		// SOCK_STREAM 表示使用TCP协议
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    
    		// 定义套接字地址
        struct sockaddr_in sin = {0};
        sin.sin_family = AF_INET;
        sin.sin_port = htons(80);
        // char * --> unsigned int(inet_addr函数将点分十进制字符串转化为网络字节序)
        sin.sin_addr.s_addr = inet_addr(ip);
    
        // 连接服务器
        if (0 != connect(sockfd, (struct sockaddr *)&sin, sizeof(struct sockaddr_in))) {
            return -1;
        }
    
        // 使用非阻塞IO
        fcntl(sockfd, F_SETFL, O_NONBLOCK);
    
        return sockfd;
    }
    
    char *http_send_request(const char *hostname, const char *resource) {
        char *ip = host_to_ip(hostname);
        int sockfd = http_create_socket(ip);
    
        char buffer[BUFFER_SIZE] = {0};
        sprintf(buffer, 
            "GET %s %s\r\n\
    HOST: %s\r\n\
    %s\r\n\
    \r\n",
            resource, HTTP_VERSION,
            hostname,
            CONNETION_TYPE
        );
    
        send(sockfd, buffer, strlen(buffer), 0);
    
        // select:监听网络IO中是否有可读数据
        // fd_set是一个数组,里面放着sockfd
        fd_set fdread;
        // 置零初始化
        FD_ZERO(&fdread);
        FD_SET(sockfd, &fdread);
    
        // 定义select多长时间轮询一次
        struct timeval tv;
        tv.tv_sec = 5;
        tv.tv_usec = 0;
    
    
        char* result = malloc(sizeof(int));
    	memset(result, 0, sizeof(int));
        // 可能一次性发送不完
        while (1) {
            // 监听的文件描述符的数量
            // 可读文件描述符集合
            // 可写文件描述符集合
            // 出错文件描述符集合
            // 多长时间轮询一次
            int selection =  select(sockfd + 1, &fdread, NULL, NULL, &tv);
            // 如果超时或者从集合里面取出文件描述符出错
            if (!selection || !FD_ISSET(sockfd, &fdread)) {
                break;
            } else {
                memset(buffer, 0, BUFFER_SIZE);
                // 如果没出错就开始接收,返回读出的长度
                int len = recv(sockfd, buffer, BUFFER_SIZE, 0);
                if (len == 0) {
                    break;
                }
                result = realloc(result, (strlen(result) + len + 1) * sizeof(char));
    			strncat(result, buffer, len);
            }
        }
        return result;
    }
    
    int main(int argc, char *argv[]) {
        if (argc < 3) return -1;
        
        char *response = http_send_request(argv[1], argv[2]);
        printf("response : %s\n", response);
    
        free(response);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120

    CMakeLists.txt文件

    PROJECT(HTTPCLIENT)
    ADD_EXECUTABLE(http HttpRequest.c)
    
    • 1
    • 2

    执行结果

    在这里插入图片描述

  • 相关阅读:
    为什么用户在注册时需要使用邮箱或手机号作为注册名?
    QT With OpenGL(延时着色法)(Deferred Shading)
    IDaaS 系统 ArkID 一账通内置插件:图形验证码认证因素的配置流程
    SQL SELECT 语句进阶
    1:引文;
    优化理论12---- 既约梯度法
    Java低代码:jvs-list (子列表)表单回显及触发逻辑引擎配置说明
    vue2技能树(6)-事件处理和表单输入绑定
    conda常用指令
    【工作中问题解决实践 一】最小单元染色法的应用
  • 原文地址:https://blog.csdn.net/qq_49723651/article/details/125609831