MFC Socket


第1章
同步TCP通讯    1

1.1 同步通讯与异步通讯    1

1.2 同步通讯类    1

1.3 同步TCP通讯客户端    4

1.3.1 界面    4

1.3.2 界面类声明    4

1.3.3 界面类构造函数    5

1.3.4 连接服务器    5

1.3.5 写数据    6

1.3.6 读数据    6

1.3.7 断开连接    7

1.4 同步TCP通讯服务端    7

1.4.1 界面    7

1.4.2 界面类声明    8

1.4.3 CSocketTcpListen    8

1.4.4 界面类构造函数    9

1.4.5 开始监听    9

1.4.6 客户端上线    9

1.4.7 写数据    11

1.4.8 读数据    11

1.4.9 客户端下线    12

1.4.10 停止监听    12

第2章
异步TCP通讯    14

2.1 异步通讯类    14

2.2 异步TCP通讯客户端    21

2.2.1 连接服务器    21

2.2.2 写数据    22

2.3 异步TCP通讯服务端    24

第3章
同步UDP通讯    25

3.1 界面    25

3.2 界面类声明    25

3.3 界面类构造函数    26

3.4 创建套接字    26

3.5 写数据    27

3.6 读数据    27

3.7 关闭套接字    28

第4章
异步UDP通讯    29

第1章
同步TCP通讯

本文将说明如何使用MFC的CSocket和CAsyncSocket实现同步、异步TCP/UDP通讯。文中的代码已被上传至git服务器:

https://github.com/hanford77/Exercise

https://git.oschina.net/hanford/Exercise

在目录 Socket 里。

1.1 同步通讯与异步通讯

同步通讯与异步通讯就好比SendMessage与PostMessage。

SendMessage直接把消息提交给窗口过程进行处理。它返回时,消息已经被处理完毕。

PostMessage只是把消息放入消息队列,在系统空闲的时候才会调用窗口过程处理消息。

同步通讯每调用一次读写函数,实际的读写工作都将被完成。因此,它的特点就是效率较低,但是代码逻辑结构较为简单。

异步通讯每调用一次读写函数,实际的读写工作可能并没有完成。因此,它的特点就是效率较高,但是代码逻辑结构较为复杂。

1.2 同步通讯类

同步通讯类CSocketSync派生自CSocket,用于TCP/UDP的同步通讯。代码如下:


class CSocketSync : public CSocket

{

protected:

enum { UDP_MAX_PKG = 65507 }; //UDP 数据包最大字节数

ISocketEventHandler*m_pEventHandler; //套接字事件处理器

public:

CSocketSync(ISocketEventHandler*pEvent = NULL)

{

m_pEventHandler = pEvent;

}

void SetEventHandler(ISocketEventHandler*pEvent)

{

m_pEventHandler = pEvent;

}

public:

//用于发送 UDP 数据

int SendTo(const void* lpBuf, int nBufLen,UINT nHostPort

, LPCTSTR lpszHostAddress = NULL, int nFlags = 0)

{

SOCKADDR_IN sockAddr;

if(CSocketAsync::IpAddress(lpszHostAddress,nHostPort,sockAddr))

{

return SendTo(lpBuf,nBufLen,(SOCKADDR*)&sockAddr

,sizeof(sockAddr),nFlags);

}

return 0;

}

//用于发送 UDP 数据

int SendTo(const void*pData,int nLen,const SOCKADDR*lpSockAddr

,int nSockAddrLen,int nFlags = 0)

{

if(pData && lpSockAddr && nSockAddrLen > 0)

{

if(nLen < 0)

{

nLen = strlen((const char*)pData);

}

if(nLen > 0)

{

int n = 0; //单次发送的字节数

int nSum = 0; //发送的累计字节数

for(;;)

{

n = nLen - nSum; //待发送的字节数

if(n > UDP_MAX_PKG)

{//待发送的字节数不能太大

n = UDP_MAX_PKG;

}

n = CSocket::SendTo(pData,n,lpSockAddr

,nSockAddrLen,nFlags);

if(n > 0)

{

nSum += n; //累计

if(nSum >= nLen)

{//发送完毕

return nLen;

}

}

else if(n == SOCKET_ERROR)

{

return nSum;

}

}

}

}

return 0;

}

protected:

virtual void OnReceive(int nErrorCode)

{//接收到数据时,会触发该事件

if(m_pEventHandler)

{

m_pEventHandler->OnReceive(this,nErrorCode);

}

}

virtual void OnClose(int nErrorCode)

{//连接断了,会触发该事件

if(m_pEventHandler)

{

m_pEventHandler->OnClose(this,nErrorCode);

}

}

};

上述代码中,SendTo函数用于UDP通讯,暂时不用管它。重点是事件处理,如:CSocket接收到OnReceive事件时,会调用virtual void OnReceive(int nErrorCode)函数。后者会调用 m_pEventHandler->OnReceive(this,nErrorCode);让m_pEventHandler来处理这个事件。

