文章目录
一个最简单的WEB服务器
– 用VC++6.0 写成,60行代码,谁说C/C++不够简捷?
HTTP是一个基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)。HTTP属于应用层协议。
目前互联网是四层结构的:应用层、传输层、IP层(网络层)、链路层。
HTTP 工作原理概述
- HTTP协议工作于客户端-服务端架构上。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。
- Web服务器根据接收到的请求后,向客户端发送响应信息。
- HTTP默认端口号为80,但是你也可以改为8080,8081,8181,8888或者其他端口。
- 常用的Web服务器有:Apache服务器,IIS服务器(Internet Information Services),Tomcat,Resin等。 其实,要了解服务器的工作原理,最好自己写一个。
- 用Python,Node.js 可以很简单地写一个web服务器出来,用C++似乎会难一点,但可从代码中,对http WEB传输协议做到知根知底。
HTTP协议通信过程
HTTP是基于客户端/服务端(C/S)的架构模型,通过一个可靠的链接来交换信息,是一个无状态的请求/响应协议。
一个HTTP”客户端”是一个应用程序(Web浏览器或其他任何客户端,如命令行的wget,curl等),通过连接到服务器达到向服务器发送一个或多个HTTP的请求的目的。
一个HTTP”服务器”同样也是一个应用程序(通常是一个Web服务,如Apache Web服务器或IIS服务器等),通过接收客户端的请求并向客户端发送HTTP响应数据。
HTTP使用统一资源标识符(Uniform Resource Identifiers,URI)来传输数据和建立连接。一旦建立连接后,数据消息就通过类似Internet邮件所使用的格式[RFC5322]和多用途Internet邮件扩展(MIME)[RFC2045]来传送。
客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。如下:
HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。如下:
关于http协议的详细信息可参考相关文档。
源程序分析
过程
最简版HTTP web 服务器的VC++源码共60行,其实还可再精简。程序过程如下:
- 建立套接字,TCP/IP Socket 初始化
- IP,端口分配,设置监听模式,设置客户端模式
- 进入监听循环:检测监听条件,监听所设端口上的消息
- 依据监听消息和HTTP协议与客户浏览器进行文本字串交互
源码分析
头文件包含:
#include "stdafx.h"
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#include <WS2tcpip.h>
#include <windows.h>
#include <iostream>
#include <string>
using namespace std;
其中,WinSock2
和WS2tcpip
是TCP/IP通信库,其中会涉及 STLport,他不支持单线程运行时库 ,而vc6控制台程序默认采用单线程运行时库. 所以,在VC6: Project > Settings > C/C++ > Code Generation >
Use run-time library: 不能选择single threaded或者debug single threaded. 选其他MD,MDd,MT,MTd都是可以的.
主程序中,首先定义了套接字并初始化:
WSADATA _wsa;
WSAStartup(MAKEWORD(2, 0), &_wsa); //套接字初始化,分配套接字版本信息2.0,WSADATA变量地址
然后,建立套接字,完成IP地址和端口的初始化,设定端口号为8081. 为下面分配IP和端口做准备。
int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//建立套接字,失败返回-1
sockaddr_in addr = { 0 };
addr.sin_family = AF_INET; //指定地址族
addr.sin_addr.s_addr = INADDR_ANY;//IP初始化
addr.sin_port = htons(8081);//端口号初始化为 8081
接下来,分配IP和端口,设置监听,设置客户端。成功后,就可进入监听循环。如果设置的端口号已被占用,则listen(sock, 0)
函数将返回-1
,因此可用之判断端口占用与否。
int rc;
rc = bind(sock, (sockaddr*)&addr, sizeof(addr));//分配IP和端口
rc = listen(sock, 0);//设置监听
//设置客户端
sockaddr_in clientAddr;
int clientAddrSize = sizeof(clientAddr);
int clientSock;
cout <<"服务器启动成功,开始接受请求......(Ctrl+C)结束\n";//接受客户端请求
然后进入监听循环:检测监听条件,监听所设端口上的消息。
while (-1 != (clientSock = accept(sock,(sockaddr*)&clientAddr, (socklen_t*)&clientAddrSize)))
{
// (1) 监听并接受浏览器送来的文本数据
// (2) 解析文本数据(在文本第一行)判断以作为执行的分支条件,确定返回信息
// (3) 依照http协议向浏览器发送响应头
// (4) 通过UrlRouter(clientSock, url)函数向浏览器发送html文本
// (5) 关闭客户端套接字, 等待下一个请求
}
在UrlRouter(clientSock, url)
函数中,依据参数url分支进行处理向clientSock
所指向的客户端地址回传html文本。
//处理URL: 这里定义了3种处理情况
void UrlRouter(int clientSock, string const & url)
{
string hint;
if (url == "/") {
hint = "文本内容1 ";
}
else if (url == "/hello") {
hint = "文本内容2 ";
} else {
hint = "未定义URL!";
}
// 向的客户端地址回传html文本串 hint
send(clientSock, hint.c_str(), hint.length(), 0);
}
源码60行(simplehttpserver.cpp)
所以, 一个简单的http web服务器,核心代码并不多,以下代码simplehttpserver.cpp
共60行。
// simplehttpserver.cpp
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#include <WS2tcpip.h>
#include <windows.h>
#include <iostream>
#include <string>
using namespace std;
void UrlRouter(int clientSock, string const & url);
int main()
{
WSADATA _wsa;
WSAStartup(MAKEWORD(2, 0), &_wsa); //套接字初始化,分配套接字版本信息2.0,WSADATA变量地址
int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//建立套接字,失败返回-1
sockaddr_in addr = { 0 };
addr.sin_family = AF_INET; //指定地址族
addr.sin_addr.s_addr = INADDR_ANY;//IP初始化
addr.sin_port = htons(8081);//端口号初始化为 8081
int rc;
rc = bind(sock, (sockaddr*)&addr, sizeof(addr));//分配IP和端口
rc = listen(sock, 0);//设置监听
//设置客户端
sockaddr_in clientAddr;
int clientAddrSize = sizeof(clientAddr);
int clientSock;
while (-1 != (clientSock = accept(sock,(sockaddr*)&clientAddr, (socklen_t*)&clientAddrSize)))
{
string requestStr;
int bufSize = 4096;
requestStr.resize(bufSize);
recv(clientSock, &requestStr[0], bufSize, 0); //接受数据
string firstLine = requestStr.substr(0, requestStr.find("\r\n"));
取得第一行并取得URL以解析确定返回信息
firstLine = firstLine.substr(firstLine.find(" ") + 1);//substr,复制函数,参数为起始位置(默认0),复制的字符数目
string url = firstLine.substr(0, firstLine.find(" "));
string response =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=gbk\r\n"
"Connection: close\r\n"
"\r\n";
send(clientSock, response.c_str(), response.length(), 0); //发送HTTP响应头
cout << "\n服务器向客户端浏览器发送响应头为:\n\n"<< response;
UrlRouter(clientSock, url); //处理URL
closesocket(clientSock); //关闭客户端套接字
}
closesocket(sock);//关闭服务器套接字
return 0;
}
void UrlRouter(int clientSock, string const & url)
{
string hint;
if (url == "/") {
hint = "文本内容1 ";
}else if (url == "/hello") {
hint = "文本内容2 ";
} else {
hint = "未定义URL!";
}
send(clientSock, hint.c_str(), hint.length(), 0);// 向的客户端地址回传html文本串 hint
cout<<"服务器已发送:"<<hint<<endl;
}
在VC6.0下以控制台程序模板写成,在MSVC++ 6.0下编译。由于使用了Socket,需链接ws2_32.lib
库。可用#pragma comment(lib, "ws2_32.lib")
完成,也可在链接器配置完成(点击工程->设置(Alt+F7)->链接->对象/库模块后面添加ws2_32.lib,注意!每一项后面都要有一个空格隔开)。
此外,VC6模式以单线程运行库模式编译,由于Socket工作在多线程下,所以编译时要采用多线程模式运行库。也在工程->设置(Alt+F7)->C/C++ 选单下设置),如图。
编码过程和编译说明
启动 VC6 , 新建工程(Ctrl +N )
建立一个空的工程即可:
然后,在工程中新建一个cpp源文件。
在该cpp文件中编写代码。将以上60行代码拷贝进来即可。
现在,直接编译(用F7)将出错。
还要用Alt+F7配置编译器才行。选运行库为多线程的。
重新编译,通过,但有一个警告:
--------------------Configuration: simplehttpserver - Win32 Debug--------------------
Compiling...
simplehttpserver.cpp
Linking...
xilink6: executing 'D:\PROGRA~1\VC6\VC98\Bin\link.exe'
LINK : warning LNK4098: defaultlib "LIBCMTD" conflicts with use of other libs; use /NODEFAULTLIB:library
simplehttpserver.exe - 0 error(s), 1 warning(s)
提示,编译时用/NODEFAULTLIB:library
选项去掉警告。
还可编译为Release版本。编译器仍要将运行库设为多线程的。
将活动工程设置为Win32 Release。
将Release的工程设定为为多线程的run-time library。
如果代码中不加:
#pragma comment(lib, "ws2_32.lib")
则应在编译器的link设置中加入ws2_32.lib
。
编译生成了simplehttpserver.exe
。
运行之(windows防火墻可能会提示,确定即可),并在浏览器中输入http://127.0.0.1:8081
, 将显示”文本内容1“
而在服务器simplehttpserver.exe
的控制台窗口中,将显示:
服务器向客户端浏览器发送响应头为:
HTTP/1.1 200 OK
Content-Type: text/html; charset=gbk
Connection: close
服务器已发送:文本内容1
就此, 一个静态WEB服务器就做成了。
浏览器输入http://127.0.0.1:8081/hello
,服务器则反回由url == "/hello"
所决定的文本:hint = "文本内容2 ";
。
如果浏览器输入其他路径,如http://127.0.0.1:8081/world
,则服务器返回hint = "未定义URL!";
部分。如下:
可见,这样就实现了利用服务器端程序接受浏览器相应指令完成相应任务的功能。
这个功能实在是太强大了,试想,服务器端一段小代码,可指定一个任意的端口号,然后互联网上的客户端就可以通过这个端口号操纵服务器端,这就是典型的木马行为啊。所以,木马工作原理与服务器的工作原理是一致的。
正因如此,在服务器端,为了防止程序能通过任意端口与外界通信,就出现了防火墻,来控制程序的通信行为。
所以,本服务器启动时,如windows开着防火墻,将出现告警提示。
VS2010中的编译问题
本代码也可在 VS2010编译。方法类似,只是VC2010默认采用了多线程运行时库,所以无需更改配置。
同样,ws2_32.lib
的添加方法如下。
进一步的问题
这个服务器还有许多的方要改进。
- 端口是固定的,还不能改。
- 未能显示浏览器端发送来的数据。
- 如果初始化时端口号被占用,不能提示。
- 向浏览器发送的还不是html格式的文本。等等。毕竟,60行代码写成的,足够强大了。架构已成,后面的事都是锦上添花的东西了。
源码
https://pan.baidu.com/s/1DOC2sjMfl3eWFjuZtl_izg
编译平台
VC6 for windows 10 绿色版。(140M)
https://pan.baidu.com/s/1KkgAMYF1ksWleRxCYhqfBw
VS2010 for windows 10 绿色版。(590M)
https://pan.baidu.com/s/15Fn19Pi4PMnE9duBBtxHSw
团队同学可试着添砖加瓦,做成你们自己的东西。