根据csapp第三部分,来写个小小的总结
web服务器使用http协议和他们的客户端进行通信,浏览器向服务器请求静态或动态的内容。对于静态请求,当它接收到来自客户端的url访问后,它需要解析url,获得客户端欲访问文件的路径,服务端请求到相应的html文件并显示,再返回给客户端相应的http状态码,这就实现了一个web服务器最最基本的静态显示功能。另外还有动态显示,对此需要创建出一个子进程来进行相应的请求操作。其中需要用到的有进程控制,Unix I/O,套接字接口和http,接下来看看怎么一步步实现并完善它。
1.首先来看main函数
int main(int argc,char **argv)
{
int listenfd,confd;
socklen_t clientlen;
char hostname[100],port[100];
struct sockaddr_storage client;
if(argc != 2 ){
fprintf(stderr,"usage: %s <port>\n",argv[0]);
exit(0);
}
listenfd = open_listen(argv[1]);
while(1){
clientlen = sizeof(client);
confd = accept(listenfd,(sockaddr *)&client,&clientlen);
getnameinfo((sockaddr *)&client,clientlen,hostname,50,port,50,0);
printf("accept from hostname:%s,port is %s",hostname,port);
doit(confd);
close(confd);
}
}
通过一个while循环,是服务器不断地accept,当没有客户端请求连接是变阻塞在此,有连接后就获取客户端主机名,执行相关实务,最后关闭连接。
2.doit()函数来读和解析请求行,它读取到来自客户端的请求行后,分解得到方法(只支持get),URL,http版本。再根据文件路径获得文件内容,如果是静态访问,就是一个普通的文件,我们可以直接显示文件内容,如果是动态内容,我们就提供相应的动态内容
void doit(int fd)
{
int is_static;
struct stat sbuf;
char buf[50],method[50],uri[50],version[50];
rio_t rio;
char filename[50],cnt[50];
rio_readinitb(&rio,fd);
rio_readlineb(&rio,buf,100);
printf("request header is: %s",buf);
sscanf(buf,"%s %s %s",method,uri,version);
if(!strcasecmp(method,"get")){
printf("cant achieve");
return;
}
read_request(&rio); //把输入的内容输出到控制台
is_static = parse_uri(uri,filename,cnt); //获取文件状态
if(stat(filename,&sbuf)<0){
printf("404 NOT FOUND");
}
if(is_static){
server_static(fd,filename,sbuf.st_size); //景泰
}
else{
server_dynamic(fd,filename,cnt);
}
}
2.如果是静态内容就直接读取该文件并发送相应的响应行和响应报头给客户端
void server_static(int fd,char *filename,int filesize)
{
int srcfd;
char *srcp,filetype[50],buf[50];
sprintf(buf,"http/1.0 200 ok\r\n");
sprintf(buf,"%sserver:tiny\r\n",buf);
sprintf(buf,"%scontent-length:%d\r\n",buf,filesize);
rio_writen(fd,buf,strlen(buf));
printf("reponse header:\n");
printf("%s",buf);
srcfd = open(filename,O_RDONLY,0);
close(srcfd);
rio_writen(fd,srcp,filesize);
}
3.如果是动态的内容请求就派生一个子进程,子进程用来自请求URL的GGI参数初始化环境变量,然后子进程重定向它的标准输出到已连接的文件描述符,父进程阻塞在wait()函数处等待子进程终止,子进程运行GGI程序,来提供各类动态内容
void server_dynamic(int fd,char *filename,char *cnt)
{
char buf[50],*emptylist[] = {NULL};
sprintf(buf,"http/1.0 200 ok\r\n");
rio_writen(fd,buf,strlen(buf));
sprintf(buf,"server\r\n");
rio_writen(fd,buf,strlen(buf));
if(fork() == 0){
setenv("query_string",cnt,1);
dup2(fd,STDOUT_FILENO);
execve(filename,emptylist,environ);
}
wait(NULL);
}
自此最简单的服务器的主要功能就完成了,以下是完整代码
#include <iostream>
#include <netdb.h>
#include <cstring>
#include <unistd.h>
#include <sys/stat.h>
#include "rio.cpp"
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <wait.h>
#include <slcurses.h>
int open_listen(char *port);
void read_request(rio_t *rio);
void doit(int fd);
void read_request(rio_t *rp);
int parse_uri(char *uri,char *filename,char *cnt);
void server_static(int fd,char *filename,int filesize);
void server_dynamic(int fd,char *filename,char *cnt);
int open_listen(char *port)
{
struct addrinfo hints,*listp,*p;
int listen,optval=1;
memset(&hints,0, sizeof(addrinfo));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG;
hints.ai_flags |= AI_NUMERICSERV;
getaddrinfo(NULL,port,&hints,&listp);
for(p = listp;p;p->ai_next){
if((listen = socket(p->ai_family,p->ai_socktype,p->ai_protocol))<0){
continue;
}
setsockopt(listen,SOL_SOCKET,SO_REUSEADDR,(const void *)&optval, sizeof(int));
if(bind(listen,p->ai_addr,p->ai_addrlen)== 0)
break;
close(listen);
}
freeaddrinfo(listp);
if(!p)
return -1;
else
return listen;
}
void doit(int fd)
{
int is_static;
struct stat sbuf;
char buf[50],method[50],uri[50],version[50];
rio_t rio;
char filename[50],cnt[50];
rio_readinitb(&rio,fd);
rio_readlineb(&rio,buf,100);
printf("request header is: %s",buf);
sscanf(buf,"%s %s %s",method,uri,version);
if(!strcasecmp(method,"get")){
printf("cant achieve");
return;
}
read_request(&rio); //把输入的内容输出到控制台
is_static = parse_uri(uri,filename,cnt); //获取文件状态
if(stat(filename,&sbuf)<0){
printf("404 NOT FOUND");
}
if(is_static){
server_static(fd,filename,sbuf.st_size); //景泰
}
else{
server_dynamic(fd,filename,cnt);
}
}
void read_request(rio_t *rp) //
{
char buf[50];
rio_readlineb(rp,buf, 50);
while(strcmp(buf,"\r\n")){
rio_readlineb(rp,buf,100);
printf("%s",buf);
}
}
int parse_uri(char *uri,char *filename,char *cnt)
{
char *p;
if(!strstr(uri,"cgi-bin")){ //静态
strcpy(filename,".");
strcat(filename,uri);
return 1;
}
else {
p = index(uri, '?');
if (p) {
strcpy(cnt, p + 1);
} else {
strcpy(filename, ".");
strcat(filename, uri);
return 0;
}
}
}
void server_static(int fd,char *filename,int filesize)
{
int srcfd;
char *srcp,filetype[50],buf[50];
sprintf(buf,"http/1.0 200 ok\r\n");
sprintf(buf,"%sserver:tiny\r\n",buf);
sprintf(buf,"%scontent-length:%d\r\n",buf,filesize);
rio_writen(fd,buf,strlen(buf));
printf("reponse header:\n");
printf("%s",buf);
srcfd = open(filename,O_RDONLY,0);
close(srcfd);
rio_writen(fd,srcp,filesize);
}
void server_dynamic(int fd,char *filename,char *cnt)
{
char buf[50],*emptylist[] = {NULL};
sprintf(buf,"http/1.0 200 ok\r\n");
rio_writen(fd,buf,strlen(buf));
sprintf(buf,"server\r\n");
rio_writen(fd,buf,strlen(buf));
if(fork() == 0){
setenv("query_string",cnt,1);
dup2(fd,STDOUT_FILENO);
execve(filename,emptylist,environ);
}
wait(NULL);
}
int main(int argc,char **argv)
{
int listenfd,confd;
socklen_t clientlen;
char hostname[100],port[100];
struct sockaddr_storage client;
if(argc != 2 ){
fprintf(stderr,"usage: %s <port>\n",argv[0]);
exit(0);
}
listenfd = open_listen(argv[1]);
while(1){
clientlen = sizeof(client);
confd = accept(listenfd,(sockaddr *)&client,&clientlen);
getnameinfo((sockaddr *)&client,clientlen,hostname,50,port,50,0);
printf("accept from hostname:%s,port is %s",hostname,port);
doit(confd);
close(confd);
}
return 0;
}
但这样的服务器性能非常地低,没来到一个连接请求,服务器就要创建相应的套接字,结束后销毁,就会不断地创建销毁,同时服务器还要不停地accept,并且服务器一时间只能服务一个客户端,这就需要引进并发,并发就是可以运行多个应用程序,有三种基本的构造并发程序的方法:
(1)进程,fork多个进程进行不同的任务
(2)I/O多路复用,应用程序在一个进程的上下文显示地调用自己的逻辑流,即程序来使进程某个函数调用(回调函数)
(3)线程,线程是运行在一个进程上下的逻辑流。
以下来具体讨论
1.首先实现基于进程的并发服务器,对于每个新的客户端,服务器都创建一个新的进程来为其提供服务,需要注意对于每个子进程在结束连接后都必须关闭它的connfd,不然会造成内存泄露,实现代码如下
每到来一个客户请求就要fork一个新的进程,进程都有自己独立的地址空间,这其实对于服务器的开销很大。同时设想当有几个客户端连接这服务器时,服务器应该对他们都要同时响应,这就是IO复用
2.基于IO复用的并发编程
IO复用就要借助一些特殊的linux下的函数,如select,poll,epoll,他们可以让服务器在没有响应时挂起,当有多个IO事件发生后将控制返回给应用程序
int main(int argc,char **argv) {
int listenfd, confd;
struct sockaddr_storage client;
char hostname[50], port[50];
socklen_t clientlen;
fd_set read_set, ready_set;
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(0);
}
FD_ZERO(&read_set); //创建一个空的读集合
FD_SET(STDIN_FILENO, &read_set);
FD_SET(listenfd, &read_set);
while (1) {
ready_set = read_set;
select(listenfd + 1, &ready_set, NULL, NULL, NULL);
if (FD_ISSET(STDIN_FILENO, &ready_set))
command();
if (FD_ISSET(listenfd, &ready_set)) {
clientlen = sizeof(struct sockaddr_storage);
confd = accept(listenfd, (sockaddr *) &client, &clientlen);
doit(confd);
close(confd);
}
}
return 0;
}
其中关键函数就是select,程序中每当一个连接就创建一个套接字,存储在read_set中,然后select函数监听这些套接字,当其中有IO事件发生时就告知服务器,没有就阻塞在此
3.基于I/O多路复用的并发事件驱动服务器
typedef struct {
int maxfd;
fd_set ready_set;
fd_set read_set;
int nready;
int maxi;
int clientfd[100];
rio_t clientrio[100];
}pool;
int byte_cnt = 0;
void init_pool(int listenfd,pool *p) //初始化客户端池
{
int i;
p->maxi = -1;
for(int i=0;i<100;i++){
p->clientfd[i] = -1;
p->maxfd = listenfd;
FD_ZERO(&p->read_set);
FD_SET(listenfd,&p->read_set);
}
}
void add_client(int confd,pool *p) //将connfd加入到池中
{
int i;
p->nready--;
for(int i=0;i<100;i++){
if(p->clientfd[i] <0){
p->clientfd[i] = confd;
rio_readinitb(&p->clientrio[i],confd);
FD_SET(confd,&p->read_set);
if(confd>=p->maxfd)
p->maxfd = confd;
if(i>p->maxi){
p->maxi = i;
}
}
}
}
void check_clients(pool *p)
{
int i,confd,n;
}
int main(int argc,char **argv)
{
int listenfd,confd;
socklen_t clientlen;
struct sockaddr_storage client;
static pool pool;
if(argc != 2 ){
fprintf(stderr,"usage: %s <port>\n",argv[0]);
exit(0);
}
listenfd = open_listen(argv[1]);
init_pool(listenfd,&pool);
while(1){
pool.ready_set = pool.read_set;
pool.nready = select(pool.maxfd+1,&pool.ready_set,NULL,NULL,NULL);
if(FD_ISSET(listenfd,&pool.ready_set)){
clientlen = sizeof(struct sockaddr_storage);
confd = accept(listenfd,(sockaddr *)&client,&clientlen);
add_client(confd,&pool);
}
check_clients(pool);
}
}