TinyHTTPd forWindows

前言

TinyHTTPd是一个开源的简易学习型的HTTP服务器,项目主页在:http://tinyhttpd.sourceforge.net/,源代码下载:https://sourceforge.net/projects/tinyhttpd/,因为是学习型的代码,已经有好多年没更新了,也没什么更新必要,整个代码才500多行,10多个函数,对于学习HTTP服务器的原理来说非常有帮助,把代码读一遍,再按照执行处理流程调试一下,基本上可以搞清楚Web服务器在收到静态页面请求和CGI请求的一些基本处理逻辑。源代码的注释我这里就不讲了,本身代码比较简单,而且网上这样的文章汗牛充栋,可以去后面的参考文档阅读下。

本文主要是将TinyHTTPd进行一些简单移植,使其可以在Windows上面运行调试,让只有Windows开发调试环境的小伙伴也能够学习学习。

修改明细

支持Windows部分

1、  Windows下的socket支持(微小改动,变量,宏调整等等)。

2、  接入基于Windows平台的一个微型线程池库(项目在用),对于每个新的http请求抛到线程池处理,源代码使用的是基于linux的pthread。

3、  部分字符串比较函数修改为windows平台的对应支持函数,以及其它一些相应兼容性修改。

4、  CGI部分支持了Python脚本和Windows批处理脚本,其它的如果需要支持可以进行修改,另外,CGI部分目前实现比较粗糙,完全是为了体现一下CGI请求的原理(POST的CGI处理仅仅是把提交的数据返回给客户端显示)。

5、  CGI部分使用了匿名管道,匿名管道不支持异步读写数据,因此需要控制读写匿名管道的次数(建议仅读一次,并且CGI的返回字符长度不要超过2048字节)。

优化

1、  给客户端返回数据时,合并了需要发送的数据,使用send一次发送,而不是每次发送几个字符,不过这里没有进行send失败的错误处理,学习代码吧,如果是商用代码,send失败是需要重试的(当然,商用代码一般都是使用异步socket),这个地方之前作者分成多次发送的目的可能是为了体现网络数据传输的原理。

2、  合并了一些公用代码。

3、  代码里面直接写死了绑定80端口,如果需要由系统自动分配端口就把这句代码:u_short port = 80修改为u_short port = 0,绑死80端口是为了使用浏览器测试时比较方便。

bug修改

1、  cat函数里面使用fgets读取文件进行数据发送时,有可能发送不完整。

资源补充

1、  补充批处理的cgi支持和py脚本的cgi支持(都比较简单,需要注意的是py脚本支持需要本地安装python2.x的环境)。

测试情况

主页

URL:http://127.0.0.1/

其它静态页面

URL:http://127.0.0.1/detect.html

Python CGI

URL:http://127.0.0.1/cgipy?p.py

批处理 CGI

URL:http://127.0.0.1/cgibat?p.bat

POST CGI

URL:http://127.0.0.1/index.html

源代码

本来不想帖代码的,还是贴一点吧,工程下载请点这里

/* -------------------------------------------------------------------------
//	文件名		:	tinyhttp.cpp
//	创建者		:	magictong
//	创建时间	:	2016/11/16 17:13:55
//	功能描述	:	support windows of tinyhttpd, use mutilthread...
//
//	$Id: $
// -----------------------------------------------------------------------*/

/* J. David's webserver */
/* This is a simple webserver.
 * Created November 1999 by J. David Blackstone.
 * CSE 4344 (Network concepts), Prof. Zeigler
 * University of Texas at Arlington
 */
/* This program compiles for Sparc Solaris 2.6.
 * To compile for Linux:
 *  1) Comment out the #include <pthread.h> line.
 *  2) Comment out the line that defines the variable newthread.
 *  3) Comment out the two lines that run pthread_create().
 *  4) Uncomment the line that runs accept_request().
 *  5) Remove -lsocket from the Makefile.
 */ 

#include "stdafx.h"
#include "windowcgi.h"
#include "ThreadProc.h"

