简介

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源码分析的更多相关文章

  1. HTTP服务器的本质:tinyhttpd源码分析及拓展

    已经有一个月没有更新博客了,一方面是因为平时太忙了,另一方面是想积攒一些干货进行分享.最近主要是做了一些开源项目的源码分析工作,有c项目也有python项目,想提升一下内功,今天分享一下tinyhtt ...

  2. ABP源码分析一:整体项目结构及目录

    ABP是一套非常优秀的web应用程序架构,适合用来搭建集中式架构的web应用程序. 整个Abp的Infrastructure是以Abp这个package为核心模块(core)+15个模块(module ...

  3. HashMap与TreeMap源码分析

    1. 引言     在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Ja ...

  4. nginx源码分析之网络初始化

    nginx作为一个高性能的HTTP服务器,网络的处理是其核心,了解网络的初始化有助于加深对nginx网络处理的了解,本文主要通过nginx的源代码来分析其网络初始化. 从配置文件中读取初始化信息 与网 ...

  5. zookeeper源码分析之五服务端(集群leader)处理请求流程

    leader的实现类为LeaderZooKeeperServer,它间接继承自标准ZookeeperServer.它规定了请求到达leader时需要经历的路径: PrepRequestProcesso ...

  6. zookeeper源码分析之四服务端(单机)处理请求流程

    上文: zookeeper源码分析之一服务端启动过程 中,我们介绍了zookeeper服务器的启动过程,其中单机是ZookeeperServer启动,集群使用QuorumPeer启动,那么这次我们分析 ...

  7. zookeeper源码分析之三客户端发送请求流程

    znode 可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个功能是zookeeper对于应用最重要的特性,通过这个特性可以实现的功能包括配置的 ...

  8. java使用websocket,并且获取HttpSession,源码分析

    转载请在页首注明作者与出处 http://www.cnblogs.com/zhuxiaojie/p/6238826.html 一:本文使用范围 此文不仅仅局限于spring boot,普通的sprin ...

  9. ABP源码分析二:ABP中配置的注册和初始化

    一般来说,ASP.NET Web应用程序的第一个执行的方法是Global.asax下定义的Start方法.执行这个方法前HttpApplication 实例必须存在,也就是说其构造函数的执行必然是完成 ...

随机推荐

  1. ControlTemplate in WPF

    Shared in all file window Button CheckBox Radiobutton Textbox ComboBox ListBox ItemsControl TreeView ...

  2. Csla One or more properties are not registered for this type

    在实际运行中,好好运行的程序出现了以下问题: 2019-12-27 10:40:00,164 [DefaultQuartzScheduler_Worker-2] ERROR IBeam.BCPool. ...

  3. java面试记录一:跳表、判断二叉树相同、冒泡排序、cookie和session的区别、设计模式(单例、工厂、模板方法、原型、代理、策略)、抽象类与接口的区别

    1.什么是跳表? 跳表实际上就是多层链表 跳表可用在让链表的元素查询接近线性时间 代码结构及java实现参考博客园随笔 2.判断两棵二叉树是否相同?(结构相同,内容相同) 思路:(1)先定义树节点Tr ...

  4. Mybatis的解析和运行原理

    Mybatis的解析和运行原理 Mybatis的运行过程大致分为两大步:第一步,读取配置文件缓存到Configuration对象,用以创建 SqlSessionFactory:第二步,SqlSessi ...

  5. IDEA模板注释及相关快捷键设置

    IDEA模板注释及相关快捷键设置 最近使用IDEA时发现自带的模板注释不怎么好用,因此自己根据网上的教程总结了适合自己的模板设置,可以一键生成类和方法的注释,废话不多说一起看看吧: 第9步的类模板注释 ...

  6. Keep、小红书、美图…独角兽App能拿到新一轮救命钱吗?

    大多数人热爱手机,不是因为时尚的外观或者结实的零部件,而是因琳琅满目的App赋予其太多的功能.智能手机最先是清理掉人类的零碎时间,现如今又开始肢解我们大块的时间,或者说,智能手机本身就是生活.在如此背 ...

  7. mui H5+ 调取 相册 拍照 功能 上传图片 + 裁剪功能

    H5+ 相册拍照图片上传 点击用户头像后,弹出actionSheet,选择从相册或是拍照:选取照片后调用上传方法: 上传图片后调用PhotoClip.js  插件进行裁剪 具体流程 弹出actionS ...

  8. 关于EasyUI DataGrid行编辑时嵌入时间控件

    本人做一个名为“安徽中控”项目时,为快速开发基础数据增删改模块,遂采用EasyUIDatagrid将所有增删改查的操作都集中于表格中,并且所有增删改查操作都集中于泛型对象,从而不必为每个表写具体的增删 ...

  9. Codeforces Round #614 (Div. 2) C - NEKO's Maze Game

    题目链接:http://codeforces.com/contest/1293/problem/C 题目:给定一个 2*n的地图,初始地图没有岩浆,都可以走, 给定q个询问,每个询问给定一个点(x,y ...

  10. MySQL常用命令符

    收集于网络!!!! 解决字符乱码问题:显示汉语而非乱码:set names utf8: 修改新密码:update user set password=PASSWORD('新密码') where use ...