m_pEventHandler是一个事件处理器(ISocketEventHandler*),接口ISocketEventHandler的定义如下:


class ISocketEventHandler

{

public:

virtual void OnAccept (CAsyncSocket*pSender,int nErrorCode){}

virtual void OnClose (CAsyncSocket*pSender,int nErrorCode){}

virtual void OnConnect(CAsyncSocket*pSender,int nErrorCode){}

virtual void OnReceive(CAsyncSocket*pSender,int nErrorCode){}

virtual void OnSend (CAsyncSocket*pSender,int nErrorCode){}

};

1.3 同步TCP通讯客户端

1.3.1 界面

TCP客户端界面如下:

图1.1

1.3.2 界面类声明

上面的界面,对应于类CDlgTcpClientSync,其声明代码如下:


class CDlgTcpClientSync : public CDialog , public ISocketEventHandler

{

... ... ...

protected://ISocketEventHandler

virtual void OnReceive(CAsyncSocket*pSender,int nErrorCode);

virtual void OnClose (CAsyncSocket*pSender,int nErrorCode);

protected:

CSocketSync m_Socket;

std::string m_sRecv;

};

上述代码的重点:

1)定义了CSocketSync m_Socket,用于套接字通讯;

2)成员变量std::string m_sRecv用来存储接收到的数据;

3)CDlgTcpClientSync继承了ISocketEventHandler,并重写了OnReceive、OnClose函数。m_Socket的套接字事件,将由OnReceive、OnClose函数来处理。

1.3.3 界面类构造函数

界面类构造函数如下


CDlgTcpClientSync::CDlgTcpClientSync(CWnd* pParent /*=NULL*/)

: CDialog(CDlgTcpClientSync::IDD, pParent)

{

m_Socket.SetEventHandler(this);

}

设置m_Socket的事件处理器为this。其含义为:m_Socket的OnReceive被调用时,就会调用函数CDlgTcpClientSync::OnReceive。

1.3.4 连接服务器

单击图1.1的"连接"按钮,将连接服务器。示例代码如下:


CString sIP = _T("127.0.0.1"); //服务器 IP

int nPort = 2001; //服务器端口

if(m_Socket.Socket() //创建套接字成功

&& m_Socket.Connect(sIP,nPort) //连接服务器成功

)

{//连接成功

... ... ...

}

else

{//连接失败

m_Socket.Close(); //关闭套接字

... ... ...

}

重点在于:

1)调用m_Socket.Socket创建套接字;

2)调用m_Socket.Connect连接服务器。

1.3.5 写数据

单击图1.1的"发送"按钮,将发送数据给服务器。示例代码如下:


std::string s(1024,,‘1‘);

m_Socket.Send(s.c_str(),s.length());

重点就是调用CSocket::Send函数发送数据。上面的代码将发送1024字节的‘1‘给服务器。因为是同步的,所以数据未发送完毕之前,这个函数是不会返回的。

1.3.6 读数据

服务器发送数据过来时,CSocket::OnReceive会被调用。m_Socket所属的类CSocketSync重写了OnReceive函数,因此CSocketSync::OnReceive会被调用。代码m_pEventHandler->OnReceive(this,nErrorCode);会被调用。这里的m_pEventHandler是一个CDlgTcpClientSync*,后者重写了ISocketEventHandler::OnReceive函数,因此:程序一旦接收到服务器发来的数据,函数CDlgTcpClientSync::OnReceive将被调用。其代码如下:


char buf[1024];

int nRead = 0;

while((nRead = m_Socket.CAsyncSocket::Receive(buf,sizeof(buf))) > 0)

{//异步读取。读取到的数据加入变量m_sRecv

m_sRecv += std::string((const char*)buf,nRead);

}

重点在于:调用CAsyncSocket::Receive函数读取发送过来的数据。注意:CAsyncSocket::Receive是异步读取的,它仅仅从套接字输入缓冲区内读取数据。如果输入缓冲区为空,它就直接返回-1。

如果把m_Socket.CAsyncSocket::Receive中的".CAsyncSocket"删除,那么调用的就是CSocket::Receive函数。CSocket::Receive是同步读取的,亦即在未读取到预定字节数(上面代码中的sizeof(buf))之前,它是不会返回的。结果就是整个程序会阻塞在这一行,将处于假死状态。

1.3.7 断开连接

单击图1.1的"断开"按钮,将执行如下代码:


m_Socket.ShutDown(CAsyncSocket::both); //禁止TCP连接收、发数据

m_Socket.Close(); //关闭套接字

如果是服务器断开了此TCP连接,则CDlgTcpClientSync::OnClose会被调用。代码如下:


void CDlgTcpClientSync::OnClose(CAsyncSocket*pSender,int nErrorCode)

{

m_Socket.ShutDown(CAsyncSocket::both);

m_Socket.Close();

}

1.4 同步TCP通讯服务端

1.4.1 界面

TCP服务端界面如下:

图1.2

1.4.2 界面类声明

上面的界面,对应于类CDlgTcpServerSync,其声明代码如下:


