服务器是C/S模式的核心,最近在看网络编程的东西,参考了一些书籍,做了一个Web服务器。整体的实现分为四个部分:界面控制、服务流程的实现、HTTP协议的实现、协议的辅助实现部分。
界面控制部分
主窗体设计比较简陋,
Web服务器的运行离不开HTTP协议,定义一个类CHttpProtocol,用来实现HTTP协议。
class CHttpProtocol
{
public:
HWND m_hwndDlg;
SOCKET m_listenSocket;
map<CString, char *> m_typeMap; // 保存content-type和文件后缀的对应关系map
CWinThread* m_pListenThread;
HANDLE m_hExit;
static HANDLE None; // 标志是否有Client连接到Server
static UINT ClientNum; // 连接的Client数量
static CCriticalSection m_critSect; // 互斥变量
CString m_strRootDir; // web的根目录
UINT m_nPort; // http server的端口号
public:
CHttpProtocol(void);
void DeleteClientCount();
void CountDown();
void CountUp();
HANDLE InitClientCount();
void StopHttpSrv();
bool StartHttpSrv();
static UINT ListenThread(LPVOID param);
static UINT ClientThread(LPVOID param);
bool RecvRequest(PREQUEST pReq, LPBYTE pBuf, DWORD dwBufSize);
int Analyze(PREQUEST pReq, LPBYTE pBuf);
void Disconnect(PREQUEST pReq);
void CreateTypeMap();
void SendHeader(PREQUEST pReq);
int FileExist(PREQUEST pReq);
void GetCurentTime(LPSTR lpszString);
bool GetLastModified(HANDLE hFile, LPSTR lpszString);
bool GetContenType(PREQUEST pReq, LPSTR type);
void SendFile(PREQUEST pReq);
bool SendBuffer(PREQUEST pReq, LPBYTE pBuf, DWORD dwBufSize);
public:
~CHttpProtocol(void);
};
同时定义两个结构体REQUEST和HTTPSTATS分别用来保存一个网络连接的客户端和服务器的信息。
// 连接的Client的信息
typedef struct REQUEST
{
HANDLE hExit;
SOCKET Socket; // 请求的socket
int nMethod; // 请求的使用方法:GET或HEAD
DWORD dwRecv; // 收到的字节数
DWORD dwSend; // 发送的字节数
HANDLE hFile; // 请求连接的文件
char szFileName[_MAX_PATH]; // 文件的相对路径
char postfix[10]; // 存储扩展名
char StatuCodeReason[100]; // 头部的status cod以及reason-phrase
bool permitted; // 用户权限判断
char * authority; // 用户提供的认证信息
char key[1024]; // 正确认证信息
void* pHttpProtocol; // 指向类CHttpProtocol的指针
}REQUEST, *PREQUEST;
typedef struct HTTPSTATS
{
DWORD dwRecv; // 收到字节数
DWORD dwSend; // 发送字节数
}HTTPSTATS, *PHTTPSTATS;
向主界面中声明变量:
CHttpProtocol *pHttpProtocol;
bool m_bStart;
同时声明函数:
afx_msg LRESULT AddLog(WPARAM wParam, LPARAM lParam);
afx_msg LRESULT ShowData(WPARAM wParam, LPARAM lParam);
用来建立程序的事件驱动机制,同时需要添加实时日志和显示数据收发流量,当然需要在相应的实现文件中添加消息映射宏。
“开启”按钮的事件响应控制如下:
void ChtpSrverDlg::OnStartStop()
{
// TODO: 在此添加控件通知处理程序代码
this->UpdateData();
if(m_strRootDir.IsEmpty())
{
AfxMessageBox("请设置本站Web页存放的根路径!");
return;
}
if ( !m_bStart )
{
pHttpProtocol = new CHttpProtocol;
pHttpProtocol->m_strRootDir = m_strRootDir;
pHttpProtocol->m_nPort = m_nPort;
pHttpProtocol->m_hwndDlg = m_hWnd;
if (pHttpProtocol->StartHttpSrv())
{
m_StartStop.SetWindowTextA("关闭");
//
LocaIP.EnableWindow(false);
locaPort.EnableWindow(false);
rootdir.EnableWindow(false);
m_exit.EnableWindow(false);
//
m_bStart = true;
}
else
{
if(pHttpProtocol)
{
delete pHttpProtocol;
pHttpProtocol = NULL;
}
}
}
else
{
pHttpProtocol->StopHttpSrv();
m_StartStop.SetWindowTextA("开启");
//
LocaIP.EnableWindow(true);
locaPort.EnableWindow(true);
rootdir.EnableWindow(true);
m_exit.EnableWindow(true);
//
if(pHttpProtocol)
{
delete pHttpProtocol;
pHttpProtocol = NULL;
}
m_bStart = false;
}
}
服务开启后,主程序窗体上应当动态显示运行信息,便于管理员监控服务器,该功能由前面定义的AddLog和ShowData方法来实现,其实现如下:
// 显示日志信息
LRESULT ChtpSrverDlg::AddLog(WPARAM wParam, LPARAM lParam)
{
char szBuf[284];
CString *strTemp = (CString *)wParam;
SYSTEMTIME st;
GetLocalTime(&st);
wsprintf(szBuf,"%02d:%02d:%02d.%03d %s", st.wHour, st.wMinute, st.wSecond,
st.wMilliseconds, *strTemp);
m_StatLst.AddString(szBuf);
m_StatLst.SetTopIndex(m_StatLst.GetCount() - 1);
delete strTemp;
strTemp = NULL;
return 0L;
}
// 显示接收和发送的数据流量
LRESULT ChtpSrverDlg::ShowData(WPARAM wParam, LPARAM lParam)
{
PHTTPSTATS pStats = (PHTTPSTATS)wParam;
dwReceived += pStats->dwRecv;
dwTransferred += pStats->dwSend;
TRACE1("Rev %d\n", pStats->dwRecv);
TRACE1("Send %d\n", pStats->dwSend);
TRACE1("Total Rev %d\n", dwReceived);
TRACE1("Total Send %d\n", dwTransferred);
UpdateData(false);
return 0L;
}
前面定义的HTTPSTATS结构体就可以用来实时监控服务器收发数据的流量。
若服务器故障或者网站需要维护,管理员会暂时关闭服务器。“EXIT”按钮的程序控制实现如下:
void ChtpSrverDlg::OnCancel()
{
// TODO: Add extra cleanup here
if (m_bStart)
{
pHttpProtocol->StopHttpSrv();
}
if(pHttpProtocol)
{
delete pHttpProtocol;
pHttpProtocol = NULL;
}
m_bStart = false;
CDialog::OnCancel();
}
Web服务流程
考虑到这样一个问题,服务器开启后就自主运行,管理员无法进一步干涉一个服务的内部执行流程,原因很简单,界面控制程序只能通过HTTP协议类对象的指针pHttpProtocol调用StartHttpSrv方法开启服务,调用StopHttpSrv方法关闭服务,而服务自身的操作则封装于协议类ChttpProtocol的内部,外部代码无权访问。但是,在现实中,一个服务器要为多个客户端提供服务,其上的Web服务进程必须具有与很多客户进程同时交互的能力,这就必须采用多线程和多Socket实现,此外服务进程的流程必须符合HTTP协议所规定的交互时序,即具有同步的特点。因此在协议类CHttpProtocol的构造方法中初始化服务器监听线程指针m_pListenThread和主对话框窗口句柄m_hwndDlg:
CHttpProtocol::CHttpProtocol(void)
{
m_pListenThread = NULL;
m_hwndDlg = NULL;
}
网络管理员启动服务、关闭服务时,程序的执行流程如下:
整个过程还是比较清楚的,就是普通Socket程序的流程:
WSAStartup()→WSASocket()→获取IP→bind()→listen(),所不同的是开了一个线程ListenThread用于监听。StartHttpSrv方法的实现如下:
bool CHttpProtocol::StartHttpSrv()
{
WORD wVersionRequested = WINSOCK_VERSION;
WSADATA wsaData;
int nRet;
// 启动Winsock
nRet = WSAStartup(wVersionRequested, &wsaData); // 加载成功返回0
if (nRet)
{
// 错误处理
AfxMessageBox("Initialize WinSock Failed");
return false;
}
// 检测版本
if (wsaData.wVersion != wVersionRequested)
{
// 错误处理
AfxMessageBox("Wrong WinSock Version");
return false;
}
m_hExit = CreateEvent(NULL, TRUE, FALSE, NULL);
if (m_hExit == NULL)
{
return false;
}
//创建套接字
m_listenSocket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
if (m_listenSocket == INVALID_SOCKET)
{
// 异常处理
CString *pStr = new CString;
*pStr = "Could not create listen socket";
SendMessage(m_hwndDlg, LOG_MSG, (UINT)pStr, NULL);
return false;
}
SOCKADDR_IN sockAddr;
LPSERVENT lpServEnt;
if (m_nPort != 0)
{
// 从主机字节顺序转为网络字节顺序赋给sin_port
sockAddr.sin_port = htons(m_nPort);
}
else
{
// 获取已知http服务的端口,该服务在tcp协议下注册
lpServEnt = getservbyname("http", "tcp");
if (lpServEnt != NULL)
{
sockAddr.sin_port = lpServEnt->s_port;
}
else
{
sockAddr.sin_port = htons(HTTPPORT); // 默认端口HTTPPORT=80
}
}
sockAddr.sin_family = AF_INET;
BYTE nFild[4];
CString sIP;
((ChtpSrverDlg*)(AfxGetApp()->m_pMainWnd))->LocaIP.GetAddress(nFild[0],nFild[1],nFild[2],nFild[3]);
sIP.Format("%d.%d.%d.%d",nFild[0],nFild[1],nFild[2],nFild[3]);
sockAddr.sin_addr.S_un.S_addr = inet_addr(sIP);
// 初始化content-type和文件后缀对应关系的map
CreateTypeMap();
// 套接字绑定
nRet = bind(m_listenSocket, (LPSOCKADDR)&sockAddr, sizeof(struct sockaddr));
if (nRet == SOCKET_ERROR)
{
// 绑定发生错误
CString *pStr = new CString;
*pStr = "bind() error";
SendMessage(m_hwndDlg, LOG_MSG, (UINT)pStr, NULL);
closesocket(m_listenSocket); // 断开链接
return false;
}
// 套接字监听。为客户连接创建等待队列,队列最大长度SOMAXCONN在windows sockets头文件中定义
nRet = listen(m_listenSocket, SOMAXCONN);
if (nRet == SOCKET_ERROR)
{
// 异常处理
CString *pStr = new CString;
*pStr = "listen() error";
SendMessage(m_hwndDlg, LOG_MSG, (UINT)pStr, NULL);
closesocket(m_listenSocket); // 断开链接
return false;
}
// 创建listening进程,接受客户机连接要求
m_pListenThread = AfxBeginThread(ListenThread, this);
if (!m_pListenThread)
{
// 线程创建失败
CString *pStr = new CString;
*pStr = "Could not create listening thread" ;
SendMessage(m_hwndDlg, LOG_MSG, (UINT)pStr, NULL);
closesocket(m_listenSocket); // 断开链接
return false;
}
CString strTemp;
char hostname[255];
gethostname(hostname, sizeof(hostname));
// 显示web服务器正在启动
CString *pStr = new CString;
*pStr = "****** htpSrver(WebServer) is Starting now! *******";
SendMessage(m_hwndDlg, LOG_MSG, (UINT)pStr, NULL);
// 显示web服务器的信息,包括主机名,IP以及端口号
CString *pStr1 = new CString;
pStr1->Format("%s", hostname);
*pStr1 = *pStr1 + "[" + sIP + "]" + " Port ";
strTemp.Format("%d", htons(sockAddr.sin_port));
*pStr1 = *pStr1 + strTemp;
SendMessage(m_hwndDlg, LOG_MSG, (UINT)pStr1, NULL);
return true;
}
StopHttpSrv方法的实现如下:
void CHttpProtocol::StopHttpSrv()
{
int nRet;
SetEvent(m_hExit);
nRet = closesocket(m_listenSocket);
nRet = WaitForSingleObject((HANDLE)m_pListenThread, 10000);
if (nRet == WAIT_TIMEOUT)
{
CString *pStr = new CString;
*pStr = "TIMEOUT waiting for ListenThread";
SendMessage(m_hwndDlg, LOG_MSG, (UINT)pStr, NULL);
}
CloseHandle(m_hExit);
CString *pStr1 = new CString;
*pStr1 = "Server Stopped";
SendMessage(m_hwndDlg, LOG_MSG, (UINT)pStr1, NULL);
}
关闭Web服务的本质就是先后关闭监听套接字m_listenSocket和监听线程ListenThread。上面两个方法都用到了系统内置的函数SendMessage及时向前段传递日志信息。这些信息反映了程序运行过程中每一步的执行情况、异常故障等,可以使管理员实时监控服务的运行状况。通用的方法是将要发送的信息字符串赋值给CString类指针pStr,再在LOG_MSG映射下执行AddLog方法,通过窗口句柄m_hwndDlg返回给主窗体界面。
在服务器创建套接字并启动监听线程后,接下来的任务就交给监听线程去完成。监听线程的实现如下:
UINT CHttpProtocol::ListenThread(LPVOID param)
{
CHttpProtocol *pHttpProtocol = (CHttpProtocol *)param;
SOCKET socketClient;
CWinThread* pClientThread;
SOCKADDR_IN SockAddr;
PREQUEST pReq;
int nLen;
DWORD dwRet;
// 初始化ClientNum,创建"no client"事件对象
HANDLE hNoClients;
hNoClients = pHttpProtocol->InitClientCount();
while(1) // 循环等待,如有客户连接请求,则接受客户机连接要求
{
nLen = sizeof(SOCKADDR_IN);
// 套接字等待链接,返回对应已接受的客户机连接的套接字
socketClient = accept(pHttpProtocol->m_listenSocket, (LPSOCKADDR)&SockAddr, &nLen);
if (socketClient == INVALID_SOCKET)
{
break;
}
// 将客户端网络地址转换为用点分割的IP地址
CString *pstr = new CString;
pstr->Format("%s Connecting on socket:%d", inet_ntoa(SockAddr.sin_addr), socketClient);
SendMessage(pHttpProtocol->m_hwndDlg, LOG_MSG, (UINT)pstr, NULL);
pReq = new REQUEST;
if (pReq == NULL)
{
// 处理错误
CString *pStr = new CString;
*pStr = "No memory for request";
SendMessage(pHttpProtocol->m_hwndDlg, LOG_MSG, (UINT)pStr, NULL);
continue;
}
pReq->hExit = pHttpProtocol->m_hExit;
pReq->Socket = socketClient;
pReq->hFile = INVALID_HANDLE_VALUE;
pReq->dwRecv = 0;
pReq->dwSend = 0;
pReq->pHttpProtocol = pHttpProtocol;
// 创建client进程,处理request
pClientThread = AfxBeginThread(ClientThread, pReq);
if (!pClientThread)
{
// 线程创建失败,错误处理
CString *pStr = new CString;
*pStr = "Couldn‘t start client thread";
SendMessage(pHttpProtocol->m_hwndDlg, LOG_MSG, (UINT)pStr, NULL);
delete pReq;
}
} //while
// 等待线程结束
WaitForSingleObject((HANDLE)pHttpProtocol->m_hExit, INFINITE);
// 等待所有client进程结束
dwRet = WaitForSingleObject(hNoClients, 5000);
if (dwRet == WAIT_TIMEOUT)
{
// 超时返回,并且同步对象未退出
CString *pStr = new CString;
*pStr = "One or more client threads did not exit";
SendMessage(pHttpProtocol->m_hwndDlg, LOG_MSG, (UINT)pStr, NULL);
}
pHttpProtocol->DeleteClientCount();
return 0;
}
监听线程ListenThread的执行流程为:初始化ClientNum→获取连接的客户端信息→创建客户线程ClientThread→删除ClientCount。其中第一步和最后一步即初始化和删除ClientCount的过程分别如下:
InitClientCount():
HANDLE CHttpProtocol::InitClientCount()
{
ClientNum = 0;
//创建“no client”事件对象
None = CreateEvent(NULL, TRUE, TRUE, NULL);
return None;
}
DeleteClientCount():
void CHttpProtocol::DeleteClientCount()
{
CloseHandle(None);
}
监听线程从REQUEST结构体获取连接客户端的信息,并创建Client线程进行处理,服务器会为每一个与之连接的客户端建立一个专门的线程ClientThread,这个线程将按照HTTP协议的规范与客户端交流,为客户提供Web服务。
HTTP协议的实现
HTTP协议请求报文的处理过程如下:
从客户线程ClientThread开始,将遵照HTTP协议所规定的标准去处理HTTP请求报文。
ClientThread的实现如下:
UINT CHttpProtocol::ClientThread(LPVOID param)
{
int nRet;
BYTE buf[1024];
PREQUEST pReq = (PREQUEST)param;
CHttpProtocol pHttpProtocol = (CHttpProtocol )pReq->pHttpProtocol;
pHttpProtocol->CountUp();// 记数
// 接收request data
if (!pHttpProtocol->RecvRequest(pReq, buf, sizeof(buf)))
{
pHttpProtocol->Disconnect(pReq);
delete pReq;
pHttpProtocol->CountDown();
return 0;
}
// 分析request信息
nRet = pHttpProtocol->Analyze(pReq, buf);
if (nRet)
{
// 处理错误
CString *pStr = new CString;
*pStr = “Error occurs when analyzing client request”;
SendMessage(pHttpProtocol->m_hwndDlg, LOG_MSG, (UINT)pStr, NULL);
pHttpProtocol->Disconnect(pReq);
delete pReq;
pHttpProtocol->CountDown();
return 0;
}
// 生成并返回头部
pHttpProtocol->SendHeader(pReq);
// 向client传送数据
if(pReq->nMethod == METHOD_GET)
{
pHttpProtocol->SendFile(pReq);
}
pHttpProtocol->Disconnect(pReq);
delete pReq;
pHttpProtocol->CountDown();// client数量减1
return 0;
}
CountUp()代码如下:
void CHttpProtocol::CountUp()
{
//进入临界区
m_critSect.Lock();
ClientNum++;
//离开临界区
m_critSect.Unlock();
//重置为无信号事件对象
ResetEvent(None);
}
服务器管理众多Socket使用的是Winsock的一种深入的编程机制,即“套接字I/O模型”,它与操作系统的进程管理机制相似,也要用到“临界区”。临界区的初始化代码如下:
UINT CHttpProtocol::ClientNum = 0;
CCriticalSection CHttpProtocol::m_critSect; //临界区初始化
HANDLE CHttpProtocol::None = NULL;
RecvRequest():
bool CHttpProtocol::RecvRequest(PREQUEST pReq, LPBYTE pBuf, DWORD dwBufSize)
{
WSABUF wsabuf; //发送/接收缓冲区结构
WSAOVERLAPPED over; //指向调用重叠操作时指定的WSAOVERLAPPED结构
DWORD dwRecv;
DWORD dwFlags;
DWORD dwRet;
HANDLE hEvents[2];
bool fPending;
int nRet;
memset(pBuf, 0, dwBufSize); //初始化缓冲区
wsabuf.buf = (char *)pBuf;
wsabuf.len = dwBufSize; //缓冲区的长度
over.hEvent = WSACreateEvent(); //创建一个新的事件对象
dwFlags = 0;
fPending = FALSE;
//接收数据
nRet = WSARecv(pReq->Socket, &wsabuf, 1, &dwRecv, &dwFlags, &over, NULL);
if (nRet != 0)
{
//错误代码WSA_IO_PENDING表示重叠操作成功启动
if (WSAGetLastError() != WSA_IO_PENDING)
{
//重叠操作未能成功
CloseHandle(over.hEvent);
return false;
}
else
{
fPending = true;
}
}
if (fPending)
{
hEvents[0] = over.hEvent;
hEvents[1] = pReq->hExit;
dwRet = WaitForMultipleObjects(2, hEvents, FALSE, INFINITE);
if (dwRet != 0)
{
CloseHandle(over.hEvent);
return false;
}
//重叠操作未完成
if (!WSAGetOverlappedResult(pReq->Socket, &over, &dwRecv, FALSE, &dwFlags))
{
CloseHandle(over.hEvent);
return false;
}
}
pReq->dwRecv += dwRecv; //统计接收数量
CloseHandle(over.hEvent);
return true;
}
成功收到客户的请求信息之后,就要按照HTTP协议规范的定义对其进行分析,这是网络协议得以最终实现的关键。Analyze的实现如下:
//分析request信息
int CHttpProtocol::Analyze(PREQUEST pReq, LPBYTE pBuf)
{
//分析接收到的信息
char szSeps[] = " \n";
char *cpToken;
//防止非法请求
if (strstr((const char *)pBuf, "..") != NULL)
{
strcpy(pReq->StatuCodeReason, HTTP_STATUS_BADREQUEST);
return 1;
}
//判断ruquest的mothed
cpToken = strtok((char *)pBuf, szSeps); //缓存中字符串分解为一组标记串
if (!_stricmp(cpToken, "GET")) //GET命令
{
pReq->nMethod = METHOD_GET;
}
else if (!_stricmp(cpToken, "HEAD")) //HEAD命令
{
pReq->nMethod = METHOD_HEAD;
}
else
{
strcpy(pReq->StatuCodeReason, HTTP_STATUS_NOTIMPLEMENTED);
return 1;
}
//获取Request-URL
cpToken = strtok(NULL, szSeps);
if (cpToken == NULL)
{
strcpy(pReq->StatuCodeReason, HTTP_STATUS_BADREQUEST);
return 1;
}
strcpy(pReq->szFileName, m_strRootDir);
if (strlen(cpToken) > 1)
{
strcat(pReq->szFileName, cpToken); //把该文件名添加到结尾处形成路径
}
else
{
strcat(pReq->szFileName, "/index.html");
}
return 0;
}
服务器向客户端返回网页文档头用的是SendHeader()方法。SendHeader方法实现如下:
//发送头部
void CHttpProtocol::SendHeader(PREQUEST pReq)
{
int n = FileExist(pReq);
if(!n) //文件不存在,则返回
{
return;
}
char Header[2048] = " ";
char curTime[50] = " ";
GetCurentTime((char*)curTime);
//取得文件长度
DWORD length;
length = GetFileSize(pReq->hFile, NULL);
//取得文件的last-modified时间
char last_modified[60] = " ";
GetLastModified(pReq->hFile, (char*)last_modified);
//取得文件的类型
char ContenType[50] = " ";
GetContenType(pReq, (char*)ContenType);
sprintf((char*)Header, "HTTP/1.0 %s\r\nDate: %s\r\nServer: %s\r\n
Content-Type: %s\r\nContent-Length: %d\r\nLast-Modified: %s\r\n\r\n",
HTTP_STATUS_OK,
curTime, // Date
"My Http Server", // Server
ContenType, // Content-Type
length, // Content-length
last_modified); // Last-Modified
//发送头部
send(pReq->Socket, Header, strlen(Header), 0);
}
假如用户向服务器发出的是“GET”命令(索取特定的网页),则服务器在响应中除了要返回文档头之外还要用SendFile()方法向客户端传送数据,SendFile方法实现如下:
//发送文件
void CHttpProtocol::SendFile(PREQUEST pReq)
{
int n = FileExist(pReq);
if(!n) //文件不存在,则返回
{
return;
}
CString *pStr = new CString;
*pStr = *pStr + &pReq->szFileName[strlen(m_strRootDir)];
SendMessage(m_hwndDlg, LOG_MSG, UINT(pStr), NULL);
static BYTE buf[2048];
DWORD dwRead;
BOOL fRet;
int flag = 1;
//读写数据,直到完成
while(1)
{
//从文件中读入buffer中
fRet = ReadFile(pReq->hFile, buf, sizeof(buf), &dwRead, NULL);
if (!fRet)
{
static char szMsg[512];
wsprintf(szMsg, "%s", HTTP_STATUS_SERVERERROR);
//向客户端发送出错信息
send(pReq->Socket, szMsg, strlen(szMsg), 0);
break;
}
//完成
if (dwRead == 0)
{
break;
}
//将buffer内容传送给客户端
if (!SendBuffer(pReq, buf, dwRead))
{
break;
}
pReq->dwSend += dwRead;
}
//关闭文件
if (CloseHandle(pReq->hFile))
{
pReq->hFile = INVALID_HANDLE_VALUE;
}
else
{
CString *pStr = new CString;
*pStr = "Error occurs when closing file";
SendMessage(m_hwndDlg, LOG_MSG, (UINT)pStr, NULL);
}
}
服务完成,连接会关闭。Disconnect()方法实现如下:
void CHttpProtocol::Disconnect(PREQUEST pReq)
{
//关闭套接字:释放所占有的资源
int nRet;
CString *pStr = new CString;
pStr->Format("Closing socket: %d", pReq->Socket);
SendMessage(m_hwndDlg, LOG_MSG, (UINT)pStr, NULL);
nRet = closesocket(pReq->Socket);
if (nRet == SOCKET_ERROR)
{
//处理错误
CString *pStr1 = new CString;
pStr1->Format("closesocket() error: %d", WSAGetLastError() );
SendMessage(m_hwndDlg, LOG_MSG, (UINT)pStr1, NULL);
}
HTTPSTATS stats;
stats.dwRecv = pReq->dwRecv;
stats.dwSend = pReq->dwSend;
SendMessage(m_hwndDlg, DATA_MSG, (UINT)&stats, NULL);
}
连接关闭后,客户端计数减1,CountDown()方法实现如下:
void CHttpProtocol::CountDown()
{
//进入排斥区
m_critSect.Lock();
if(ClientNum > 0)
{
ClientNum--;
}
//离开排斥区
m_critSect.Unlock();
if(ClientNum < 1)
{
//重置为有信号事件对象
SetEvent(None);
}
}
辅助实现
除了协议的实现以外,还需要在某些操作上进行细节的处理。
FileExist()函数用于判断服务器上是否有用户需要的网页文件,其实现如下:
int CHttpProtocol::FileExist(PREQUEST pReq)
{
pReq->hFile = CreateFile(pReq->szFileName, GENERIC_READ, FILE_SHARE_READ, NULL,OPEN_ EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
//如果文件不存在,则返回出错信息
if (pReq->hFile == INVALID_HANDLE_VALUE)
{
strcpy(pReq->StatuCodeReason, HTTP_STATUS_NOTFOUND);
return 0;
}
else
{
return 1;
}
}
GetCurentTime()函数实现如下:
//活动本地时间
void CHttpProtocol::GetCurentTime(LPSTR lpszString)
{
//活动本地时间
SYSTEMTIME st;
GetLocalTime(&st);
//时间格式化
wsprintf(lpszString, "%s %02d %s %d %02d:%02d:%02d GMT",
week[st.wDayOfWeek],st.wDay,month[st.wMonth- 1],
st.wYear, st.wHour, st.wMinute, st.wSecond);
}
这个函数获取的时间信息是显示服务器运行日志用的,统一采用格林尼治标准时间,为此还要在HttpProtocol.cpp源文件中定义星期和月份的转换表,代码如下:
//格林尼治时间的星期转换
char *week[] = { "Sun,","Mon,","Tue,","Wed,","Thu,","Fri,","Sat,",};
//格林尼治时间的月份转换
char *month[] = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep", "Oct","Nov","Dec",};
GetLastModified()函数用于取得文件上次修改的时间,其实现如下:
bool CHttpProtocol::GetLastModified(HANDLE hFile, LPSTR lpszString)
{
//获得文件的last-modified 时间
FILETIME ftCreate, ftAccess, ftWrite;
SYSTEMTIME stCreate;
FILETIME ftime;
//获得文件的last-modified的UTC时间
if (!GetFileTime(hFile, &ftCreate, &ftAccess, &ftWrite))
return false;
FileTimeToLocalFileTime(&ftWrite,&ftime);
//UTC时间转化成本地时间
FileTimeToSystemTime(&ftime, &stCreate);
//时间格式化
wsprintf(lpszString, "%s %02d %s %d %02d:%02d:%02d GMT", week[stCreate. wDayOfWeek], stCreate.wDay, month[stCreate.wMonth-1], stCreate.wYear, stCreate.wHour,stCreate.wMinute, stCreate. wSecond);
}
GetContenType()函数用于取得文件的类型,其实现如下:
bool CHttpProtocol::GetContenType(PREQUEST pReq, LPSTR type)
{
//取得文件的类型
CString cpToken;
cpToken = strstr(pReq->szFileName, ".");
strcpy(pReq->postfix, cpToken); //“pReq->postfix”存储的是文件扩展名
//遍历搜索该文件类型对应的content-type
map<CString, char *>::iterator it = m_typeMap.find(pReq->postfix);
if(it != m_typeMap.end())
{
wsprintf(type,"%s",(*it).second);
}
return TRUE;
}
为了使客户端能够浏览服务器上的多种类型的文件,必须提供对计算机上不同文件类型的识别机制。这里可以用Iterator来实现,将客户程序映射至一个“容器”(map),“容器”中存储了众多不同种类的文件后缀,而map中保存content-type和文件后缀的对应关系,具体的实现在函数CreateTypeMap()中。最后还有一段SendBuffer,调用Socket接口的WSASend向客户端发送数据。
下面给出测试例子。在D盘新建一个文件夹MyShare,以该文件夹作为服务器的根目录,存放测试用的资源。用IE访问武汉大学主页,并将该主页保存到该文件夹中,
浏览器中输入http://192.168.1.104:2258/whu.htm
状态信息如下:
工程源码
版权声明:本文为博主原创文章,未经博主允许不得转载。