http服务器实现(一)

xiaoxiao2021-02-28  7

前言

在实践的过程中,我发现,协议理解的深浅,阅读协议文档 < 看协议实现源码 < 自己实现协议的代码。 深入学习http服务器,这是本文的目的,而不是实现一个真正可用的http服务器。毕竟实现一个成熟可用http服务器的难度很大。软件都经历过很多版本的迭代,在不断测试、bug调试和完善功能的过程中,最终才变得成熟可用的。像BAT等大公司听说也是用现有的成熟框架来裁剪开发服务器的。本文参考的源码有boa服务器源码。boa源码下载 本文只是一个服务器的框架程序,在接下来的文章中,我将一步一步完善这个http服务器的功能,并把实验的成果分享出来。我想体现的是一个程序从零开发的思路,因为当面对一大坨一大坨完整的程序,有时会显得很茫然,没经验的很难体会到作者的设计意图。 多年的经验告诉我,如果想要一次性写出完美程序,那么最后就可能因为无从下手而什么都没有写。允许缺陷,开始动手吧!无论过程多么丑陋,最后也会结出经验的果实。 这是第一篇,希望自己能够坚持下去(确实写文章也需要花费很多时间)。

一、select机制

因为下文的程序框架运用到了select机制,这里有必要再回顾一下,参考我以前的博文:TCP socket select用法分析 首先,我们来看看select函数的定义和参数的含义:

int select( int nfds, fd_set FAR* readfds, fd_set * writefds, fd_set * exceptfds, const struct timeval * timeout)

参数含义:

nfds:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!在Windows中这个参数的值无所谓,可以设置不正确。 readfds:(可选)指针,指向一组等待可读性检查的套接口。 writefds:(可选)指针,指向一组等待可写性检查的套接口。 exceptfds:(可选)指针,指向一组等待错误检查的套接口。 timeout:select()最多等待时间,对阻塞操作则为NULL。

返回值: select()调用返回处于就绪状态并且已经包含在fd_set结构中的描述字总数;如果超时则返回0;否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError获取相应错误代码。

当返回为-1时,所有描述符集清0。 当返回为0时,表示超时。 当返回为正数时,表示已经准备好的描述符数。

select()返回后,在3个描述符集里,依旧是1的位就是准备好的描述符。这也就是为什么,每次用select后都要用FD_ISSET的原因。 select函数实现I/O多路复用,可以用来监视多个描述符,之后我们调用FD_ISSET函数确定具体是哪一个描述符准备好了。 那怎样才算准备好了呢?《unix环境高级编程》中,提到:

若对读集中的一个描述符进行的read操作不会阻塞,则认为此描述符是准备好的。若对写集中的一个描述符进行的write操作不会阻塞,则认为此描述符是准备好的。若对异常条件集中的一个描述符有一个未决异常条件,则认为此描述符是准备好的。对于读、写和异常条件,普通文件的文件描述符总是认为准备好的。

操作select函数,还需要以下几个函数配合。 void FD_CLR(int fd, fd_set *set) // 清除set集合中描述符fd int FD_ISSET(int fd, fd_set *set) //判断set集合中描述符fd是否准备好 void FD_SET(int fd, fd_set *set) //将描述符fd添加进集合set(其实是将某一位置1)。 void FD_ZERO(fd_set *set) //将set集全部清除

我们来看看下文使用到的select程序片段:

fd_set block_read_fdset; int max_fd; void select_loop(int server_s) { FD_ZERO(&block_read_fdset); max_fd = server_s+1; while (1) { BOA_FD_SET(server_s, &block_read_fdset); //没有可读的文件描述符,就阻塞。 if (select(max_fd + 1, &block_read_fdset,NULL, NULL,NULL) == -1) { if (errno == EINTR) continue; else if (errno != EBADF) { perror("select"); } } if (FD_ISSET(server_s, &block_read_fdset)) process_requests(server_s); } }

上面的程序,定义一个block_read_fdset读集合,然后调用FD_ZERO(&block_read_fdset) 初始化,接着把监听socket连接的文件描述符加入到读集合block_read_fdset。注意,这时的socket_s还不能用read或write进行读写,必须调用accept函数返回的描述符才行!在while循环中调用select函数监听是否有客户端连接进来,当有客户端向服务器发起connect的时候,则认为server_s描述符是准备好,select函数就会返回,否则select会一直阻塞。因为我们这里没有设置select的超时时间,所以当监听的描述符没有准备好的时候,select默认会阻塞。当select返回的时候,会把在block_read_fdset读集合中没有准备好的文件描述相对应的位给清零。select返回后,我们调用FD_ISSET判断block_read_fdset中server_s描述符对应的位是否被置1了,如果是,说明文件描述符可读,然后调用process_requests函数处理客户端的请求。因为这里select只有添加了一个server_s文件描述符,所以有没有用FD_ISSET判断都无所谓。但是为了适应以后多个描述符的情况,还是添加了FD_ISSET判断,方便移植。

二、服务器源码

下面的服务器程序中,在main函数使用了socket套接字创建TCP面向连接的套接字,流程已经模式化了,在最后调用select_loop函数处理客户端请求。select_loop函数在前面已经分析过了,最后会调用process_requests函数处理客户端的请求。我们可以看到process_requests函数调用accept函数,之后就可以利用accept函数返回的文件描述符来与客户端通信。使用read函数来读取客户端发送过来的信息,并打印到终端。程序显示了一个服务器基本的框架。在后续的文章中,我将在process_requests函数中解析http报文。协议类的都是这样子,客户端发送过来一连串的字符,服务器根据协议约定好的规则去解析这些报文,并根据解析出来的字段去干某些事。如果,客户端发送的是加密的字符,还需要解密之后再进行解析。

//web-server.c, an http server #include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <unistd.h> #include <netinet/in.h> #include <errno.h> #define BUFFER_SIZE 4096 #define MAX_QUE_CONN_NM 5 #define PORT 6000 //#define MAXSOCKFD 10 #define FILE_NAME_MAX 512 #define SOCKADDR sockaddr_in #define S_FAMILY sin_family #define SERVER_AF AF_INET fd_set block_read_fdset; int max_fd; #define BOA_FD_SET(fd, where) { FD_SET(fd, where); \ if (fd > max_fd) max_fd = fd; \ } void select_loop(int server_s); int process_requests(int server_s); int main(int argc,char* argv[]) { int sockfd; int sin_size = sizeof(struct sockaddr); struct sockaddr_in server_sockaddr, client_sockaddr; int i = 1;/* 使得重复使用本地地址与套接字进行绑定 */ /*建立socket连接*/ if ((sockfd = socket(AF_INET,SOCK_STREAM,0))== -1) { perror("socket"); exit(1); } printf("Socket id = %d\n",sockfd); /*设置sockaddr_in 结构体中相关参数*/ server_sockaddr.sin_family = AF_INET; server_sockaddr.sin_port = htons(PORT); server_sockaddr.sin_addr.s_addr = INADDR_ANY; bzero(&(server_sockaddr.sin_zero), 8); setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i)); /*绑定函数bind*/ if (bind(sockfd, (struct sockaddr *)&server_sockaddr, sizeof(struct sockaddr))== -1) { perror("bind"); exit(1); } printf("Bind success!\n"); /*调用listen函数*/ if (listen(sockfd, MAX_QUE_CONN_NM) == -1) { perror("listen"); exit(1); } printf("Listening....\n"); select_loop(sockfd); return 0; } void select_loop(int server_s) { FD_ZERO(&block_read_fdset); max_fd = server_s+1; while (1) { BOA_FD_SET(server_s, &block_read_fdset); //没有可读的文件描述符,就阻塞。 if (select(max_fd + 1, &block_read_fdset,NULL, NULL,NULL) == -1) { if (errno == EINTR) continue; /* while(1) */ else if (errno != EBADF) { perror("select"); } } if (FD_ISSET(server_s, &block_read_fdset)) process_requests(server_s); } } int process_requests(int server_s) { int fd; /* socket */ struct SOCKADDR remote_addr; /* address */ int remote_addrlen = sizeof (struct SOCKADDR); size_t len; char buff[BUFFER_SIZE]; bzero(buff,BUFFER_SIZE); //remote_addr.S_FAMILY = 0xdead; fd = accept(server_s, (struct sockaddr *) &remote_addr, &remote_addrlen); if (fd == -1) { if (errno != EAGAIN && errno != EWOULDBLOCK) /* abnormal error */ perror("accept"); return -1; } int bytes = read(fd, buff, BUFFER_SIZE); if (bytes < 0) { if (errno == EINTR) bytes = 0; else return -1; } printf("recv from client:%s\n",buff); return 0; }

三、客户端测试程序

接下来,写一个简单的客户端程序来测试服务器程序。程序如下:

/*client.c*/ #include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <unistd.h> #include <netdb.h> #include <netinet/in.h> #include <pthread.h> #define PORT 6000 #define BUFFER_SIZE 4096 #define FILE_NAME_MAX 512 int main(int argc,char* argv[]) { int sockfd; //char buff[BUFFER_SIZE]; struct hostent *host; struct sockaddr_in serv_addr; if(argc != 2) { fprintf(stderr,"Usage: ./client Hostname(or ip address) \ne.g. ./client 127.0.0.1 \n"); exit(1); } //地址解析函数 if ((host = gethostbyname(argv[1])) == NULL) { perror("gethostbyname"); exit(1); } //创建socket if ((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1) { perror("socket"); exit(1); } bzero(&serv_addr,sizeof(serv_addr)); //设置sockaddr_in 结构体中相关参数 serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(PORT); //将16位的主机字符顺序转换成网络字符顺序 serv_addr.sin_addr = *((struct in_addr *)host->h_addr); //获取IP地址 bzero(&(serv_addr.sin_zero), 8); //填充0以保持struct sockaddr同样大小 //调用connect函数主动发起对服务器端的连接 if(connect(sockfd,(struct sockaddr *)&serv_addr, sizeof(serv_addr))== -1) { perror("connect"); exit(1); } char buff[BUFFER_SIZE]= "<letter>getPwd</letter>"; //读写缓冲区 int count; count=send(sockfd,buff,100,0); if(count<0) { perror("Send file informantion"); exit(1); } printf("client send OK count = %d\n",count); return 0; }

程序很简单,只是使用socket的框架建立一个客户端,然后用send函数发送一段字符串。我们打开终端实验一下。 打开一个终端,A 终端,先运行服务器程序,结果如下:

ubuntu@ubuntu:~/project/web-server$ ./web-server Socket id = 3 Bind success! Listening....

然后再打开一个终端,B终端,运行客户端程序,结果如下:

ubuntu@ubuntu:~/project/web-server$ ./client 127.0.0.1 client send OK count = 100

再看看A终端,发现服务器程序接收到了来自客户端发送的字符串。

ubuntu@ubuntu:~/project/web-server$ ./web-server Socket id = 3 Bind success! Listening.... recv from client:<letter>getPwd</letter>

下一篇:将会讲到http服务器是如何解析http报文的。

转载请注明原文地址: https://www.6miu.com/read-542668.html

最新回复(0)