#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <WinSock2.h>

#pragma comment(lib, "wsock32.lib")
#pragma warning(disable : 4267)

#define ISspace(x) isspace((int)(x))
#define SERVER_STRING "Server: tinyhttp /0.1.0\r\n"
// -------------------------------------------------------------------------

// -------------------------------------------------------------------------
// 类名		: CTinyHttp
// 功能		:
// 附注		:
// -------------------------------------------------------------------------
class CTinyHttp
{
public:
	typedef struct tagSocketContext
	{
		SOCKET socket_Client;
		tagSocketContext() : socket_Client(-1) {}
	} SOCKET_CONTEXT, *PSOCKET_CONTEXT;

/**********************************************************************/
/* A request has caused a call to accept() on the server port to
 * return.  Process the request appropriately.
 * Parameters: the socket connected to the client */
/**********************************************************************/
void accept_request(nilstruct&, SOCKET_CONTEXT& socket_context)
{
	printf("Tid[%u] accept_request\n", (unsigned int)::GetCurrentThreadId());

#ifdef _DEBUG
	// 测试是否可以并发
	::Sleep(200);
#endif

	char buf[1024] = {0};
	int numchars = 0;
	char method[255] = {0};
	char url[255] = {0};
	char path[512] = {0};
	int i = 0, j = 0;
	struct stat st;
	int cgi = 0;      /* becomes true if server decides this is a CGI program */
	char* query_string = NULL;
	SOCKET client = socket_context.socket_Client;

	numchars = get_line(client, buf, sizeof(buf));

	// 获取HTTP的请求方法名
	while (j < numchars && !ISspace(buf[j]) && (i < sizeof(method) - 1))
	{
		method[i] = buf[j];
		i++; j++;
	}
	method[i] = '\0';

	if (_stricmp(method, "GET") != 0 && _stricmp(method, "POST"))      // 只处理GET请求
	{
		if (numchars > 0)
		{
			discardheaders(client);
		}

		unimplemented(client);
		closesocket(client);
		return;
	}

	if (_stricmp(method, "POST") == 0)
		cgi = 1; // POST请求,当成CGI处理

	// 获取到URL路径,存放到url字符数组里面
	i = 0;
	while (ISspace(buf[j]) && (j < sizeof(buf)))
	{
		j++;
	}

	while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf)))
	{
		url[i] = buf[j];
		i++;
		j++;
	}
	url[i] = '\0';

	if (_stricmp(method, "GET") == 0)
	{
		query_string = url;
		while ((*query_string != '?') && (*query_string != '\0'))
			query_string++;

		if (*query_string == '?')
		{
			// URL带参数,当成CGI处理
			cgi = 1;
			*query_string = '\0';
			query_string++;
		}
	}

	sprintf_s(path, 512, "htdocs%s", url);
	if (path[strlen(path) - 1] == '/')
	{
		// 补齐
		strcat_s(path, 512, "index.html");
	}

	if (stat(path, &st) == -1)
	{
		// 文件不存在
		if (numchars > 0)
		{
			discardheaders(client);
		}

		not_found(client);
	}
	else
	{
		// 如果是文件夹则补齐
		if ((st.st_mode & S_IFMT) == S_IFDIR)
			strcat_s(path, 512, "/index.html");

		if (st.st_mode & S_IEXEC)
			cgi = 1; // 具有可执行权限

		if (!cgi)
		{
			serve_file(client, path);
		}
		else
		{
			execute_cgi(client, path, method, query_string);
		}
	}

	closesocket(client);
}

/**********************************************************************/
/* Execute a CGI script.  Will need to set environment variables as
 * appropriate.
 * Parameters: client socket descriptor
 *             path to the CGI script */
