基于完成端口模型的web服务器

使用C++实现一个完成端口模型,可以使用浏览器且支持并行访问的网页服务器

完成端口模型流程:
服务器流程图
服务器代码逻辑:
Step1初始化Windows Sockets,绑定,监听
Step2创建完成端口对象。
Step3创建n个工作线程,n等于当前计算机中CPU核心的数量。将新建的完成端口对象作为参数传递到工作线程中。工作线程的主要功能是检测完成端口的状态,如果有来自客户端的数据,则接收数据,并将接收到的数据发送回客户端程序。
Step4监听来自客户端的连接请求。
Step5接收来自客户端的连接请求,得到与客户端进行通信的套接字AcceptSocket,并将该套接字与步骤②中创建的完成端口对象相关联。
Step6以异步方式在套接字AcceptSocket上接收Get请求,并从请求中获取URL中的页面
Step7根据页面前往文件系统搜索相应文件,若找到文件则发送200 Get Respond并发送文件,若找不到文件则发送 404 Get Respond

完整代码:

// CompletionPortTcpServer.cpp : 定义控制台应用程序的入口点。

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS
#include <winsock2.h>
#include <windows.h>
#include <stdio.h>
#include <string>

using namespace std;

#define PORT 9990							// 监听的端口
#define DATA_BUFSIZE 8192					// 发送和接收消息的最大长度
#pragma comment(lib, "Ws2_32")

const int BUFFER_MAX = 1000;
const int DEBUG_MODE = 0; 

// HTTP头格式
const char OKHeaderFormat[] =
"HTTP/1.1 200 OK\n"
"Accept-Ranges: bytes\n"
"Connection:Keep-Alive\n"
"Content-Type: text/html\n"
"charset = ISO-8859-1\n"
"Content-Length: %d\n"
"\n";

const char NotFoundHeaderFormat[] =
"HTTP/1.1 404 NotFound\r\n"
"Content-Type: text/html\r\n"
"Accept-Ranges: bytes\r\n"
"Content-Length: 0\r\n\r\n";

void GetPage(const char* recv_buff, const int length, string& filename); //Pick Filename from Get request
int FileToBuffer(const string filename, char*& buffer, int& bufferlength); //read file and write the file to Buffer



// 定义I/O操作的结构体
typedef struct
{
	OVERLAPPED Overlapped;					// 重叠结构
	WSABUF DataBuf;							// 缓冲区对象
	CHAR Buffer[DATA_BUFSIZE];				// 缓冲区数组
	DWORD BytesSEND;                         // 发送字节数
	DWORD BytesRECV;                         // 接收的字节数    
} PER_IO_OPERATION_DATA, * LPPER_IO_OPERATION_DATA;

// 套接字句柄结构体
typedef struct
{
	SOCKET Socket;
} PER_HANDLE_DATA, * LPPER_HANDLE_DATA;

// 服务器端工作线程
DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID);


