通用异步 Windows Socket TCP 客户端组件的设计与实现

http://www.cnblogs.com/ldcsaa/archive/2012/02/15/2351756.html

 

 编写 Windows Socket TCP 客户端其实并不困难,Windows 提供了6种 I/O 通信模型供大家选择。但本座看过很多客户端程序都把 Socket 通信和业务逻辑混在一起,剪不断理还乱。每个程序都 Copy / Parse 类似的代码再进行修改,实在有点情何以堪。因此本座利用一些闲暇时光写了一个基于 IOCP 的通用异步 Windows Socket TCP 高性能服务端组件和一个通用异步 Windows Socket TCP 客户端组件供各位看官参详参详,希望能激发下大家的灵感。本篇文章讲述客户端组件。闲话少说,我们现在步入正题。

  • 最重要的第一个问题:如何才能达到通用?

  答:很简单。

    1、限制组件的职能,说白了,通信组件的唯一职责就是接受和发送字节流,绝对不能参与上层协议解析等工作。不在其位不谋其政就是这个意思。

    2、与上层使用者解耦、互不依赖,组件与使用者通过接口方法进行交互,组件实现 ISocketClient 接口为上层提供操作方法;使用者通过 IClientSocketListener 接口把自己注册为组件的 Listener,接收组件通知。因此,任何使用者只要实现了 IClientSocketListener 接口都可以使用组件;另一方面,你甚至可以自己重新写一个实现方式完全不同的组件实现给使用者调用,只要该组件遵从 ISocketClient 接口。这也是 DIP 设计原则的体现(若想了解更多关于设计原则的内容请猛击这里 ^_^)。

 

  • 最重要的第二个问题:可用性如何,也就是说使用起来是否是否方便?

  答:这个问题问得很好,可用性对所有通用组件都是至关重要的,如果太难用还不如自己重头写一个来得方便。因此,ISocketClient 和 IClientSocketListener 接口设计得尽量简单易用(通俗来说就是“傻瓜化”),这两个接口的主要方法均不超过 5 个。

 

  • 最重要的第三个问题:组件的性能如何?

  作为底层的通用组件,性能问题是必须考虑的,绝对不能成为系统的瓶颈。而另一方面,从实际出发,毕竟只是一个客户端组件,它的并发性要求远没有服务端那么高。因此,组件在设计上充分考虑了性能、现实使用情景、可用性和实现复杂性等因素,确保满足性能要求的同时又不会写得太复杂。做出以下两点设计决策:

    1. 在单独线程中实现 Socket 通信交互。这样可以避免与主线程或其他线程相互干扰。
    2. I/O 模型选择 WSAEventSelect。细说一下选择这种 I/O 模型的原因:(各种 I/O 模型的性能比较可以参考:《Windows 网络编程(中文第二版)》第 154 页)
      • 阻塞模型:(不解析,你懂的^_^)
      • 非阻塞模型:(性能太低)
      • WSAAsyncSelect: (两个原因:a、性能太低;b、对于纯 Console 程序还要背负 HWND 实在是伤不起呀!)
      • 重叠 I/O:(有点复杂了)
      • 完成端口:(何必呢?)

 

  唉,理论的东西就先别吹那么多了,直接上代码吧,求你了 !!

  OK!先看看 ISocketClientIClientSocketListener 的接口定义:

复制代码
// 组件操作类型
enum EnSocketOperation
{
    SO_UNKNOWN    = 0,
    SO_ACCEPT    = 1,
    SO_CONNECT    = 2,
    SO_SEND        = 3,
    SO_RECEIVE    = 4,
};

// 组件监听器基接口
class ISocketListener
{
public:
    enum EnHandleResult
    {
        HR_OK        = 0,
        HR_IGNORE    = 1,
        HR_ERROR    = 2,
    };

public:
  // 已发出数据通知
  virtual EnHandleResult OnSend(DWORD dwConnectionID, const BYTE* pData, int iLength) = 0;
  // 已接收数据通知
  virtual EnHandleResult OnReceive(DWORD dwConnectionID, const BYTE* pData, int iLength) = 0;
  // 关闭连接通知
  virtual EnHandleResult OnClose(DWORD dwConnectionID) = 0;
  // 通信错误通知
  virtual EnHandleResult OnError(DWORD dwConnectionID, EnSocketOperation enOperation, int iErrorCode) = 0;

public:
    virtual ~ISocketListener() {}
};

