【Tinyhttpd学习】C 语言实现最简单的 HTTP 服务器
整体工作流程

(1)服务器启动,在指定端口或者随机端口绑定httpd服务
(2)在收到客户端请求后 ,服务器(主线程)accept这个连接,然后创建一个子线程,执行accept_request函数对这个请求进行处理。
(3)通过accept_request函数 提取出 请求的 方法,url 从而判断客户端的请求 ,如果客户端请求的是静态页面,执行 serve_file函数,直接将请求的数据发送给客户端。如果客户端请求的是cgi文件,执行execute_cgi函数,调用cgi文件,服务器将cgi执行得到的结果发送给客户端。
int main(void)
{
int server_sock = -1;
u_short port = 0; //监听端口号
int client_sock = -1;
struct sockaddr_in client_name;
socklen_t client_name_len = sizeof(client_name);
pthread_t newthread;
server_sock = startup(&port);//服务器端监听套接字设置
printf("httpd running on port %d\n", port);
while(1)
{
client_sock = accept(server_sock,
(struct sockaddr *)&client_name,
&client_name_len);
if (client_sock == -1)//accept失败
error_die("accept");
if (pthread_create(&newthread , NULL, accept_request,(void *)&client_sock) != 0)
perror("pthread_create");
}
return(0);
}
各功能模块详述
startup
初始化httpd服务, 建立套接字、绑定端口、进行监听
socket()----bind()—listen()
int startup(u_short * port )
{
int httpd=0;
struct sockaddr_in name; // 套接字地址 (地址族/端口地址/ip地址)
/*int socket(int domain,int type,int protocol)*/
httpd=socket(PF_INET,SOCK_STREAM,0);
if(http=-1) //初始化套接口失败
error_die("socket");
/*清空套接字地址*/
memset(&name,0,sizeof(name));
name.sin_family=AF_INET;//ipv4
name.sin_port= htons(*port);//大小端字节表示转换
name.sin_addr.s_addr=htonl(INADDR_ANY);//INADDR_ANY表示本机ip
if(bind(httpd,(struct sockaddr *)&name, sizeof(name))<0)// 绑定失败
error_die("bind");
if(*port == 0) /*动态分配一个端口*/
{
int namelen = sizeof(name);
/*在以端口0调用bind 后 ,getsockname 用于返回由内核赋予(动态)的本地端口号*/
if(getsockname(httpd,(struct sockaddr*)&name,&namelen)==-1) //失败
error_die("getsockname");
*port= ntohs(name.sinport);// 网络字节顺序(大端)转换为主机字节顺序
}
if(listen(httpd,5)<0) //服务器监听客户端请求。连接队列的最大连接个数设为5
error_die("listen");
return(httpd);
涉及一些网络编程的函数
1.socket()
初始化接口,返回socket描述符
int socket(int domain,int type,int protocol);
/*
domain用于设置网络通信的域 PF_INET (Ipv4)
type 接口类型
SOCK_STREAM TCP套接口
SOCK_DGRAM UDP套接口
protocol 用于制定某个协议的特定类型
通常设为0
*/
- bind()
绑定套接口,成功返回0失败返回-1
int bind(int sockfd,const struct sockaddr * addr,
socklen_t addrlen);
/*
sockfd: socket()返回的socket描述符
addr:该套接字的地址
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
IP地址和端口一起储存
struct sockaddr_in
{
sa_family_t sin_family; //地址族
uinit16_t sin_port; //16位端口地址
struct in_addr sin_addr;//32位IP地址
char zero[8];//不使用 //与 struct sockaddr一样大
}
struct in_addr
{
in_addr_t s_addr;
}
而在 struct sockaddr_in 结构体中 ip地址和端口号是分开储存。
通常 使用struct sockaddr_in设置后,让后将其强制转换为struct sockaddr类型,然后传递给bind函数即可。
addrlen:所指定的结构体变量的大小
*/
3.listen()
使主动连接套接口变为被连接套接口,listen函数只是将 sockfd 和 backlog 告诉内核, 然后就返回。
成功返回0,失败返回1。
int listen(int sockfd,int backlog);
/*
sockfd:socket描述符
backlog:tcp连接队列上限
*/
4.accept()
它提取出所监听套接字的已连接队列中第一个连接请求,创建一个新的套接字,并返回指向该套接字的文件描述符。新建立的套接字不在监听状态,原来所监听的套接字也不受该系统调用的影响。新建立的套接字用于与客户端发送和接受消息。
int accept(int sockfd ,struct sockaddr *addr,socklen_t * addrlen);
/*
sockfd: socket函数返回socket描述符
addr:获得客户端的套接字地址
addrlen:指定的结构体变量的大小
*/
accept_request
子线程回调函数。负责处理客户端发送的请求,解析请求报文,判断客户端请求方式,若客户端请求静态文件直接返回对应的html文件,若客户端请求的是cgi文件,执行cgi程序,服务器将cgi执行得到的结果发送给客户端。

void accept_request(void *pclient)
{
int client = *(int*)pclient;//客户端socket描述符
char buf[1024];//读取行数据时的缓冲区
size_t numchars; //读取了多少字符
char method[255];//方法字段保存在method变量中
char url[255];//url
char path[512];//路径
size_t i, j;
struct stat st; //文件信息的结构体
int cgi = 0;
char *query_string=NULL; //GET的请求参数
/*读取一行数据*/
numchars= get_line(client,buf,sizeof(bud));
i=0;
j=0;
/*ISspace(char ) 判断一个字符是否为空*/
while(! ISspace(buf[j]) && (i<sizeof(method)-1)) //获取请求方法
{
mothod[i]=buf[j];
i++;
j++;
}
method[i]=='\0';
// 该 web服务器只能处理 GET 和 POST方法
if(strcasecmp(method,"GET") && strcasecmp(method,"POST"))//请求的方法非GET 、POST
{
/* int strcasecmp(const char *s1,const char *s2);
忽略大小写比较字符串
相等返回0
s1>s2 返回大于0的值
s1<s2则返回小于0的值
*/
unimplemented(client);
return NULL;
}
//POST 的时候开启cgi
if(strcasecmp(method,"POST")==0)
cgi=1;
i=0;
//读取url地址 第一个while为了跳过空格。方便从url实际开始字符串读取
while(ISspace(buf[j]) && (j<sizeof(buf)))
j++;
while(!ISspace(buf[j]) && (i<sizeof(url)-1)&& (j<sizeof(buf)))
{
/*存下url*/
url[i]=buf[j];
i++;
j++;
}
url[i]='\0';
/*如果客户端发来的是GET请求*/
if(strcasecmp(method,"GET")==0)
{
/*GET 请求参数 和对应的值附加在url后面,利用?代表 URL的结尾与请求参数的开始
如 index.jsp?10023 其中10023就是要传递的参数
url储存 url(路径)
query_string储存get的请求参数
*/
query_string=url;// query_string指向url
while( (*query_string !='?') && (*query_string!='\0'))
{
query_string++; //使query_string 指向?
}
if(*query_string =='?')
{
cgi=1;
*query_string='\0' // 将?替换成\0 之前的真正的url;
query_string++;//query_string 指向get参数开头
}
}
/*格式化路径*/
sprintf(path ,"htdocs%s",url); // 格式化url到path数组,html文件都在htdoce中
/*
int sprintf(char *str,const char *format);
发送格式化输出到str所指的字符串 有点像 printf
*/
if(path[strlen(path)-1]=='/')
/*默认情况下为 index.html*/
strcat(path,"index.html");
/*
char* strcat(char *dest,const char *src)*
把src所指向的字符串追加到dest所指向的字符串的结尾中
*/
if(stat(path,&st)==-1) //失败
{
/*
int stat(const char* pathname,struct stat *statbuf);
取得指定文件的文件属性 ,将文件属性储存在结构体stat中
返回-1失败 0成功
*/
while((numchars>0) && strcmp("\n",buf)) //读入的请求行不为空
numchars =get_line(client,buf,sizeof(buf));
/*把请求headers的信息都丢弃 */
not_found(client);
}
else //获取文件信息成功
{
/*如果该文件是个目录,默认使用该目录下index.html*/
if(st.st_mode & S_IFMT)==S_IFDIR)
strcat(path,"/index.html");
if ((st.st_mode & S_IXUSR) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH)
/*文件所有者具有可执行权限 用户组具有可执行权限 其他人具有可执行权限
如果是可执行的文件 就要执行cgi
*/
{
cgi=1;
}
if(!cgi) //不使用cgi直接返回服务器文件,否则执行cgi
serve_file(client,path);
else
execute_cgi(client,path,method,query_string);
}
close(client); //服务端断开与客户端的连接
}
get_line
获得客户端http请求的一行储存在 buf中 返回获取数据的长度,获取失败返回0
int get_line(int sock,char *buf,int size)
{
//sock 服务端与客户端通信的socket
int i=0;
char c='\0';
int n;
while((i<size-1) && (c != '\n'))
{
/*这里是一个字符一个字符拷贝*/
n=recv(sock ,&c,1,0);
{
if(c=='\r')// h=回车符
{
n=recv(sock,&c,1,MSG_PEEK);
/*MSG_PEEK: 从 接收缓冲区中仅仅拷贝数据,不把已读取的数据从 接收缓冲区中删除,下次调用recv仍可以读到刚才读到的数据
*/
if( (n>0) && (c=='\n'))
recv(sock,&c,1,0);
else
c='\n';
//将 \r 、 \r\n 都转为符合linux换行的'\n'
}
buf[i++]=c;
}
else // 读取失败
c='\n';
}
buf[i]='\0';
return (i);//返回该字符串长度(拷贝字符个数)
}
涉及一些网络编程的函数
recv()
ssize_t recv(int sockfd, void* buf ,size_t len,int flags);
flags表示信息,默认设置为0
recv用于 从 sockfd的接受缓冲区(内核)中拷贝一份值放入buf(用户态)中
返回值<0 出错
>0 接收到的数据大小
=0 对方调用了close 来关闭连接
send()
ssize_t send(int sockfd, void* buf ,size_t len,int flags);
send将buf中的数据拷贝到sockfd的发送缓冲区中。