class CDlgTcpServerSync : public CDialog , public ISocketEventHandler

{

... ... ...

protected://ISocketEventHandler

virtual void OnAccept (CAsyncSocket*pSender,int nErrorCode);

virtual void OnReceive(CAsyncSocket*pSender,int nErrorCode);

virtual void OnClose (CAsyncSocket*pSender,int nErrorCode);

protected:

typedef std::map<CSocketSync*,CString> MapClient;

CSocketTcpListen m_SocketListen; //一个监听套接字

MapClient m_mapClient; //一个客户端对应一个通讯套接字

CString m_sRecv; //接收到的数据

};

上述代码的重点:

1)定义了 CSocketTcpListen m_SocketListen,该套接字用于监听客户端发送过来的连接请求;

2)定义了std::map<CSocketSync*,CString> m_mapClient,用来存储多个通讯套接字,每个通讯套接字对应于一个客户端;

3)成员变量m_sRecv用来存储接收到的数据;

4)CDlgTcpServerSync继承了ISocketEventHandler,并重写了Socket事件处理函数。

1.4.3 CSocketTcpListen

CSocketTcpListen继承自CAsyncSocket,用来监听客户端发送过来的连接请求。其代码如下:


class CSocketTcpListen : public CAsyncSocket

{

public:

CSocketTcpListen(ISocketEventHandler*pEvent = NULL)

{

m_pEventHandler = pEvent;

}

void SetEventHandler(ISocketEventHandler*pEvent)

{

m_pEventHandler = pEvent;

}

protected:

virtual void OnAccept(int nErrorCode)

{//客户端发送连接请求时,会触发该事件

if(m_pEventHandler)

{

m_pEventHandler->OnAccept(this,nErrorCode);

}

}

public:

ISocketEventHandler*m_pEventHandler; //套接字事件处理器

};

1.4.4 界面类构造函数

界面类构造函数如下


CDlgTcpServerSync::CDlgTcpServerSync(CWnd* pParent /*=NULL*/)

: CDialog(CDlgTcpServerSync::IDD, pParent)

{

m_SocketListen.SetEventHandler(this);

}

设置m_SocketListen的事件处理器为this。其含义为:m_SocketListen的OnAccept被调用时,就会调用函数CDlgTcpServerSync::OnAccept。

1.4.5 开始监听

单击图1.2的"开始监听"按钮,示例代码如下:


int nPort = 2001; //监听该端口

CSocketTcpListen& s = m_SocketListen;

if(s.Create(nPort) //创建套接字,绑定端口

&& s.Listen()) //开始监听

{//监听成功

EnableControls();

}

else

{//监听失败

s.Close();

}

1.4.6 客户端上线

服务端接收到客户端的连接请求时,函数CSocketTcpListen::OnAccept和CDlgTcpServerSync::OnAccept会被依次调用。后者的代码如下:


void CDlgTcpServerSync::OnAccept(CAsyncSocket*pSender,int nErrorCode)

{

if(0 == nErrorCode)

{

CSocketSync*pClient = new CSocketSync(this);

if(m_SocketListen.Accept(*pClient))

{//接受连接请求成功

m_mapClient[pClient] = GetPeerName(pClient);

}

else

{//接受连接请求失败

delete pClient;

}

}

}

上面代码的重点在于:

1)调用CAsyncSocket::Accept函数,接受客户端的连接请求;

2)将客户端套接字加入到m_mapClient里。m_mapClient的Key是CSocketSync*pClient可用于与客户端通讯;m_mapClient的Value由函数GetPeerName获得,是客户端的IP地址和端口号,如:"127.0.0.1:5000"。函数GetPeerName的代码如下:


inline CString GetPeerName(CAsyncSocket*p)

{

CString s;

if(p)

{

CString sIP;

UINT nPort;

if(p->GetPeerName(sIP,nPort))

{

s.Format(_T("%s:%u"),sIP,nPort);

}

}

return s;

}

3)new CSocketSync(this) 将 Socket 事件处理器设置为 this,也就是CDlgTcpServerSync。因此,当CSocketSync::OnReceive、CSocketSync::OnClose被调用时,CDlgTcpServerSync::OnReceive、CDlgTcpServerSync::OnClose也会被调用。

1.4.7 写数据

单击图1.2的"发送"按钮,将发送数据给客户端。下面的代码将1024个‘1‘发送给所有的客户端:


std::string s(1024,‘1‘);

for(MapClient::iterator it = m_mapClient.begin();

it != m_mapClient.end();++it)

{

it->first->Send(s.c_str(),s.length());

}

重点就是调用CSocket::Send函数发送数据。因为是同步的,所以数据未发送完毕之前,这个函数是不会返回的。

1.4.8 读数据

某个客户端发送数据过来时,CDlgTcpServerSync::OnReceive将被调用。代码如下:


void CDlgTcpServerSync::OnReceive(CAsyncSocket*pSender

,int nErrorCode)

