想实现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中.
另外还需要注意报文中的换行符
.
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开始计数的.
readfds
、writefds
和exceptfds
参数分别指向可读、可写和异常等事件对应的文件描述符集合. 应用程序调用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)
执行结果