完成端口(CompletionPort)之客户端篇

**

完成端口之客户端篇


**
首先说一下这篇文章的初衷。不久前工作中要用到网络通信进行数据交换,既然要通讯当然要有服务器和客户端,于是乎把MFC中的CAsyncSocket搬过来用了,简单的重载几个函数就完成了数据收发,但是后续遇到了较多问题,首先多线程使用时很多时候无法触发OnReceive事件,再加上接收到数据后需要较多的等待,所以整个界面都遭殃了,窗口卡到拖不动····
于是乎决定修炼一下网络编程,首选就是大名鼎鼎的完成端口模型了,于是看到了PiggyXP的一篇讲完成端口的文章,讲的很详细也很易懂,看了之后犹如醍醐灌顶,但是原文中对数据发送部分一笔略过,但是如果看懂这篇博文的话,我想自己加上发送部分也就是几分钟的事,感谢PiggyXP,学习本文时如果对完成端口不了解的话还请大家先看PiggyXP博主关于完成端口的博文,本文用到了PiggyXP博主中的完成端口模型,方便大家参考学习,贴上原文地址:http://blog.csdn.net/piggyxp/article/details/6922277

现在说一说关于客户端的事,一般情况我们的客户端需要连接到服务器,并和服务器发生数据交互(数据发送、数据接收),与服务器不同的是客户端的功能多样化,比如我其中一个客户端只想查询网络时间,另一个客户端只想关心服务器的忙碌状态,如果你将多种多样的功能都集成到一个客户端数据接收部分,无疑是不明智的决定,这样不仅会使得客户端变得很臃肿,而且代码更不好维护。何不让不同的客户端做不同的事,这样分工明确。
网上找了一下关于客户端的内容,但是资料少的可怜,人们大谈阔论的几乎都是服务器端的设计,难道客户端就不重要了吗?
于是乎小弟不才决定搞一搞,既然完成端口可用作服务器程序,为何就不可以用作客户端程序呢?于是乎就有了本文···
接下来就让我们一起把PiggyXP的完成端口模型稍加修改,让其变成一个强大的完成端口模型的客户端,让其拥有更高效的处理能力,并支持多线程数据收发和超多的并发连接。。。
具体思路是这样的
1、建立完成端口,启动工作线程(CIOCPModel完成)
2、客户端发起到某个服务器的连接(CClient发起)
3、客户端的连接申请提交给CIOCPModel,由CIOCPModel完成连接过程,并将该连接绑定到完成端口
4、绑定成功后即可享用完成端口的妙用了,数据到来后会自动调用CClient的OnReceive,数据发送完毕后会调用OnSendComplete,Socket关闭时会调用OnClose;
5、从CIOCPClient派生你自己的子类实现你想要的功能即可
注:CIOCPModel是完成端口模型类
CIOCPClient是与完成端口接口的虚类
CClient是由CIOCPClient派生的子类

一起动手实现吧!
首先为CIOCPModel添加链接到服务器的代码

SOCKET CIOCPModel::conn(CString ip,UINT nPort,CIOCPClient* pClient)
{
    if(!isStart)
        return INVALID_SOCKET;
    struct sockaddr_in ServerAddress;

    PER_SOCKET_CONTEXT* m_pNewContext = new PER_SOCKET_CONTEXT;
    m_pNewContext->m_pClient = pClient;
    m_pNewContext->m_Socket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
    if (INVALID_SOCKET == m_pNewContext->m_Socket) 
    {
        this->_ShowMessage(0,"初始化Socket失败,错误代码: %d.\n", WSAGetLastError());
        delete m_pNewContext;
        return INVALID_SOCKET;
    }
    else
    {
        TRACE("WSASocket() 完成.\n");
    }
    // 填充地址信息
    ZeroMemory((char *)&ServerAddress, sizeof(ServerAddress));
    ServerAddress.sin_family = AF_INET;
    ServerAddress.sin_addr.s_addr = inet_addr(ip);             
    ServerAddress.sin_port = htons(nPort);                          

    if(connect(m_pNewContext->m_Socket,(struct sockaddr*)&ServerAddress,sizeof(sockaddr_in)) == -1)
    {
        this->_ShowMessage(0,"连接到服务器失败!错误代码: %d/n", WSAGetLastError());
        RELEASE_SOCKET( m_pNewContext->m_Socket );
        delete m_pNewContext;
        return INVALID_SOCKET;
    }
    // 将Socket绑定至完成端口中
    if( NULL== CreateIoCompletionPort( (HANDLE)m_pNewContext->m_Socket, m_hIOCompletionPort,(DWORD)m_pNewContext, 0))  
    {  
        this->_ShowMessage(0,"绑定 Socket至完成端口失败!错误代码: %d/n", WSAGetLastError());  
        RELEASE_SOCKET( m_pListenContext->m_Socket );
        delete m_pNewContext;
        return INVALID_SOCKET;
    }
    else
    {
        TRACE("Socket绑定完成端口 完成.\n");
    }
    PER_IO_CONTEXT* pNewIOContext = m_pNewContext->GetNewIoContext();
    pNewIOContext->m_sockAccept = m_pNewContext->m_Socket;

    if(NULL == _PostRecv(pNewIOContext))
    {
        m_pNewContext->RemoveContext(pNewIOContext);
        RELEASE_SOCKET( m_pListenContext->m_Socket );
        delete m_pNewContext;
        return INVALID_SOCKET;
    }
    _AddToContextList(m_pNewContext);
    return m_pNewContext->m_Socket;
}

