C语言实现http请求器

2023-05-16

C语言实现http请求器

项目介绍

本项目完成一个http客户端请求器,该请求器往服务器发送请求,并接受服务器发来的响应数据。

程序执行流程

  1. 建立TCP连接
  2. 在TCP连接获得的socket的基础上,发送http协议请求
  3. 服务器通过TCP连接的socket,返回http响应报文(response)

前置知识

http请求报文格式

在这里插入图片描述

http响应报文的格式

在这里插入图片描述

报文中各种空格和换行都是非常严格要求的。

程序实现拆解

假如我们要访问百度的根目录,即主页,则应该:

  1. 将 www.baidu.com 转换为对应的IP地址(DNS)
  2. TCP连接第一步获得的IP地址和端口(http在80端口)
  3. 发送http请求报文
  4. 接受服务器返回的响应报文

通过域名查询IP地址

这个功能的实现,我们使用Linux提供的gethostbyname函数来进行实现,功能和我们前面写的DNS请求器功能差不多。

//函数原型
struct hostent *gethostbyname(const char *name);

struct hostent {
               char  *h_name;            /* official name of host */
               char **h_aliases;         /* alias list */
               int    h_addrtype;        /* host address type */
               int    h_length;          /* length of address */
               char **h_addr_list;       /* list of addresses */
           }

通过域名获得IP地址是网络字节序,所以要转换为点分十进制,供后面socket来进行TCP连接使用,具体实现为

char * host_to_ip(const char *hostname)
{
    struct hostent *host_entry = gethostbyname(hostname);

    if(host_entry)
    {
        //h_addr_list其实是一个指针数组,数组中每个元素char*都是in_addr型指针(都是指针当然可转换)
        //host_entry->h_addr_list类型为 char **, 表明是char*类型数组
        //*host_entry->h_addr_list类型为 char *,即第一个ip地址(数组的第一个元素)
        //网络字节序地址(大端)转换为点分十进制地址(如0x13131313 -> 19.19.19.19)
        return inet_ntoa(*(struct in_addr *)*host_entry->h_addr_list);
    }

    return NULL;
}

这里struct in_addr中in_addr_t一般为32位的unsigned int,用于表示IPV4地址

typedef uint32_t in_addr_t;
struct in_addr {

    in_addr_t s_addr;

};

通过TCP SOCKET连接到服务器上

这是一个客户端连接到服务端的过程,其中的步骤基本上是套路,自从开发出这些函数以来都是这么个使用流程:

int http_create_socket(char *ip)
{
    //http使用TCP协议
    //1.创建socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    //2.设置地址的相关参数(IP PORT PROTOCOL)
    struct sockaddr_in sin = {0};
    sin.sin_family = AF_INET;
    sin.sin_port = htons(80); //http默认端口号为80
    sin.sin_addr.s_addr = inet_addr(ip);//点分十进制地址转为网络字节序地址

    //3.connect
    //connect成功返回0
    if(0 != connect(sockfd, (struct sockaddr*)&sin, sizeof(struct sockaddr_in)))
    {
        return -1;
    }

    //socket设置为非阻塞,保证read没有读出数据时也能够立马返回
    //而不会一直阻塞导致后面的代码不执行
    fcntl(sockfd, F_SETFL, O_NONBLOCK);

    return sockfd;
}

通过socket获得的文件描述符来发送http请求并接受响应报文

这里的主要步骤如下:

  1. 通过域名获得IP地址(host_to_ip)
  2. 创建TCPsocket(socket)
  3. 组织请求的报文
    这里报文的格式必须严格按照标准,不能多一个空格也不能少一个空格。
  4. 发送报文(send)
  5. 重点)使用select循环检测前面获得的socket的fd是否有事件被触发可读

代码实现如下:

char * http_send_request(const char *hostname, const char *resource)
{
    //1.通过域名查询获得ip地址
    char *ip = host_to_ip(hostname);
    
    //2.创建socket(采用TCP连接)
    int sockfd = http_create_socket(ip);

    //3.组织请求报文
    char buffer[BUFFER_SIZE] = {0};// 或者memset清零
    //字符串不在同一行的时候每行结尾要加反斜杠
    //这里格式一定要注意,报文中空格不能多也不能少
    sprintf(buffer,
    "GET %s %s\r\n\
Host: %s\r\n\
%s\r\n\
\r\n",
    resource, HTTP_VERSION,
    hostname,
    CONNECTION_TYPE); //CONNECTION_TYPE我们设置为close


    //4.发送http请求报文
    //最后一个参数为0,表示为阻塞式发送
    //即送不成功会一直阻塞,直到被某个信号终端终止,或者直到发送成功为止。
    send(sockfd, buffer, strlen(buffer), 0);

    //不能简单使用recv()接受响应报文,因为我们创建的socket是非阻塞
    //如果使用recv可能没收到数据也返回了。

    //5.用select实现多路复用IO,循环检测是否有可读事件到来,从而进行recv
    fd_set fdread; // 可读fd的集合
    FD_ZERO(&fdread); //清零
    FD_SET(sockfd, &fdread);//将sockfd添加到待检测的可读fd集合

    struct timeval tv;
    tv.tv_sec = 5;
    tv.tv_usec = 0;


    char *result = malloc(sizeof(int));
    memset(result, 0x00, sizeof(int));
    while(1)
    {
        //第一参数:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1
        //第二参数:可读文件描述符的集合[in/out]
        //第三参数:可写文件描述符的集合[in/out]
        //第四参数:出现错误文件描述符的集合[in/out]
        //第五参数:轮询的间隔时间
        //返回值: 成功返回发生变化的文件描述符的个数
        //0:等待超时,没有可读写或错误的文件
        //失败返回-1, 并设置errno值.
        int selection = select(sockfd + 1,&fdread, NULL, NULL, &tv);
        
        //FD_ISSET判断fd是否在集合中
        if(!selection || !FD_ISSET(sockfd, &fdread))
        {
            break;
        }
        else
        {
            memset(buffer, 0x00, BUFFER_SIZE);
            //返回接受到的字节数
            int len = recv(sockfd, buffer, BUFFER_SIZE, 0);
            if(len == 0)
            {
                //disconnect
                break;
            }
            //如果是扩大内存操作会把 result 指向的内存中的数据复制到新地址
            result = realloc(result, (strlen(result) + len + 1) * sizeof(char));
            strncat(result, buffer, len);
        }
    }

    return result;
}

完整代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <netdb.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define HTTP_VERSION        "HTTP/1.1"
#define CONNECTION_TYPE     "Connection: close\r\n"
//keep-alive

#define BUFFER_SIZE         4096

//通过DNS将域名转为 IP
char * host_to_ip(const char *hostname)
{
    struct hostent *host_entry = gethostbyname(hostname);

    if(host_entry)
    {
        //h_addr_list其实是一个指针数组,数组中每个元素char*都是in_addr型指针(都是指针当然可转换)
        //host_entry->h_addr_list类型为 char **, 表明是char*类型数组
        //*host_entry->h_addr_list类型为 char *,即第一个ip地址(数组的第一个元素)
        //网络字节序地址转换为点分十进制地址
        return inet_ntoa(*(struct in_addr *)*host_entry->h_addr_list);
    }

    return NULL;
}

int http_create_socket(char *ip)
{
    //客户端连接到服务端

    //http使用TCP协议
    //1.创建socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    //2.设置地址的相关参数(IP PORT PROTOCOL)
    struct sockaddr_in sin = {0};
    sin.sin_family = AF_INET;
    sin.sin_port = htons(80); //http默认端口号为80
    sin.sin_addr.s_addr = inet_addr(ip);//点分十进制地址转为网络字节序地址

    //3.connect
    //connect成功返回0
    if(0 != connect(sockfd, (struct sockaddr*)&sin, sizeof(struct sockaddr_in)))
    {
        return -1;
    }

    //socket设置为非阻塞,保证read没有读出数据时也能够立马返回
    //而不会一直阻塞导致后面的代码不执行
    fcntl(sockfd, F_SETFL, O_NONBLOCK);

    return sockfd;
}

