Bootstrap

c++实现httpd服务器

网络的初始化

这是网络通信需要包含的头文件以及库文件

#include <WinSock2.h>
#pragma comment (lib,"WS2_32.lib")

附加一个 需要利用里面的一些定义

#include <Windows.h>

定义如下

在这里插入图片描述
在这里插入图片描述

我们进行严格的端口定义是需要定义为 WORD 即 unsigned short

端口的分配

为了方便端口的调配 ,我们讲端口的分配设置为动态 其中的InterInit函数为网络初始化函数

WORD port = 0;
   int server_socker = InterInit(&port);

然后打印出信息

	printf("httpd服务已经启动,正在监听 %d 端口..", port);

总的代码是这样的

#include <stdio.h>
#include <Windows.h>
#include <WinSock2.h>
#pragma comment (lib,"WS2_32.lib")

int InterInit(PWORD port);


int InterInit(PWORD port)//实现网络的初始化
{
	 
	return;
}


int main(void)
{

	WORD port = 0;
	int server_socker = InterInit(&port);
	printf("httpd服务已经启动,正在监听 %d 端口..", port);
	return 0;
}

协议的使用

在这里插入图片描述

官方文档

我们需要用到这个函数进行网路的初始化

WSAStartup函数

int WSAAPI WSAStartup(
  WORD      wVersionRequested,
  LPWSADATA lpWSAData
);

WSAStartup

  • W:windows
  • S:socket
  • A:Asynchronous 异步
    • 同步:阻塞、卡死状态
    • 异步:多个工作同时进行
  • Startup:启动
参数1:WORD wVersionRequested

调用者可以使用的Windows套接字规范的最高版本。 高位字节指定次要版本号; 低位字节指定主要版本号。

在这里插入图片描述

#define MAKEWORD(a, b)      ((WORD)(((BYTE)(((DWORD_PTR)(a)) & 0xff)) | ((WORD)((BYTE)(((DWORD_PTR)(b)) & 0xff))) << 8))
#define MAKELONG(a, b)      ((LONG)(((WORD)(((DWORD_PTR)(a)) & 0xffff)) | ((DWORD)((WORD)(((DWORD_PTR)(b)) & 0xffff))) << 16))
#define LOWORD(l)           ((WORD)(((DWORD_PTR)(l)) & 0xffff))
#define HIWORD(l)           ((WORD)((((DWORD_PTR)(l)) >> 16) & 0xffff))
#define LOBYTE(w)           ((BYTE)(((DWORD_PTR)(w)) & 0xff))
#define HIBYTE(w)           ((BYTE)((((DWORD_PTR)(w)) >> 8) & 0xff))
参数2:LPWSADATA lpWSAData

指向WSADATA数据结构的指针,该数据结构将接收Windows套接字实现的详细信息。

在这里插入图片描述

typedef struct WSAData {
        WORD                    wVersion;//我们要使用的版本
        WORD                    wHighVersion;//系统能提供给我们的版本
#ifdef _WIN64
        unsigned short          iMaxSockets;
        unsigned short          iMaxUdpDg;
        char FAR *              lpVendorInfo;
        char                    szDescription[WSADESCRIPTION_LEN+1];
        char                    szSystemStatus[WSASYS_STATUS_LEN+1];
#else
        char                    szDescription[WSADESCRIPTION_LEN+1];
        char                    szSystemStatus[WSASYS_STATUS_LEN+1];//当前库的秒数信息,2.0是第2版的意思
        unsigned short          iMaxSockets;//返回可用的socket数量,2版本只有就没用了
        unsigned short          iMaxUdpDg;//UDP数据报信息大小,2版本只有就没用了
        char FAR *              lpVendorInfo;//供应商特定的信息,2版本只有就没用了
#endif
} WSADATA;

返回值 int

如果成功,则WSAStartup函数将返回0。 否则,它将返回下面列出的错误代码之一。
WSAStartup函数直接在该函数的返回值中返回扩展错误代码。 不需要调用WSAGetLastError函数,并且不应使用该调用。

