1. 程式人生 > >跨平臺(Windows+Linux)的Socket通訊程式(一)—底層封裝

跨平臺(Windows+Linux)的Socket通訊程式(一)—底層封裝

【摘要】編寫Socket通訊程式是一個老話題。本文重點介紹Windows平臺和Linux平臺Socket通訊的不同,採用C++,編制了一個簡單的跨平臺的Socket通訊庫。

一、Socket通訊的基礎知識

Socket通訊是兩個計算機之間最基本的通訊方法,有TCP和UDP兩種協議。關於這兩種協議的區別,不少文章已有詳述,這裡,稍微總結一下:

1.TCP是面向連線的,是“流”式的,意即通訊兩端建立了一個“數碼流管”,該流無頭無尾,接收端保證接收順序,但不保證包的分割。

2.UDP是面向無連線的,是“包”式的,意即通訊兩端自由傳送資料包,接收端不保證接收順序,但保證包的分割與傳送端一致。

正是基於上述二者的不同,在程式設計上,它們的區別如下:對TCP連線,伺服器端過程(bind->listen->accept->send/receive)與客戶端不相同(connect->send/receive),對UDP連線,二者似乎更對等一些(伺服器端僅需要bind)。

二、socket在windows下和linux下的區別

一些文章也已涉及,這裡,也是綜合一下,並加上自己的理解。

專案 Windows Linux
主要標頭檔案 winsock.h/winsock2.h sys/socket.h fcntl.h  errno.h
連結庫 ws2_32.dll/lib 連線是使用引數:-lstdc
執行時需要libstdc++.so.5,可在/usr/lib目錄中建立一個連結。
初始化及退出 初始化需要呼叫WSAStartup,退出需呼叫WSACleanup
關閉Socket closesocket 與檔案操作相同close
Socket型別 SOCKET 與檔案控制代碼相同int
錯誤檢視 WSAGetLastError 全域性變數errno
設定非阻塞模式 int i=1
ioctlsocket(sockethandle,FIONBIO,&i)   
fcntl(ockethandle,F_SETFL, O_NONBLOCK)
send/recv函式最後一個引數 一般設定為0 可以有多種組合:MSG_NOSIGNAL,MSG_DONTWAIT,MSG_WAITALL
send的異常 當連線斷開,還發資料的時候,不僅send()的返回值會有反映,而且還會像系統傳送一個異常訊息,如果不作處理,程式會退 出。為此,send()函式的最後一個引數可以設定MSG_NOSIGNAL,禁止send()函式向系統傳送異常訊息。
WSA巨集 除了可以使用標準的socket函式外,微軟自己有許多以WSA開始的函式,作為對標準socket函式的封裝(可能微軟感覺這些函式更好用一些吧)

三、跨平臺的Socket輔助程式

以下給出原始碼。

sock_wrap.h程式碼如下,其中用到了platform.h,定義_WIN32_PLATFROM_和_LINUX_PLATFROM_兩個巨集。

#ifndef _SOCK_WRAP_H_
#define _SOCK_WRAP_H_

#include "platform.h"

#if defined(_WIN32_PLATFROM_)
#include <winsock2.h>
typedef SOCKET HSocket;
#endif

#if defined(_LINUX_PLATFORM_)
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>

typedef int HSocket;
#define SOCKET_ERROR  (-1)
#define INVALID_SOCKET  0
#endif


typedef struct
{
    int block;
    int sendbuffersize;
    int recvbuffersize;
    int lingertimeout;
    int recvtimeout;
    int sendtimeout;
} socketoption_t;

typedef struct
{
   int nbytes;
   int nresult;
} transresult_t;

int InitializeSocketEnvironment();
void FreeSocketEnvironment();
void GetAddressFrom(sockaddr_in *addr, const char *ip, int port);
void GetIpAddress(char *ip, sockaddr_in *addr);
bool IsValidSocketHandle(HSocket handle);
int GetLastSocketError();