serve_file
cgi=0,服务器将客户端请求的静态页面发送给客户端
void serve_file(int client, const char * filename)
{
FILE *resource = NULL;
int numchars=1;j
char buf[1024];
buf[0]='A';
buf[1]='\0';
//将请求头部读取并丢弃
while((numchars>0) && strcmp("\n",buf))
numchars=get_line(client,buf,sizeof(buf));
//打开文件 index.html
resource= fopen(filename,"r");
if(resource == NULL)
not_find(client);//文件打开失败
else
{
//模拟http响应 先向客户端发送响应头部
headers(client,filename);
// 调用cat()函数发送数据实体部分 即文件内容
cat(client,resource);
}
fclose(resource);
}
/*把HTTP响应的头部拷贝到套接字的发送缓冲区中*/
void headers(int client, const char *filename)
{
char buf[1024];
(void)filename; /* could use filename to determine file type 没什么用? */
/*正常的 HTTP header */
strcpy(buf, "HTTP/1.0 200 OK\r\n");
send(client, buf, strlen(buf), 0);
/*服务器信息*/
strcpy(buf, SERVER_STRING);
send(client, buf, strlen(buf), 0);
sprintf(buf, "Content-Type: text/html\r\n");
send(client, buf, strlen(buf), 0);
strcpy(buf, "\r\n");
send(client, buf, strlen(buf), 0);
}
/*传输静态文件数据 响应实体*/
void cat(int client, FILE *resource)
{
char buf[1024];
fgets(buf,sizeof(buf),resource);
while(!feof(resource))
{
send(client,buf,strlen(buf),0);
fgets(buf,sizeof(buf),resource);
}
}
execute_cgi
cgi=1,服务端指向cgi程序,将执行后的结果返回给客户端
cgi:通用网关接口(Common Gateway Interface)是一个Web服务器主机提供信息服务的标准接口。通过CGI接口,Web服务器就能够获取客户端提交的信息,转交给服务器端的CGI程序进行处理,最后返回结果给客户端。

