LinuxC/C++ 实现HTTP Request

14 篇文章 2 订阅

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);
  • 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;	// 微秒数
    	
    }
    

    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);
}

CMakeLists.txt文件

PROJECT(HTTPCLIENT)
ADD_EXECUTABLE(http HttpRequest.c)

执行结果

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Linux系统上使用VS Code配置C/C++环境可以提供一个方便的开发环境。以下是配置步骤: 1. 安装VS Code:首先,你需要在Linux系统上安装VS Code。你可以从VS Code官方网站下载适用于Linux的安装包,并按照指示进行安装。 2. 安装C/C++扩展:打开VS Code,点击左侧的扩展图标(四个方块组成的图标),搜索并安装"C/C++"扩展。这个扩展提供了C/C++开发所需的功能和工具。 3. 安装GCC编译器:C/C++代码需要使用GCC编译器进行编译。在终端中运行以下命令安装GCC编译器: ``` sudo apt-get install build-essential ``` 4. 配置任务:在VS Code中,按下Ctrl+Shift+P打开命令面板,输入"Tasks: Configure Task"并选择"Create tasks.json file from template"。然后选择"C++"模板,这将创建一个tasks.json文件。 5. 配置编译任务:编辑tasks.json文件,将以下内容添加到"tasks"数组中: ``` { "label": "build", "type": "shell", "command": "g++", "args": [ "-g", "${file}", "-o", "${fileDirname}/${fileBasenameNoExtension}" ], "group": { "kind": "build", "isDefault": true } } ``` 这个配置将使用g++编译器编译当前打开的文件,并将可执行文件保存在与源文件相同的目录中。 6. 配置调试器:在VS Code中,点击左侧的调试图标(类似于虫子的图标),然后点击旁边的齿轮图标,选择"C++ (GDB/LLDB)"。这将在.vscode目录下创建一个launch.json文件。 7. 配置调试任务:编辑launch.json文件,将以下内容添加到"configurations"数组中: ``` { "name": "(gdb) Launch", "type": "cppdbg", "request": "launch", "program": "${fileDirname}/${fileBasenameNoExtension}", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}", "environment": [], "externalConsole": true, "MIMode": "gdb", "miDebuggerPath": "/usr/bin/gdb" } ``` 这个配置将使用GDB调试器来调试编译后的可执行文件。 8. 开始编写和调试:现在你已经完成了C/C++环境的配置。你可以创建一个新的C/C++文件,编写代码,并使用Ctrl+B进行编译。然后,你可以使用调试功能来运行和调试你的程序。 希望以上步骤对你有帮助!如果你有任何问题,请随时提问。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值