/**********************************************************************/
void execute_cgi(SOCKET client, const char *path, const char* method, const char* query_string)
{
	char buf[1024] = {0};
	int cgi_output[2] = {0};
	int cgi_input[2] = {0};

	int i = 0;
	char c = 0;
	int numchars = 1;
	int content_length = -1;

	buf[0] = 'A'; buf[1] = '\0';
	if (_stricmp(method, "GET") == 0)
	{
		discardheaders(client);
	}
	else    /* POST */
	{
		numchars = get_line(client, buf, sizeof(buf));
		while ((numchars > 0) && strcmp("\n", buf))
		{
			buf[15] = '\0';
			if (_stricmp(buf, "Content-Length:") == 0)
			{
				content_length = atoi(&(buf[16]));
			}

			numchars = get_line(client, buf, sizeof(buf));
		}

		if (content_length == -1)
		{
			bad_request(client);
			return;
		}
	}

	CWinCGI cgi;
	if (!cgi.Exec(path, query_string))
	{
		bad_request(client);
		return;
	}

	//SOCKET client, const char *path, const char* method, const char* query_string
	if (_stricmp(method, "POST") == 0)
	{
		for (i = 0; i < content_length; i++)
		{
			recv(client, &c, 1, 0);
			cgi.Write((PBYTE)&c, 1);
		}

		c = '\n';
		cgi.Write((PBYTE)&c, 1);
	}

	cgi.Wait();
	char outBuff[2048] = {0};
	cgi.Read((PBYTE)outBuff, 2047);
	send(client, outBuff, strlen(outBuff), 0);
}

/**********************************************************************/
/* Put the entire contents of a file out on a socket.  This function
 * is named after the UNIX "cat" command, because it might have been
 * easier just to do something like pipe, fork, and exec("cat").
 * Parameters: the client socket descriptor
 *             FILE pointer for the file to cat */
/**********************************************************************/
void cat(SOCKET client, FILE *resource)
{
	char buf[1024] = {0};

	do
	{
		fgets(buf, sizeof(buf), resource);
		size_t len = strlen(buf);
		if (len > 0)
		{
			send(client, buf, len, 0);
		}
	} while (!feof(resource));
}

/**********************************************************************/
/* Print out an error message with perror() (for system errors; based
 * on value of errno, which indicates system call errors) and exit the
 * program indicating an error. */
/**********************************************************************/
void error_die(const char *sc)
{
	perror(sc);
	exit(1);
}  

/**********************************************************************/
/* Get a line from a socket, whether the line ends in a newline,
 * carriage return, or a CRLF combination.  Terminates the string read
 * with a null character.  If no newline indicator is found before the
 * end of the buffer, the string is terminated with a null.  If any of
 * the above three line terminators is read, the last character of the
 * string will be a linefeed and the string will be terminated with a
 * null character.
 * Parameters: the socket descriptor
 *             the buffer to save the data in
 *             the size of the buffer
 * Returns: the number of bytes stored (excluding null) */
/**********************************************************************/
int get_line(SOCKET sock, char *buf, int size)
{
	int i = 0;
	char c = '\0';
	int n;

	while ((i < size - 1) && (c != '\n'))
	{
		n = recv(sock, &c, 1, 0);
		/* DEBUG printf("%02X\n", c); */
		if (n > 0)
		{
			if (c == '\r')
			{
				n = recv(sock, &c, 1, MSG_PEEK);
				/* DEBUG printf("%02X\n", c); */
				if ((n > 0) && (c == '\n'))
				{
					recv(sock, &c, 1, 0);
				}
				else
				{
					c = '\n';
				}
			}
			buf[i] = c;
			i++;
		}
		else
		{
			c = '\n';
		}
	}
	buf[i] = '\0';

	return(i);
}  

/**********************************************************************/
/* Return the informational HTTP headers about a file. */
/* Parameters: the socket to print the headers on
 *             the name of the file */
/**********************************************************************/
void headers(SOCKET client, const char *filename)
{
	(void)filename;

	char* pHeader = "HTTP/1.0 200 OK\r\n"\
		SERVER_STRING \
		"Content-Type: text/html\r\n\r\n";

	send(client, pHeader, strlen(pHeader), 0);
}  