HSocket SocketOpen(int tcpudp);
void SocketClose(HSocket &handle);

int SocketBlock(HSocket hs, bool bblock);
int SocketTimeOut(HSocket hs, int recvtimeout, int sendtimeout, int lingertimeout);

int SocketBind(HSocket hs, sockaddr_in *addr);
HSocket SocketAccept(HSocket hs, sockaddr_in *addr);
int SocketListen(HSocket hs, int maxconn);

void SocketSend(HSocket hs, const char *ptr, int nbytes, transresult_t &rt);
void SocketRecv(HSocket hs, char *ptr, int nbytes, transresult_t &rt);
void SocketTryRecv(HSocket hs, char *ptr, int nbytes, int milliseconds, transresult_t &rt);
void SocketTrySend(HSocket hs, const char *ptr, int nbytes, int milliseconds, transresult_t &rt);

void SocketClearRecvBuffer(HSocket hs);

class CSockWrap
{
public:
    CSockWrap(int tcpudp);
    ~CSockWrap();
    void SetAddress(const char *ip, int port);
    void SetAddress(sockaddr_in *addr);
    int SetTimeOut(int recvtimeout, int sendtimeout, int lingertimeout);
    int SetBufferSize(int recvbuffersize, int sendbuffersize);
    int SetBlock(bool bblock);

    HSocket  GetHandle () { return m_hSocket;}
    void Reopen(bool bForceClose);
    void Close();
    transresult_t Send(void *ptr, int nbytes);
    transresult_t Recv(void *ptr, int nbytes );
    transresult_t TrySend(void *ptr, int nbytes, int milliseconds);
    transresult_t TryRecv(void *ptr, int nbytes, int  milliseconds );
    void ClearRecvBuffer();

protected:
    HSocket  m_hSocket;
    sockaddr_in m_stAddr;
    int m_tcpudp;
};


#endif 

sock_wrap.cpp程式碼如下,其中引用了lightThread.h和spantime.h,它們的程式碼見“”。
#include "platform.h"

#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include "lightthread.h"
#include "sock_wrap.h"
#include "TimeSpan.h"

#define INVALIDSOCKHANDLE   INVALID_SOCKET

#if defined(_WIN32_PLATFROM_)
#include <windows.h>
#define ISSOCKHANDLE(x)  (x!=INVALID_SOCKET)
#define BLOCKREADWRITE      0
#define NONBLOCKREADWRITE   0
#define SENDNOSIGNAL        0
#define ETRYAGAIN(x)     (x==WSAEWOULDBLOCK||x==WSAETIMEDOUT)
#define gxsprintf   sprintf_s

#endif


#if defined(_LINUX_PLATFORM_)
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define ISSOCKHANDLE(x)    (x>0)
#define BLOCKREADWRITE      MSG_WAITALL
#define NONBLOCKREADWRITE   MSG_DONTWAIT
#define SENDNOSIGNAL        MSG_NOSIGNAL
#define ETRYAGAIN(x)        (x==EAGAIN||x==EWOULDBLOCK)
#define gxsprintf           snprintf

#endif


void GetAddressFrom(sockaddr_in *addr, const char *ip, int port)
{
    memset(addr, 0, sizeof(sockaddr_in));
    addr->sin_family = AF_INET;            /*地址型別為AF_INET*/
    if(ip)
    {
        addr->sin_addr.s_addr = inet_addr(ip);
    }
    else
    {
        /*網路地址為INADDR_ANY,這個巨集表示本地的任意IP地址,因為伺服器可能有多個網絡卡,每個網絡卡也可能繫結多個IP地址,
        這樣設定可以在所有的IP地址上監聽,直到與某個客戶端建立了連線時才確定下來到底用哪個IP地址*/
        addr->sin_addr.s_addr = htonl(INADDR_ANY);
    }
    addr->sin_port = htons(port);   /*埠號*/
}
void GetIpAddress(char *ip, sockaddr_in *addr)
{
    unsigned char *p =(unsigned char *)( &(addr->sin_addr));
    gxsprintf(ip, 17, "%u.%u.%u.%u", *p,*(p+1), *(p+2), *(p+3) );
}

