1. 程式人生 > >VC++6.0下用60行程式寫成一個最簡單的WEB伺服器

VC++6.0下用60行程式寫成一個最簡單的WEB伺服器

文章目錄

一個最簡單的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;

其中,WinSock2WS2tcpip是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

團隊同學可試著添磚加瓦,做成你們自己的東西。