本项目完成一个http客户端请求器,该请求器往服务器发送请求,并接受服务器发来的响应数据。
报文中各种空格和换行都是非常严格要求的。
假如我们要访问百度的根目录,即主页,则应该:
这个功能的实现,我们使用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;
};
这是一个客户端连接到服务端的过程,其中的步骤基本上是套路,自从开发出这些函数以来都是这么个使用流程:
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;
}
这里的主要步骤如下:
host_to_ip
)socket
)send
)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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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