int GetLastSocketError()
{
#if defined(_WIN32_PLATFROM_)
    return WSAGetLastError();
#endif

#if defined(_LINUX_PLATFORM_)
    return errno;
#endif
}

bool IsValidSocketHandle(HSocket handle)
{
    return ISSOCKHANDLE(handle);
}

void SocketClose(HSocket &handle)
{
    if(ISSOCKHANDLE(handle))
    {
#if defined(_WIN32_PLATFROM_)
        closesocket(handle);
#endif

#if defined(_LINUX_PLATFORM_)
        close(handle);
#endif
        handle = INVALIDSOCKHANDLE;
    }
}

HSocket SocketOpen(int tcpudp)
{
    int protocol = 0;
    HSocket hs;
#if defined(_WIN32_PLATFROM_)
    if(tcpudp== SOCK_STREAM) protocol=IPPROTO_TCP;
    else if (tcpudp== SOCK_DGRAM) protocol = IPPROTO_UDP;
#endif
    hs = socket(AF_INET, tcpudp, protocol);
    return hs;
}
int SocketBind(HSocket hs, sockaddr_in *paddr)
{
    return bind(hs, (struct sockaddr *)paddr, sizeof(sockaddr_in));
}
int SocketListen(HSocket hs, int maxconn)
{
    return listen(hs,maxconn);
}
HSocket SocketAccept(HSocket hs, sockaddr_in *paddr)
{
#if defined(_WIN32_PLATFROM_)
    int cliaddr_len = sizeof(sockaddr_in);
#endif
#if defined(_LINUX_PLATFORM_)
    socklen_t cliaddr_len = sizeof(sockaddr_in);
#endif
    return accept(hs, (struct sockaddr *)paddr, &cliaddr_len);
}
//
// if timeout occurs, nbytes=-1, nresult=1
// if socket error, nbyte=-1, nresult=-1
// if the other side has disconnected in either block mode or nonblock mode, nbytes=0, nresult=-1
// otherwise nbytes= the count of bytes sent , nresult=0
void SocketSend(HSocket hs, const char *ptr, int nbytes, transresult_t &rt)
{
    rt.nbytes = 0;
    rt.nresult = 0;
    if(!ptr|| nbytes<1) return;

    //Linux: flag can be MSG_DONTWAIT, MSG_WAITALL, 使用MSG_WAITALL的時候, socket 必須是處於阻塞模式下,否則WAITALL不能起作用
    rt.nbytes = send(hs, ptr, nbytes, BLOCKREADWRITE|SENDNOSIGNAL);
    if(rt.nbytes>0)
    {
        rt.nresult = (rt.nbytes == nbytes)?0:1;
    }
    else if(rt.nbytes==0)
    {
       rt.nresult=-1;
    }
    else
    {
        rt.nresult = GetLastSocketError();
        rt.nresult = ETRYAGAIN(rt.nresult)? 1:-1;
    }
}