// 服务端组件监听器接口(暂时无视之)
class IServerSocketListener : public ISocketListener
{
public:
    // 接收连接通知
    virtual EnHandleResult OnAccept(DWORD dwConnectionID)    = 0;
    // 服务关闭通知
    virtual EnHandleResult OnServerShutdown()                = 0;
};

// 客户端组件监听器接口
class IClientSocketListener : public ISocketListener
{
public:
  // 连接完成通知
  virtual EnHandleResult OnConnect(DWORD dwConnectionID) = 0;
};

// 服务端组件接口(暂时无视之)
class ISocketServer
{
public:
    enum En_ISS_Error
    {
        ISS_OK                        = 0,
        ISS_SOCKET_CREATE            = 1,
        ISS_SOCKET_BIND                = 2,
        ISS_SOCKET_LISTEN            = 3,
        ISS_CP_CREATE                = 4,
        ISS_WORKER_THREAD_CREATE    = 5,
        ISS_SOCKE_ATTACH_TO_CP        = 6,
        ISS_ACCEPT_THREAD_CREATE    = 7,
    };

public:
    virtual BOOL Start    (LPCTSTR pszBindAddress, USHORT usPort, long lThreadCount)            = 0;
    virtual BOOL Stop    ()                                                                    = 0;
    virtual BOOL Send    (DWORD dwConnID, const BYTE* pBuffer, int iLen)                        = 0;
    virtual BOOL HasStarted                ()                                                    = 0;
    virtual En_ISS_Error GetLastError    ()                                                    = 0;
    virtual LPCTSTR        GetLastErrorDesc()                                                    = 0;
    virtual BOOL GetConnectionAddress(DWORD dwConnID, CString& strAddress, USHORT& usPort)    = 0;


public:
    virtual ~ISocketServer() {}
};

// 服务端组件接口智能指针
typedef auto_ptr<ISocketServer>    ISocketServerPtr;

// 客户端组件接口
class ISocketClient
{
public:
    // 操作结果码
    enum En_ISC_Error
    {
        ISC_OK                        = 0,
        ISC_CLIENT_HAD_STARTED        = 1,
        ISC_CLIENT_NOT_STARTED        = 2,
        ISC_SOCKET_CREATE_FAIL        = 3,
        ISC_CONNECT_SERVER_FAIL        = 4,
        ISC_WORKER_CREATE_FAIL        = 5,
        ISC_NETWORK_ERROR            = 6,
        ISC_PROTOCOL_ERROR            = 7,
    };

public:
  // 启动通信
  virtual BOOL Start (LPCTSTR pszRemoteAddress, USHORT usPort) = 0;
  // 关闭通信
  virtual BOOL Stop () = 0;
  // 发送数据
  virtual BOOL Send (DWORD dwConnID, const BYTE* pBuffer, int iLen) = 0;
  // 是否已启动
  virtual BOOL HasStarted () = 0;
  // 获取错误码
  virtual En_ISC_Error GetLastError () = 0;
  // 获取错误描述
  virtual LPCTSTR GetLastErrorDesc() = 0;

public:
    virtual ~ISocketClient() {}
};

// 客户端组件接口智能指针
typedef auto_ptr<ISocketClient>    ISocketClientPtr;
复制代码

  

  ISocketClient 接口主要有以下三个方法:

  • Start():启动通信
  • Send():发送数据
  • Stop():停止通信

  IClientSocketListener 接口有以下五个通知方法:

  • OnConnect()
  • OnSend()
  • OnReceive()
  • OnClose()
  • OnError() 

   够简单了吧^_^,使用者只需通过三个方法操作组件,然后处理五个组件通知。下面我们再看看组件的具体实现,先看组件类定义:

复制代码
/* 组件实现类 */
class CSocketClient : public ISocketClient
{
// ISocketClient 接口方法
public:
  virtual BOOL Start (LPCTSTR pszRemoteAddress, USHORT usPortt);
  virtual BOOL Stop ();
  virtual BOOL Send (DWORD dwConnID, const BYTE* pBuffer, int iLen);
  virtual BOOL HasStarted () {return m_bStarted;}
  virtual En_ISC_Error GetLastError () {return sm_enLastError;}
  virtual LPCTSTR GetLastErrorDesc();

private:
    BOOL CreateClientSocket();
    BOOL ConnectToServer(LPCTSTR pszRemoteAddress, USHORT usPort);
    BOOL CreateWorkerThread();
    // 网络事件处理方法
    BOOL ProcessNetworkEvent();
    void WaitForWorkerThreadEnd();
    BOOL ReadData();
    BOOL SendData();

    void SetLastError(En_ISC_Error code, LPCTSTR func, int ec);

// 通信线程函数
static 
#ifndef _WIN32_WCE
    UINT
#else
    DWORD
#endif
     WINAPI WorkerThreadProc(LPVOID pv);

private:
    static const int RECEIVE_BUFFER_SIZE    = 8 * 1024;
    static const int WORKER_THREAD_END_TIME    = 3 * 1000;

    static const long    DEFALUT_KEEPALIVE_TIMES        = 3;
    static const long    DEFALUT_KEEPALIVE_INTERVAL    = 10 * 1000;


// 构造函数
public:
  CSocketClient(IClientSocketListener* pListener)
  : m_pListener(pListener) // 设置监听器对象
  , m_soClient(INVALID_SOCKET)
  , m_evSocket(NULL)
  , m_dwConnID(0)
  , m_hWorker(NULL)
  , m_dwWorkerID(0)
  , m_bStarted(FALSE)
#ifdef _WIN32_WCE
  , sm_enLastError(ISC_OK)
#endif
  {
    ASSERT(m_pListener);
  }

    virtual ~CSocketClient()    {if(HasStarted()) Stop();}

private:
  // 这是神马 ???
  CInitSocket m_wsSocket;
    
    SOCKET            m_soClient;
    HANDLE            m_evSocket;
    DWORD            m_dwConnID;

    CCriSec            m_scStop;
    CEvt            m_evStop;
    HANDLE            m_hWorker;

#ifndef _WIN32_WCE
    UINT
#else
    DWORD
#endif
                    m_dwWorkerID;

    CBufferPtr        m_sndBuffer;
    CCriSec            m_scBuffer;
    CEvt            m_evBuffer;

    volatile BOOL    m_bStarted;

private:
  // 监听器对象指针
  IClientSocketListener* m_pListener;

#ifndef _WIN32_WCE
    __declspec(thread) static En_ISC_Error    sm_enLastError;
#else
    volatile En_ISC_Error                    sm_enLastError;
#endif
};
复制代码

 

  从上面的定义可以看出,组件实现类本身并没有提供额外的公共方法,它完全是可以被替换的。组件在构造函数中接收监听器对象,并且保存为其成员属性,因此可以在需要的时候向监听器发送事件通知。

  另外,不知各位看官是否注意到一个奇怪的成员属性:“CInitSocket m_wsSocket; ”,这个属性在其它地方从来都不会用到,那么它是干嘛的呢?在回答这个问题之前,首先想问问大家:Windows Socket 操作的整个操作过程中,第一个以及最后一个被调用的方法是什么?是 socket()、connect()、bind()、还是 closesocket() 吗?都错!答案是 —— ::WSAStartup() 和 ::WSACleanup()。每个程序都要调用一下这两个方法确实是很烦的,又不雅观。 其实,m_wsSocket 的唯一目的就是为了避免手工调用者两个方法,看看它的定义就明白了:

复制代码
class CInitSocket
{
public:
    CInitSocket(LPWSADATA lpWSAData = NULL, BYTE minorVersion = 2, BYTE majorVersion = 2)
    {
        LPWSADATA lpTemp = lpWSAData;
        if(!lpTemp)
            lpTemp    = (LPWSADATA)_alloca(sizeof(WSADATA));

        m_iResult    = ::WSAStartup(MAKEWORD(minorVersion, majorVersion), lpTemp);
    }