同时还有关闭连接的disconn()函数

// 断开客户端到服务器的连接
bool CIOCPModel::disconn(SOCKET sock)
{
    if(!isStart)
        return true;

    return PostQueuedCompletionStatus(m_hIOCompletionPort, 0, (DWORD)CLOSE_SOCKET, (LPOVERLAPPED)sock);
}

isStart是CIOCPModel中一个标识完成端口是否启动的变量
关键的部分来了,此功能能得以实现关键就是绑定完成端口时用到的CompletionKey,也就是一个PER_SOCKET_CONTEXT结构体,我在原文基础上增加了一个变量 CIOCPClient* m_pClient; 来保存客户端指针,用于调用客户端的数据处理过程,文章末尾我会加上完整客户端的代码供大家参考。

这样将每个连接成功的客户端和完成端口绑定的时候就自动加入了客户端的信息,当有数据到来时,轻而易举的就可以调用对应客户端的处理过程了。
只需在工作线程中加入

                case RECV_POSTED:
                    {
                        if(pSocketContext->m_pClient != NULL)
                            pSocketContext->m_pClient->OnReceive(pIoContext);
                        pIOCPModel->_DoRecv( pSocketContext,pIoContext );
                    }
                    break;

                    // SEND

                case SEND_POSTED:
                    {
                        if(pSocketContext->m_pClient != NULL)
                            pSocketContext->m_pClient->OnSendComplete(pIoContext);
                        pIOCPModel->_DoSend(pSocketContext,pIoContext);
                    }
                    break;

我稍稍改装了一下CIOCPModel类,加入了发送数据部分,使用WSASend函数,所以在发送完成后会进入case SEND_POSTED:中进行处理;如果不想使用异步发送,也可直接调用send函数进行阻塞发送,那么也就不会进入发送完成的部分了
客户端的虚父类如下:

#pragma once
#include "IOCPModel.h"

class CIOCPModel;

class CIOCPClient
{
public:
    CIOCPClient();
    CIOCPClient(CIOCPModel* pModel);
    ~CIOCPClient(void);

    bool Connect(CString ip,UINT nPort);
    bool Close();
    void SetIOCPModel(CIOCPModel* pModel){m_pIOCPModel = pModel;}


    virtual int  SendMsg(const char* buff,DWORD dwByte,int nFlag = 0);
    virtual void OnReceive(PER_IO_CONTEXT* pContext ) = 0;
    virtual void OnSendComplete(PER_IO_CONTEXT* pContext) = 0;
    virtual void OnClose() = 0;
protected:
    CIOCPModel* m_pIOCPModel;
    SOCKET m_sock;

    bool isConnected;
};

#include "stdafx.h"
#include "IOCPClient.h"


CIOCPClient::CIOCPClient(void)
{
    m_pIOCPModel = NULL;
    m_sock = INVALID_SOCKET;
    isConnected = false;
}

CIOCPClient::CIOCPClient(CIOCPModel* pModel)
{
    m_pIOCPModel = pModel;
    m_sock = INVALID_SOCKET;
    isConnected = false;
}

CIOCPClient::~CIOCPClient(void)
{
    if(isConnected)
        this->Close();
}



bool CIOCPClient::Connect(CString ip,UINT nPort)
{
    if(isConnected)
        return true;

    if(m_pIOCPModel != NULL)
    {
        m_sock = m_pIOCPModel->conn(ip,nPort,this);
        if(m_sock == INVALID_SOCKET)
            return FALSE;
        else
        {
            isConnected = TRUE;
        }
    }
    return isConnected;

}
bool CIOCPClient::Close()
{
    if(isConnected && m_sock != INVALID_SOCKET && m_pIOCPModel)
    {
        if(m_pIOCPModel->disconn(m_sock))
        {
            isConnected = false;
            m_sock = INVALID_SOCKET;
            return true;
        }
        else
        {
            return false;
        }
    }
    else
        return false;
}