{

if(pSender)

{

CSocketSync*pClient = (CSocketSync*)pSender;

char buf[1024];

int nRead = 0;

std::string s;

while((nRead = pClient->CAsyncSocket::Receive(buf,sizeof(buf))) > 0)

{//异步读取

s += std::string((const char*)buf,nRead);

}

}

}

要点如下:

1)多个客户端是通过OnReceive的第一个参数pSender进行区分的;

2)读取数据时使用的是CAsyncSocket::Receive函数,这个函数是异步读取数据的。

1.4.9 客户端下线

某个客户端下线时,CDlgTcpServerSync::OnClose将被调用。代码如下:


void CDlgTcpServerSync::OnClose(CAsyncSocket*pSender,int nErrorCode)

{

if(pSender)

{

CSocketSync*pClient = (CSocketSync*)pSender;

MapClient::iterator it = m_mapClient.find(pClient);

if(it != m_mapClient.end())

{//从m_mapClient里删除该客户端

m_mapClient.erase(pClient);

}

pClient->ShutDown(2);

pClient->Close();

delete pClient;

}

}

要点如下:

1)从m_mapClient里删除该客户端;

2)销毁该客户端对应的 CSocketSync 对象。

1.4.10 停止监听

单击图1.2的"停止监听"按钮,将执行如下代码:


CSocketSync*pClient = NULL;

for(MapClient::iterator it = m_mapClient.begin();

it != m_mapClient.end();++it)

{//断开所有与客户端的 TCP 连接

if(pClient = it->first)

{

pClient->ShutDown(2);

pClient->Close();

delete pClient;

}

}

m_mapClient.clear();

//停止监听

if(m_SocketListen.m_hSocket != INVALID_SOCKET)

{

m_SocketListen.ShutDown(2);

m_SocketListen.Close();

}

要点如下:

1)遍历m_mapClient,断开所有与客户端的 TCP 连接;

2)关闭监听套接字 m_SocketListen。

第2章
异步TCP通讯

同步通讯较为简单,但是执行效率较低。如:多个TCP客户端都是通过GPRS上网的,网速低得只有十几KB/s。TCP服务器给这些客户端发送数据时,将会等待很长时间,整个程序也可能会处于假死状态。

为了提高通讯效率,可以使用异步通讯。

2.1 异步通讯类

异步通讯类CSocketAsync派生自CAsyncSocket,用于TCP/UDP的异步通讯。代码如下:


class CSocketAsync : public CAsyncSocket