cgi程序通过标准输入输出以及环境变量与服务器进行通信。
因此 建立两个管道,cgi_input和cgi_output,并fork一个子进程。
在子进程 中:设置环境变量,且将 cgi_input[0]重定向为标准输入,将cgi_output[1]重定向为标准输出,调用cgi程序,使得 cgi 程序能从 cgi_input中获取数据,向cgi_output中输出数据。
在父进程中,父进程将POST请求的参数写入cgi_input ,将 cgi_output中的数据发送给客户端。

void execute_cgi(int client, const char*path,const char *method,const char*query_string)
{
char buf[1024];
int cgi_output[2];
int cgi_input[2];
pid_t pid;
int status;
int i;
char c;
int numchars=1;
int content_length=-1;
buf[0]='A';
buf[1]='\0'; //这是什么作用 就是初始化一下buf?
/*get请求 执行cgi*/
if(strcasecmp(method,"GET")==0)
{
/*把所有http 的header 读取并丢弃*/
while((numchars>0) && strcmp("\n",buf))
numchars=get_line(client,buf,sizeof(buf));
}
else /*POST请求*/
{
/*对POST的http请求中找出 content_length
http请求头部字段 每一行都有
key:(空格)value \r\n
*/
numchars=get_line(client,buf,sizeof(buf));
while((numchars>0) && strcmp("\n",buf))
{
buf[15]='\0';
/*查找 Content-Length 并把其他首部字段丢弃*/
if(strcasecmp(buf,"Content-Length:")==0)
/* int atoi(const char *str)
字符中有非数字字符会被忽视!
*/
content_length=atoi(&(buf[16]));
numchars=get_line(client,buf,sizeof(buf));
}
/*没有找到 content_length*/
if(content_length==-1)
{
/*错误请求*/
bad_request(client);
return;
}
}
/*建立管道*/
/*
int pipe( int fd[2]);
成功返回0 失败返回-1
*/
if(pipe(cgi_output)<0)
{
/*错误处理*/
cannot_execute(client);
return;
}
/*
int pipe( int fd[2]);
成功返回0 失败返回-1
*/
if(pipe(cgi_intput)<0)
{
/*错误处理*/
cannot_execute(client);
return;
}
/*创建子进程*/
/*
pid_t fork(void)
子进程返回0
父进程返回子进程PID
出错 返回-1
*/
if((pid =fork())<0)
{
cannot_execute(client);
return;
}
/* 正确 发送 请求成功 响应*/
sprintf(buf,"HTTP/1.0 200 OK\r\n");
send(client,buf,strlen(buf),0);
/*子进程调用cgi脚本*/
if(pid == 0) //子进程
{
char meth_env[255];
char query_env[255];
char length_env[255];
//重定向管道
/*dup2(fd,1)
关闭标准输出,本来应该写入到stdout的内容,标准输出重定向到fd的目标文件中
dup2(fd,0)
关闭标准输入 目标文件fd中的内容被当做标准输入
*/
dup2(cgi_output[1],1); //把(屏幕)STDOUT重定向到cgi_output的写入端
dup2(cgi_input[0],0); //把(键盘)STDIN重定向到cgi_input的读取端
close(cgi_output[0]);
close(cgi_input[1]);
/*
设置环境变量
*/
/*1.request_method的环境变量*/
sprintf(meth_env,"REQUEST_METHOD=%s",method);
putenv(meth_env);
/*get方法 设置 query_string的环境变量*/
if(strcasecmp(method,"GET")==0)
{
sprintf(query_env,"QUERY_STRING=%s",query_string);
putenv(query_env);
}
else
{
/*POST 设置 content_ength的环境变量*/
sprintf(length_env,"CONTENT_LENGTH=%d",content_length);
putenv(length_env);
}
/*执行cgi程序 */
execl(path,path,NULL);
/*
执行文件函数
int execl(const char*path,const char*arg,...);
path:文件路径
arg 参数数组
最后一个参数必须用空指针作结束
*/
exit(0);
}
/*将请求参数写入 cgi_input管道*/
else //父进程
{
close(cgi_output[1]);
close(cgi_input[0]);
if(strcasecmp(method,"POST")==0)
{
/*post 接收post传过来的数据*/
for(int i=0;i<content_length;i++)
{
recv(client,&c,1,0);
//一个字节一个字节从接收缓冲区中读取数据
write(cgi_input[1],&c,1);// 写入 cgi_input管道 重定向到STDIN
}
}
/*等待 cgi执行完返回数据
父进程将数据返回给客户端 (发送缓冲区)
*/
while(read(cgi_cgi_output[0],&c,1)>0)
send(client,&c,1,0);
close(cgi_output[0]);
close(cgi_input[1]);
/*等待子进程 避免僵尸进程*/
waitpid(pid,&status,0);
}
测试功能

更多推荐
所有评论(0)