    ~CInitSocket()
    {
        if(IsValid())
      ::WSACleanup();
    }

    int        GetResult()    {return m_iResult;}
    BOOL    IsValid()    {return m_iResult == 0;}

private:
    int        m_iResult;
};
复制代码

 

   现在我们看看组件类实现文件中几个重要方法的定义:

复制代码
// 组件事件触发宏定义
#define FireConnect(id)                    m_pListener->OnConnect(id)
#define FireSend(id, data, len)            (m_bStarted ? m_pListener->OnSend(id, data, len)    : ISocketListener::HR_IGNORE)
#define FireReceive(id, data, len)        (m_bStarted ? m_pListener->OnReceive(id, data, len)    : ISocketListener::HR_IGNORE)
#define FireClose(id)                    (m_bStarted ? m_pListener->OnClose(id)                : ISocketListener::HR_IGNORE)
#define FireError(id, op, code)            (m_bStarted ? m_pListener->OnError(id, op, code)    : ISocketListener::HR_IGNORE)

// 启动组件
BOOL CSocketClient::Start(LPCTSTR pszRemoteAddress, USHORT usPort)
{
    BOOL isOK = FALSE;

    if(HasStarted())
    {
        SetLastError(ISC_CLIENT_HAD_STARTED, _T(__FUNCTION__), 0);
        return isOK;
    }

    // 创建 socket
    if(CreateClientSocket())
    {
        // 连接服务器(内部会调用 FireConnect()
        if(ConnectToServer(pszRemoteAddress, usPort))
        {
            // 创建工作线程
            if(CreateWorkerThread())
                isOK = TRUE;
            else
                SetLastError(ISC_WORKER_CREATE_FAIL, _T(__FUNCTION__), 0);
        }
        else
            SetLastError(ISC_CONNECT_SERVER_FAIL, _T(__FUNCTION__), ::WSAGetLastError());
    }
    else
        SetLastError(ISC_SOCKET_CREATE_FAIL, _T(__FUNCTION__), ::WSAGetLastError());

    isOK ? m_bStarted = TRUE : Stop();

    return isOK;
}

// 关闭组件
BOOL CSocketClient::Stop()
{
    {
        CCriSecLock locallock(m_scStop);

        m_bStarted = FALSE;

        if(m_hWorker != NULL)
        {
            // 停止工作线程
            if(::GetCurrentThreadId() != m_dwWorkerID)
                WaitForWorkerThreadEnd();

            ::CloseHandle(m_hWorker);
            m_hWorker        = NULL;
            m_dwWorkerID    = 0;
        }

        if(m_evSocket != NULL)
        {
            // 关闭 WSAEvent
            ::WSACloseEvent(m_evSocket);
            m_evSocket    = NULL;
        }

        if(m_soClient != INVALID_SOCKET)
        {
            // 关闭socket
            shutdown(m_soClient, SD_SEND);
            closesocket(m_soClient);
            m_soClient    = INVALID_SOCKET;
        }

        m_dwConnID = 0;
    }

 // 释放其它资源
    m_sndBuffer.Free();
    m_evBuffer.Reset();
    m_evStop.Reset();

    return TRUE;
}

// 发送数据
BOOL CSocketClient::Send(DWORD dwConnID, const BYTE* pBuffer, int iLen)
{
    ASSERT(iLen > 0);

    if(!HasStarted())
    {
        SetLastError(ISC_CLIENT_NOT_STARTED, _T(__FUNCTION__), 0);
        return FALSE;
    }

    CCriSecLock locallock(m_scBuffer);
    
    // 把数据存入缓冲器
    m_sndBuffer.Cat(pBuffer, iLen);
  // 唤醒工作现场,发送数据
    m_evBuffer.Set();

    return TRUE;
}

// 工作线程函数
#ifndef _WIN32_WCE
    UINT
#else
    DWORD