{

protected:

enum { UDP_MAX_PKG = 65507 }; //UDP 数据包最大字节数

class SendToData

{

public:

std::string sAddr;

std::string sData;

};

ISocketEventHandler*m_pEventHandler; //套接字事件处理器

std::string m_sSend; //Send 函数缓存的发送数据

std::list<SendToData>

m_lstSendTo; //SendTo 函数缓存的发送数据

public:

CSocketAsync(ISocketEventHandler*pEvent = NULL)

{

m_pEventHandler = pEvent;

}

void SetEventHandler(ISocketEventHandler*pEvent)

{

m_pEventHandler = pEvent;

}

/***********************************************************

转换 IP 地址格式

szIP [in] 字符串格式的 IP 地址。如:192.168.1.200

nPort [in] 端口,范围 [0,65535]。如:2001

addr [out] 地址

\***********************************************************/

static bool IpAddress(LPCTSTR szIP,UINT nPort

,SOCKADDR_IN&addr)

{

USES_CONVERSION;

memset(&addr,0,sizeof(addr));

char*szIpA = T2A((LPTSTR)szIP);

if(szIpA)

{

addr.sin_addr.s_addr = inet_addr(szIpA);

if(addr.sin_addr.s_addr == INADDR_NONE)

{

LPHOSTENT lphost = gethostbyname(szIpA);

if(lphost)

{

addr.sin_addr.s_addr =

((LPIN_ADDR)lphost->h_addr)->s_addr;

}

else

{

return false;

}

}

}

else

{

addr.sin_addr.s_addr = htonl(INADDR_BROADCAST);

}

addr.sin_family = AF_INET;

addr.sin_port = htons((u_short)nPort);

return true;

}

public:

//用于发送 TCP 数据

virtual int Send(const void*pData,int nLen,int nFlags = 0)

{

if(pData)

{

if(nLen < 0)

{

nLen = strlen((const char*)pData);

}

if(nLen > 0)

{

if(m_sSend.empty())

{//缓存数据 m_sSend 为空,发送数据

int n = 0; //单次发送的字节数

int nSum = 0; //发送的累计字节数

for(;;)

{

n = nLen - nSum;

n = send(m_hSocket,(const char*)pData + nSum,n,0);

if(n > 0)

{

nSum += n;

if(nSum >= nLen)

{

return nLen;

}

}

else if(n == SOCKET_ERROR)

{

if(WSAGetLastError() == WSAEWOULDBLOCK)

{//将数据加入缓存

m_sSend += std::string((const char*)pData + nSum,nLen - nSum);

return nLen;

}

else

{

return nSum;

}

}

}

}

else

{//缓存数据 m_sSend 不为空,直接将数据加入缓存

m_sSend += std::string((const char*)pData,nLen);

return nLen;

}

}

}

return 0;

}

//用于发送 UDP 数据

int SendTo(const void* lpBuf, int nBufLen,UINT nHostPort

, LPCTSTR lpszHostAddress = NULL, int nFlags = 0)

{

SOCKADDR_IN sockAddr;

if(IpAddress(lpszHostAddress,nHostPort,sockAddr))

{

return SendTo(lpBuf,nBufLen,(SOCKADDR*)&sockAddr

,sizeof(sockAddr), nFlags);

}

return 0;

}

//用于发送 UDP 数据

int SendTo(const void*pData,int nLen,const SOCKADDR*lpSockAddr

,int nSockAddrLen,int nFlags = 0)

{

if(pData && lpSockAddr && nSockAddrLen > 0)

{

if(nLen < 0)

{

nLen = strlen((const char*)pData);

}

if(nLen > 0)

{

SendToData data;

if(m_lstSendTo.empty())

{//无缓存数据,发送

int n = 0; //单次发送的字节数

int nSum = 0; //发送的累计字节数

for(;;)

{

n = nLen - nSum; //待发送的字节数

if(n > UDP_MAX_PKG)

{//待发送的字节数不能太大

n = UDP_MAX_PKG;

}

n = CAsyncSocket::SendTo(

(const char*)pData + nSum,n,lpSockAddr,nSockAddrLen,nFlags);

if(n > 0)

{

nSum += n; //累计

if(nSum >= nLen)

{//发送完毕

return nLen;

}

}

else if(n == SOCKET_ERROR)

{

switch(GetLastError())

{

//case WSAEMSGSIZE: //超过 65507 字节

case WSAEWOULDBLOCK://操作被挂起

data.sAddr.assign((const char*)lpSockAddr,nSockAddrLen); //地址

data.sData.assign((const char*)pData + nSum,nLen - nSum); //数据

m_lstSendTo.push_back(data); //缓存地址和数据

return nLen;

default:

return nSum;

}

}

}

}

else

{//有缓存数据,直接缓存

data.sAddr.assign((const char*)lpSockAddr,nSockAddrLen); //地址

data.sData.assign((const char*)pData,nLen); //数据

m_lstSendTo.push_back(data); //缓存地址和数据

return nLen;

}

}

}

return 0;

}

protected:

virtual void OnConnect(int nErrorCode)

{//连接上服务器了

if(m_pEventHandler)

{

m_pEventHandler->OnConnect(this,nErrorCode);

}

}

virtual void OnReceive(int nErrorCode)

{//接收到数据

if(m_pEventHandler)

{

m_pEventHandler->OnReceive(this,nErrorCode);

}

}

virtual void OnSend(int nErrorCode)

{//输出缓冲区空了,可以发送数据了

if(m_sSend.length() > 0)

{//发送缓存数据 m_sSend

int n = 0; //单次发送的字节数

int nSum = 0; //发送的累计字节数

int nTotal = m_sSend.length(); //待发送的总字节数

for(;;)

{

n = nTotal - nSum; //待发送的字节数

n = send(m_hSocket,m_sSend.c_str() + nSum,n,0);

if(n > 0)

{

nSum += n;

if(nSum >= nTotal)

{

break;

}

}

else if(n == SOCKET_ERROR)

{

//WSAGetLastError() == WSAEWOULDBLOCK

break;

}

}

if(nSum > 0)

{

m_sSend = m_sSend.substr(nSum);

}

}

if(!m_lstSendTo.empty())

{//发送缓存数据 m_lstSendTo

for(std::list<SendToData>::iterator it = m_lstSendTo.begin();

it != m_lstSendTo.end();)

{

if(DoSendToData(*it))

{

it = m_lstSendTo.erase(it);

}

else

{

break;

}

}

}

}

virtual void OnClose(int nErrorCode)

{//连接断了

if(m_pEventHandler)

{

m_pEventHandler->OnClose(this,nErrorCode);

}

}

protected:

//发送一包数据

bool DoSendToData(SendToData&data)

{

int nTotal = data.sData.length(); //总字节数

int nSum = 0; //发送字节数的累计值

int n = 0; //单次发送的字节数

for(;;)

{

n = nTotal - nSum;

if(n <= 0)

{

return true; //这一包数据发送完毕了

}

if(n > UDP_MAX_PKG)

{//每次发送的字节数不能过大

n = UDP_MAX_PKG;

}

n = sendto(m_hSocket,data.sData.c_str() + nSum,n,0

,(const struct sockaddr*)data.sAddr.c_str()

,data.sAddr.length());

if(n > 0)

{

nSum += n;

}

else if(n == SOCKET_ERROR)

{

data.sData = data.sData.substr(nSum);

//WSAGetLastError() == WSAEWOULDBLOCK

break;

}

}

return false; //这一包数据没有发送完毕

}

};