Error codeMeaning
WSASYSNOTREADY基础网络子系统尚未准备好进行网络通信。
WSAVERNOTSUPPORTED此特定的Windows套接字实现未提供所请求的Windows套接字支持的版本。
WSAEINPROGRESSWindows Sockets 1.1的阻止操作正在进行中。
WSAEPROCLIMWindows套接字实现所支持的任务数已达到限制。
WSAEFAULTlpWSAData 参数不是有效的指针
代码实现
int InterInit(PWORD port)//实现网络的初始化
{
	 
	WSADATA data;//初始化数据
	//网络通信初始化
	int ret = WSAStartup(MAKEWORD(1, 1), &data);

	if (ret)
	{
		switch (ret)
		{
		case WSASYSNOTREADY:
			printf("请检查网络库");
			break;
		case WSAVERNOTSUPPORTED:
			printf("请更新网络库");
			break;
		case WSAEINPROGRESS:
			printf("请重新启动");
			break;
		case WSAEPROCLIM:
			printf("请尝试关闭不必要软件,以为当前网络运行提供充足的资源");
			break;
		}
		return -1;
	}
	//返回值:套接字	
	return;
}

创建套接字

在这里插入图片描述

SOCKET WSAAPI socket(
  int af, //地址族规范。 地址族的可能值在Winsock2.h头文件中定义。
  int type,//新套接字的类型规范。
  int protocol//要使用的协议。
);

int af表示套接字的类型(网络套接字 文件套接字)

