完成端口(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

//一个简单的使用例子 //连接远程服务器成功 或 接收到一个远程连接时,本函数将会被ioc.dll回调.在本函数中,应该向客户端列表中添加节点,记得加锁 // //2.s :套接字句柄,标志着一个新的连接 //3.u_addr:对端的IP地址,网络字节序 //4.u_port:对端的端口号,网络字节序 //5.flag :如果是本地连接上了一个远程服务器,flag值为0,或者,是接收到一个远程客户端的连接,这时,flag值为1 //6.返回值:返回一个自定义的值,这个值将在其他回调函数中作为参数传递(注意:就如套接字句柄一样,这个值也最 //好能标志一个连接,比如,客户端列表的某个节点的指针,假设用STL中的MAP来维护客户端列表,那么用KEY作为返回值也不错。) long _stdcall ioc_call_connect(HIOC hIoc,HINT s,long u_addr,long u_port,long flag) { long res=0; printf("socket(%d) connected\n",s); return res; } //断开与远程服务器的连接 或 远程客户端断开,本函数将会被ioc.dll回调.在本函数中,你可以删除客户节点,记得加锁 //s :套接字句柄,标志着哪个连接断开了(在本回调函数返回之前,套接字句柄s不可能被重新利用,所以,用s作为关键字构建客户端列表也是没有问题的) //res:ioc_call_connect回调函数返回的那个值,如果它是客户端列表的某个节点的话,那么可以删除了。 void _stdcall ioc_call_disconnect(HIOC hIoc,HINT s,long res,long flag) { printf("socket(%d) disconnected\n",s); } //当ioc内部一收到数据就会回调本函数,所谓数据有可能是多个数据包,也有可能是一个不完整的数据包 //hIoc,s,flag都不再多做解释 //res :ioc_call_oprate_dat和ioc_call_disconnect一定是被线性回调的,不可能存在同时执行的情况,所以,res如果指向某个节点的话,在本函数中可以不加锁地尽情访问,在本函数返回之前,res不会被释放掉 //hArg:数据调度句柄 //data:数据 //len :数据大小 //返回值:返回剩余未处理完的字节数 long _stdcall ioc_call_oprate_dat(HIOC hIoc,HARG hArg,HINT s,long res,long flag,char *data,long len) { //假如数据包格式是这样: //struct DATAPACK //{ // long size; long cmd; ...... //}; //如果是这样的话,那么典型的处理方法如下: char *p=data; long size_res=len;//收到数据的总字节数 long size_per;//其中某一个数据包的字节数 while(1) { if(size_res<4) return size_res;// size_per=*(long*)(p+0x00);//取数据包的实际长度 if(size_per<0 || size_per>某个最大值) { ::iocCommon_CloseSocket(hIoc,s); return 0; } if(size_res<size_per) return size_res;//剩余数据不够一个完整的数据包,返回 //得到一个完整的数据包,可以就地处理,但如果处理这个数据包将会很耗时,那么为了不阻塞工作线程, //只好将其调度给数据处理线程,这里其实可以定义一个结构,除了将数据包调度出去,还可以附带一些其他信息 if(数据包处理起来比较简单) { //处理 } else//调度给数据处理线程 { char *msg=(char*)::malloc(len); ::memcpy(msg,p,size_per); ::iocCommon_DispatchMessage(hIoc,hArg,msg);//hArg就只有这么一个作用 } p+=size_per; size_res-=size_per; } } //阻塞数据处理线程回调函数 void _stdcall ioc_call_oprate_msg(HIOC hIoc,void *msg) { //处理数据包 ::free(msg);//根据谁分配谁释放的原则,释放msg } int main() { ::iocCommon_Startup(); long addr_con=(long)ioc_call_connect; long addr_dis=(long)ioc_call_disconnect; long addr_dat=(long)ioc_call_oprate_dat; long addr_msg=(long)ioc_call_oprate_msg;//这个参数是可选的,可以不要专门的阻塞数据处理线程 HIOC hIoc=::iocCommon_Create(3072,128,addr_con,0,addr_dis,0,addr_dat,0,addr_msg,0);//创建IOC ::iocCommon_SetOprateThread(hIoc);//增加一个工作线程 ::iocCommon_SetOprateThread(hIoc);//增加一个工作线程 //启动服务器,内部循环调用阻塞的accept函数,ioc不考虑客户端连接服务器有多困难,而只考虑如何高效地进行数据传输 //可以再创建几个线程,多调用几个iocServer_Start,各个iocServer_Start绑定不同端口也可以 ::iocServer_Start(hIoc,NULL,6800); ::iocCommon_Cleanup(); return 0; }
完成端口是一种异步输入/输出(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、付费专栏及课程。

余额充值