// if timeout occurs, nbytes=-1, nresult=1
// if socket error, nbyte=-1, nresult=-1
// if the other side has disconnected in either block mode or nonblock mode, nbytes=0, nresult=-1
void SocketRecv(HSocket hs, char *ptr, int nbytes, transresult_t &rt)
{
    rt.nbytes = 0;
    rt.nresult = 0;
    if(!ptr|| nbytes<1) return;

    rt.nbytes = recv(hs, ptr, nbytes, BLOCKREADWRITE);
    if(rt.nbytes>0)
    {
        return;
    }
    else if(rt.nbytes==0)
    {
       rt.nresult=-1;
    }
    else
    {
        rt.nresult = GetLastSocketError();
        rt.nresult = ETRYAGAIN(rt.nresult)? 1:-1;
    }

}
//  nbytes= the count of bytes sent
// if timeout occurs, nresult=1
// if socket error,  nresult=-1,
// if the other side has disconnected in either block mode or nonblock mode, nresult=-2
void SocketTrySend(HSocket hs, const char *ptr, int nbytes, int milliseconds, transresult_t &rt)
{
    rt.nbytes = 0;
    rt.nresult = 0;
    if(!ptr|| nbytes<1) return;


    int n;
    CMyTimeSpan start;
    while(1)
    {
        n = send(hs, ptr+rt.nbytes, nbytes, NONBLOCKREADWRITE|SENDNOSIGNAL);
        if(n>0)
        {
            rt.nbytes += n;
            nbytes -= n;
            if(rt.nbytes >= nbytes) {    rt.nresult = 0;  break; }
        }
        else if( n==0)
        {
            rt.nresult= -2;
            break;
        }
        else
        {
            n = GetLastSocketError();
            if(ETRYAGAIN(n))
            {
                CLightThread::DiscardTimeSlice();
            }
            else
            {
                rt.nresult = -1;
                break;
            }
        }
        if(start.GetSpaninMilliseconds()>milliseconds)  { rt.nresult= 1; break;}
    }
}
// if timeout occurs, nbytes=-1, nresult=1
// if socket error, nbyte=-1, nresult=-1
// if the other side has disconnected in either block mode or nonblock mode, nbytes=0, nresult=-1
void SocketTryRecv(HSocket hs, char *ptr, int nbytes, int milliseconds, transresult_t &rt)
{
    rt.nbytes = 0;
    rt.nresult = 0;
    if(!ptr|| nbytes<1) return;

    if(milliseconds>2)
    {
        CMyTimeSpan start;
        while(1)
        {
            rt.nbytes = recv(hs, ptr, nbytes, NONBLOCKREADWRITE);
            if(rt.nbytes>0)
            {
               break;
            }
            else if(rt.nbytes==0)
            {
                rt.nresult = -1;
                break;
            }
            else
            {
                rt.nresult = GetLastSocketError();
                if( ETRYAGAIN(rt.nresult))
                {
                   if(start.GetSpaninMilliseconds()>milliseconds)  { rt.nresult= 1; break;}
                   CLightThread::DiscardTimeSlice();
                }
                else
                {
                    rt.nresult = -1;
                    break;
                }
            }

        }
    }
    else
    {
        SocketRecv(hs, ptr, nbytes, rt);
    }
}

void SocketClearRecvBuffer(HSocket hs)
{
#if defined(_WIN32_PLATFROM_)
    struct timeval tmOut;
    tmOut.tv_sec = 0;
    tmOut.tv_usec = 0;
    fd_set    fds;
    FD_ZERO(&fds);
    FD_SET(hs, &fds);
    int   nRet = 1;
    char tmp[100];
    int rt;
    while(nRet>0)
    {
        nRet= select(FD_SETSIZE, &fds, NULL, NULL, &tmOut);
        if(nRet>0)
        {
           nRet = recv(hs, tmp, 100,0);
        }
    }
#endif

#if defined(_LINUX_PLATFORM_)
   char tmp[100];
   while(recv(hs, tmp, 100, NONBLOCKREADWRITE)> 0);
#endif
}

int SocketBlock(HSocket hs, bool bblock)
{
    unsigned long mode;
    if( ISSOCKHANDLE(hs))
    {
#if defined(_WIN32_PLATFROM_)
        mode = bblock?0:1;
        return ioctlsocket(hs,FIONBIO,&mode);
#endif

#if defined(_LINUX_PLATFORM_)
        mode = fcntl(hs, F_GETFL, 0);                  //獲取檔案的flags值。
        //設定成阻塞模式      非阻塞模式
        return bblock?fcntl(hs,F_SETFL, mode&~O_NONBLOCK): fcntl(hs, F_SETFL, mode | O_NONBLOCK);
#endif
    }
    return -1;
}