char * http_send_request(const char *hostname, const char *resource)
{
    //1.通过域名查询获得ip地址
    char *ip = host_to_ip(hostname);
    
    //2.创建socket(采用TCP连接)
    int sockfd = http_create_socket(ip);

    //3.组织请求报文
    char buffer[BUFFER_SIZE] = {0};// 或者memset清零
    //字符串不在同一行的时候每行结尾要加反斜杠
    //这里格式一定要注意,报文中空格不能多也不能少
    sprintf(buffer,
    "GET %s %s\r\n\
Host: %s\r\n\
%s\r\n\
\r\n",
    resource, HTTP_VERSION,
    hostname,
    CONNECTION_TYPE); //CONNECTION_TYPE我们设置为close


    //4.发送http请求报文
    //最后一个参数为0,表示为阻塞式发送
    //即送不成功会一直阻塞,直到被某个信号终端终止,或者直到发送成功为止。
    send(sockfd, buffer, strlen(buffer), 0);

    //不能简单使用recv()接受响应报文,因为我们创建的socket是非阻塞
    //如果使用recv可能没收到数据也返回了。

    //5.用select实现多路复用IO,循环检测是否有可读事件到来,从而进行recv
    fd_set fdread; // 可读fd的集合
    FD_ZERO(&fdread); //清零
    FD_SET(sockfd, &fdread);//将sockfd添加到待检测的可读fd集合

    struct timeval tv;
    tv.tv_sec = 5;
    tv.tv_usec = 0;


    char *result = malloc(sizeof(int));
    memset(result, 0x00, sizeof(int));
    while(1)
    {
        //第一参数:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1
        //第二参数:可读文件描述符的集合[in/out]
        //第三参数:可写文件描述符的集合[in/out]
        //第四参数:出现错误文件描述符的集合[in/out]
        //第五参数:轮询的时间
        //返回值: 成功返回发生变化的文件描述符的个数
        //0:等待超时,没有可读写或错误的文件
        //失败返回-1, 并设置errno值.
        int selection = select(sockfd + 1,&fdread, NULL, NULL, &tv);
        
        //FD_ISSET判断fd是否在集合中
        if(!selection || !FD_ISSET(sockfd, &fdread))
        {
            break;
        }
        else
        {
            memset(buffer, 0x00, BUFFER_SIZE);
            //返回接受到的字节数
            int len = recv(sockfd, buffer, BUFFER_SIZE, 0);
            if(len == 0)
            {
                //disconnect
                break;
            }
            //如果是扩大内存操作会把 result 指向的内存中的数据复制到新地址
            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);
}

编译指令

gcc -o httprequest httprequest.c

测试效果

1.向百度的根目录发请求

在这里插入图片描述

问题

向bing或者163等网站发请求,都是返回状态码301

在这里插入图片描述

在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C语言实现http请求器 的相关文章

  • Non-Authoritative-Reason 标头字段 [HTTP]

    当我有响应标头时 我很难找出它的含义Non Authoritative Reason HSTS 我搜索了很多 但只是想出了一些关于 HSTS 从 HTTP 重定向到 HTTPS 的解释 有人能帮我吗 顺便说一句 我正在使用 Chrome T
  • C# - 如何进行 HTTP 调用

    我想对网站进行 HTTP 调用 我只需要点击 URL 不想上传或下载任何数据 最简单 最快的方法是什么 我尝试了下面的代码 但它很慢 并且在第二次重复请求后 它只是超时 59 秒 然后恢复 WebRequest webRequest Web
  • 为什么使用 HTTP 动词?

    因为动词的目标是像 server domain getallrecords 或 server domain delete1record 或类似的 URL 而getallrecords delete1record都是专门为特定目的而设计的 为
  • ReverseProxy取决于golang中的request.Body

    我想构建一个 http 反向代理 它检查 HTTP 主体 然后将 HTTP 请求发送到它的上游服务器 你怎么能在 Go 中做到这一点 初始尝试 如下 失败 因为 ReverseProxy 复制传入请求 修改它并发送 但正文已被读取 func
  • 是否有用于通过 HTTP、HTTP 隧道发送二进制数据的 Java 库?

    我想通过 HTTP 以二进制格式发送相当大的数据块 也称为HTTP 隧道 http en wikipedia org wiki HTTP tunnel 我想通过 Java 将这种技术用于一些 Java Swing 应用程序 也可能是 And
  • 在 Go 中跟踪 HTTP 请求时指定超时

    我知道通过执行以下操作来指定 HTTP 请求超时的常用方法 httpClient http Client Timeout time Duration 5 time Second 但是 我似乎不知道在跟踪 HTTP 请求时如何执行相同的操作
  • 通过 HTTPS 加载页面但请求不安全的 XMLHttpRequest 端点

    我有一个页面 上面有一些 D3 javascript 该页面位于 HTTPS 网站内 但证书是自签名的 当我加载页面时 我的 D3 可视化效果不显示 并且出现错误 混合内容 页面位于 https integration jsite com
  • 如何禁用 HTTP 的 HSTS 标头?

    我已将以下内容插入到我网站的 htaccess 中 以便能够访问HSTS预加载列表 https hstspreload appspot com
  • ASP.NET 中 HTTP 缓存相关标头的有效含义

    我正在 ASP NET 2 0 中开发一个 Web 应用程序 其中涉及通过资源处理程序 ashx 提供图像 我刚刚实现了处理缓存标头和条件 GET 请求 这样我就不必为每个请求提供所有图像 但我不确定我是否完全理解浏览器缓存发生了什么 图像
  • 面向服务的架构 - AMQP 或 HTTP

    一点背景 非常大的整体 Django 应用程序 所有组件都使用相同的数据库 我们需要分离服务 以便我们可以独立升级系统的某些部分而不影响其余部分 我们使用 RabbitMQ 作为 Celery 的代理 现在我们有两个选择 使用 REST 接
  • 诸如用于测试 HTTP 请求的虚拟 REST 服务器之类的东西? [关闭]

    Closed 此问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 我一直在四处寻找 但找不到任何这样的网站 我想知道是否有一些虚拟服务器可以响应测试 GET 请求并返回
  • 如何在flutter项目中使用http拦截器?

    我必须向我的所有 Api 添加标头 有人告诉我为此使用 http 拦截器 但我无法理解如何做到这一点 因为我是颤振的新手 谁能帮我举个例子吗 您可以使用http 拦截器 https pub dev packages http interce
  • JS 库请求的常见 HTTP 标头是什么?

    使用JavaScript 框架原型 http www prototypejs org 我注意到 Ajax 请求通过一个名为X Requested With 其他 JavaScript 库 如 jQuery dojo 和 YUI 是否会向其
  • HTTP Header Key 可以重复吗?

    在 JAVA HttpUrlConnection 中 请求 Header 设置的主要逻辑代码如下 public synchronized void set String k String v for int i nkeys i gt 0 i
  • 使用 flash 或 java servlet 将麦克风数据从浏览器上传到服务器的教程? [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 There was a question on how to get data from a microphone on a client
  • Node.js 未处理的“错误”事件

    我编写了一个简单的代码并将其保存在文件 try js 中 var http require http var makeRequest function message var options host localhost port 8080
  • 最适合“正在进行的作业”的 HTTP 状态代码

    向客户端提供的最合适的 HTTP 状态代码是什么 表示 您的请求很好 但仍在进行中 请稍后在完全相同的位置回来查看 例如 假设客户端提交初始请求以启动繁重的查询 服务器立即返回一个 URL 客户端可以定期轮询该 URL 以获取结果 如果客户
  • 使用 python requests 模块时出现 HTTP 503 错误

    我正在尝试发出 HTTP 请求 但当前可以从 Firefox 浏览器访问的网站响应 503 错误 代码本身非常简单 在网上搜索一番后我添加了user Agent请求参数 但也没有帮助 有人能解释一下如何消除这个 503 错误吗 顺便说一句
  • 为什么我的 Github 托管网站响应 HTTP 302 而不是 200?

    我拥有该域名penkov id au http penkov id au 我主持一个blog http michael penkov id au blog 2014 01 02 reinventing the wheel html usin
  • 如何从 Retrofit2 获取字符串响应?

    我正在做 android 正在寻找一种方法来执行超级基本的 http GET POST 请求 我不断收到错误 java lang IllegalArgumentException Unable to create converter for

随机推荐