文章目录
  1. 1. 实验要求
  2. 2. 开始
    1. 2.1. 开发环境
  3. 3. Socket核心类分析
    1. 3.1. 公开方法
    2. 3.2. 回调接口
    3. 3.3. 线程函数定义
    4. 3.4. 类型定义
  4. 4. Socket核心类实现
    1. 4.1. 初始化和退出
    2. 4.2. 开始与结束服务
    3. 4.3. 线程函数实现
    4. 4.4. 两个帮助方法
  5. 5. UI界面
  6. 6. 结束语
  7. 7. 参考

平时如果一个项目需要网络连接,一般都是直接用如C#Java等高级语言直接调用相关的函数去实现功能,甚至从未关注过在底层这些函数是怎么运作的;而截至不久前,也只是用C#中的Socket相关类做过一点有关长连接的事情。

而最近,恰好由于网络课程实验要求,使用Windows Socket API + C/C++来实现一个简单的多线程Web服务器,于是开始一边摸索,一边实现功能。在这个实验中,不仅仅是简单的Windows Socket函数调用,其实主要的工作还在于多线程(包括GUI线程)之间的通信,对于HTTP请求,也只是简单的实现了GET方法。

实验要求

  1. 可配置IP地址、监听端口和主目录
  2. 能够在监听端口上进行监听
  3. 支持服务的启动
  4. 支持服务的关闭
  5. 能够响应客户端的请求,并定位相应的html文件
  6. 对每个请求能够创建单独的响应线程
  7. 能够发送可被客户端解析的响应报文
  8. 对于错误的请求能够定位错误的原因并给出相应的响应
  9. 支持多种类型文件的输出
  10. 具备图形GUI界面

开始

在介绍主要流程之前,先说一些完成实验之后的体会,或者说学到的东西吧。首先是在调试时,启动服务器后需要绑定IP地址和端口,这里IP地址只能是127.0.0.1或者0.0.0.0,据说也可以是你的网卡IP地址,但是我没有测试成功;而端口号必须大于1024,否则虽然绑定地址时虽然不会发生错误,但是也无法监听客户端请求。

其次,为了及时响应GUI界面的操作,需要使用异步I/O模式,因此需要将Socket操作设置为非阻塞模式,这时在Socket上的各种操作就需要自己去干涉了;虽然这看起来更复杂,但是也可以更好的认识Socket。

第三,要注意线程之间的通信,特别是传递引用类型时,更需要注意局部变量的陷阱。因为我们是要在另一个线程中访问数据,所以需要保证在另一个线程结束之前,传递的数据是有效的,通常可以采用静态变量/全局变量、动态分配内存等方式来实现,但是在这里,由于需要确保线程函数是线程安全的,也就是可重入的,因此不宜采用静态变量方式,所以将需要传递的数据保存在堆中,只是最后需要记得手动释放这块内存。

最后说一句,无论什么时候,要记得及时释放资源,如果你的代码中有未释放的资源,特别是内核资源,请不要急着调试或运行程序,虽然一般调试器都会在程序运行结束之后释放其占用的资源,但是万一出现某些内核对象没有释放的情况,也许下一次再运行你的程序时会出现本不该出现的错误。当然,重启是一个办法。

开发环境

由于要求有图形化界面,所以使用 Qt 进行快速开发,其中核心类是完全用Windows API实现,通过预留的接口与Qt界面进行交互。

由于安装的Qt版本是MSVC2013 版的,所以安装完成后Qt无法自动设置调试器,因此无法在Qt中进行调试(如果希望在Qt中调试,需要单独下载WinDbg工具,并在Qt中进行配置),前期还好,通过在关键处打印的方式,即使不用调试器也能找到错误并修正,但是到后来,需要调试多线程以及指针等,仅仅凭借简单的打印方式效率太低了,所以又去Qt官网下载了Qt-VS-Addin,这是一个适用于Visual Studio 的Qt插件,安装之后,就可以在VS中编写和调试Qt项目了,而相比Qt,我更喜欢用VS调试。

Socket核心类分析

