简单的 c 语言实现 http 请求
2026/5/8 16:43:18 网站建设 项目流程

http 协议

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

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

代码实现

代码语言:c

AI代码解释

#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

AI代码解释

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

AI代码解释

#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

AI代码解释

#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: close\r\n" 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 %s\r\n\ Host: %s\r\n\ %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

AI代码解释

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

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


需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询