/**********************************************************************/
/* Give a client a 404 not found status message. */
/**********************************************************************/
void not_found(SOCKET client)
{
	char* pResponse = "HTTP/1.0 404 NOT FOUND\r\n"\
		SERVER_STRING \
		"Content-Type: text/html\r\n\r\n"\
		"<HTML><TITLE>Not Found</TITLE>\r\n"\
		"<BODY><P>The server could not fulfill\r\n"\
		"your request because the resource specified\r\n"\
		"is unavailable or nonexistent.\r\n"\
		"</BODY></HTML>\r\n";

	send(client, pResponse, strlen(pResponse), 0);
}

/**********************************************************************/
/* Inform the client that the requested web method has not been
 * implemented.
 * Parameter: the client socket */
/**********************************************************************/
void unimplemented(SOCKET client)
{
	char* pResponse = "HTTP/1.0 501 Method Not Implemented\r\n"\
		SERVER_STRING \
		"Content-Type: text/html\r\n\r\n"\
		"<HTML><HEAD><TITLE>Method Not Implemented\r\n"\
		"</TITLE></HEAD>\r\n"\
		"<BODY><P>HTTP request method not supported.</P>\r\n"\
		"</BODY></HTML>\r\n";

	send(client, pResponse, strlen(pResponse), 0);
}

/**********************************************************************/
/* Inform the client that a CGI script could not be executed.
 * Parameter: the client socket descriptor. */
/**********************************************************************/
void cannot_execute(SOCKET client)
{
	char* pResponse = "HTTP/1.0 500 Internal Server Error\r\n"\
		"Content-Type: text/html\r\n\r\n"\
		"<P>Error prohibited CGI execution.</P>\r\n";

	send(client, pResponse, strlen(pResponse), 0);
}

/**********************************************************************/
/* Inform the client that a request it has made has a problem.
 * Parameters: client socket */
/**********************************************************************/
void bad_request(SOCKET client)
{
	char* pResponse = "HTTP/1.0 400 BAD REQUEST\r\n"\
		"Content-Type: text/html\r\n\r\n"\
		"<P>Your browser sent a bad request, such as a POST without a Content-Length.</P>\r\n";

	send(client, pResponse, strlen(pResponse), 0);
}

/**********************************************************************/
/* Send a regular file to the client.  Use headers, and report
 * errors to client if they occur.
 * Parameters: a pointer to a file structure produced from the socket
 *              file descriptor
 *             the name of the file to serve */
/**********************************************************************/
void serve_file(SOCKET client, const char *filename)
{
	FILE *resource = NULL;
	discardheaders(client);

	fopen_s(&resource, filename, "r");
	if (resource == NULL)
	{
		not_found(client);
	}
	else
	{
		headers(client, filename);
		cat(client, resource);
	}
	fclose(resource);
}

// -------------------------------------------------------------------------
// 函数		: discardheaders
// 功能		: 清除http头数据(从网络中全部读出来)
// 返回值	: void
// 参数		: SOCKET client
// 附注		:
// -------------------------------------------------------------------------
void discardheaders(SOCKET client)
{
	char buf[1024] = {0};
    int numchars = 1;
    while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
	{
        numchars = get_line(client, buf, sizeof(buf));
	}
}

/**********************************************************************/
/* This function starts the process of listening for web connections
 * on a specified port.  If the port is 0, then dynamically allocate a
 * port and modify the original port variable to reflect the actual
 * port.
 * Parameters: pointer to variable containing the port to connect on
 * Returns: the socket */
/**********************************************************************/
SOCKET startup(u_short* port)
{
	SOCKET httpd = 0;
	struct sockaddr_in name = {0};

	httpd = socket(AF_INET, SOCK_STREAM, 0);
	if (httpd == INVALID_SOCKET)
	{
		error_die("startup socket");
	}

	name.sin_family = AF_INET;
	name.sin_port = htons(*port);
	name.sin_addr.s_addr = inet_addr("127.0.0.1");
	if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
	{
		error_die("startup bind");
	}

	if (*port == 0)  /* if dynamically allocating a port */
	{
		int namelen = sizeof(name);
		if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
		{
			error_die("getsockname");
		}

		*port = ntohs(name.sin_port);
	}

	if (listen(httpd, 5) < 0)
	{
		error_die("listen");
	}

	return httpd;
}

}; // End Class CTinyHttp