int SocketTimeOut(HSocket hs, int recvtimeout, int sendtimeout, int lingertimeout)   //in milliseconds
{
    int rt=-1;
    if (ISSOCKHANDLE(hs) )
    {
        rt=0;
#if defined(_WIN32_PLATFROM_)
        if(lingertimeout>-1)
        {
            struct linger  lin;
            lin.l_onoff = lingertimeout;
            lin.l_linger = lingertimeout ;
            rt = setsockopt(hs,SOL_SOCKET,SO_DONTLINGER,(const char*)&lin,sizeof(linger)) == 0 ? 0:0x1;
        }
        if(recvtimeout>0 && rt == 0)
        {
            rt = rt | (setsockopt(hs,SOL_SOCKET,SO_RCVTIMEO,(char *)&recvtimeout,sizeof(int))==0?0:0x2);
        }
        if(sendtimeout>0 && rt == 0)
        {
            rt = rt | (setsockopt(hs,SOL_SOCKET, SO_SNDTIMEO, (char *)&sendtimeout,sizeof(int))==0?0:0x4);
        }
#endif

#if defined(_LINUX_PLATFORM_)
   struct timeval timeout;
        if(lingertimeout>-1)
        {
            struct linger  lin;
            lin.l_onoff = lingertimeout>0?1:0;
            lin.l_linger = lingertimeout/1000 ;
            rt = setsockopt(hs,SOL_SOCKET,SO_LINGER,(const char*)&lin,sizeof(linger)) == 0 ? 0:0x1;
        }
        if(recvtimeout>0 && rt == 0)
        {
            timeout.tv_sec = recvtimeout/1000;
            timeout.tv_usec = (recvtimeout % 1000)*1000;
            rt = rt | (setsockopt(hs,SOL_SOCKET,SO_RCVTIMEO,&timeout,sizeof(timeout))==0?0:0x2);
        }
        if(sendtimeout>0 && rt == 0)
        {
            timeout.tv_sec = sendtimeout/1000;
            timeout.tv_usec = (sendtimeout % 1000)*1000;
            rt = rt | (setsockopt(hs,SOL_SOCKET, SO_SNDTIMEO, &timeout,sizeof(timeout))==0?0:0x4);
        }
#endif
    }
    return rt;
}


int InitializeSocketEnvironment()
{
#if defined(_WIN32_PLATFROM_)
    WSADATA  Ws;
    //Init Windows Socket
    if ( WSAStartup(MAKEWORD(2,2), &Ws) != 0 )
    {
        return -1;
    }
#endif
    return 0;
}
void FreeSocketEnvironment()
{
#if defined(_WIN32_PLATFROM_)
    WSACleanup();
#endif
}
//==============================================================================================================
//================================================================================================================
CSockWrap::CSockWrap(int tcpudp)
{
    memset(&m_stAddr, 0, sizeof(sockaddr_in));
    m_tcpudp = tcpudp;
    m_hSocket = INVALIDSOCKHANDLE;
    Reopen(false);
}


CSockWrap::~CSockWrap()
{
    SocketClose(m_hSocket);
}
void CSockWrap::Reopen(bool bForceClose)
{

    if (ISSOCKHANDLE(m_hSocket) && bForceClose) SocketClose(m_hSocket);
    if (!ISSOCKHANDLE(m_hSocket) )
    {
        m_hSocket=SocketOpen(m_tcpudp);
    }

}
void CSockWrap::SetAddress(const char *ip, int port)
{
    GetAddressFrom(&m_stAddr, ip, port);
}
void CSockWrap::SetAddress(sockaddr_in *addr)
{
    memcpy(&m_stAddr, addr, sizeof(sockaddr_in));
}