#define PF_UNSPEC       AF_UNSPEC
#define PF_UNIX         AF_UNIX
#define PF_INET         AF_INET
#define PF_IMPLINK      AF_IMPLINK
#define PF_PUP          AF_PUP
#define PF_CHAOS        AF_CHAOS
#define PF_NS           AF_NS
#define PF_IPX          AF_IPX
#define PF_ISO          AF_ISO
#define PF_OSI          AF_OSI
#define PF_ECMA         AF_ECMA
#define PF_DATAKIT      AF_DATAKIT
#define PF_CCITT        AF_CCITT
#define PF_SNA          AF_SNA
#define PF_DECnet       AF_DECnet
#define PF_DLI          AF_DLI
#define PF_LAT          AF_LAT
#define PF_HYLINK       AF_HYLINK
#define PF_APPLETALK    AF_APPLETALK
#define PF_VOICEVIEW    AF_VOICEVIEW
#define PF_FIREFOX      AF_FIREFOX
#define PF_UNKNOWN1     AF_UNKNOWN1
#define PF_BAN          AF_BAN
#define PF_ATM          AF_ATM
#define PF_INET6        AF_INET6
AfMeaning
AF_UNSPEC 0地址族未指定。
AF_INET 2Internet协议版本4(IPv4)地址族。
AF_IPX 6IPX / SPX地址族。 仅当安装了NWLink IPX / SPX NetBIOS兼容传输协议时,才支持此地址系列。WindowsVista和更高版本不支持此地址系列。
AF_APPLETALK 16AppleTalk地址族。 仅当安装了AppleTalk协议时才支持此地址系列。WindowsVista和更高版本不支持此地址系列。
AF_INET6 23Internet协议版本6(IPv6)地址族。
AF_IRDA 26红外数据协会(IrDA)地址族。仅当计算机安装了红外端口和驱动程序时,才支持此地址族。
AF_NETBIOS 17NetBIOS地址族。 仅当安装了用于NetBIOS的Windows套接字提供程序时,才支持此地址系列。在32位版本的Windows上支持用于NetBIOS的Windows套接字提供程序。 默认情况下,此提供程序安装在32位版本的Windows上.NetBIOS的Windows套接字提供程序在64位版本的Windows(包括Windows 7,Windows Server 2008,Windows Vista,Windows Server 2003或Windows XP)上不受支持。 用于NetBIOS的Windows套接字提供程序仅支持将* type 参数设置为* SOCK_DGRAM **的套接字。用于NetBIOS的Windows套接字提供程序与[NetBIOS]不直接相关(https://docs.microsoft.com/en -us / previous-versions / windows / desktop / netbios / portal)编程界面。 Windows Vista,Windows Server 2008和更高版本不支持NetBIOS编程接口。
AF_BTH 32蓝牙地址系列。如果计算机安装了蓝牙适配器和驱动程序,则Windows XP SP2或更高版本支持此地址系列。

int type数据流 数据报

#define SOCK_STREAM     1               /* stream socket */
#define SOCK_DGRAM      2               /* datagram socket */
#define SOCK_RAW        3               /* raw-protocol interface */
#define SOCK_RDM        4               /* reliably-delivered message */
#define SOCK_SEQPACKET  5       

在这里插入图片描述

在Windows套接字1.1中,唯一可能的套接字类型是SOCK_DGRAM和SOCK_STREAM

int protocol 通信协议 TCP & UDP

#define IPPROTO_IP              0               /* dummy for IP */
#define IPPROTO_ICMP            1               /* control message protocol */
#define IPPROTO_IGMP            2               /* group management protocol */
#define IPPROTO_GGP             3               /* gateway^2 (deprecated) */
#define IPPROTO_TCP             6               /* tcp */
#define IPPROTO_PUP             12              /* pup */
#define IPPROTO_UDP             17              /* user datagram protocol */
#define IPPROTO_IDP             22              /* xns idp */
#define IPPROTO_ND              77              /* UNOFFICIAL net disk proto */

在这里插入图片描述

返回值

如果没有发生错误,socket将返回一个引用新socket的描述符。否则,将返回INVALID_SOCKET 的值,并且可以通过调用WSAGetLastError检索特定的错误代码

在这里插入图片描述

代码


int server_socket =  socket(PF_INET,SOCK_STREAM,	IPPROTO_TCP);
	
	if (server_socker == -1) {
		printf("socket function failed with error = %d\n", server_socker);
		switch (server_socker)
		{
		case WSANOTINITIALISED:
		
			printf("	A successful WSAStartup call must occur before using this function.");
			break;
			
		case WSAENETDOWN:
			printf("The network subsystem or the associated service provider has failed.");
			break;
			
		case WSAEAFNOSUPPORT:
			printf("The specified address family is not supported. For example, an application tried to create a socket for the AF_IRDA address family but an infrared adapter and device driver is not installed on the local computer.");
			break;
			
		case WSAEINPROGRESS:
			printf("A blocking Windows Sockets 1.1 call is in progress, or the service provider is still processing a callback function.");
			break;
		case WSAEMFILE:
			printf("	No more socket descriptors are available.");
			break;
		case WSAEINVAL:
			printf("An invalid argument was supplied. This error is returned if the af parameter is set to AF_UNSPEC and the type and protocol parameter are unspecified.");
			break;
		case WSAEINVALIDPROVIDER:
			printf("	The service provider returned a version other than 2.2.");
			break;
		case WSAEINVALIDPROCTABLE:
			printf("The service provider returned an invalid or incomplete procedure table to the WSPStartup.");
			break;
		case WSAENOBUFS:
			printf("No buffer space is available. The socket cannot be created.");
			break;
		case WSAEPROTONOSUPPORT:
			printf("The specified protocol is not supported.");
			break;
		case WSAEPROTOTYPE:
			printf("The specified protocol is the wrong type for this socket.");
			break;
		case WSAEPROVIDERFAILEDINIT:
			printf("The service provider failed to initialize. This error is returned if a layered service provider (LSP) or namespace provider was improperly installed or the provider fails to operate correctly.				");
			break;
		case WSAESOCKTNOSUPPORT:
			printf("The specified socket type is not supported in this address family.");
			break;

		default:
			break;
		}

	}

优化

函数说明

perror ( )用 来 将 上 一 个 函 数 发 生 错 误 的 原 因 输 出 到 标 准 设备 (stderr) 。参数 s 所指的字符串会先打印出,后面再加上错误原因字符串。此错误原因依照全局变量error 的值来决定要输出的字符串。在库函数中有个error变量,每个error值对应着以字符串表示的错误类型。当你调用"某些"函数出错时,该函数已经重新设置了error的值。perror函数只是将你输入的一些信息和现在的error所对应的错误一起输出。

void error_die(const char* str) {
	perror(str);
}

在这里插入图片描述

设置端口套接字可复用特性(端口复用)

在这里插入图片描述

在这里插入图片描述

int PASCAL FAR setsockopt (
                           _In_ SOCKET s,
                           _In_ int level,
                           _In_ int optname,
                           _In_reads_bytes_opt_(optlen) const char FAR * optval,
                           _In_ int optlen);
		//设置端口套接字可复用特性
	int opt = 1;
	ret = setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR,(const char*) & opt, sizeof(opt));
	if (ret == -1) {
		error_die("setsockopt");
	}