int _tmain(int argc, _TCHAR* argv[])
{
	SOCKET server_sock = INVALID_SOCKET;
	//u_short port = 0;
	u_short port = 80;
	struct sockaddr_in client_name = {0};
	int client_name_len = sizeof(client_name);
	typedef CMultiTaskThreadPoolT<CTinyHttp, CTinyHttp::SOCKET_CONTEXT, nilstruct, 5, CComMultiThreadModel::AutoCriticalSection> CMultiTaskThreadPool;
	CTinyHttp tinyHttpSvr;

	// init socket
	WSADATA wsaData = {0};
	WSAStartup(MAKEWORD(2, 2), &wsaData);

	server_sock = tinyHttpSvr.startup(&port);
	printf("httpd running on port: %d\n", port);
	CMultiTaskThreadPool m_threadpool(&tinyHttpSvr, &CTinyHttp::accept_request);

	while (1)
	{
		CTinyHttp::SOCKET_CONTEXT socket_context;
		socket_context.socket_Client = accept(server_sock, (struct sockaddr *)&client_name, &client_name_len);
		if (socket_context.socket_Client == INVALID_SOCKET)
		{
			tinyHttpSvr.error_die("accept");
		}

		printf("Tid[%u] accetp new connect: %u\n", (unsigned int)::GetCurrentThreadId(), (unsigned int)socket_context.socket_Client);
		m_threadpool.AddTask(socket_context);
	}

	// can not to run this
	m_threadpool.EndTasks();
	closesocket(server_sock);
	WSACleanup();
	return 0;
}
// -------------------------------------------------------------------------
// $Log: $

参考文档

[1] tinyhttp源码阅读(注释) http://www.cnblogs.com/oloroso/p/5459196.html

[2] 【源码剖析】tinyhttpd —— C 语言实现最简单的 HTTP 服务器http://blog.csdn.net/jcjc918/article/details/42129311

[3] Tinyhttp源码分析http://blog.csdn.net/yzhang6_10/article/details/51534409

[4] tinyhttpd源码详解http://blog.csdn.net/baiwfg2/article/details/45582723

[5] CGI介绍 http://www.jdon.com/idea/cgi.htm

Tinyhttpd for Windows的更多相关文章

  1. Tinyhttpd for Windows(学习型的项目,才500多行代码)

    前言 TinyHTTPd是一个开源的简易学习型的HTTP服务器,项目主页在:http://tinyhttpd.sourceforge.net/,源代码下载:https://sourceforge.ne ...

  2. Tinyhttpd for Windows(500多行)

    TinyHTTPd forWindows 前言 TinyHTTPd是一个开源的简易学习型的HTTP服务器,项目主页在:http://tinyhttpd.sourceforge.NET/,源代码下载:h ...

  3. Tinyhttpd精读解析

    首先,本人刚刚开始开源代码精读,写的不对的地方,大家轻拍,一起进步.本文是对Tinyhttpd的一次精读,大家每天都在用着http服务,很多人也一直活跃在上层,使用IIS.Apache等,大家是否想看 ...

  4. Windows server 2012 添加中文语言包(英文转为中文)(离线)

    Windows server 2012 添加中文语言包(英文转为中文)(离线) 相关资料: 公司环境:亚马孙aws虚拟机 英文版Windows2012 中文SQL Server2012安装包,需要安装 ...

  5. Windows Server 2012 NIC Teaming介绍及注意事项

    Windows Server 2012 NIC Teaming介绍及注意事项 转载自:http://www.it165.net/os/html/201303/4799.html Windows Ser ...

  6. C# 注册 Windows 热键

    闲扯: 前几日,一个朋友问我如何实现按 F1 键实现粘贴(Ctrl+V)功能,百度了一个方法,发给他,他看不懂(已经是 Boss 的曾经的码农),我就做了个Demo给他参考.今日得空,将 Demo 整 ...

  7. Windows 7上执行Cake 报错原因是Powershell 版本问题

    在Windows 7 SP1 电脑上执行Cake的的例子 http://cakebuild.net/docs/tutorials/getting-started ,运行./Build.ps1 报下面的 ...

  8. 在离线环境中发布.NET Core至Windows Server 2008

    在离线环境中发布.NET Core至Windows Server 2008 0x00 写在开始 之前一篇博客中写了在离线环境中使用.NET Core,之后一边学习一边写了一些页面作为测试,现在打算发布 ...

  9. Windows平台分布式架构实践 - 负载均衡

    概述 最近.NET的世界开始闹腾了,微软官方终于加入到了对.NET跨平台的支持,并且在不久的将来,我们在VS里面写的代码可能就可以通过Mono直接在Linux和Mac上运行.那么大家(开发者和企业)为 ...