int  CIOCPClient::SendMsg(const char* buff,DWORD dwByte,int nFlag)
{
    if(isConnected && m_sock != INVALID_SOCKET && m_pIOCPModel)
    {
        return m_pIOCPModel->SendMsg(m_sock,buff,dwByte,nFlag);
    }
    return -1;
}

以上关键代码已经完成,剩下的就是从CIOCPClient派生自己的子类进行数据处理即可;
实例程序运行界面

下面是完整的客户端实例程序,需要的兄弟可以下载试试,由于本人才疏学浅,难免很多地方理解不到位,或者有错漏的地方,如有高人路过还请指点,共同进步,谢谢!
完整客户端实例代码下载地址:
http://download.csdn.net/detail/ylj135cool/8897737

///////////////////////////////////////////////////////////////// // 初始化Socket bool CIOCPModel::_InitializeListenSocket() { // AcceptEx 和 GetAcceptExSockaddrs 的GUID,用于导出函数指针 GUID GuidAcceptEx = WSAID_ACCEPTEX; GUID GuidGetAcceptExSockAddrs = WSAID_GETACCEPTEXSOCKADDRS; // 服务器地址信息,用于绑定Socket struct sockaddr_in ServerAddress; // 生成用于监听的Socket的信息 m_pListenContext = new PER_SOCKET_CONTEXT; // 需要使用重叠IO,必须得使用WSASocket来建立Socket,才可以支持重叠IO操作 m_pListenContext->m_Socket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED); if (INVALID_SOCKET == m_pListenContext->m_Socket) { this->_ShowMessage("初始化Socket失败,错误代码: %d.\n", WSAGetLastError()); return false; } else { TRACE("WSASocket() 完成.\n"); } // 将Listen Socket绑定至完成端口中 if( NULL== CreateIoCompletionPort( (HANDLE)m_pListenContext->m_Socket, m_hIOCompletionPort,(DWORD)m_pListenContext, 0)) { this->_ShowMessage("绑定 Listen Socket至完成端口失败!错误代码: %d/n", WSAGetLastError()); RELEASE_SOCKET( m_pListenContext->m_Socket ); return false; } else { TRACE("Listen Socket绑定完成端口 完成.\n"); } // 填充地址信息 ZeroMemory((char *)&ServerAddress, sizeof(ServerAddress)); ServerAddress.sin_family = AF_INET; // 这里可以绑定任何可用的IP地址,或者绑定一个指定的IP地址 //ServerAddress.sin_addr.s_addr = htonl(INADDR_ANY); ServerAddress.sin_addr.s_addr = inet_addr(m_strIP.GetString()); ServerAddress.sin_port = htons(m_nPort); // 绑定地址和端口 if (SOCKET_ERROR == bind(m_pListenContext->m_Socket, (struct sockaddr *) &ServerAddress, sizeof(ServerAddress))) { this->_ShowMessage("bind()函数执行错误.\n"); return false; } else { TRACE("bind() 完成.\n"); } // 开始进行监听 if (SOCKET_ERROR == listen(m_pListenContext->m_Socket,SOMAXCONN)) { this->_ShowMessage("Listen()函数执行出现错误.\n"); return false; } else { TRACE("Listen() 完成.\n"); } // 使用AcceptEx函数,因为这个是属于WinSock2规范之外的微软另外提供的扩展函数 // 所以需要额外获取一下函数的指针, // 获取AcceptEx函数指针 DWORD dwBytes = 0; if(SOCKET_ERROR == WSAIoctl
完成端口是一种异步输入/输出(I/O)模型,在网络编程中起到了重要的作用。它是Windows操作系统中提供的一种高效的I/O完成机制。 完成端口的工作方式是通过一个预先创建的I/O完成端口对象来管理I/O操作。在应用程序中,可以创建多个完成端口对象,用于不同的I/O操作。完成端口对象会与一个执行线程相关联,这个线程会在I/O操作完成时被唤醒。当一个I/O操作完成时,操作系统会将完成的消息发送给完成端口对象,并唤醒相应的线程。 使用完成端口的好处是可以实现高效的并发I/O操作。通过使用线程池,可以有效地处理多个客户端请求,并且不会因为等待I/O操作而造成线程的闲置。此外,完成端口还可以用于实现高性能的服务器应用程序,因为它能够轻松地处理大量的并发I/O操作。 完成端口使用步骤如下: 1. 创建完成端口对象,并绑定执行线程。 2. 创建一个I/O请求(例如读取或写入操作)。 3. 将I/O请求与完成端口对象关联。 4. 执行I/O操作,并等待操作完成。 5. 当操作完成时,线程被唤醒,并处理完成的I/O请求。 总之,完成端口是一种强大而高效的I/O完成机制,在网络编程中非常实用。它能够提供高并发的I/O操作能力,使得应用程序能够高效地处理多个客户端请求。通过合理地利用完成端口,在网络编程中可以实现高性能和高效率。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值