上述代码量是同步通讯类CSocketSync的近四倍。具体含义下文进行说明。

2.2 异步TCP通讯客户端

同步TCP通讯客户端使用的是CSocketSync m_Socket;异步TCP通讯客户端使用的是CSocketAsync m_Socket。两者的用法基本相同,不同之处如下:

2.2.1 连接服务器

单击图1.1的"连接"按钮,将连接服务器。示例代码如下:


if(m_Socket.Socket())

{//创建套接字成功

CString sIP = _T("127.0.0.1"); //服务器 IP

int nPort = 2001; //服务器端口

if(m_Socket.Connect(sIP,nPort))

{//连接服务器成功

OnConnect(&m_Socket,0);

}

else if(GetLastError() == WSAEWOULDBLOCK)

{//连接操作被挂起,连接操作完成时会调用 OnConnect 函数

}

else

{

m_Socket.Close();

SetDlgItemText(IDC_TXT_LOCAL,_T("连接失败"));

}

}


void CDlgTcpClientAsync::OnConnect(CAsyncSocket*pSender

,int nErrorCode)

{//连接完成时的回调函数

if(0 == nErrorCode)

{//连接成功

}

else

{//连接失败

}

}

重点在于:

1)m_Socket.Connect(sIP,nPort)返回TRUE,表示连接成功;

2)m_Socket.Connect(sIP,nPort)返回FALSE,GetLastError() == WSAEWOULDBLOCK表示连接操作被挂起。当连接操作完成时,会调用CDlgTcpClientAsync::OnConnect函数,该函数的第二个参数nErrorCode用来说明连接是否成功。

m_Socket.Connect返回时,连接操作可能并没有完成,这就是异步操作。

2.2.2 写数据

异步写数据的代码与同步写数据的代码完全相同,如下所示:


std::string s(1024,,‘1‘);

m_Socket.Send(s.c_str(),s.length());

不过上面的m_Socket.Send调用的是CSocketAsync::Send函数。这个函数有些复杂,其精简代码如下:


virtual int Send(const void*pData,int nLen,int nFlags = 0)

{

int n = 0; //单次发送的字节数

int nSum = 0; //发送的累计字节数

for(;;)

{

n = nLen - nSum;

n = send(m_hSocket,(const char*)pData + nSum,n,0);

if(n > 0)

{

nSum += n;

if(nSum >= nLen)

{

return nLen;

}

}

else if(n == SOCKET_ERROR)

{

if(WSAGetLastError() == WSAEWOULDBLOCK)

{//将数据加入缓存

m_sSend += std::string((const char*)pData + nSum

,nLen - nSum);

return nLen;

}

else

{

return nSum;

}

}

}

}

重点如下:

1)send函数的返回值大于零,表明发送成功了一些数据;

2)send函数的返回值等于SOCKET_ERROR且WSAGetLastError() == WSAEWOULDBLOCK,说明该写操作被挂起了。send函数发送数据的实质是给套接字输出缓冲区内填入数据,当输出缓冲区满了,无法填入数据时就会这种情况,此即为写操作被挂起。那么,何时才能继续发送数据呢?当输出缓冲区为空时,系统会给套接字发送OnSend事件,在这里继续发送数据。代码如下:


virtual void OnSend(int nErrorCode)

{//输出缓冲区空了,可以发送数据了

if(m_sSend.length() > 0)

{//发送缓存数据 m_sSend

int n = 0; //单次发送的字节数

int nSum = 0; //发送的累计字节数

int nTotal = m_sSend.length(); //待发送的总字节数

for(;;)

{

n = nTotal - nSum; //待发送的字节数

n = send(m_hSocket,m_sSend.c_str() + nSum,n,0);

if(n > 0)

{

nSum += n;

if(nSum >= nTotal)

{

break;

}

}

else if(n == SOCKET_ERROR)

{//输出缓冲区又满了,停止发送

//WSAGetLastError() == WSAEWOULDBLOCK

break;

}

}

if(nSum > 0)

{//舍弃掉已经发送的缓存数据

m_sSend = m_sSend.substr(nSum);

}

}

}

上面的代码调用send函数把缓存数据std::string m_sSend发送出去。发送时输出缓冲区再次满的时候就停止发送。等输出缓冲区再次为空时,会再次调用OnSend函数,继续发送缓存数据……

可见:异步写数据需要缓存发送失败的数据,并且在OnSend函数里发送这些缓存数据。

2.3 异步TCP通讯服务端

"异步TCP通讯服务端"与"同步TCP通讯服务端"的不同之处在于m_mapClient的定义:


std::map<CSocketAsync*,CString> m_mapClient; //异步

std::map<CSocketSync*,CString> m_mapClient; //同步

与客户端的通讯,异步使用的是CSocketAsync,同步使用的是CSocketSync。其余代码基本相同。

第3章
同步UDP通讯

3.1 界面

UDP通讯界面如下

图3.1

3.2 界面类声明

上面的界面,对应于类CDlgTcpClientSync,其声明代码如下:


class CDlgUdpSync : public CDialog , public ISocketEventHandler

{

... ... ...

protected://ISocketEventHandler

virtual void OnReceive(CAsyncSocket*pSender,int nErrorCode);

protected:

CSocketSync m_Socket; //套接字

CString m_sRecv; //接收到的数据

};

上述代码的重点:

1)定义了CSocketSync m_Socket,用于套接字通讯;

2)成员变量std::string m_sRecv用来存储接收到的数据;

3)CDlgUdpSync继承了ISocketEventHandler,并重写了OnReceive函数。m_Socket的套接字事件,将由OnReceive函数来处理。

3.3 界面类构造函数

界面类构造函数如下


CDlgUdpSync::CDlgUdpSync(CWnd* pParent /*=NULL*/)

: CDialog(CDlgUdpSync::IDD, pParent)

{

m_Socket.SetEventHandler(this);

}

设置m_Socket的事件处理器为this。其含义为:m_Socket的OnReceive被调用时,就会调用函数CDlgUdpSync::OnReceive。

3.4 创建套接字

单击图3.1中的"打开"按钮,将创建UDP套接字。示例代码如下:


BOOL bOK = FALSE;

if(((CButton*)GetDlgItem(IDC_CHK_BIND))->GetCheck())

{//绑定某个端口

int nPort = 2001;

bOK = m_Socket.Create(nPort,SOCK_DGRAM); //创建套接字并绑定

}

else

{//不绑定

bOK = m_Socket.Socket(SOCK_DGRAM); //创建套接字,不绑定

}

if(bOK)

{//创建套接字成功

}

else

{//创建套接字失败

m_Socket.Close();

}

调用m_Socket.Socket或m_Socket.Create创建套接字,两者的区别在于:前者不绑定端口,后者绑定端口。

3.5 写数据

单击图3.1中的"发送"按钮,即可给"127.0.0.1:2001"发送数据,其代码如下:


std::string s(1024,‘1‘);

CString sIP = _T("127.0.0.1");

UINT nPort = 2001;

m_Socket.SendTo(s.c_str(),s.length(),nPort,sIP);

3.6 读数据

程序一旦接收到对方发来的数据,函数CDlgTcpClientSync::OnReceive将被调用。其代码如下:


void CDlgUdpSync::OnReceive(CAsyncSocket*pSender,int nErrorCode)

{

char buf[64 * 1024];

int nRead = 0;

CString sIP;

UINT nPort = 0;

CString sFrom;

std::map<CString,std::string>

mapRecv;

SOCKADDR_IN sockAddr;

int nAddrLen = sizeof(sockAddr);

memset(&sockAddr,0,sizeof(sockAddr));

while((nRead = recvfrom(m_Socket.m_hSocket,buf,sizeof(buf),0

,(struct sockaddr*)&sockAddr,&nAddrLen)) > 0)

{

nPort = ntohs(sockAddr.sin_port); //对方的端口

sIP = inet_ntoa(sockAddr.sin_addr); //对方的IP

sFrom.Format(_T("%s:%d"),sIP,nPort);

mapRecv[sFrom] += std::string((const char*)buf,nRead);

}

if(!mapRecv.empty())

{

for(std::map<CString,std::string>::iterator it = mapRecv.begin();

it != mapRecv.end();++it)

{

const std::string&s = it->second;

m_sRecv += it->first + _T(" > ")

+ CString(s.c_str(),s.length()) + _T("\r\n");

}

if(m_sRecv.GetLength() > MAXRECV)

{

m_sRecv = m_sRecv.Right(MAXRECV);

}

SetEditText(::GetDlgItem(m_hWnd,IDC_TXT_RECV),m_sRecv);

}

}

重点在于:

1)调用recvfrom函数异步读取UDP数据包;

2)buf要足够大,否则无法读取一包UDP数据。一包UDP数据最多64KB,所以这里的buf也设置为64KB;

3)读取到一包UDP数据后,即可获得对方的IP地址、端口号;

4)UDP数据包可能是由不同的终端发送过来的,其IP地址、端口号不尽相同,因此使用std::map<CString,std::string> mapRecv;来存储读取到的UDP数据;

5)最后一段代码把mapRecv里的数据显示到图3.1中的"接收"文本框内。

3.7 关闭套接字

单击图3.1中的"关闭"按钮,将关闭套接字。代码如下:


m_Socket.Close();

第4章
异步UDP通讯

"异步UDP通讯"与"同步UDP通讯"的写数据代码是相同的,如下:


std::string s(1024,‘1‘);

CString sIP = _T("127.0.0.1");

UINT nPort = 2001;

m_Socket.SendTo(s.c_str(),s.length(),nPort,sIP);

不过上面的m_Socket.SendTo调用的是CSocketAsync::SendTo函数。这个函数与CSocketAsync::Send类似,也会将挂起的写数据缓存起来(std::list<SendToData> m_lstSendTo)。在输出缓冲区为空,系统调用CSocketAsync::OnSend函数时,将缓存数据发送出去。