在编写Socket核心类之前,首先需要想好要预留那些接口与GUI进行交互,毕竟虽然实际上这个核心类和GUI类是在同一个项目工程中,但是为了让核心类的可移植性更高,或者说将UI和底层操作隔离开来,我们应该设计好Socket类的通信方式,这样,在完成Socket类时可以不用考虑UI操作,而在UI设计过程中也无需深入探究Socket实现方式。

在这里,根据实验的要求,图形化界面应该提供启动、停止服务的操作,而且还要能设置一些基本参数,因此在Socket 类中,应该提供一个启动和停止监听的接口,而为了简单考虑,我们将在UI界面设定的一些基本配置参数通过启动接口传递到Socket 类中,而不是要求在初始化Socket 类时传入,这样在GUI类中就可以将Socket 类的初始化和启动操作放到不同的方法中,而且通过重新调用启动方法就可以实现监听新的端口,而不用去构造一个新的Socket 类。此外,为了让UI界面知道当前监听的状态(是否正在监听端口),还需要在Socket 类中提供一个只读属性方法。

这里,Socket核心类被命名为SocketUnits

公开方法

Socket核心类定义的公开方法如下:

bool Start(const string &ip, unsigned short port, const wstring &baseDir);
void Stop();
bool GetIsListening()
{
    DWORD extCode = 0;
    GetExitCodeThread(_lastThread, &extCode);
    return extCode == STILL_ACTIVE;
}

其中GetIsListening用于获取监听状态,此处作为内联函数存在。_lastThread保存了监听线程的句柄,如果还没有创建监听线程,则_lastThread = INVALID_HANDLE_VALUE

回调接口

上面是外部与Socket类通信的主要接口,由外部方法调用,设置Socket的属性,但却无法获取Socket类的当前状态,因为采用了异步Socket实现,所以如果还是像上面一样单纯的提供接口,让调用者使用轮询的方式查询Socket状态,那么不仅让调用类的实现难度增加,同时也会无端的消耗CPU时间片。因此这里采用了一种类似Windows消息事件的回调机制,利用纯虚类定义回调接口,当Socket类满足某一状态时会自动调用回调接口中定义的对应方法。

回调接口中需要哪些方法,这个我们也要根据实际情况来决定,在这里,我们暂时只需将一些基本状态传递给UI即可。首先是服务的启动和关闭信息,虽然提供了公开的启动和关闭方法,但是由于在该方法内部是需要创建新的线程来进行Socket 的监听工作,因此,当启动方法(SocketUnits::Start 函数)返回时,并不意味着监听Socket 创建成功,所以我们需要在监听线程中来告诉调用者服务是否启动;同理,关闭时也需要在监听线程中实现。其次,对于每一个来自客户端的连接Socket,我们也需要向外通知这个连接的状态,包括建立连接、关闭连接,此外为了方便查看连接信息,将与客户端的具体请求和响应消息也发送出去。最后,还需要一个出现错误时的回调接口,方便调用者在出错时及时作出反应。

回调接口ISocketEvent 定义如下:

class ISocketEvent
{
public:
    virtual void OnListenSocketChanged(const char *ip, unsigned short port, 
        const wchar_t *dir) = 0;
    /* stopCode:结束状态码 */
    virtual void OnServiceStopped(int stopCode) = 0;
    virtual void OnSocketConnected(const char *ip) = 0;
    virtual void OnSocketFinished(const char *ip) = 0;
    /* isSend:指示是发送还是接收数据。 info:发送或接收的数据 */
    virtual void OnNewLogs(const char *ip, const char *info, bool isSend) = 0;
    /* eor:错误代码。 msg:错误说明 */
    virtual void OnError(int eor, const char *msg) = 0;
};

这里的形参ip是字符串形式的点分十进制IP地址。OnListenSocketChanged函数的形参含义依次为IP地址、监听端口号、主目录。

ISocketEvent 类中各方法解释:

  • OnListenSocketChanged:当监听Socket配置发生变化时(包括启动)调用
  • OnServiceStopped:当监听Socket关闭时调用
  • OnSocketConnected:当有来自客户端的Socket请求时调用
  • OnSocketFinished:当与客户端的Socket连接关闭时调用
  • OnNewLogs:当与客户端进行数据传递(发送或者接收)时调用
  • OnError:当Socket连接出现错误时调用

线程函数定义

