loading

简单的 c 语言实现 http 请求

  • Home
  • Blog
  • 简单的 c 语言实现 http 请求

简单的 c 语言实现 http 请求

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

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

代码实现代码语言:c复制#include

#include

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

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

#include

#include

#include

#include

#include

#include

#include

#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复制int main() {

...//创建socket之类

int epfd = epoll_create(...); //创建一个epfd

epoll_ctl(epfd, ...); //将请求的描述符添加到 epfd

while(1){

nfds = epoll_wait(epoll_fd, ...); //等待

for(...){ //寻找发生时间的fd

}

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