Windows Socket 速查筆記

Last Update: 03/10/2004

MFC 7.0 與 Winsock

MFC 為了與以往版本保持相容性的關係,所以它所提供的 CAsyncSocket 與 CSocket 等都是用 Winsock 1.1 來做的,因此若不想用 Winsock 1.1 的話,就必須自己做 socket implementation,或是使用 MFC 7.0 中的新類別: CInternetSession, CHttpConnection, CHttpFile, ... 等。這些新的類別會直接叫用 IE 6.x 的元件 (利用 COM+),對於單純的 HTTP 傳輸而言,這些元件的效能和穩定性都非常值得信賴,而且它也一並為你處理十份惱人的 proxy 與 redirection 的問題。TCP 層次的東西就沒有太大選擇,還是得自己做 (CSocket 與 CAsyncSocket 的效能並不太好),以下是一些 best practices。

做 Server application 時,因為 ATL/MFC 提供了好用的 thread pool,所以大部份是採行 thread-pooling + socket。但這個方法必須去修改兩個 Windows registry:

HKLM\System\CurrentControlSet\Services\Tcpip\Parameters\

TcpNumConnections    DWORD    0xfffffe
MaxUserPort               DWORD    0xfffffe

若需要做更高容量的 server,則 thread-pooling 是行不通的 (Windows 最高大約只可以開 2000 個 thread),此時就必須使用 I/O completion port 才行,詳情可參考 www.codeproject.com 上許多關於這方面的文章,或是 Windows Network Programming 一書。

I/O mode 的選擇

Winsock 提供 blocking, non-blocking, overlapped I/O, completion port 等模式供選擇,Windows Network Programming 一書中指出 overlapped I/O 的效能最好,這對目前的 Windows System 而言是不精確的。若你的程式中有 message loop 的話,則採用 overlapped I/O 可得到不錯的效能,但試驗的結果對 client application 而言 non-blocking 的效能是最佳的 (因為它的 overhead 最少)。

Overlapped I/O 只有在與 Completion port 結合,同時在應付 high-volume 的情形下才會顯現出其威力。

Winsock 的 event based API 除了文件不全用起來難用之外,效能也不彰,不如使用 message based API 或是 BSD-style API。

初始化

Windows socket 是需要初始化的 (歷史包袱,因為 Win95/98/Me 不直接支援 socket),以下的設定可以免除修改 project file 的煩惱。

#include <winsock2.h>
#pragma comment(lib, "ws2_32")

#define WSA_VERSION MAKEWORD(2, 2) // using winsock 2.2

bool initWinsock()
{
    WSADATA WSAData = { 0 };
    if (WSAStartup(WSA_VERSION, &WSAData) != 0)
    {
        // Tell the user that we could not find a usable WinSock DLL.
        if (LOBYTE(WSAData.wVersion) != LOBYTE(WSA_VERSION) ||
            HIBYTE(WSAData.wVersion) != HIBYTE(WSA_VERSION))
            printf("Incorrect winsock version\n");

        WSACleanup();
        return false;
    }
    return true;
}

開 non-blocking socket

int nFd = socket(AF_INET, SOCK_STREAM, 0);
if (nFd == INVALID_SOCKET)
{
    printf("Error creating socket, ec: %d\n", WSAGetLastError());
    return false;
}

unsigned long nNonBlocking = 1;
if (ioctlsocket(nFd, FIONBIO, &nNonBlocking) == SOCKET_ERROR)
{
    printf("Unable to set nonblocking mode, ec:%d\n", WSAGetLastError());
    closesocket(nFd);
    return false;
}

設定 linger

linger 設定影響了 socket 在做 TCP handshaking 時的 behavior,Winsock 的 linger 設定方式與傳統 UNIX 的語意 (semantic) 是不同的,請參考 MSDN。這裡的設定是 graceful shutdown + handshaking timeout 3 seconds。另外這裡列的是 Winsock 2 的語法 (syntax),與 Winsock 1.1 有些出入。

linger oLinger;
oLinger.l_onoff = 1;
oLinger.l_linger = 3; // wait 3 seconds for TCP handshake
if (setsockopt(nFd, SOL_SOCKET, SO_LINGER, (char*)&oLinger, sizeof(oLinger)) == SOCKET_ERROR)
{
    printf("error setsockopt, ec:%d\n", WSAGetLastError());
    return false;
}