前面的都是为实现Socket类与外部通信和发送数据所定义的方法,而实现一个多线程WEB服务器最核心的部分则是此处要说的,线程以及Socket的处理。我们需要将监听Socket和响应Socket都放在单独的线程中进行处理,因此需要定义两个线程函数,分别用于处理监听操作和响应操作。在C++中,如果一个线程函数是类的成员,那么它必须被定义为static的,这里涉及到一个this指针的问题。

因此,按照线程函数的格式定义如下两个函数:

static DWORD WINAPI ListenThread(LPVOID lpParameter);
static DWORD WINAPI WorkerThread(LPVOID lpParameter);

形参lpParameter是一个void *型的指针,它的值在创建线程时通过线程创建方法传入。返回值会作为线程的退出值,可以由方法bool GetExitCodeThread(HANDLE, LPDWORD)获取。

此外,还定义了两个静态方法,以便在线程函数中调用,完成相应的功能:

static int GetType(const wstring &fileName, string &type);
static bool SignalSend(SOCKET s, const char *buf, size_t len, bool *stop);

GetType函数用于获取指定文件(文件名由fileName指示)的类型,在这里,仅仅对一些WEB中常用的文件进行判断,返回值指示文件是二进制格式还是文本格式。SignalSend函数则是对Socket对象非阻塞模式下的send方法进行了一个封装。

类型定义

这里简要说明一下Socket类中用到的一些自定义数据格式。

首先定义了一个HANDLE列表的标识符:

typedef list<HANDLE> ThreadList;

之后在SocketUnits 类中定义了两个结构体类型,用于向线程函数传递参数:

/* 监听线程函数参数类型定义 */
struct ListenParameter
{
    HANDLE lastThread;
    ISocketEvent *event;
    unsigned short port;
    string ip;
    wstring baseDir;
    bool *stop;
    bool *notifyStop;
};

/* 响应线程函数参数类型定义 */
struct ThreadParameter
{
    sockaddr_in remoteAddr;
    SOCKET remoteSocket;
    ISocketEvent *event;
    wstring baseDir;
    bool *stop;
};

Socket核心类实现

完成Socket类的定义之后,可以开始实现每个方法的功能了。

初始化和退出

首先当然是Socket类的构造函数了,这里我们定义一个构造函数,带一个ISocketEvent * 类型的形参,这里不对实际传入的值做任何检查,也就是要求调用者必须传入一个在Socket类生存期间保持有效的值,这个值被用来初始化Socket类,从而实现回调。

初始化主要代码如下:

SocketUnits::SocketUnits(ISocketEvent *event)
{
    WSADATA wsd;
    _stop = false;
    _lastThread = INVALID_HANDLE_VALUE;
    _lsParam.event = event;
    _lsParam.stop = &_stop;
    _lsParam.notifyStop = &_notifyStop;

    _nRC = WSAStartup(MAKEWORD(2, 2), &wsd);
    if (_nRC)
    {
        event->OnError(-1, "初始化WinSock时出错!");
        return;
    }
    if (wsd.wVersion != MAKEWORD(2, 2))
    {
        _nRC = -1;
        WSACleanup();
        event->OnError(-2, "不受支持的WinSock版本。");
        return;
    }
}

其中_lsParam是一个SocketUnit::ListenParameter类型的变量,此处对它进行初始化,当创建监听线程时,它被作为一个参数传入到线程中。

接下来就是调用WSAStartup函数完成Windows Socket的一系列初始化工作,这个函数是应用程序应该第一个调用的 Winsock API 函数。

而在Socket类被析构时,则应该完成一些清理工作。

SocketUnits::~SocketUnits()
{
    Stop();
    if (!_nRC)
        WSACleanup();
}

WSACleanup就是用来清除WinSock的函数。

开始与结束服务

虽然真正的监听工作是在另外一个线程中完成的,但是对于开始方法来说,我们也必须返回一个状态表示方法是否调用成功,而不能让调用者仅仅依赖OnListenSocketChanged方法回调。

Start方法中,做了一些简单的判断,来确定其返回状态值。

