使用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内对每一个访问进行相应。