#endif
    WINAPI CSocketClient::WorkerThreadProc(LPVOID pv)
{
    CSocketClient* pClient = (CSocketClient*)pv;

    TRACE0("---------------> 启动工作线程 <---------------\n");

    HANDLE hEvents[] = {pClient->m_evSocket, pClient->m_evBuffer, pClient->m_evStop};

    while(pClient->HasStarted())
    {
        // 等待 socket 事件、发送数据事件和停止通信事件
        DWORD retval = ::MsgWaitForMultipleObjectsEx(3, hEvents, WSA_INFINITE, QS_ALLINPUT, MWMO_INPUTAVAILABLE);

        if(retval == WSA_WAIT_EVENT_0)
        {
            // 处理网络消息
            if(!pClient->ProcessNetworkEvent())
            {
                if(pClient->HasStarted())
                    pClient->Stop();

                break;
            }
        }
        else if(retval == WSA_WAIT_EVENT_0 + 1)
        {
            // 发送数据(内部调用 FireSend()
            if(!pClient->SendData())
            {
                if(pClient->HasStarted())
                    pClient->Stop();

                break;
            }
        }
        else if(retval == WSA_WAIT_EVENT_0 + 2)
            break;
        else if(retval == WSA_WAIT_EVENT_0 + 3)
            // 消息循环
            ::PeekMessageLoop();
        else
            ASSERT(FALSE);
    }

    TRACE0("---------------> 退出工作线程 <---------------\n");

    return 0;
}

// 处理网络消息
BOOL CSocketClient::ProcessNetworkEvent()
{
    ::WSAResetEvent(m_evSocket);

    WSANETWORKEVENTS events;
    
    int rc = ::WSAEnumNetworkEvents(m_soClient, m_evSocket, &events);
    
    if(rc == SOCKET_ERROR)
    {
        int code = ::WSAGetLastError();
        SetLastError(ISC_NETWORK_ERROR, _T(__FUNCTION__), code);
     FireError(m_dwConnID, SO_UNKNOWN, code);

        return FALSE;
    }

    /* 可读取 */
    if(events.lNetworkEvents & FD_READ)
    {
        int iCode = events.iErrorCode[FD_READ_BIT];

        if(iCode == 0)
            // 读取数据(内部调用 FireReceive()
            return ReadData();
        else
     {
      SetLastError(ISC_NETWORK_ERROR, _T(__FUNCTION__), iCode);
      FireError(m_dwConnID, SO_RECEIVE, iCode);
      return FALSE;
        }
    }

    /* 可发送 */
    if(events.lNetworkEvents & FD_WRITE)
    {
        int iCode = events.iErrorCode[FD_WRITE_BIT];

        if(iCode == 0)
            // 发送数据(内部调用 FireSend()
            return SendData();
        else
        {
            SetLastError(ISC_NETWORK_ERROR, _T(__FUNCTION__), iCode);
        FireError(m_dwConnID, SO_SEND, iCode);
            return FALSE;
        }
    }

    /* socket 已关闭 */
    if(events.lNetworkEvents & FD_CLOSE)
    {
        int iCode = events.iErrorCode[FD_CLOSE_BIT];

        if(iCode == 0)
      FireClose(m_dwConnID);
        else
        {
            SetLastError(ISC_NETWORK_ERROR, _T(__FUNCTION__), iCode);
        FireError(m_dwConnID, SO_UNKNOWN, iCode);
        }

        return FALSE;
    }

    return TRUE;
}
复制代码

  

  从上面的代码可以看出:通信过程中,组件的使用者不需要对通信过程进行任何干预,整个底层通信过程对使用者来说是透明的,使用只需集中精力处理好几个组件通知。下面来看看组件的一个使用示例:

复制代码
/* 组件使用者:实现 IClientSocketListener */
class CMainClient : public IClientSocketListener
{
// 这些方法会操作组件
public:
    bool Login(LPCTSTR pszAddress, USHORT usPort, const T_101_Data* pData);
    bool Logout(const T_201_Data* pData);
        BOOL SendData(EnCommandType enCmdType, const TCommandData* pCmdData, WORD wCmdDataLen);
    long    GetLastError();
    LPCTSTR    GetLastErrorDesc();

// 实现 IClientSocketListener
public:
  virtual EnHandleResult OnConnect(DWORD dwConnectionID);
  virtual EnHandleResult OnSend(DWORD dwConnectionID, const BYTE* pData, int iLength);
  virtual EnHandleResult OnReceive(DWORD dwConnectionID, const BYTE* pData, int iLen);
  virtual EnHandleResult OnClose(DWORD dwConnectionID);
  virtual EnHandleResult OnError(DWORD dwConnectionID, EnSocketOperation enOperation, int iErrorCode);

private:
    BOOL ParseReceiveBuffer();
    
