Tinyhttp源码分析
简介
Tinyhttp是一个轻量型Http Server,使用C语言开发,全部代码只500多行,还包括一个简单Client。
Tinyhttp程序的逻辑为:一个无线循环,一个请求,创建一个线程,之后线程函数处理每个请求,然后解析HTTP请求,做一些判断,之后判断文件是否可执行,不可执行,打开文件,输出给客户端(浏览器),可执行就创建管道,父子进程进行通信。父子进程通信,用到了dup2和execl函数。
模型图
源码剖析
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <strings.h>
#include <string.h>
#include <sys/stat.h>
#include <pthread.h>
#include <sys/wait.h>
#include <stdlib.h> #define ISspace(x) isspace((int)(x)) #define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n" void *accept_request(void *);
void bad_request(int);
void cat(int, FILE *);
void cannot_execute(int);
void error_die(const char *);
void execute_cgi(int, const char *, const char *, const char *);
int get_line(int, char *, int);
void headers(int, const char *);
void not_found(int);
void serve_file(int, const char *);
int startup(u_short *);
void unimplemented(int); /**********************************************************************/
/*功能:处理请求
*参数:连接到客户端的套接字*/
/**********************************************************************/
void *accept_request(void *arg)
{
int client = *(int *)arg; //接收客户端的套接字
char buf[];
int numchars;
char method[];
char url[];
char path[];
size_t i, j;
struct stat st;
int cgi = ; char *query_string = NULL;
// "GET /index.html HTTP/1.1\n",'\000' <repeats 319 times>...
numchars = get_line(client, buf, sizeof(buf)); //读取一行存放buf中
i = ;
j = ;
//判断buf中第一个空格前面的字符串的请求方式
while (!ISspace(buf[j]) && (i < sizeof(method) - ))
{
method[i] = buf[j]; //解析出请求的方法放在method中
i++;
j++;
}
method[i] = '\0';
//如果是其他的请求方式,除了GET和POST外,如:HEAD、DELETE等回复未实现方法
if (strcasecmp(method, "GET") && strcasecmp(method, "POST")) //strcasecmp用忽略大小写比较字符串
{
unimplemented(client); //回复请求的方法未实现
return ;
} //如果是POST方式请求,表示执行cgi
if (strcasecmp(method, "POST") == )
cgi = ; i = ;
//从上面的第一个空格后继续开始
while (ISspace(buf[j]) && (j < sizeof(buf)))
j++;
//POST或者GET空格后面的内容 如:"GET /index.html HTTP/1.1\n"
while (!ISspace(buf[j]) && (i < sizeof(url) - ) && (j < sizeof(buf)))
{
url[i] = buf[j]; //解析出url要请求的地址,如:"/index.html"
i++;
j++;
}
url[i] = '\0'; //如果是GET方式请求,如:/login.cgi?user=123&password=456
if (strcasecmp(method, "GET") == )
{
query_string = url;
while ((*query_string != '?') && (*query_string != '\0'))
query_string++;
if (*query_string == '?') //如果遇到了?表示执行cgi
{
cgi = ;
*query_string = '\0';
query_string++; //?后面的内容是发送的信息(如用户名和密码信息)
}
}
//拼接url地址路径
sprintf(path, "htdocs%s", url); //文件的路径放在path中 if (path[strlen(path) - ] == '/') //判断是否是根目录
strcat(path, "index.html"); // "htdocs/index.html" if (stat(path, &st) == -)
{ //获取文件信息到st结构体失败
while ((numchars > ) && strcmp("\n", buf)) //读并丢弃标题(浏览器的信息),如果不读,浏览器发送来的数据,会使服务器处于阻塞状态
numchars = get_line(client, buf, sizeof(buf));
not_found(client); //给客户端一条404未找到的状态消息
}
else //成功获得文件信息
{
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程序 if (!cgi) //根据cgi的值,为0执行文件
serve_file(client, path);
else //cgi为1,执行cgi
execute_cgi(client, path, method, query_string);
}
close(client);
return ;
} /**********************************************************************/
/* 通知客户它提出的请求有问题
* 参数: 接收客户端套接字描述符 */
/**********************************************************************/
void bad_request(int client)
{
char buf[]; sprintf(buf, "HTTP/1.0 400 BAD REQUEST\r\n");
send(client, buf, sizeof(buf), );
sprintf(buf, "Content-type: text/html\r\n");
send(client, buf, sizeof(buf), );
sprintf(buf, "\r\n");
send(client, buf, sizeof(buf), );
sprintf(buf, "<P>Your browser sent a bad request, ");
send(client, buf, sizeof(buf), );
sprintf(buf, "such as a POST without a Content-Length.\r\n");
send(client, buf, sizeof(buf), );
} /**********************************************************************/
/*不停的从resource所指的文件中读取内容,发送给客户端
*参数:接受客户端套接字,请求的文件的指针*/
/**********************************************************************/
void cat(int client, FILE *resource)
{
char buf[]; fgets(buf, sizeof(buf), resource); //
while (!feof(resource))
{
send(client, buf, strlen(buf), );
fgets(buf, sizeof(buf), resource);
}
} /**********************************************************************/
/* 通知客户端无法执行CGI脚本。
* 参数:客户端套接字描述符。*/
/**********************************************************************/
void cannot_execute(int client)
{
char buf[]; sprintf(buf, "HTTP/1.0 500 Internal Server Error\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "Content-type: text/html\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "<P>Error prohibited CGI execution.\r\n");
send(client, buf, strlen(buf), );
} /**********************************************************************/
/* 打印带有perror()的错误消息并退出指示错误的程序。 */
/**********************************************************************/
void error_die(const char *sc)
{
perror(sc);
exit();
} /**********************************************************************/
/* 执行一个cgi程序,需要环境的支持
* 参数: 接受客户端套接字描述符client,拼接后的地址路径path,请求的方法method,接受的url?后面的内容*/
/**********************************************************************/
void execute_cgi(int client, const char *path, const char *method, const char *query_string)
{
char buf[];
int cgi_output[];
int cgi_input[];
pid_t pid;
int status;
int i;
char c;
int numchars = ;
int content_length = -; buf[] = 'A';
buf[] = '\0';
if (strcasecmp(method, "GET") == )
while ((numchars > ) && strcmp("\n", buf)) //读并丢弃标题(浏览器的信息),如果不读,浏览器发送来的数据,会使服务器处于阻塞状态
numchars = get_line(client, buf, sizeof(buf)); else //POST
{
numchars = get_line(client, buf, sizeof(buf)); //读取一行到buf中
while ((numchars > ) && strcmp("\n", buf))
{
buf[] = '\0';
if (strcasecmp(buf, "Content-Length:") == ) //拷贝Content-Length:到字符串中
content_length = atoi(&(buf[])); //获得content_length内容长度
numchars = get_line(client, buf, sizeof(buf));
}
if (content_length == -)
{
bad_request(client);
return;
}
} sprintf(buf, "HTTP/1.0 200 OK\r\n"); //给浏览器一个回复
send(client, buf, strlen(buf), ); //建立两根管道,分别是输出管道和输入管道
if (pipe(cgi_output) < )
{
cannot_execute(client);
return;
}
if (pipe(cgi_input) < )
{
cannot_execute(client);
return;
}
//创建一个进程
if ((pid = fork()) < )
{
cannot_execute(client);
return;
}
if (pid == ) //子进程
{
char meth_env[];
char query_env[];
char length_env[]; dup2(cgi_output[], ); //dup2做了重定向,Linux中0是标准输入(键盘),1是标准输出(屏幕)
dup2(cgi_input[], ); //将cgi_output[1]描述符拷贝到标准输出,原来输出到屏幕的,现在写到cgi_output[1管道中]
//cgi_output[1]用来写
//cgi_input[0]用来读
close(cgi_output[]);
close(cgi_input[]); sprintf(meth_env, "REQUEST_METHOD=%s", method); //将请求的方法加到环境变量
putenv(meth_env); //putenv增加环境变量的内容
if (strcasecmp(method, "GET") == )
{
sprintf(query_env, "QUERY_STRING=%s", query_string);
putenv(query_env);
}
else
{ //POST
sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
putenv(length_env);
}
execl(path, path, NULL); //execl执行给出的path路径下的path程序(cgi程序),l表示以list方式传参,经过重定向后,path路径中的程序执行后,读数据从cgi_intput[0],中读取,写数据到cgi_output[1]中欧个
exit();
}
else
{ //父进程
//cgi_output[0]管道用来读
//cgi_input[1]管道用来写
close(cgi_output[]);
close(cgi_input[]);
if (strcasecmp(method, "POST") == ) //如果是POST,循环每次一个字符,写入管道
for (i = ; i < content_length; i++)
{
recv(client, &c, , );
write(cgi_input[], &c, );
}
while (read(cgi_output[], &c, ) > ) //循环从管道中读出数据发送给客户端
send(client, &c, , ); close(cgi_output[]);
close(cgi_input[]);
waitpid(pid, &status, );
}
} /**********************************************************************/
/*返回从接受客户端套接字中读取一行的字节数
*参数:客户端套接字sock,缓冲区的指针buf,缓冲区大小size*/
/**********************************************************************/
int get_line(int sock, char *buf, int size)
{
int i = ;
char c = '\0';
int n; while ((i < size - ) && (c != '\n'))
{
n = recv(sock, &c, , ); if (n > )
{
if (c == '\r')
{
n = recv(sock, &c, , MSG_PEEK); if ((n > ) && (c == '\n'))
recv(sock, &c, , );
else
c = '\n';
}
buf[i] = c;
i++;
}
else
c = '\n';
}
buf[i] = '\0'; return (i);
} /**********************************************************************/
/*发送有关HTTP头文件的信息
*参数:客户端接收套接字,文件的名称*/
/**********************************************************************/
void headers(int client, const char *filename)
{
char buf[];
(void)filename; /*可以使用文件名来确定文件类型*/ strcpy(buf, "HTTP/1.0 200 OK\r\n");
send(client, buf, strlen(buf), );
strcpy(buf, SERVER_STRING);
send(client, buf, strlen(buf), );
sprintf(buf, "Content-Type: text/html\r\n");
send(client, buf, strlen(buf), );
strcpy(buf, "\r\n");
send(client, buf, strlen(buf), );
} /**********************************************************************/
/* 给客户端一条404未找到的状态消息。*/
/**********************************************************************/
void not_found(int client)
{
char buf[]; sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, SERVER_STRING);
send(client, buf, strlen(buf), );
sprintf(buf, "Content-Type: text/html\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "<HTML><TITLE>Not Found</TITLE>\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "<BODY><P>The server could not fulfill\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "your request because the resource specified\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "is unavailable or nonexistent.\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "</BODY></HTML>\r\n");
send(client, buf, strlen(buf), );
} /**********************************************************************/
/*向客户端发送一个常规文件。使用标头,并在出现错误时向客户端报告错误。
*参数:客户端的接收文件描述符,要服务的文件的名称*/
/**********************************************************************/
void serve_file(int client, const char *filename)
{
FILE *resource = NULL;
int numchars = ;
char buf[]; buf[] = 'A';
buf[] = '\0';
while ((numchars > ) && strcmp("\n", buf)) //读并丢弃标题(浏览器的信息),如果不读,浏览器发送来的数据,会使服务器处于阻塞状态
numchars = get_line(client, buf, sizeof(buf)); resource = fopen(filename, "r"); //打开文件
if (resource == NULL)
not_found(client);
else //正常打开
{
headers(client, filename); //返回一些头文件相关的信息
cat(client, resource); //将文件内容发给客户端
}
fclose(resource);
} /**********************************************************************/
/*返回创建指定的端口的监听套接字
*参数:指定的端口port*/
/**********************************************************************/
int startup(u_short *port)
{
int httpd = ;
struct sockaddr_in name; httpd = socket(PF_INET, SOCK_STREAM, );
if (httpd == -)
error_die("socket");
memset(&name, , sizeof(name));
name.sin_family = AF_INET;
name.sin_port = htons(*port);
name.sin_addr.s_addr = inet_addr("192.168.137.114"); int on = ;
setsockopt(httpd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < )
error_die("bind");
if (*port == ) /* if dynamically allocating a port */
{
socklen_t namelen = sizeof(name);
if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -)
error_die("getsockname");
*port = ntohs(name.sin_port);
}
if (listen(httpd, ) < )
error_die("listen");
return (httpd);
} /**********************************************************************/
/*通知客户端,所请求的web方法尚未实现
*参数:接受客户端套接字 */
/**********************************************************************/
void unimplemented(int client)
{
char buf[]; sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, SERVER_STRING);
send(client, buf, strlen(buf), );
sprintf(buf, "Content-Type: text/html\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "<HTML><HEAD><TITLE>Method Not Implemented\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "</TITLE></HEAD>\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "<BODY><P>HTTP request method not supported.\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "</BODY></HTML>\r\n");
send(client, buf, strlen(buf), );
} /**********************************************************************/
/*主函数入口*/
/**********************************************************************/
int main(void)
{
int server_sock = -;
u_short port = ;
int client_sock = -;
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 ()
{
//父进程每接收一个客户端创建一个线程
client_sock = accept(server_sock, (struct sockaddr *)&client_name, &client_name_len);
if (client_sock == -)
error_die("accept");
//线程标识符、线程属性、线程运行函数、运行函数参数(接收套接字)
if (pthread_create(&newthread, NULL, accept_request, &client_sock) != )
perror("pthread_create");
} close(server_sock); return ();
}
Tinyhttp源码分析的更多相关文章
- HTTP服务器的本质:tinyhttpd源码分析及拓展
已经有一个月没有更新博客了,一方面是因为平时太忙了,另一方面是想积攒一些干货进行分享.最近主要是做了一些开源项目的源码分析工作,有c项目也有python项目,想提升一下内功,今天分享一下tinyhtt ...
- ABP源码分析一:整体项目结构及目录
ABP是一套非常优秀的web应用程序架构,适合用来搭建集中式架构的web应用程序. 整个Abp的Infrastructure是以Abp这个package为核心模块(core)+15个模块(module ...
- HashMap与TreeMap源码分析
1. 引言 在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Ja ...
- nginx源码分析之网络初始化
nginx作为一个高性能的HTTP服务器,网络的处理是其核心,了解网络的初始化有助于加深对nginx网络处理的了解,本文主要通过nginx的源代码来分析其网络初始化. 从配置文件中读取初始化信息 与网 ...
- zookeeper源码分析之五服务端(集群leader)处理请求流程
leader的实现类为LeaderZooKeeperServer,它间接继承自标准ZookeeperServer.它规定了请求到达leader时需要经历的路径: PrepRequestProcesso ...
- zookeeper源码分析之四服务端(单机)处理请求流程
上文: zookeeper源码分析之一服务端启动过程 中,我们介绍了zookeeper服务器的启动过程,其中单机是ZookeeperServer启动,集群使用QuorumPeer启动,那么这次我们分析 ...
- zookeeper源码分析之三客户端发送请求流程
znode 可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个功能是zookeeper对于应用最重要的特性,通过这个特性可以实现的功能包括配置的 ...
- java使用websocket,并且获取HttpSession,源码分析
转载请在页首注明作者与出处 http://www.cnblogs.com/zhuxiaojie/p/6238826.html 一:本文使用范围 此文不仅仅局限于spring boot,普通的sprin ...
- ABP源码分析二:ABP中配置的注册和初始化
一般来说,ASP.NET Web应用程序的第一个执行的方法是Global.asax下定义的Start方法.执行这个方法前HttpApplication 实例必须存在,也就是说其构造函数的执行必然是完成 ...
随机推荐
- mysql远程连接失败的两种解决方法
---恢复内容开始--- (这是转载别人的,因为我觉得很有用,每次都是参考这个的第二种方法解决的,不管你听不听得到,先说声谢谢!也记下来方便大家看看) mysql解决远程不能访问的二种方法,需要的朋友 ...
- int long的数据范围
java 整数型 byte 1字节 -128~127 -2^7~2^7-1 short 2字节 -32768~32767 int 4个字节 -2147483648~2147483647 -2 ...
- 数据预处理 | 使用 Pandas 进行数值型数据的 标准化 归一化 离散化 二值化
1 标准化 & 归一化 导包和数据 import numpy as np from sklearn import preprocessing data = np.loadtxt('data.t ...
- python之爬虫(爬取.ts文件并将其合并为.MP4文件——以及一些异常的注意事项)
//20200115 最近在看“咱们裸熊——we bears”第一季和第三季都看完了,单单就第二季死活找不到,只有腾讯有资源,但是要vip……而且还是国语版……所以就瞄上了一个视频网站——可以在线观看 ...
- 云服务器 使用 onedrive 快速同步
重大更新:支持微软的onedrive网盘,可以自动实时双向同步数据,也可以多台服务器和网盘之间实时同步数据.新增了一个虚拟环境python367,支持pytorch1.2:-----------微软O ...
- 实用技巧之while里面使用getchar或sleep函数
我们经常需要打印一些变量的取值来调试程序,使用while(1)是常用的手段. ) { char letter = getchar(); printf("test_point is %d \t ...
- 解决Oracle ORA-01033: ORACLE initialization or shutdown in progress错误 和 ORA-01589错误 要打开数据库则必须使用 RESETLOGS 或 NORESETLOGS 选项
要打开数据库则必须使用 RESETLOGS 或 NORESETLOGS 选项 SQL> startupORACLE 例程已经启动. Total System Global Area 13533 ...
- Docker最全教程——从理论到实战(十三)
前言 树莓派(Raspberry Pi)是一台卡片电脑(只有信用卡大小),我们可以使用树莓派做很多事情,比如智能家居的中控.航空器.BT下载器.挖矿机.智能机器人.小型服务器(花生壳+网站)等等. 目 ...
- JUC-多线程锁
多线程锁的练习题 1.标准访问,先打印短信还是邮件 class Phone { public synchronized void sendSMS() throws Exception { Thread ...
- git的分支
git branch : git branch -r #查看远程分支 git branch -a #查看本地分支和远程分支 git branch -v #查看本地库的所有分支 git br ...