http 协议
http 协议基本算是网络的基础了,因此长话短说,直接上代码。
首先 http 协议一般需要 dns 协议的配合向服务端发送请求,因此首先需要解析 IP 地址。c 语言中其实有专门的解析函数。
代码实现
代码语言:c复制#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复制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 <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复制#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: closern"
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 %srn
Host: %srn
%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 使用的是红黑树来保存请求描述符,同时有时间发生的时候,会通过回调函数将事件发送到链表,方便了查找。在这方面后边可以进一步探究,今天就到这里了。