Name Resolution

Winsock 最大的缺點就在於它的 asynchronous name resolution 只有 WSAAsyncGetHostByName() 這一系列的可以用,WSAAsync 系的 API 是透過 Window Message 來做 notification,也因此你的程式裡必須要有 message loop 來處理。但是,對於 NT Service program 而言,通常是沒有 message loop 的!也因此這限制了 asynchronous name resolution 的使用。底下是 blocking name resolution 的 best practice:

sockaddr_in oAddr;
hostent* poHost = 0;
memset((void*)&oAddr, 0, sizeof(oAddr));
oAddr.sin_family = AF_INET;
unsigned long uIP = inet_addr(szHost);
if (uIP == INADDR_NONE)
{
    poHost = gethostbyname(szHost);
    if (poHost != 0)
    {
        struct in_addr** pptr = (struct in_addr**)poHost->h_addr_list;
        oAddr.sin_addr = **pptr; // memberwise clone
    }
    else
    {
        printf("Invalid host name %s\n", szHost);
        closesocket(nFd);
        return false;
    }
}
else
{
    oAddr.sin_addr.s_addr = uIP;
}
oAddr.sin_port = htons(nPort);

Bind、Listen 與 Accept

若 listening socket 為 non-blocking socket,則它 accept 進來的也是 non-blocking socket。若 listening socket 為 blocking socket,則它 accept 進來的也是 blocking socket。在 Winsock 裡,你可以將 accept 進來的 blocking socket 透過上面列出的 setsockopt() 的方式改造成 non-blocking socket,但若你想把 non-blocking socket 改為 blocking 則可能會吃 winsock error。

底下的例子是 blocking accept:

sockaddr_in oAddr;

oAddr.sin_family = AF_INET;
oAddr.sin_addr.s_addr = INADDR_ANY;
oAddr.sin_port = htons((u_short)SERVER_PORT);

if (bind(nFd,(sockaddr*)&oAddr, sizeof(oAddr)) != 0)
{
    printf("Error binding socket, ec: %d\n", WSAGetLastError());
    closesocket(nFd);
    return false;
}

if (listen(nFd, MAXCONN) != 0)
{
    printf("Error listening, ec: %d\n", WSAGetLastError());
    closesocket(nFd);
    return false;
}

SOCKET c;
sockaddr_in oAddrFrom;
int nLen = sizeof(oAddrFrom);

while (true)
{
    c = accept(nFd, (struct sockaddr*)&oAddrFrom, &nLen);
    // handle incoming socket
}

底下的例子是 non-blocking accept

sockaddr_in oAddr;

oAddr.sin_family = AF_INET;
oAddr.sin_addr.s_addr = INADDR_ANY;
oAddr.sin_port = htons((u_short)SERVER_PORT);

if (bind(nFd,(sockaddr*)&oAddr, sizeof(oAddr)) != 0)
{
    printf("Error binding socket, ec: %d\n", WSAGetLastError());
    closesocket(nFd);
    return false;
}

if (listen(nFd, MAXCONN) != 0)
{
    printf("Error listening, ec: %d\n", WSAGetLastError());
    closesocket(nFd);
    return false;
}

int nCliFd = 0;
fd_set oRSet;
int nReady, nCliLen, nError;

while (true)
{
    FD_ZERO(&oRSet);
    FD_SET(nFd, &oRSet);
    nReady = select(FD_SETSIZE, &oRSet, NULL, NULL, NULL);

    if (FD_ISSET(nFd, &oRSet))
    {
        nCliLen = sizeof(oCliAddr);
        nCliFd = accept(m_nFd, (struct sockaddr*)&oCliAddr, &nCliLen);

        if (nCliFd < 0)
        {
            nError = WSAGetLastError();
            if (nError == WSAEWOULDBLOCK)
                continue;
            else
            {
                printf("Accept error, ec:%d\n", nError);
                return false;
            }
        }
    }
    ::Sleep(100);
}

Non-Blocking Connect with Timeout

以下是從 UNIX Socket Programming FAQ 改造過來的

// nonblocking connect
struct timeval oTV;
oTV.tv_sec = TIMEOUT / 1000;
oTV.tv_usec = TIMEOUT;
fd_set oRead, oWrite;
FD_ZERO(&oRead);
FD_ZERO(&oWrite);
int nResult, nError;