    // 其它方法 。。。

// 构造函数
public:
  CMainClient()
  // 创建组件,并把自己设置为组件的监听器
  : m_pscClient(new CSocketClient(this))
  , m_dwConnID(0)
  {
  }

    virtual ~CMainClient()    {}

private:
  // 组件属性
  ISocketClientPtr m_pscClient;
    DWORD               m_dwConnID;
    
    // 其它属性 。。。
};
复制代码

 

复制代码
BOOL CMainClient::Login(LPCTSTR pszAddress, USHORT usPort, const T_101_Data* pData)
{
    // 启动通信
    return    m_pscClient->Start(pszAddress, usPort) &&
            SendData(CS_C_LOGIN_REQ, pData, sizeof(T_101_Data));
}

BOOL CMainClient::Logout(const T_201_Data* pData)
{
    if(pData)
    {
        SendData(CS_C_SET_STATUS, pData, sizeof(T_201_Data));
        ::WaitWithMessageLoop(LOGOUT_WAIT_TIME);
    }

    // 停止通信
    return m_pscClient->Stop();
}

BOOL CMainClient::SendData(EnCommandType enCmdType, const TCommandData* pCmdData, WORD wCmdDataLen)
{
    const WORD wBufferLen    = CMD_ADDITIVE_SIZE + wCmdDataLen;
    CPrivateHeapByteBuffer buffer(m_hpPrivate, wBufferLen);
    BYTE* pBuffer    = buffer;

    memcpy(pBuffer, &wBufferLen, CMD_LEN_SIZE);
    pBuffer += CMD_LEN_SIZE;
    memcpy(pBuffer, &enCmdType, CMD_TYPE_SIZE);
    pBuffer += CMD_TYPE_SIZE;
    memcpy(pBuffer, pCmdData, wCmdDataLen);
    pBuffer += wCmdDataLen;
    memcpy(pBuffer, &CMD_FLAG, CMD_FLAG_SIZE);

    // 发送数据
    return m_pscClient->Send(m_dwConnID, buffer, wBufferLen);
}

long CMainClient::GetLastError()
{
    // 获取通信错误码
    return m_pscClient->GetLastError();
}

LPCTSTR CMainClient::GetLastErrorDesc()
{
    // 获取通信错误描述
    return m_pscClient->GetLastErrorDesc();
}

/* 处理连接成功事件 */
ISocketListener::EnHandleResult CMainClient::OnConnect(DWORD dwConnectionID)
{
    TRACE1("<CNNID: %d> 已连接\n", dwConnectionID);
    m_dwConnID = dwConnectionID;
    return HR_OK;
}

/* 处理数据已发出事件 */
ISocketListener::EnHandleResult CMainClient::OnSend(DWORD dwConnectionID, const BYTE* pData, int iLength)
{
    TRACE2("<CNNID: %d> 发出数据包 (%d bytes)\n", dwConnectionID, iLength);
    return HR_OK;
}

/* 处理接收到数据事件*/
ISocketListener::EnHandleResult CMainClient::OnReceive(DWORD dwConnectionID, const BYTE* pData, int iLen)
{
    TRACE2("<CNNID: %d> 接收数据包 (%d bytes)\n", dwConnectionID, iLen);

    ASSERT(pData != NULL && iLen > 0);

    // 保存数据
    m_rcBuffer.Cat(pData, iLen);

    // 解析数据
    return ParseReceiveBuffer() ? HR_OK : HR_ERROR;;
}

/* 处理通信关闭事件*/
ISocketListener::EnHandleResult CMainClient::OnClose(DWORD dwConnectionID)
{
    TRACE1("CNNID: %d> 关闭连接\n", dwConnectionID);

    // 清理缓冲区
    m_rcBuffer.Realloc(0);
    return HR_OK;
}