int main()
{
	SOCKADDR_IN InternetAddr;							// 服务器地址
	SOCKET Listen;										// 监听套接字
	SOCKET Accept;										// 与客户端进行通信的套接字
	HANDLE CompletionPort;								// 完成端口句柄
	SYSTEM_INFO SystemInfo;								// 获取系统信息(这里主要用于获取CPU数量)
	LPPER_HANDLE_DATA PerHandleData;					// 套接字句柄结构体
	LPPER_IO_OPERATION_DATA PerIoData;					// 定义I/O操作的结构体
	DWORD RecvBytes;									// 接收到的字节数
	DWORD Flags;										// WSARecv()函数中指定的标识位
	DWORD ThreadID;										// 工作线程编号
	WSADATA wsaData;									// Windows Socket初始化信息
	DWORD Ret;											// 函数返回值

	// 初始化Windows Sockets环境
	if ((Ret = WSAStartup(0x0202, &wsaData)) != 0)
	{
		printf("WSAStartup failed with error %d\n", Ret);
		return -1;
	}

	// 创建新的完成端口
	if ((CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0)) == NULL)
	{
		printf("CreateIoCompletionPort failed with error: %d\n", GetLastError());
		return -1;
	}

	// 获取系统信息
	GetSystemInfo(&SystemInfo);

	// 根据CPU数量启动线程
	for (int i = 0; i < SystemInfo.dwNumberOfProcessors; i++)
	{
		HANDLE ThreadHandle;
		// 创建线程,运行ServerWorkerThread()函数            
		if ((ThreadHandle = CreateThread(NULL, 0, ServerWorkerThread, CompletionPort,
			0, &ThreadID)) == NULL)
		{
			printf("CreateThread() failed with error %d\n", GetLastError());
			return -1;
		}
		CloseHandle(ThreadHandle);
	}

	// 创建监听套接字
	if ((Listen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0,
		WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET)
	{
		printf("WSASocket() failed with error %d\n", WSAGetLastError());
		return -1;
	}

	// 绑定到本地地址的9990端口
	InternetAddr.sin_family = AF_INET;
	InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	InternetAddr.sin_port = htons(PORT);
	if (bind(Listen, (PSOCKADDR)& InternetAddr, sizeof(InternetAddr)) == SOCKET_ERROR)
	{
		printf("bind() failed with error %d\n", WSAGetLastError());
		return -1;
	}

	// 开始监听
	if (listen(Listen, 5) == SOCKET_ERROR)
	{
		printf("listen() failed with error %d\n", WSAGetLastError());
		return -1;
	}

	// 监听端口打开,就开始在这里循环,一有socket连上,WSAAccept就创建一个socket,
	// 这个socket 和完成端口联上
	while (TRUE)
	{
		// 等待客户连接
		if ((Accept = WSAAccept(Listen, NULL, NULL, NULL, 0)) == SOCKET_ERROR)
		{
			printf("WSAAccept() failed with error %d\n", WSAGetLastError());
			return -1;
		}

		// 分配并设置套接字句柄结构体
		if ((PerHandleData = (LPPER_HANDLE_DATA)GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA))) == NULL)
		{
			printf("GlobalAlloc() failed with error %d\n", GetLastError());
			return -1;
		}
		PerHandleData->Socket = Accept;

		// 将与客户端进行通信的套接字Accept与完成端口CompletionPort相关联
		if (CreateIoCompletionPort((HANDLE)Accept, CompletionPort, (DWORD)PerHandleData,
			0) == NULL)
		{
			printf("CreateIoCompletionPort failed with error %d\n", GetLastError());
			return -1;
		}

		// 为I/O操作结构体分配内存空间
		if ((PerIoData = (LPPER_IO_OPERATION_DATA)GlobalAlloc(GPTR, sizeof(PER_IO_OPERATION_DATA))) == NULL)
		{
			printf("GlobalAlloc() failed with error %d\n", GetLastError());
			return -1;
		}
		// 初始化I/O操作结构体
		ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED));
		PerIoData->BytesSEND = 0;
		PerIoData->BytesRECV = 0;
		PerIoData->DataBuf.len = DATA_BUFSIZE;
		PerIoData->DataBuf.buf = PerIoData->Buffer;
		Flags = 0;

		// 接收数据,放到PerIoData中,而perIoData又通过工作线程中的ServerWorkerThread函数取出,
		if (WSARecv(Accept, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags,
			&(PerIoData->Overlapped), NULL) == SOCKET_ERROR)
		{
			if (WSAGetLastError() != ERROR_IO_PENDING)
			{
				printf("WSARecv() failed with error %d\n", WSAGetLastError());
				return -1;
			}
		}
	}
	return 0;
}