时间: 2024-10-18 04:00:08

MFC Socket的相关文章

MFC Socket双向通信

记录点滴. 服务端部分程序: 1 //MySocket.h 2 3 // CMySocket 命令目标 4 5 #pragma once 6 7 #define SOCKET_EVENT WM_USER + 11 8 #define WM_MYMESSAGE WM_USER + 12 9 10 // 同意连接 发送 接收 关闭 11 enum { ACCEPT = 0, SEND = 1, RETR = 2, CLOSE = 3 }; 12 13 14 class CMySocket : pub

mfc socket编程

socket编程用法---- 随着计算机网络化的深入,计算机网络编程在程序设计的过程中变得日益重要.由于C++语言对底层操作的优越性,许多文章都曾经介绍过用VC++进行Socket编程的方法.但由于都是直接利用动态连接库wsock32.dll进行操作,实现比较繁琐.其实,VC++的MFC类库中提供了CAsyncSocket这样一个套接字类,用他来实现Socket编程,是非常方便的. ---- 本文将用一个Echo例程来介绍CAsyncSocket类的用法. ---- 一. 客户端 ---- 1.

MFC socket编程(浅出+深度:服务端和客户端端口问题)

要写网络程序就必须用Socket,这是程序员都知道的.而且,面试的时候,我们也会问对方会不会Socket编程?一般来说,很多人都会说,Socket编程基本就是listen,accept以及send,write等几个基本的操作.是的,就跟常见的文件操作一样,只要写过就一定知道. 对于网络编程,我们也言必称TCP/IP,似乎其它网络协议已经不存在了.对于TCP/IP,我们还知道TCP和UDP,前者可以保证数据的正确和可靠性,后者则允许数据丢失.最后,我们还知道,在建立连接前,必须知道对方的IP地址和

MFC socket编程(一)

一.基本概念 a) 同步:指发送方发出数据后,等收到接收方发回的响应,才发下一个数据包的通信方式. nb)异步:指的是发送方不等接收方响应,便接着发下个数据包的通信方式. c) 阻塞:指调用某函数时,直到该函数完成操作,才返回:否则一直阻塞在该调用上. d) 非阻塞:指调用某操作时,不管操作是否成功都立即返回,而不会挂在该操作上. 二.soeket简介 Client/Server (客户机/服务器)模型为最常用的模型.在这种方案中客户应用程序向服务器程序请求服务,一个服务器程序通常用一个众所周知

MFC socket编程(二)

一.大端.小端法定义 1.1小端法(Little-Endian)就是低位字节排放在内存的低地址端即该值的起始地址,高位字节排放在内存的高地址端. (主机字节顺序) 1.2 大端法(Big-Endian)就是高位字节排放在内存的低地址端即该值的起始地址,低位字节排放在内存的高地址端.(网络字节顺序) 举个简单的例子,对于整形0x12345678.它在大端法和小端法的系统内中,分别如图所示的方式存放. 二.字节顺序转换 Windows Sockets 的htons函数将把一个u_short类型的值从

CSocket必须使用stream socket不能够使用数据报 socket

如果使用MFC socket类CSoket通讯,必须使用stream socket,不能够使用 SOCK_DGRAM 类型socket.原因如下: 1 stream socket和数据报socket的区别就是, 前者是可靠传输,数据会被拆成多个数据包发送: (1) 收发两端的发送的数据包顺序要一样. (2)数据包还不能重复. (3)每个数据包没有界限. 后者恰好相反. 2 CSocket通过CArchive来从socket中读写数据.主要原理是,创建一个和CSocket关联的CSocketFil

MFC_网络编程socket套接字

MFC socket编程 MFC socket编程 ---- 一. 客户端 ---- 1. 创建一个Dialog Based项目:CSockClient. ---- 2. 设计对话框 ---- 去掉Ok和Cancle两个按钮,增加ID_Connect(连接).ID_Send(发送).ID_Exit(关闭)按钮,增加ListBox控件IDC_LISTMSG和Edit控件IDC_EDITMSG,并按下表在ClassWizard中为CCSockClientDlg类添加变量. Control ID Ty

在自己的电脑上搭建环境

http://wenku.baidu.com/link?url=dPkWm8Zal1Nce9wiq89xD4nGghAaH4vvdZUpdjF7AnxXl1Xr6siH-peuzsYm0OSSA5Cxp1_CaB4wxBiCCVopx2v7wIjo12b1XCdeKu83ht3 测试环境搭建及举例 http://jingyan.baidu.com/article/676629974557c254d51b84da.html MFC/Socket网络编程:[1]服务器

MFC的socket类

MFC的socket类,部分封装了这些以WSA开头的socket函数,使用更加简单方便,只适合小型的网络通信编程的开发 1.CAsyncSocket类 -部分封装了WSA开头的socket函数,提供了socket通信更加简单的操作,是一个异步socket类 2.CSocket类 -继承自CAsyncSocket类,是一个同步socket类 3.使用MFC的socket类完成一个简单的文件传输的功能