你好,欢迎来到电脑编程技巧与维护杂志社! 杂志社简介广告服务读者反馈编程社区  
合订本订阅
 
 
您的位置:杂志经典 / 编程语言
非阻塞Winsock编程的问题及解决办法
 

  要:本文分析了非阻塞模式下,用Winsock编写TCP/IP程序的一些问题,并给

        出了一种解决办法。

关键词:非阻塞  Winsock  TCP/IP  CAsyncSocket

 

一、概述

    TCP/IPWinsock编程有两种模式:阻塞及非阻塞。Visual C++ 通过MFC

CAsyncSocket提供对后者的支持。但是在用""方式(TCP)传输数据时,非阻

塞模式会带来一些问题。

    为了把问题描述清楚,先简要介绍一下""方式下用CAsyncSocket编写TCP

程序的步骤:

    客户端:

    (1) CAsyncSocket派生自己的类并构造对象;

    (2) 调用成员函数Create创建SOCKET

    (3) 调用成员函数Connect发起连接;

    (4) 重载虚函数OnConnect,当连接成功时,系统会调用该函数。

    服务器端:

    (1) CAsyncSocket派生自己的类并构造对象;

    (2) 调用成员函数Create创建SOCKET

    (3) 调用成员函数Listen进行"监听"

(4) 重载虚函数OnAccept,当有客户端请求连接时,系统调用此函数,用成

员函数Accept接受请求并建立连接。调用Accept时,要构造一个新的        CAsyncSocket派生类对象作为函数参数,Accept用它创建连接客户端的        SOCKET,原来的对象仍然保持监听状态。

    连接成功后,无论是客户端,还是服务器端,都需要重载虚函数OnSend

OnReceive:当可以发送数据时,系统调用OnSend,这时可以用成员函数Send

送数据; 当有数据接收时,系统会调用OnReceive,可以用Receive函数接收数

据。需要关闭连接时,任意一方调用成员函数Close即可。

    有关TCP/IPCAsyncSocket 编程的详情,请查阅Visual C++的联机帮助。

二、非阻塞模式存在的问题

    现在假设用户想发送一个4096字节的数据块,于是他使用语句Send(pBuf,4096)

实际发送的字节数可能和预期的一样,为4096,但也可能是14095中的任何一

个数。如果是后者,还要用 Send来发送剩下的数据。在最坏的情况下,也许不

得不执行4096Send

    同样,即使对方成功地一次性发送过来4096字节,用Receive(pBuf,4096)接收时,实际接收的字节数可以是14096中的任何一个数,需要反复执行Receive。如果对方连续用Send函数发送少量数据, 比如每次发送10个字节,连续发10次,与刚才的情况正好相反,可能一次Receive 就把这些数据全部接收下来。(在接收数据时,阻塞式编程也存在同样的问题)

三、解决办法

    从用户的角度考虑,最理想的情况是: 建立连接后,想发数据时就可以发送,毋须等待系统调用OnSend,而且用一次发送就能搞定; 接收时,不论数据量多少,只要对方发送一次,这边就接收一次。

    为了解决这些问题,笔者从CAsyncSocket派生出一个子类CTransferSocket_hawk

它通过使用一个队列(用数组实现)来缓存待发送的数据块, 克服了基类发送数据的不便。用户只需把要发送的数据块指针传给新定义的成员函数TransferData,然后就不用管了。该函数把数据复制一份送入缓存队列,并负责发送。(TransferData返回后,用户可以安全地删除指针释放内存)

    CTransferSocket_hawk重载了OnSend函数,当可以发送数据时,调用CAsyncSocket::Send最多发送4096字节,根据函数返回值(即实际发送的字节数)计算下一次发送时的偏移量。 因为正常情况下,系统调用OnSend后,不会再调用它,为了发送剩余的数据,要用CAsyncSocket::AsyncSelect提名FD_WRITE事件,使系统再次调用OnSend。发送完一个数据块时,会用虚函数OnOneDataSent通知用户。如果调用TransferData时,队列中已有数据,将按先后顺序排队发送(如果用户希望确认自己的数据发送完毕,最好等队列空时再调用TransferData,否则系统调用OnOneDataSent时,用户无从知道发送完的究竟是哪一个数据块)

如果发送出错,SOCKET被关闭,并用虚函数OnTransferClose通知用户。

    另外,为了解决CAsyncSocket接收数据的缺陷, CTransferSocket_hawk发送数据时,自动在正文前面添加当前数据块尺寸(以字节为单位,long 型,占4个字节)和起始标记BEGIN_TAG(BEGIN_TAG_LENGTH个字节), 本文中使用字符串begin做起始标记,占5个字节。注意:数据块尺寸不包括这4+BEGIN_TAG_LENGTH个字节的额外开销。

    CTransferSocket_hawk重载了OnReceive函数,当有数据接收时,调用CAsyncSocket::Receive。 开始4个字节被当作即将接收的数据块尺寸,如果紧跟着BEGIN_TAG_LENGTH个字节不是BEGIN_TAG,则关闭SOCKET,并调用虚函数OnTransferClose。然后开始接收数据正文,每次最多接收4096字节,根据函数返回值(即实际接收到的字节数)计算下一次接收时的偏移量。 与OnSend不同,只要数据没收完,系统就会自动调用OnReceive。收满指定的字节数后,调用虚函数OnOneDataReceived,通知用户收到一个数据块。再次收到数据时,重复上述步骤。如果接收时出错,丢弃已收到的数据,关闭SOCKET,并调用虚函数OnTransferClose

四、类CTransferSocket_hawk的用法

1. 成员函数说明:

BOOL Create(UINT nSockPort0,LPCTSTR lpszSocketAddress=NULL);
用于创建SOCKET,功能基本与CAsyncSocket::Create一致,但SOCKET类型总是""类型。函数返回TRUE,表示成功,否则创建 SOCKET失败。参数说明如下:

   nSockPort:绑定端口号,如果是客户端,一般取缺省值0

    lpszSocketAddressIP地址,一般取缺省值NULL

    进一步说明参见CAsyncSocket::Create

    BOOL TransferData(const void *data,long size);

用于传送数据(实际上只是把数据送入队列,等待发送)。返回TRUE,表示数据已送入队列;返回FASLE,表示SOCKET还没有建立。执行完该函数后,用户可以安全地删除指针释放内存,因为数据已经备份下来。参数说明如下:

    pData:要传送的数据块指针。

    size:数据块尺寸。

    int GetQueueCount();

    用于返回队列中待传数据块的数目。

    virtual void Close();

    用于关闭SOCKET,中止数据收发。

    virtual void OnTransferClose(int reason,int nErrorCode=0);

    当连接正常关闭,或者数据传送错误时,调用此虚函数。调用时,  已关闭SOCKET。该函数什么也不做。用户可以重载它,以便出错时得到通知。参数说明如下:

    reason:表明关闭SOCKET的原因,可以为以下值:

    NORMAL    正常关闭;

    SENDERR   Send数据时出错导致关闭;

    RECVERR   Receive数据时出错导致关闭;

   RECVBEGINTAGERR 接收到的"起始"标记不正确导致关闭。

    nErrorCode:当reasonSENDERRRECVERR时,为SOCKET

    出错代码;当reason为其它值时,忽略此参数。

     virtual void OnOneDataSent();

    每发送完一个数据块,会调用此虚函数,该函数什么也不做。用户可以重载它,以便在数据发送完毕时得到通知。

     virtual void OnOneDataReceived(void *pData,long size);

    每接收完一个数据块,会调用此虚函数,该函数什么也不做。用户可以重载它,以便在接收到数据时得到通知。注意:用户重载该函数时,需要备份接收到的数据。因为执行完该函数,接收到的数据将被丢弃。参数说明如下:

                pData:指向接收到的数据。

                size:  接收到的数据尺寸(以字节为单位)

    还有一些类成员,如GetLastErrorGetPeerNameGetSockName Accept

ConnectListenOnAcceptOnConnectm_hSocket等都是直接继承自基类,

说明参见联机帮助中CAsyncSocket的相关部分。

2. 如何在自己的程序中使用CTransferSocket_hawk

    首先把类的头文件及实现文件加到项目(Project)中, 然后用它派生自己的

类,建立连接的步骤与CAsyncSocket基本一致,只不过调用Accept接受客户端连

接请求时,必须使用CTransferSocket_hawk的派生类对象作为函数参数。连接成

功后,双方就可以调用TransferData发送数据。通过重载虚函数OnOneDataSent

OnOneDataReceived,在数据发送/接收完毕后得到通知。

3.注意事项:

    (1) 因为CTransferSocket_hawk覆写了基类CAsyncSocket的一些函数,用户

        在从CTransferSocket_hawk派生自己的类时,最好不要覆写以下函数:

        CreateCloseOnCloseOnSendOnReceive。 如果必须覆写,别忘

        了调用基类版函数;

    (2) 服务器端用于监听的SOCKET不涉及收发数据,可以直接从CAsyncSocket

        派生,但在接受客户端连接请求时,必须使用CTransferSocket_hawk

        派生类对象作为函数Accept的参数。

五、结束语

    本来还可以给CTransferSocket_hawk添加以下两个功能:

    (1) 在数据传输中,每收/发一次数据(即执行一次OnReceive/OnSend),都        调用一个虚函数,方便用户作一些工作,比如显示进度条什么的;

    (2) 如果接收时出错,已收到的数据并不丢弃,还传给用户。

    后来考虑这两个功能跟用户联系比较紧密,在用户程序中实现更合适一些。比如为了用进度条指示传输进度,可以先把数据大小预先送给对方(这里的尺寸是一个独立的数据块,不是数据块的组成部分,类型可由用户自己定义), 然后把数据拆分成若干小块,一块发送完毕后,才发下一块,进度条的更新放在虚函数OnOneDataSent中进行。而接收方则在OnOneDataReceived中更新进度条。

    如果用户希望在传输出错时,保留已收到的数据,上述拆分原始数据的方法也同样适用,这样出错时,只有当前正在接收的那一小块数据被丢弃。

    CTransferSocket_hawk的源代码附在本文末尾,另外,我还编写了一个应用例程,需要的话可以从网上下载。

    URL http://grwy.online.ha.cn/oldsong/example1.zip

    所有代码均在 Win95OSR2VC++ 5.0上调试通过。

参考文献:

    (1) 刘彦明 王鹏,实用网络编程技术,西安电子科技大学出版社,1998

    (2) 蒋东兴等,Windows Sockets 网络程序设计大全,清华大学出版社,1999
  推荐精品文章

·2024年2月目录 
·2024年1月目录
·2023年12月目录
·2023年11月目录
·2023年10月目录
·2023年9月目录 
·2023年8月目录 
·2023年7月目录
·2023年6月目录 
·2023年5月目录
·2023年4月目录 
·2023年3月目录 
·2023年2月目录 
·2023年1月目录 

  联系方式
TEL:010-82561037
Fax: 010-82561614
QQ: 100164630
Mail:gaojian@comprg.com.cn

  友情链接
 
Copyright 2001-2010, www.comprg.com.cn, All Rights Reserved
京ICP备14022230号-1,电话/传真:010-82561037 82561614 ,Mail:gaojian@comprg.com.cn
地址:北京市海淀区远大路20号宝蓝大厦E座704,邮编:100089