简单的 c 语言实现 http 请求

2024-04-28 23:14:23 浏览数 (4)

http 协议

http 协议基本算是网络的基础了,因此长话短说,直接上代码。

首先 http 协议一般需要 dns 协议的配合向服务端发送请求,因此首先需要解析 IP 地址。c 语言中其实有专门的解析函数。

代码实现

代码语言:c复制
#include <netdb.h>
#include <arpa/inet.h>

char* host_to_ip(const char* hostname)
{
    struct hostent *host_entry = gethostbyname(hostname);
    if(host_entry){
        return inet_ntoa(*(struct in_addr*) host_entry->h_addr_list[0]);
    }
    return NULL;
}

特意加上了头文件,其中 gethostbyname 这个函数是头文件 netdb.h 中的函数。他返回了一个结构体,具体结构体代码如下:

代码语言:c复制
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 from name server */
#if !defined(_POSIX_C_SOURCE) || defined(_DARWIN_C_SOURCE)
#define	h_addr	h_addr_list[0]	/* address, for backward compatibility */
#endif /* (!_POSIX_C_SOURCE || _DARWIN_C_SOURCE) */
};

其中 h_addr_list 是保存着 IP 地址,只不过这个地址不是我们常见的那种 192.168.1.1 之类的地址,所以我们需要 inet_ntoa 函数进行一个转换。

然后就是一个常规的 http 请求发送,然后返回 response,不过在这之前我们为了缩减代码先使用一个生成 socket 的函数

代码语言:c复制
#include <fcntl.h>

int http_create_socket(char* ip)
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);  //tcp socket
    
    struct sockaddr_in sin = {0};
    sin.sin_family = AF_INET;
    sin.sin_port = 80;
    sin.sin_addr.s_addr = inet_addr(ip);  //配置信息
    
    if(0 != connect(sockfd, (struct sockaddr*)&sin, sizeof(struct sockaddr)))// 连接服务器
    {
        return -1;
    }
    fcntl(sockfd,  F_SETFL, O_NONBLOCK); //非阻塞
    
    return sockfd;
    
}

这里有一个阻塞的概念,阻塞简单就是当我们的线程进行活动需要一些资源,如果当前资源不满足那么就有两种方式,一种是我等着,等条件满足了,我再进一步执行,一般是像加锁之类的,另一种就是条件不行,我直接报错,一分钟也不等了,这就是非阻塞,这里我们的业务简单直接非阻塞。

最后就是我们的最后内容,发送请求。

代码语言:c复制
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/select.h>

#define BUF_SIZE 4096
#define HTTP_VERSION "HTTP/1.1"
#define CONNECTION_TYPE "Connection: closern"

char* http_send_request(const char* hostname, const char* resourse)
{
    char* ip = host_to_ip(hostname);  //通过域名解析ip
    int sockfd = http_create_socket(ip); //创建socket
    char buffer[BUF_SIZE] = {0};
    sprintf(buffer,
"GET %s %srn
Host: %srn
%s", resourse, HTTP_VERSION, hostname, CONNECTION_TYPE); //将协议头写入buffer
    send(sockfd, buffer, strlen(buffer), 0);  //发送
    
    //多路复用 收集多个文件描述符
    fd_set fdread;         //描述符集合
    FD_ZERO(&fdread);//设置为 0
    FD_SET(sockfd, &fdread); //将打开的描述符加入集合中
    
    struct timeval tv;
    tv.tv_sec = 5;     //设置多路复用的超时时间 秒级别
    tv.tv_usec = 0;  //微秒级别
    
    char* result = (char *)malloc(sizeof(int));  //开始四个自己的result
    
    while(1)
    {
        int selection = select(sockfd   1, &fdread, NULL, NULL, &tv);  //使用select多路复用
        if(!selection || !FD_ISSET(sockfd, &fdread)) //设置多个fd进程
        {
            break;
        }else{
            memset(buffer, 0, BUF_SIZE);  设置buffer
            int len = (int)recv(sockfd, buffer, BUF_SIZE, 0); //接受字节
            if(len == 0){
                break;
            }
            
            result = realloc(result, (strlen(result)   len   1) * sizeof(char)); //重新分配result
            strncat(result, buffer, len); //将接受内容加入result
        }
    }
    return result;
}

这里其他部分都比较简单,最大不同就是使用了 select I/O多路复用。我们知道 I/O 多路复用有 select, poll, epoll 三种类型,基本也是面试必考类型。

这里简单介绍一下,多路复用就是让一个进程可以处理多个发生事件,防止我们发生一件事情就创建一个进程,然后事件完了之后我们销毁,这种对我们系统性能损耗太大,其实之前的线程池也有类似作用。

线程池是系统创建的进程集中起来,来了一个事件之后我们就取出一个线程处理,而多路复用是我们把事件集中起来,然后我们通过一个线程挨个处理这一堆事情。

select 就是最简单多路复用,就是将 sockfd 也就是一个个的 socket 或者文件描述符集中在一起处理,每个请求来了之后,我们去处理。

poll 跟 select 原理一样,不过就是原来用位图存储文件描述符改成了链表,位图我们知道受计算机的位数限制,文件描述符可以存更多了。

epoll 相对来说提升更多,各种存储结构变化了。我们在应用层要使用可以这样写

代码语言:c复制
int main() {
    ...//创建socket之类
    int epfd = epoll_create(...);  //创建一个epfd
   epoll_ctl(epfd, ...);  //将请求的描述符添加到 epfd
   while(1){
       nfds = epoll_wait(epoll_fd, ...); //等待
       for(...){ //寻找发生时间的fd

       }
   }

跟 select 和 poll 不同的是,epoll 使用的是红黑树来保存请求描述符,同时有时间发生的时候,会通过回调函数将事件发送到链表,方便了查找。在这方面后边可以进一步探究,今天就到这里了。

1 人点赞