//	工作线程,循环检测完成端口状态,获取PerIoData中的数据
DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID)
{
	HANDLE CompletionPort = (HANDLE)CompletionPortID;	// 完成端口句柄   
	DWORD BytesTransferred;								// 数据传输的字节数
	LPOVERLAPPED Overlapped;							// 重叠结构体
	LPPER_HANDLE_DATA PerHandleData;					// 套接字句柄结构体
	LPPER_IO_OPERATION_DATA PerIoData;					// I/O操作结构体
	DWORD SendBytes, RecvBytes;							// 发送和接收的数量
	DWORD Flags;										// WSARecv()函数中的标识位

	while (TRUE)
	{
		// 检查完成端口的状态
		if (GetQueuedCompletionStatus(CompletionPort, &BytesTransferred,
			(PULONG_PTR)& PerHandleData, (LPOVERLAPPED*)& PerIoData, INFINITE) == 0)
		{
			printf("GetQueuedCompletionStatus failed with error %d\n", GetLastError());
			return 0;
		}

		// 如果数据传送完了,则退出
		if (BytesTransferred == 0)
		{
			if(DEBUG_MODE)
				printf("Closing socket %d\n", PerHandleData->Socket);
			// 关闭套接字
			if (closesocket(PerHandleData->Socket) == SOCKET_ERROR)
			{
				printf("closesocket() failed with error %d\n", WSAGetLastError());
				return 0;
			}
			// 释放结构体资源
			GlobalFree(PerHandleData);
			GlobalFree(PerIoData);
			continue;
		}
		// 如果还没有记录接收的数据数量,则将收到的字节数保存在PerIoData->BytesRECV中
		if (PerIoData->BytesRECV == 0)
		{
			PerIoData->BytesRECV = BytesTransferred;
			PerIoData->BytesSEND = 0;
		}
		else   // 如果已经记录了接收的数据数量,则记录发送数据量
		{
			PerIoData->BytesSEND += BytesTransferred;
		}
		string filename;
		if (DEBUG_MODE)
			printf("Recvd %s \n", PerIoData->Buffer);
		GetPage(PerIoData->Buffer, strlen(PerIoData->Buffer), filename);

		//char szHeader[1000];
		char* szHeader = (char*)malloc(BUFFER_MAX * sizeof(char));
		memset(szHeader, 0, sizeof(szHeader));

		// 向浏览器发送HTTP头和内容

		int SendBufferLenght = BUFFER_MAX;
		char* SendBuffer = (CHAR*)malloc(SendBufferLenght);

		memset(SendBuffer, 0, SendBufferLenght); // set pszBuff all to 0

		if (FileToBuffer(filename, SendBuffer, SendBufferLenght))
		{
			//send head 
			sprintf(szHeader, OKHeaderFormat, SendBufferLenght);
			int byte_sent = send(PerHandleData->Socket, szHeader, strlen(szHeader), 0);
			//printf("Data Sent\n%s\n", szHeader);
			//cout << "200 head sent to socket " << fdSocket.fd_array[i] << endl;
			//send content
			byte_sent = send(PerHandleData->Socket, SendBuffer, SendBufferLenght, 0);
			//printf("%s\n", SendBuffer);
			//cout << byte_sent << "bytes have been sent to socket " << fdSocket.fd_array[i] << endl;
			/*
			if (byte_sent == -1)
			{
				//cout << "Fail sending!" << "\nError code: " << WSAGetLastError() << endl;
				break;
			}
			*/
		}
		else // return 404
		{
			//send head only
			sprintf(szHeader, NotFoundHeaderFormat);
			int byte_sent = send(PerHandleData->Socket, szHeader, strlen(szHeader), 0);
			//cout << "404 head sent to socket " << fdSocket.fd_array[i] << endl;
		} 



		closesocket(PerHandleData->Socket);
		//GlobalFree(PerHandleData);
		//GlobalFree(PerIoData);
		
	}
}


int FileToBuffer(const string filename, char*& buffer, int& bufferlength)
{
	FILE* pFile = NULL;
	fopen_s(&pFile, filename.data(), "r");
	if (pFile == NULL)
		return 0;
	fseek(pFile, 0, SEEK_END);//move fseek to EOF
	bufferlength = ftell(pFile); //This is the length of file
	buffer[bufferlength] = '\0';
	bufferlength++; // place for'\0'
	fseek(pFile, 0, SEEK_SET); //move back

	fread(buffer, bufferlength, 1, pFile); //read file to buff
	fclose(pFile);

	return 1;
}

void GetPage(const char* recv_buff, const int length, string& filename)
{
	string buff = recv_buff;
	int location = buff.find(" HTTP", 5);
	filename = string(buff, 5, location - 5);

	if (filename == "")
		filename = "1.HTML";

	/*
	NOTE:Get head is in this format
		GET /filename HTTP/1.1
		Host: localhost:8888
		Connection: keep-alive
		Cache-Control: max-age=0
		Upgrade-Insecure-Requests: 1 ...
	*/
}

代码测试:
1 浏览器访问默认页面
在这里插入图片描述
2 浏览器访问指定页面
在这里插入图片描述
在这里插入图片描述
3 浏览器访问不存在的页面
在这里插入图片描述
6 ab并发性能测试
在这里插入图片描述
1次并行128个页面同时访问时,服务器平均延迟为8ms
在这里插入图片描述
有40000+个链接,并行访问128个链接时,服务器仍可以在50ms内对每一个访问进行相应。
在这里插入图片描述
当并行访问达到1000个链接时,服务器延迟有明显提升,但仍可在500ms内对每一个访问进行相应。

  • 0
    点赞
  • 4
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论

打赏作者

MaverickY

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值