bool SocketUnits::Start(const string &ip, unsigned short port, const wstring &baseDir)
{
    if (_stop || (_lsParam.ip.compare(ip) == 0 && _lsParam.port == port &&
                  _lsParam.baseDir.compare(baseDir) == 0))
        return false;
    _notifyStop = false;
    _stop = true;

    _lsParam.ip = ip;
    _lsParam.baseDir = baseDir;
    _lsParam.port = port;
    _lsParam.lastThread = _lastThread;
    if (_lastThread != INVALID_HANDLE_VALUE)
        CloseHandle(_lastThread);

    _lastThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)(SocketUnits::ListenThread),
        (LPVOID)&_lsParam, 0, NULL);
    if (_lastThread == INVALID_HANDLE_VALUE)
    {
        _lsParam.event->OnError(1, "启动监听失败!");
    }
    return true;
}

注意此处当创建线程失败的时候,并没有返回false,是因为线程创建失败时,则原来运行的监听线程(如果有)也会停止,那么调用者此时会接收到OnServiceStopped回调,此时,调用者应该响应这个回调函数,而不是依赖Start方法的返回值。

由于采用非阻塞I/O,所以要结束整个服务,只需要做一个类似广播的操作即可。在这里,我们只需要将_stop置为true_notifyStop也置为true即可,此时所有的线程(包括监听线程和响应线程)均会执行线程结束部分代码,并正常结束线程,回收线程所占用资源。

void SocketUnits::Stop()
{
    _notifyStop = true;
    _stop = true;
    if (_lastThread == INVALID_HANDLE_VALUE)
        return;
    CloseHandle(_lastThread);
    _lastThread = INVALID_HANDLE_VALUE;
}

线程函数实现

这里就是整个Socket类的核心实现了,主要需要注意的是线程之间传递数据时一定要保持数据在线程生存期间的有效性,否则可能导致不可预见的逻辑错误。此外就是采用异步I/O时需要进行的处理和优化。

首先是监听线程函数。

DWORD WINAPI SocketUnits::ListenThread(LPVOID lpParameter)
{
    ListenParameter *lp = (ListenParameter*)lpParameter;
    HANDLE handle = lp->lastThread;
    ISocketEvent *event = lp->event;
    bool *stop = lp->stop;
    if (handle != INVALID_HANDLE_VALUE)
        WaitForSingleObject(handle, INFINITE);
    *stop = false;

    // ......
}

在函数的开头,首先从传入参数中取出各个值,其中handle保存上一个监听线程的句柄,在这里线程会首先判断handle的有效性,如果有效,说明程序曾经创建过一个监听线程,那么此处会调用WaitForSingleObject方法来等待上一个监听线程结束,然后才会开始执行。

DWORD WINAPI SocketUnits::ListenThread(LPVOID lpParameter)
{
    // ......

    SOCKET server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (server == INVALID_SOCKET)
    {
        event->OnError(2, "无法创建Socket连接!");
        event->OnServiceStopped(WSAGetLastError());
        return 2;
    }
    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.S_un.S_addr = inet_addr(lp->ip.c_str());
    addr.sin_port = htons(lp->port);
    if (bind(server, (LPSOCKADDR)&addr, sizeof(addr)) == SOCKET_ERROR)
    {
        event->OnError(3, "无法绑定服务器地址!");
        event->OnServiceStopped(WSAGetLastError());
        return 3;
    }
    if (listen(server, SOMAXCONN) == SOCKET_ERROR)
    {
        event->OnError(4, "无法设置Socket为等待连接状态!");
        event->OnServiceStopped(WSAGetLastError());
        return 4;
    }
    u_long mode = 1;
    ioctlsocket(server, FIONBIO, &mode);
    event->OnListenSocketChanged(lp->ip.c_str(), lp->port, lp->baseDir.c_str());

    // ......
}

接下来就是创建Socket了,这个Socket就是用来监听指定端口的。这里通过调用ioctlsocket方法将此Socket设置为非阻塞模式。

