核心函数
1)main()函数
// 服务器main函数
int main(void)
{
int server_sock = ⑴;
u_short port = 4000;
int client_sock = ⑴;
struct sockaddr_in client_name;
socklen_t client_name_len = sizeof(client_name);
pthread_t newthread;
// 建立1个监听套接字,在对应的端口建立httpd服务
server_sock = startup(&port);
printf("httpd running on port %dn",port);
// 进入循环,服务器通过调用accept等待客户真个连接,Accept会以阻塞的方式运行,直到
// 有客户端连接才会返回。连接成功后,服务器启动1个新的线程来处理客户真个要求,处理
// 完成后,重新等待新的客户端要求。
while (1)
{
// 返回1个已连接套接字,套接字收到客户端连接要求
client_sock = accept(server_sock,(struct sockaddr *)&client_name,&client_name_len);
if (client_sock == ⑴)
error_die("accept");
// 派生线程用accept_request函数处理新要求。
/* accept_request(client_sock); */
if (pthread_create(&newthread,NULL,(void *)accept_request,(void *)&client_sock) != 0)
perror("pthread_create");
}
// 出现意外退出的时候,关闭socket
close(server_sock);
return(0);
}
2)startup()函数
// startup函数:依照TCP连接的正常流程顺次调用socket,bind,listen函数。
// 监听套接字端口既可以指定也能够动态分配1个随机端口
int startup(u_short *port)
{
int httpd = 0;
struct sockaddr_in name;
// 创建1个socket,建立socket连接
httpd = socket(PF_INET,SOCK_STREAM,0);
if (httpd == ⑴)
error_die("socket");
// 填充结构体
memset(&name,sizeof(name));
name.sin_family = AF_INET;
name.sin_port = htons(*port);
name.sin_addr.s_addr = htonl(INADDR_ANY);
// 将socket绑定到对应的端口上
if (bind(httpd,(struct sockaddr *)&name,sizeof(name)) < 0)
error_die("bind");
// 如果当前指定的端口是0,则动态随机分配1个端口
if (*port == 0) /* if dynamically allocating a port */
{
socklen_t namelen = sizeof(name);
// 1.getsockname()可以取得1个与socket相干的地址
// 1)服务器端可以通过它得到相干客户端地址
// 2)客户端可以得到当前已连接成功的socket的IP和端口
// 2.在客户端不进行bind而直接连接服务器时,且客户端需要知道当前使用哪一个IP地址
// 进行通讯时比较有用(如多网卡的情况)
if (getsockname(httpd,&namelen) == ⑴)
error_die("getsockname");
*port = ntohs(name.sin_port);
}
// 开始监听
if (listen(httpd,5) < 0)
error_die("listen");
// 返回socket id
return(httpd);
}
3)accept_request()函数
// 线程处理函数
void accept_request(void *arg)
{
int client = *(int*)arg;
char buf[1024]; // 读取行数据时的缓冲区
size_t numchars; // 读取了多少字符
char method[255]; // 存储HTTP要求名称(字符串)
char url[255];
char path[512];
size_t i,j;
struct stat st;
int cgi = 0; /* becomes true if server decides this is a CGI
* program */
char *query_string = NULL;
// 1个HTTP要求报文由要求行(requestline)、要求头部(header)、空行和要求数据4个部份
// 组成,要求行由要求方法字段(get或post)、URL字段和HTTP协议版本字段3个字段组成,它们
// 用空格分隔。如:GET /index.html HTTP/1.1。
// 解析要求行,把方法字段保存在method变量中。
// 读取HTTP头第1行:GET/index.php HTTP 1.1
numchars = get_line(client,buf,sizeof(buf));
i = 0; j = 0;
// 把客户真个要求方法存到method数组
while (!ISspace(buf[i]) && (i < sizeof(method) - 1))
{
method[i] = buf[i];
i++;
}
j=i;
method[i] = ' ';
// 只能辨认get和post
if (strcasecmp(method,"GET") && strcasecmp(method,"POST"))
{
unimplemented(client);
return;
}
// POST的时候开启cgi
if (strcasecmp(method,"POST") == 0)
cgi = 1;
// 解析并保存要求的URL(如有问号,也包括问号及以后的内容)
i = 0;
// 跳过空白字符
while (ISspace(buf[j]) && (j < numchars))
j++;
// 从缓冲区中把URL读取出来
while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < numchars))
{
// 存在url
url[i] = buf[j];
i++; j++;
}
url[i] = ' '; // 保存URL
// 先处理如果是GET要求的情况
// 如果是get方法,要求参数和对应的值附加在URL后面,利用1个问号(“?”)代表URL的结
// 尾与要求参数的开始,传递参数长度受限制。如index.jsp?10023,其中10023就是要传递
// 的参数。这段代码将参数保存在query_string中。
if (strcasecmp(method,"GET") == 0)
{
// 待处理要求为url
query_string = url;
// 移动指针,去找GET参数,即?后面的部份
while ((*query_string != '?') && (*query_string != ' '))
query_string++;
// 如果找到了的话,说明这个要求也需要调用脚本来处理
// 此时就把要求字符串单独抽取出来
// GET方法特点,?后面为参数
if (*query_string == '?')
{
// 开启cgi
cgi = 1;
// query_string指针指向的是真实的要求参数
*query_string = ' ';
query_string++;
}
}
// 保存有效的url地址并加上要求地址的主页索引。默许的根目录是htdocs下
// 这里是做以下路径拼接,由于url字符串以'/'开头,所以不用拼接新的分割符
// 格式化url到path数组,html文件都早htdocs中
sprintf(path,"htdocs%s",url);
// 如果访问路径的最后1个字符时'/',就为其补全,即默许访问index.html
if (path[strlen(path) - 1] == '/')
strcat(path,"index.html");
// 访问要求的文件,如果文件不存在直接返回,如果存在就调用CGI程序来处理
// 根据路径找到对应文件
if (stat(path,&st) == ⑴) {
// 如果不存在,就把剩下的要求头从缓冲区中读出去
// 把所有headers的信息都抛弃
while ((numchars > 0) && strcmp("n",buf)) /* read & discard headers */
numchars = get_line(client,sizeof(buf));
// 然后返回1个404毛病,即回应客户端找不到
not_found(client);
}
else
{
// 如果文件存在但却是个目录,则继续拼接路径,默许访问这个目录下的index.html
if ((st.st_mode & S_IFMT) == S_IFDIR)
strcat(path,"/index.html");
// 如果文件具有可履行权限,就履行它
// 如果需要调用CGI(CGI标志位置1)在调用CGI之前有1段是对用户权限的判断,对应
// 含义以下:S_IXUSR:用户可以履行
// S_IXGRP:组可以履行
// S_IXOTH:其它人可以履行
if ((st.st_mode & S_IXUSR) ||
(st.st_mode & S_IXGRP) ||
(st.st_mode & S_IXOTH) )
cgi = 1;
// 不是cgi,直接把服务器文件返回,否则履行cgi
if (!cgi)
serve_file(client,path);
else
execute_cgi(client,path,method,query_string);
}
// 断开与客户真个连接(HTTP特点:无连接)
close(client);
}
4)execute_cgi()函数
此函数履行流程以下:

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 = ⑴;
// 首先需要根据要求是Get还是Post,来分别进行处理
buf[0] = 'A'; buf[1] = ' ';
// 如果是Get,那末就疏忽剩余的要求头
if (strcasecmp(method,"GET") == 0)
// 把所有的HTTP header读取并抛弃
while ((numchars > 0) && strcmp("n",sizeof(buf));
// 如果是Post,那末就需要读出要求长度即Content-Length
else if (strcasecmp(method,"POST") == 0) /*POST*/
{
// 对POST的HTTP要求中找出content_length
numchars = get_line(client,sizeof(buf));
while ((numchars > 0) && strcmp("n",buf))
{
// 使用 进行分割
buf[15] = ' ';
// HTTP要求的特点
if (strcasecmp(buf,"Content-Length:") == 0)
content_length = atoi(&(buf[16]));
numchars = get_line(client,sizeof(buf));
}
// 如果要求长度不合法(比如根本就不是数字),那末就报错,即没有找到content_length
if (content_length == ⑴) {
// 毛病要求
bad_request(client);
return;
}
}
else/*HEAD or other*/
{
}
// 建立管道
if (pipe(cgi_output) < 0) {
// 毛病处理
cannot_execute(client);
return;
}
// 建立管道
if (pipe(cgi_input) < 0) {
// 毛病处理
cannot_execute(client);
return;
}
// fork本身,生成两个进程
if ( (pid = fork()) < 0 ) { // 复制1个线程
// 毛病处理
cannot_execute(client);
return;
}
sprintf(buf,"HTTP/1.0 200 OKrn");
send(client,strlen(buf),0);
// 子进程要调用CGI脚本
if (pid == 0) /* child: CGI script */
{
// 环境变量缓冲区,会存在溢出风险
char meth_env[255];
char query_env[255];
char length_env[255];
// 重定向管道
// 把父进程读写管道的描写符分别绑定到子进程的标准输入和输出
// dup2功能与freopen()函数类似
dup2(cgi_output[1],STDOUT); // 把STDOUT重定向到cgi_output的写入端
dup2(cgi_input[0],STDIN); // 把STDIN重定向到cgi_input的读取端
// 关闭没必要要的描写符
close(cgi_output[0]); // 关闭cgi_inout的写入端和cgi_output的读取端
close(cgi_input[1]);
// 服务器设置环境变量,即request_method的环境变量
// 设置基本的CGI环境变量,要求类型、参数、长度之类
sprintf(meth_env,"REQUEST_METHOD=%s",method);
putenv(meth_env);
if (strcasecmp(method,"GET") == 0) {
// 设置query_string的环境变量
sprintf(query_env,"QUERY_STRING=%s",query_string);
putenv(query_env);
}
else { /* POST */
// 设置content_length的环境变量
sprintf(length_env,"CONTENT_LENGTH=%d",content_length);
putenv(length_env);
}
// 用execl运行cgi程序
execl(path,NULL);
exit(0);
} else { /* parent */
// 父进程代码
// 关闭cgi_input的读取端和cgi_output的写入端
close(cgi_output[1]);
close(cgi_input[0]);
// 对Post要求,要直接write()给子进程
// 这模样进程所调用的脚本就能够从标准输入获得Post数据
if (strcasecmp(method,"POST") == 0)
// 接收POST过来的数据
for (i = 0; i < content_length; i++) {
recv(client,&c,1,0);
// 把POST数据写入cgi_input,现在重定向到STDIN
write(cgi_input[1],1);
}
// 然后父进程再从输出管道里面读出所有结果,返回给客户端
while (read(cgi_output[0],1) > 0)
send(client,0);
// 关闭管道
close(cgi_output[0]);
close(cgi_input[1]);
// 最后等待子进程结束,即等待子进程
waitpid(pid,&status,0);
}
}