Web服务器开发

原创 2015年07月10日 20:38:07

服务器是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
这里写图片描述
状态信息如下:
这里写图片描述
工程源码
下载

版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

python做web开发时用的是什么服务器?

python做web开发时用的是什么服务器?为什么说这个问题?今天一个小伙伴提了一个问题,如下图:这应该是参考我的文章:《Python入门》第一个Python Web程序——简单的Web服务器但是他想...

Java开发的简单WEB服务器源码

  • 2016年06月14日 13:30
  • 85KB
  • 下载

使用 Gulp 配置 Web 开发服务器

原文:Gulp as a Development Web Server 作者:Johanes Schickling 构建工具 Gulp.js 最近正在变得越来越流行。我们可以用它做很多事,比如合...

走进Java Web开发 ——客户端与服务器的交互原理

作者:李东龙        对于Java Web程序的学习已经有一段时间了,也正在跟做一个项目——DRP分销资源计划。          DRP这段时间已经看了不少了,把相关代码也实现了。但是还是...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Web服务器开发
举报原因:
原因补充:

(最多只允许输入30个字)