DWORD WINAPI SocketUnits::ListenThread(LPVOID lpParameter)
{
    // ......

    ThreadList workerTrds;
    while (true)
    {
        ThreadParameter *tp = new ThreadParameter();
        int nAddrLen = sizeof(sockaddr_in);
        while ((int)(tp->remoteSocket = accept(server, (LPSOCKADDR)&(tp->remoteAddr),
            &nAddrLen)) < 0)
        {
            if (*stop)
                goto finish;
            Sleep(400);
        }

        tp->baseDir = lp->baseDir;
        tp->event = lp->event;
        tp->stop = lp->stop;
        HANDLE thandle = CreateThread(NULL, 0,
            (LPTHREAD_START_ROUTINE)(SocketUnits::WorkerThread),
            tp, 0, NULL);
        if (thandle == INVALID_HANDLE_VALUE)
        {
            closesocket(tp->remoteSocket);
            delete tp;
            event->OnError(5, "无法创建与客户端通信的线程!");
        }
        else
        {
            workerTrds.push_back(thandle);
        }
    }

    // ......
}

这里开始正式监听,如果有来自客户端的请求,则会创建一个新的线程,将tp作为参数传递进去,tp中保存了这个请求的所有信息。由于此处tp是一个局部变量,因此我们需要在堆中分配它的内存,以免当本次循环结束时其内容所在内存区域被回收。

DWORD WINAPI SocketUnits::ListenThread(LPVOID lpParameter)
{
    // ......

finish:
    closesocket(server);
    ThreadList::iterator itor = workerTrds.begin();
    for (; itor != workerTrds.end(); ++itor)
    {
        WaitForSingleObject((HANDLE)*itor, INFINITE);
        CloseHandle((HANDLE)*itor);
    }
    Sleep(800);
    if (*(lp->notifyStop))
    {
        *stop = false;
        event->OnServiceStopped(0);
    }
    return 0;
}

这里就是结束代码了,当这个线程结束时,便执行这一部分代码,关闭Socket、释放资源并通知调用者。

然后便是响应函数了。

DWORD WINAPI SocketUnits::WorkerThread(LPVOID lpParameter)
{
    ThreadParameter *tp = (ThreadParameter*)lpParameter;
    SOCKET remoteSocket = tp->remoteSocket;
    sockaddr_in remoteAddr = tp->remoteAddr;
    ISocketEvent *event = tp->event;
    bool *stop = tp->stop;
    char addr[16];
    strncpy(addr, inet_ntoa(remoteAddr.sin_addr), 15);
    addr[15] = 0;
    event->OnSocketConnected(addr);

    // ......
}

首先同样是取出参数中的值。remoteSocket保存了与客户端连接的Socket对象,remoteAddr保存了客户端IP地址。

DWORD WINAPI SocketUnits::WorkerThread(LPVOID lpParameter)
{
    // ......

    char buffer[1024] = { 0 };
    int byteRecv = 0;
    stringstream ss;
    wstring filepath;
    string filetype;
    string header = "HTTP/1.1 200 OK\r\nContent-Type: ";
    while (true)    // 接收数据
    {
        byteRecv = recv(remoteSocket, buffer, 1023, 0);
        if (byteRecv == 0)
            goto finish;
        else if (byteRecv < 0)
        {
            if (*stop)
                goto finish;

            Sleep(10);
            int err = WSAGetLastError();
            //if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)
            if (err == WSAEWOULDBLOCK)
                continue;
            else
            {
                event->OnError(WSAGetLastError(), "无法从套接字接收数据。");
                goto finish;
            }
        }
        else
            break;
    }
    buffer[byteRecv] = 0;
    event->OnNewLogs(addr, buffer, false);

    // ......
}

接下来开始从客户端接收数据。注意异步调用需要进行的判断,这里仅处理了一些简单错误,实现基本的接收数据功能。同时,在循环之前定义了一些变量,这是因为在循环中含有goto语句,而C++不允许在goto语句后面进行非基本类型变量的初始化操作,因此将这些变量(string类型)的初始化移到循环体之前。

DWORD WINAPI SocketUnits::WorkerThread(LPVOID lpParameter)
{
    // ......

    ss << buffer;
    char tbuff[128];
    while (ss.getline(tbuff, sizeof(tbuff)))
    {
        char *p = strstr(tbuff, "GET");
        if (p)
        {
            strrchr(p, ' ')[0] = 0;
            string fp = strchr(p, '/');
            int len = fp.size();
            if (len > 0 && fp.c_str()[len - 1] == '/')
                fp += "index.html";
            wstring ws(fp.size(), ' ');
            copy(fp.begin(), fp.end(), ws.begin());
            filepath = tp->baseDir + ws;
            string ft;
            int tp = GetType(ws, ft);
            if (tp == 0)
                filepath += L"/index.html";
            break;
        }
    }
    if (filepath.size() == 0)
    {
        const char *notSupport = "HTTP/1.1 501 Not Implemented\r\n\r\n";
        SignalSend(remoteSocket, notSupport, strlen(notSupport), stop);
        event->OnNewLogs(addr, notSupport, true);
        goto finish;
    }

    // ......
}