绑定套接字和网络地址

		//配置服务器的网络地址
		struct sockaddr_in server_addr;
		memset(&server_addr, 0, sizeof(server_addr));
		server_addr.sin_family = PF_INET;
		server_addr.sin_port = htons(*port);
		server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //IP 服务器 INADDR_ANY 表示任意网络地址可接入

		//绑定套接字
		if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {

			error_die("bind");
		}

`

动态分配一个端口

	//动态分配一个端口
		int nameLen = sizeof(server_addr);
		if (*port == 0) {
			if (getsockname(server_socket, (struct sockaddr*)&server_addr, &nameLen) < 0)
			{
				error_die("getsockname");
			}

			*port = server_addr.sin_port;
			
		}

创建监听队列

//创建监听队列
		if (listen(server_socket, 5) < 0) {
			error_die("listen");//监听队列报错
            
		}

创建 客户端的访问

//客户端 访问
	struct sockaddr_in client_addr;
	int client_addr_len = sizeof(client_addr);

	while (1) {
		//
		
		int client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_addr_len);
		if (client_sock == -1) {
			error_die("accept"); //打印错误信息并结束
		}
	 
	}

使用多线程进行用户的访问

DWORD dwThreadID = 0;
		HANDLE handleFirst = CreateThread(NULL, 0, accept_request, (void*)client_sock, 0, &dwThreadID);

DWORD WINAPI accept_request(LPVOID arg) {
	return 0;
}

使用CreateThread函数创建线程

线程是进程中的一个实体,是被系统独立调度和分派的基本单位。一个进程可以拥有多个线程,但是一个线程必须有一个进程。线程自己不拥有系统资源,只有运行所必须的一些数据结构,但它可以与同属于一个进程的其它线程共享进程所拥有的全部资源,同一个进程中的多个线程可以并发执行。

在C/C++中可以通过CreateThread函数在进程中创建线程,函数的具体格式如下:

HANDLE CreateThread(
                   LPSECURITY_ATTRIBUTES lpThreadAttributes,
                   DWORD dwStackSize,
                   LPTHREAD_START_ROUTINE lpStartAddress,
                   LPVOID lpParameter,
                   DWORD dwCreationFlags,
                   LPDWORD lpThreadID
                  );

参数的含义如下:

lpThreadAttrivutes:指向SECURITY_ATTRIBUTES的指针,用于定义新线程的安全属性,一般设置成NULL;

dwStackSize:分配以字节数表示的线程堆栈的大小,默认值是0;

lpStartAddress:指向一个线程函数地址。每个线程都有自己的线程函数,线程函数是线程具体的执行代码;

lpParameter:传递给线程函数的参数;

dwCreationFlags:表示创建线程的运行状态,其中CREATE_SUSPEND表示挂起当前创建的线程,而0表示立即执行当前创建的进程;

lpThreadID:返回新创建的线程的ID编号;

如果函数调用成功,则返回新线程的句柄,调用WaitForSingleObject函数等待所创建线程的运行结束。函数的格式如下:

DWORD WaitForSingleObject(
                          HANDLE hHandle,
                          DWORD dwMilliseconds
                         );

参数的含义如下:

hHandle:指定对象或时间的句柄;

dwMilliseconds:等待时间,以毫秒为单位,当超过等待时间时,此函数返回。如果参数设置为0,则该函数立即返回;如果设置成INFINITE,则该函数直到有信号才返回。

一般情况下需要创建多个线程来提高程序的执行效率,但是多个线程同时运行的时候可能调用线程函数,在多个线程同时对一个内存地址进行写入操作,由于CPU时间调度的问题,写入的数据会被多次覆盖,所以要使线程同步。

就是说,当有一个线程对文件进行操作时,其它线程只能等待。可以通过临界区对象实现线程同步。临界区对象是定义在数据段中的一个CRITICAL_SECTION结构,Windows内部使用这个结构记录一些信息,确保同一时间只有一个线程访问改数据段中的数据。
使用临界区的步骤如下:

(1)初始化一个CRITICAL_SECTION结构;在使用临界区对象之前,需要定义全局CRITICAL_SECTION变量,在调用CreateThread函数前调用InitializeCriticalSection函数初始化临界区对象;

(2)申请进入一个临界区;在线程函数中要对保护的数据进行操作前,可以通过调用EnterCriticalSection函数申请进入临界区。由于同一时间内只能有一个线程进入临界区,所以在申请的时候如果有一个线程已经进入临界区,则该函数就会一直等到那个线程执行完临界区代码;

(3)离开临界区;当执行完临界区代码后,需要调用LeaveCriticalSection函数离开临界区;

(4)删除临界区;当不需要临界区时调用DeleteCriticalSection函数将临界区对象删除;

Http请求的流程

浏览器发起新的访问时,将向服务器端发送一个请求报文。例如,在浏览器地址输入 127.0.0.1:8000 回车后,服务器端收到的完整报文如下:

GET / HTTP/1.1\n
Host: 127.0.0.1:8000\n
Connection: keep-alive\n
Cache-Control: max-age=0\n
Upgrade-Insecure-Requests: 1\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\n
Sec-Fetch-Site: none\n
Sec-Fetch-Mode: navigate\n
Sec-Fetch-User: ?1\n
Sec-Fetch-Dest: document\n
Accept-Encoding: gzip, deflate, br\n
Accept-Language: zh-CN,zh;q=0.9\n
\n

在这里插入图片描述

请求报文由4四个部分组成:请求行、请求头部行、空行、请求数据。具体格式如下:

在这里插入图片描述

第一行报文详细说明

报文的第一行
报文的第一行是:GET / HTTP/1.1\n

GET表示请求方法(另外有POST方法)

1.1表示http的版本

GET和HTTP之间的"/“表示请求的资源。浏览器发起请求后的,第一次发送的请求报文中,这个位置都是”/",/表示服务器端的资源目录,这里表示不指定特定的资源。

当服务器把网页文件(例如:index.html)发送给浏览器后,浏览器收到这个网页文件后,如果网页文件中含有图片,那么浏览器会自动再发起一个http请求报文,此时请求报文的第一行数据,就类似:
GET /images/head.png HTTP/1.1\n

响应报文的格式

服务器发送数据给浏览器时,发送的响应报文,由4个部分组成:
状态行、消息头部、空行和响应正文。格式如下:

在这里插入图片描述

常用的关键字有:

在这里插入图片描述

在这里插入图片描述

POST请求报文的格式

浏览器发送的POST报文的格式,和GET报文格式其实是一致的,只是多了最后一部分内容“请求数据”,实例如下

POST /color.cgi HTTP/1.1\n
Host: 127.0.0.1:8000\n
Connection: keep-alive\n
Content-Length: 9\n
Cache-Control: max-age=0\n
Upgrade-Insecure-Requests: 1\n
Origin: http://127.0.0.1:8000\n
Content-Type: application/x-www-form-urlencoded\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\n
Sec-Fetch-Site: same-origin\n 
Sec-Fetch-Mode: navigate\n
Sec-Fetch-User: ?1\n
Sec-Fetch-Dest: document\n
Referer: http://127.0.0.1:8000/\n
Accept-Encoding: gzip, deflate, br\n
Accept-Language: zh-CN,zh;q=0.9\n
\n
color = red

最后一行“color=red”就是网页提交的数据

报文的分析

代码获取以及讨论交流

;