随机推荐

  1. hdu1022 Train Problem I---模拟栈

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1022 题目大意: 车的进出站问题,先给出N个火车,再按序列一的方式进站,判断能否以序列二的方式出站,若 ...

  2. [转]Python爬虫框架--pyspider初体验

    标签: python爬虫pyspider 2015-09-05 10:57 9752人阅读 评论(0) 收藏 举报  分类: Python(8)  版权声明:本文为博主原创文章,未经博主允许不得转载. ...

  3. ORACLE数据库之PL/SQL触发器、rownum、动态SQL、数据库之视图与索引

    WHEN子句说明触发约束条件.Condition为一个逻辑表达时,其中必须包含相关名称,而不能包含查询语句,也不能调用PL/SQL函数.WHEN子句指定的触发约束条件只能用在BEFORE和AFTER行 ...

  4. jacascript 立即执行函数(IIFE)与闭包

    前言:这是笔者学习之后自己的理解与整理.如果有错误或者疑问的地方,请大家指正,我会持续更新! 一直没搞清楚立即执行函数和闭包之间的关系,总结一下: 闭包有很多种理解:访问不到内部作用域,函数就是这样, ...

  5. 【linux之crontab,启动】

    一.计划任务 atd at命令发布的任务计划 一次性的任务计划 at time ctrl+d 提交 time: 1.绝对时间:12:00 2.相对时间:+8 3.模糊时间:noon midnight ...

  6. 微信小程序开发•模块化

    微信小程序的MINA框架,其实是许多前端开发技术的组合.这篇文章中,我们来简单地讨论一下模块化. 1.模块化标准 玩前端的同学大部分都知道模块化的几个标准,CommonJs / AMD / CMD.这 ...

  7. SSH执行hql报错:Caused by: org.hibernate.hql.ast.QuerySyntaxException: user is not mapped [from user where username = ?]

    报错信息: ERROR Dispatcher:38 - Exception occurred during processing request: user is not mapped [from u ...

  8. 【BZOJ1483】【HNOI2009】梦幻布丁

    题意:n个连续的点,有若干种颜色,每个颜色会因为某些操作变为另一种颜色,动态查询颜色段数. 解题思路:对每个颜色开一棵平衡树启发式合并应该是最裸的想法,但是我们有更优的! 考虑对每个颜色利用链表储存它 ...

  9. poj 1811 随机素数和大数分解(模板)

    Sample Input 2 5 10 Sample Output Prime 2 模板学习: 判断是否是素数,数据很大,所以用miller,不是的话再用pollard rho分解 miller : ...

  10. hihocoder #1073 : 光棍节

    描述 尽管付出了种种努力,jzp还是得过光棍节. jzp非常不爽,但也无能为力,只能够哀叹起来他的命运.他想到了一位长者的人生经验:"人的一生,不光要靠自我奋斗,也要考虑历史的进程" ...