在收到客户端发送的请求数据之后,就开始解析。此处仅仅检查GET方法,对于其他的请求方法,服务器会直接返回501 Not Implemented响应报文,表示不支持的方法。对于GET方法,这里会检查其请求的URL,并将其映射到本地文件路径,保存在filepath中。

DWORD WINAPI SocketUnits::WorkerThread(LPVOID lpParameter)
{
    // ......

    int type = GetType(filepath, filetype);
    header.append(filetype);
    header.append("\r\nConnection: close\r\n\r\n");
    if (type > 0 && type < 6)
    {
        ifstream in(filepath.c_str());
        if (!in)
            goto fileNotExist;

        if (!SignalSend(remoteSocket, header.c_str(), header.size(), stop))
            goto finish;
        event->OnNewLogs(addr, header.c_str(), true);
        string data;
        while (getline(in, data))
        {
            if (data.size() == 0) continue;
            if (!SignalSend(remoteSocket, data.c_str(), data.size(), stop))
                goto finish;
        }
    }
    else
    {
        ifstream in(filepath.c_str(), ios::binary);
        if (!in)
            goto fileNotExist;

        if (!SignalSend(remoteSocket, header.c_str(), header.size(), stop))
            goto finish;
        event->OnNewLogs(addr, header.c_str(), true);
        while (!in.eof())
        {
            char str[1024];
            in.read(str, 1024);
            if (!SignalSend(remoteSocket, str, sizeof(char) * in.gcount(), stop))
                goto finish;
        }
    }
    goto finish;

fileNotExist:
    const char *resp = "HTTP/1.1 404 Not Found\r\n\r\n";
    SignalSend(remoteSocket, resp, strlen(resp), stop);
    event->OnNewLogs(addr, resp, true);

    // ......
}

接下来就是响应客户端请求,发送指定的文件给客户端了。这里通过GetType方法简单的判断请求文件的类型,然后进行读取并发送给客户端,这里需要注意文本文件和二进制文件的读取方法是不同的,我们需要根据文件的格式来确定读取方法。发送操作通过SignalSend来完成,此方法会返回一个布尔值,表示操作是否成功完成。如果请求的文件不存在,则会发送响应报文404 Not Found给客户端,表示请求资源不存在。

DWORD WINAPI SocketUnits::WorkerThread(LPVOID lpParameter)
{
    // ......

finish:
    closesocket(remoteSocket);
    event->OnSocketFinished(addr);
    delete tp;
    Sleep(800);
    return 0;
}

结束代码,关闭Socket连接,并通知调用者。此处需要注意,要释放tp所占内存,因为这个tp是我们在ListenThread方法中通过new创建的,所以要在不再使用的时候通过delete释放它。

最后,写一下在编写这两个线程函数时学到的东西。

在对Socket进行非阻塞方法调用时,不仅要对某些调用错误进行处理,同时还需要注意调用频率,因为一般我们都是在一个死循环中执行这些方法的,所以如果不进行一些处理,肯定会严重浪费CPU时间片,甚至占用全部CPU时间。我在第一次进行调试的时候,便是因为没有进行线程调度处理,结果程序运行几秒钟之后,整个电脑便彻底无响应了,连鼠标都无法移动,CPU时间被这个死循环耗尽了,最后被迫强制关机,所以之后几次调试我都是开着任务管理器然后再进行调试的,这个也算是Windows系统程序调度机制的优点和缺点了,虽然这里跑的是一个线程,但是这个线程是可以在任意一个CPU核心上被调度。说到这里,其实我也很好奇当时明明这个程序都无法继续运行了,为什么Windows还不提示强行关闭?还有,Windows居然不给一些核心程序(例如任务管理器、CMD等)预留一些时间片……