nResult = connect(nFd, (const sockaddr*)&oAddr, sizeof(oAddr));
if (nResult == SOCKET_ERROR)
{
    if (WSAGetLastError() != WSAEWOULDBLOCK)
    {
        printf("Connection failed, ec:%d\n", WSAGetLastError());
        closesocket(nFd);
        return false;
    }
    else // need select
    {
        FD_SET(nFd, &oRead);
        oWrite = oRead;
        nResult = select(nFd+1, &oRead, &oWrite, 0, &oTV);
        if (nResult == 0)
        {
            printf("Connection timeout\n");
            closesocket(nFd);
            return false;
        }
        if (FD_ISSET(nFd, &oRead) || FD_ISSET(nFd, &oWrite))
        {
            nLen = sizeof(nError);
            if (getsockopt(nFd, SOL_SOCKET, SO_ERROR, (char*)&nError, &nLen) < 0)
            {
                printf("Connect error %d\n", nError);
                closesocket(nFd);
                return false;
            }
        }
        else
        {
            printf("Unknown err in connect\n");
            closesocket(nFd);
            return false;
        }
    }
} // else connected immediately

Non-Blocking Send / Receive

不對稱式 short request / long response 的 best practice

// nonblocking send/recv

FD_CLR(nFd, &oWrite);
nResult = send(nFd, szBuf, strlen(szBuf), 0);
FD_ZERO(&oRead);
FD_SET(nFd, &oRead);
while (true)
{
    nResult = select(nFd+1, &oRead, 0, 0, &oTV);
    if (FD_ISSET(nFd, &oRead))
    {
        memset(pbyBuf, 0, 8192);
        nLen = recv(nFd, pbyBuf, 8192, 0);
        if (nLen == 0) // end of file
        {
            shutdown(nFd, SD_BOTH); // gracefully shutdown required
            closesocket(nFd);
            break;
        }
    }

    if (nResult == 0)
    {
        printf("recv timeout\n");
        closesocket(nFd);
        return false;
    }
    else if (nResult == SOCKET_ERROR)
    {
        printf("select error, ec:%d\n", WSAGetLastError());
        closesocket(nFd);
        return false;
    }
}

對稱式的 best practice

fd_set oRead, oWrite;

FD_ZERO(&oRead);
FD_ZERO(&oWrite);
FD_SET(nFd, &oRead);
FD_SET(nFd, &oWrite);
int nEnd = 0;

while (true)
{
    select(nFd+1, &oRead, &oWrite, 0, &oTV);
    if (FD_ISSET(nFd, &oRead))
    {
       
memset(pbyBuf, 0, 8192);
        nLen = recv(nFd, pbyBuf, 8192, 0);
        if (nLen == 0) // end of file
        {
            shutdown(nFd, SD_RECEIVE); // gracefully shutdown required
            nEnd++;
        }
    }
    else if (FD_ISSET(nFd, &oWrite))
    {
        // cut fraction of buffer
        nLen = send(nFd, pbySndBuf, 8192, 0);
        // if send complete nEnd++ and shutdown(nFd, SD_SEND);
    }

    if (nEnd >= 2) break;
}

使用 MFC CHttpFile 類別去抓取 HTTP Server 上的檔案

底下是個較精簡的片段,詳細的範例請參考 MFC sample 中的 TEAR

CInternetSession session;
CHttpConnection* poCnn = NULL;
CHttpFile* poFile = NULL;

try
{
    FILE* fp;
    fp = fopen(sLocalPath, "wb");
    if (fp == NULL)
        return false;

    poCnn = session.GetHttpConnection(UPDATE_HOST, (INTERNET_PORT)UPDATE_PORT);
    poFile = poCnn->OpenRequest(CHttpConnection::HTTP_VERB_GET, sUriPath);
    poFile->SendRequest();

    DWORD dwRet;
    TCHAR sz[PAGEBUFFER];
    int nRead;
    do
    {
        nRead = poFile->Read(sz, PAGEBUFFER);
        fwrite(sz, sizeof(TCHAR), nRead, fp);
    }
    while (nRead > 0);
    fflush(fp);
    fclose(fp);

    poFile->Close();
    poCnn->Close();
}
catch (CInternetException* pEx)
{
    TCHAR szErr[1024];
    pEx->GetErrorMessage(szErr, 1024);
    pEx->Delete();
}