HTTP协议是最被广泛使用的网络传输协议,所有的www文件都必须遵守这个标准,并且,HTTP基于TCP/IP通信协议来传递数据,本文的主要内容是介绍HTTP请求的的格式,并且使用C语言以TCP的方式来实现请求的发送。
创建套接字
HTTP协议是应用层的协议,而其传输接口层的实现需要通过创建套接字。
int http_create_socket(char *ip) {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
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);//将char*转化为无符号int
if (0 != connect(sockfd, (struct sockaddr*)&sin, sizeof(struct sockaddr_in))) {
return -1;
}
fcntl(sockfd, F_SETFL, O_NONBLOCK);//设置为非阻塞
return sockfd;
}
关键技术阐述:上述代码的第3行调用socket函数用以创建套接字描述符,其中第一个参数指定使用IPv4, 第二个参数SOCK_STREAM指定后,默认使用TCP(而不是UDP)协议进行传输,第三个参数一般是具体协议选择,设置为0让系统自行决定即可。
5~12行为相对固定的编程套路,填充好套接字地址sockaddr_in这个结构体的相关内容,与本文相关唯一值得注意的点是,HTTP默认使用80端口。
第14行使用fcntl(file control函数)将连接设置为非阻塞。如果socket是阻塞的,read()的时候整个程序挂起,等待io数据到来,反之则不会。使用到的时候查询下这个函数的参数如何设置即可,不需要特别记忆。
HTTP请求报文的一般格式
发送http请求的函数实现如下
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,
CONNECTION_TYPE
);
send(sockfd, buffer, strlen(buffer), 0);
//select负责检测网络io里面有没有可读的数据
fd_set fdread;
FD_ZERO(&fdread); //描述符集置空
FD_SET(sockfd, &fdread);
struct timeval tv;
tv.tv_sec = 5;
tv.tv_usec = 0;
char *result = malloc(sizeof(int));
memset(result, 0, sizeof(int));
while (1) {
//用法:select(maxfd+1, &rset, &wset, &eset, NULL);
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));
strncat(result, buffer, len);
}
}
return result;
}
关键技术阐述:第6~15行:像TCP这种协议,数据的传输是一串一串的发送和到来的,更准确的描述是数据流,所以为了应对数据流断断续续到达的情况,需要使用到缓冲区buffer。而http请求内容符合我们上面那张图的格式即可。这里我们为了可读性和可维护性,使用了两个define:
#define HTTP_VERSION "HTTP/1.1"
#define CONNECTION_TYPE "Connection: close\r\n"
19~22行,之前讲过,本文中采用的是非阻塞的实现(read的时候不会挂起,系统继续其他的工作),所以我们可以搭配使用select函数(用于监听socket描述符)实现事件驱动,避免了忙轮询占用大量系统资源。有关细节可以看我的另一篇文章:CSAPP网络编程章节实验:Proxylab
第24~26行设置了一个超时的时间为5s,超过这个时间断开连接。避免无意义的等待。
第31~41行用while(1)循环反复读取服务器响应的内容,直到len等于0读取完毕打破循环退出。