因此,我们需要主动请求Windows对我们的线程进行调度。一般情况下是通过使用网上所说的select模式,而这里,我则是简单的通过调用Sleep函数来让Windows将线程休眠指定时间,从而让出CPU。

两个帮助方法

最后还有两个在线程函数中被调用的帮助方法:GetType方法和SignalSend方法。

先介绍GetType方法,此方法主要就是判断指定文件的类型和格式,通过文件扩展名进行判断,比如在常用WEB文件中,.html.css.js等文件就是文本格式文件,而像.jpg.png.gif等文件则是二进制文件。此方法同时还返回一个Content-Type字符串,虽然这个属性值也可以从HTTP请求头中获取。

GetType方法主要代码如下:

int SocketUnits::GetType(const wstring &fileName, string &type)
{
    size_t pos = fileName.rfind('.');
    if (pos < 0 || pos >= fileName.size() - 1)
    {
        type = "file/file";
        return 0;
    }
    wstring extName = fileName.substr(pos + 1);
    if (extName.compare(L"html") == 0)
    {
        type = "text/html";
        return 1;
    }
    if (extName.compare(L"css") == 0)
    {
        type = "text/css";
        return 2;
    }
    if (extName.compare(L"js") == 0)
    {
        type = "application/javascript";
        return 3;
    }
    if (extName.compare(L"json") == 0)
    {
        type = "application/json";
        return 4;
    }
    if (extName.compare(L"txt") == 0)
    {
        type = "plain/text";
        return 5;
    }

    string str(extName.size(), ' ');
    copy(extName.begin(), extName.end(), str.begin());
    if (extName.compare(L"jpg") == 0 || extName.compare(L"gif") == 0 ||
        extName.compare(L"png") == 0 || extName.compare(L"jpeg") == 0)
    {
        type.assign("image/");
        type.append(str);
        return 6;
    }
    if (extName.compare(L"mp3") == 0 || extName.compare(L"mp4") == 0)
    {
        type.assign("audio/");
        type.append(str);
        return 7;
    }
    type.assign("file/");
    type.append(str);
    return -1;
}

至于SignalSend方法,则是对非阻塞send方法的封装,实现发送指定长度字符串的功能。当发送期间出现错误、或者接收到结束线程的信号时,此方法会返回false值,否则返回true

SignalSend方法主要代码如下:

bool SocketUnits::SignalSend(SOCKET s, const char *buf, size_t len, bool *stop)
{
    size_t total = len;
    while (true)
    {
        int slen = send(s, buf, (int)total, 0);
        if (slen < 0)
        {
            if (*stop)
                return false;

            Sleep(10);
            int err = WSAGetLastError();
            //if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)
            if (err == WSAEWOULDBLOCK)
                continue;
            else
                return false;
        }
        else if (slen == 0)
            return false;

        if (slen == total)
            break;
        buf += slen;
        total -= len;
    }

    return true;
}

至此,Socket核心类就已经完成了,可以实现基本的WEB服务功能。

UI界面

在完成Socket类之后,就可以开始设计UI界面了,这里使用Qt创建界面,具体内容就和Socket无关了,所以此处也不做详细分析了。

唯一要注意的就是,Qt的UI类是不允许重入的,因此我们只能在主线程、或者说UI线程中去访问,因此要注意回调接口类的实现,保证接口中的方法是在主线程中被调用的。

结束语

经过几天的间断工作,终于完成了这个多线程WEB服务器的实现,虽然功能不甚完整,但是好歹是基本的工作还是能完成,一些简单的网页也可以通过浏览器进行打开(从浏览器中输入监听地址)。

这期间一边学习Windows Sockets,一边又复习了操作系统中的线程相关知识,到结束时,也算是获益匪浅了。

参考

文章目录
  1. 1. 实验要求
  2. 2. 开始
    1. 2.1. 开发环境
  3. 3. Socket核心类分析
    1. 3.1. 公开方法
    2. 3.2. 回调接口
    3. 3.3. 线程函数定义
    4. 3.4. 类型定义
  4. 4. Socket核心类实现
    1. 4.1. 初始化和退出
    2. 4.2. 开始与结束服务
    3. 4.3. 线程函数实现
    4. 4.4. 两个帮助方法
  5. 5. UI界面
  6. 6. 结束语
  7. 7. 参考