int CSockWrap::SetTimeOut(int recvtimeout, int sendtimeout, int lingertimeout)   //in milliseconds
{
  return SocketTimeOut(m_hSocket, recvtimeout, sendtimeout, lingertimeout);
}

int CSockWrap::SetBufferSize(int recvbuffersize, int sendbuffersize)   //in bytes
{
    int rt=-1;
    if (ISSOCKHANDLE(m_hSocket) )
    {
#if defined(_WIN32_PLATFROM_)
        if(recvbuffersize>-1)
        {
            rt = setsockopt( m_hSocket, SOL_SOCKET, SO_RCVBUF, ( const char* )&recvbuffersize, sizeof( int ) );
        }
        if(sendbuffersize>-1)
        {
            rt = rt | (setsockopt(m_hSocket,SOL_SOCKET,SO_SNDBUF,(char *)&sendbuffersize,sizeof(int))==0?0:0x2);
        }
#endif
    }
    return rt;
}

int CSockWrap::SetBlock(bool bblock)
{
    return SocketBlock(m_hSocket, bblock);
}
transresult_t CSockWrap::Send(void *ptr, int nbytes)
{
    transresult_t rt;
    SocketSend(m_hSocket, (const char *)ptr, nbytes,rt);
    return rt;
}
transresult_t CSockWrap::Recv(void *ptr, int nbytes )
{
    transresult_t rt;
    SocketRecv(m_hSocket, (char *)ptr, nbytes,rt);
    return rt;
}
transresult_t CSockWrap::TrySend(void *ptr, int nbytes, int milliseconds)
{
    transresult_t rt;
    SocketTrySend(m_hSocket, (const char *)ptr, nbytes,milliseconds, rt);
    return rt;
}
transresult_t CSockWrap::TryRecv(void *ptr, int nbytes, int  milliseconds )
{
    transresult_t rt;
    SocketTryRecv(m_hSocket, (char *)ptr, nbytes,milliseconds, rt);
    return rt;
}

void CSockWrap::ClearRecvBuffer()
{
    SocketClearRecvBuffer(m_hSocket);
}

上面的輔助程式實際上包含了對一些常用的socket函式的封裝和一個類CSockWrap,如果需要自己組建通訊邏輯,可以直接用這些C風格的函式,CSockWrap實際上就是這樣一個應用。傳送和接收函式的返回值有點複雜,是一個結構體transresult_t,本文的意思是,如果發生接收/傳送錯誤,直接從函式的返回值大致判斷下一步的動作。

四、關於Socket通訊過程的一些討論

1.關於send函式。Socket中Send函式的意思是隻要將應用程式的資料傳送到網絡卡中就算成功,將傳送端的網線拔掉與將接收端的網線拔掉,Send函式的返回可能不同,因此它的正常返回不能作為接收方是否收到的判斷條件。如果需要確保對方收到資訊,只能採用應答式,但這樣做可能會降低雙方的通訊效率。一般情況下,Send不會阻塞,除非網絡卡的傳送緩衝區已經滿了(傳送端直接掉線)。

2.關於recv函式。Recv是最常用的阻塞函式,但通常情況下,應設定其為非阻塞(windows將整個Socket連線都設為非阻塞,linux可以有兩種方式),因為,如果傳送方已經掉線,或者還需要幹別的事情,讓Recv阻塞顯然是不合適的。當然,也可以不用Recv,而用非阻塞的Select函式(本文沒有涉及Select函式),其實它們的效果是一樣的。

3.關於從send和recv函式的返回值來初步判斷網路狀態,見SocketSend等函式的註釋。

4.採用UDP通訊時,資料包的內容不宜過大,所以UDP特別適合於命令的傳輸(一次的通訊量小,但可能頻繁)。

5.SocketClearRecvBuffer函式一般用於TCP連線,當接收方發覺由”丟包“時,作為”對齊“資訊包之用。

(想到哪,寫到哪,以後再補充)