MFC Socket
目录
第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函数时,将缓存数据发送出去。
MFC Socket的更多相关文章
- mfc socket编程
socket编程用法---- 随着计算机网络化的深入,计算机网络编程在程序设计的过程中变得日益重要.由于C++语言对底层操作的优越性,许多文章都曾经介绍过用VC++进行Socket编程的方法.但由于都 ...
- MFC socket网络通讯核心代码
服务器: AfxSocketInit();//初始化,必须执行这个函数socket才能正常执行 server.Create(10086); server.Listen(10); while(1) { ...
- MFC/Socket网络编程
转载: https://jingyan.baidu.com/article/676629974557c254d51b84da.html
- 基于MFC的socket编程
网络编程 1.windows 套接字编程(开放的网络编程接口)添加头文件#include<windows.h> 2.套接字及其分类 socket分为两种:(1)数据报socket:无连接套 ...
- CSocket必须使用stream socket不能够使用数据报 socket
如果使用MFC socket类CSoket通讯,必须使用stream socket,不能够使用 SOCK_DGRAM 类型socket.原因如下: 1 stream socket和数据报socket的 ...
- C Socket初探
C Socket初探 前段时间写了个C# Socket初探,这次再写个C语言的Socket博文,运行效果如下: 实现步骤: 1. Server端 #include <stdio.h> // ...
- 解决Winsock2.h和afxsock.h定义冲突的办法
如果我们在工程中使用了afxsock.h,但在其它的地方又加了些 使用winsock2.h,哈哈,VC会告诉你一大堆错误,大意就是有定义重复,该怎么解决? 由于MFC的SOCKET类使用的是Winso ...
- C++软件工程师,你该会什么?
请尊重原创: 转载注明来源 原创在这里哦 C语言广泛用于基础软件.桌面系统.网络通信.音频视频.游戏娱乐等诸多领域.是世界上使用最广泛的编程语言之一.随着物联网技术的发展,C/C++技术在3G网络 ...
- MFC 配合 protobuff libevent 实现的Socket 的GM工具 框架
MFC 配合 protobuff libevent 实现的Socket 的GM工具 框架
随机推荐
- 2016年11月13日 星期日 --出埃及记 Exodus 20:4
2016年11月13日 星期日 --出埃及记 Exodus 20:4 "You shall not make for yourself an idol in the form of anyt ...
- CSS——清除浮动
<div id="main" class="clear"> <div id="left"> </div> ...
- WebForm 发送邮箱
首先在设置发件邮箱的SMTP服务,以新浪邮箱为例:设置区----客户端pop/imap/smtp----"POP3/SMTP服务"和"IMAP4服务/SMTP服务&quo ...
- 万年历---java版
程序难点 : 1. 每年每个月有多少天? 2. 每个月的1号是星期几? 3. 每年的2月份是多少天? 难点解析 : 1. 每年每个月除去1 3 5 7 8 10 12是31天以外, 其他月份(除去2月 ...
- Objective-C协议与非正式协议
http://blog.csdn.net/siemenliu/article/details/7836499
- 获取指定的系统路径 SHGetSpecialFolderPath
1.获取桌面的系统路径 TCHAR szLink[MAX_PATH + ] = { }; SHGetSpecialFolderPath(,szLink,CSIDL_DESKTOPDIRECTORY,) ...
- CodeForces 34B Sale
Sale Time Limit:2000MS Memory Limit:262144KB 64bit IO Format:%I64d & %I64u Submit Status ...
- SQL将金额转换为汉子
-- ============================================= -- Author: 苟安廷 -- Create date: 2008-8-13 -- Descrip ...
- [HDOJ5543]Pick The Sticks(DP,01背包)
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=5543 题意:往长为L的线段上覆盖线段,要求:要么这些线段都在L的线段上,要么有不超过自身长度一半的部分 ...
- [SAP ABAP开发技术总结]局部变量、全局变量
声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...