/* 处理通信错误事件 */
ISocketListener::EnHandleResult CMainClient::OnError(DWORD dwConnectionID, EnSocketOperation enOperation, int iErrorCode)
{
    TRACE3("<CNNID: %d> 网络错误 (OP: %d, CO: %d)\n", dwConnectionID, enOperation, iErrorCode);

    // 清理缓冲区
    m_rcBuffer.Realloc(0);

    return HR_OK;
}
复制代码

 

首先要理解基本的原理,2台电脑间实现TCP通讯,首先要建立起连接,在这里要提到服务器端与客户端,两个的区别通俗讲就是主动与被动的关系,两个人对话,肯定是先有人先发起会话,要不然谁都不讲,谈什么话题,呵呵!一样,TCPIP下建立连接首先要有一个服务器,它是被动的,它只能等待别人跟它建立连接,自己不会去主动连接,那客户端如何去连接它呢,这里提到2个东西,IP地址和端口号,通俗来讲就是你去拜访某人,知道了他的地址是一号大街2号楼,这个是IP地址,那么1号楼这么多门牌号怎么区分,嗯!门牌号就是端口(这里提到一点,我们访问网页的时候也是IP地址和端口号,IE默认的端口号是80),一个服务器可以接受多个客户端的连接,但是一个客户端只能连接一台服务器,在连接后,服务器自动划分内存区域以分配各个客户端的通讯,那么,那么多的客户端服务器如何区分,你可能会说,根据IP么,不是很完整,很简单的例子,你一台计算机开3个QQ,服务器怎么区分?所以准确的说是IP和端口号,但是客户端的端口号不是由你自己定的,是由计算机自动分配的,要不然就出现端口冲突了,说的这么多,看下面的这张图就简单明了了。 在上面这张图中,你可以理解为程序A和程序B是2个SOCKET程序,服务器端程序A设置端口为81,已接受到3个客户端的连接,计算机C开了2个程序,分别连接到E和D,而他的端口是计算机自动分配的,连接到E的端口为789,连接到D的为790。 了解了TCPIP通讯的基本结构后,接下来讲解建立的流程,首先声明一下我用的开发环境是Visual Studio2008版的,语言C#,组件System.Net.Sockets,流程的建立包括服务器端的建立和客户端的建立,如图所示: 二、实现: 1.客户端: 第一步,要创建一个客户端对象TcpClient(命名空间在System.Net.Sockets),接着,调用对象下的方法BeginConnect进行尝试连接,入口参数有4个,address(目标IP地址),port(目标端口号),requestCallback(连接成功后的返调函数),state(传递参数,是一个对象,随便什么都行,我建议是将TcpClient自己传递过去),调用完毕这个函数,系统将进行尝试连接服务器。 第二步,在第一步讲过一个入口参数requestCallback(连接成功后的返调函数),比如我们定义一个函数void Connected(IAsyncResult result),在连接服务器成功后,系统会调用此函数,在函数里,我们要获取到系统分配的数据流传输对象(NetworkStream),这个对象是用来处理客户端与服务器端数据传输的,此对象由TcpClient获得,在第一步讲过入口参数state,如果我们传递了TcpClient进去,那么,在函数里我们可以根据入口参数state获得,将其进行强制转换TcpClient tcpclt = (TcpClient)result.AsyncState,接着获取数据流传输对象NetworkStream ns = tcpclt.GetStream(),此对象我建议弄成全局变量,以便于其他函数调用,接着我们将挂起数据接收等待,调用ns下的方法BeginRead,入口参数有5个,buff(数据缓冲),offset(缓冲起始序号),size(缓冲长度),callback(接收到数据后的返调函数),state(传递参数,一样,随便什么都可以,建议将buff传递过去),调用完毕函数后,就可以进行数据接收等待了,在这里因为已经创建了NetworkStream对象,所以也可以进行向服务器发送数据的操作了,调用ns下的方法Write就可以向服务器发送数据了,入口参数3个,buff(数据缓冲),offset(缓冲起始序号),size(缓冲长度)。 第三步,在第二步讲过调用了BeginRead函数时的一个入口参数callback(接收到数据后的返调函数),比如我们定义了一个函数void DataRec(IAsyncResult result),在服务器向客户端发送数据后,系统会调用此函数,在函数里我们要获得数据流(byte数组),在上一步讲解BeginRead函数的时候还有一个入口参数state,如果我们传递了buff进去,那么,在这里我们要强制转换成byte[]类型byte[] data= (byte[])result.AsyncState,转换完毕后,我们还要获取缓冲区的大小int length = ns.EndRead(result),ns为上一步创建的NetworkStream全局对象,接着我们就可以对数据进行处理了,如果获取的length为0表示客户端已经断开连接。 具体实现代码,在这里我建立了一个名称为Test的类: 2.服务器端: 相对于客户端实现,服务器端的实现稍复杂一点,因为前面讲过,一个服务器端可以接受N个客户端的连接,因此,在服务器端,有必要对每个连接上来的客户端进行登记,因此服务器端的程序结构包括了2个程序结构,第一个程序结构主要负责启动服务器、对来访的客户端进行登记和撤销,因此我们需要建立2个类。 第一个程序结构负责服务器的启动与客户端连接的登记,首先建立TcpListener网络侦听类,建立的时候构造函数分别包括localaddr和port2个参数,localaddr指的是本地地址,也就是服务器的IP地址,有人会问为什么它自己不去自动获得本机的地址?关于这个举个很简单的例子,服务器安装了2个网卡,也就有了2个IP地址,那建立服务器的时候就可以选择侦听的使用的是哪个网络端口了,不过一般的电脑只有一个网络端口,你可以懒点直接写个固定的函数直接获取IP地址System.Net.Dns.GetHostAddresses(System.Net.Dns.GetHostName())[0],GetHostAddresses函数就是获取本机的IP地址,默认选择第一个端口于是后面加个[0],第2个参数port是真侦听的端口,这个简单,自己决定,如果出现端口冲突,函数自己会提醒错误的。第二步,启动服务器,TcpListener.Start()。第三步,启动客户端的尝试连接,TcpListener.BeginAcceptTcpClient,入口2个参数,callback(客户端连接上后的返调函数),state(传递参数,跟第二节介绍的一样,随便什么都可以,建立把TcpListener自身传递过去),第四步,建立客户端连接上来后的返调函数,比如我们建立个名为void ClientAccept(IAsyncResult result)的函数,函数里,我们要获取客户端的对象,第三步里讲过我们传递TcpListener参数进去,在这里,我们通过入口参数获取它TcpListener tcplst = (TcpListener)result.AsyncState,获取客户端对象TcpClient bak_tcpclient = tcplst.EndAcceptTcpClient(result),这个bak_tcpclient我建议在类里面建立个列表,然后把它加进去,因为下一个客户端连接上来后此对象就会被冲刷掉了,客户端处理完毕后,接下来我们要启动下一个客户端的连接tcplst.BeginAcceptTcpClient(new AsyncCallback(sub_ClientAccept), tcplst),这个和第三步是一样的,我就不重复了。 第二个程序结构主要负责单个客户端与服务器端的处理程序,主要负责数据的通讯,方法很类似客户端的代码,基本大同,除了不需要启动连接的函数,因此这个程序结构主要启动下数据的侦听的功能、判断断开的功能、数据发送的功能即可,在第一个程序第四步我们获取了客户端的对象bak_tcpclient,在这里,我们首先启动数据侦听功能NetworkStream ns= bak_tcpclient.GetStream();ns.BeginRead(data, 0, 1024, new AsyncCallback(DataRec), data);这个跟我在第二节里介绍的是一模一样的(第二节第10行),还有数据的处理函数,数据发送函数,判断连接已断开的代码与第二节也是一模一样的,不过在这里我们需要额外的添加一段代码,当判断出连接已断开的时候,我们要将客户端告知第一个程序结构进行删除客户端操作,这个方法我的实现方法是在建立第二个程序结构的时候,将第一个程序结构当参数传递进来,判断连接断开后,调用第一个程序结构的公开方法去删除,即从客户端列表下删除此对象。 第一个程序结构我们定义一个TSever的类,第二个程序结构我们一个TClient的类,代码如下:TSever类
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值