技术 > winsocktapi封装
目录
No. 1
  mfc提供了两个类casyncsocket和csocket来封装winsock api,这给程序员提供了一个更简单的网络编程接口。
  casyncsocket在较低层次上封装了winsock api,缺省情况下,使用该类创建的socket是非阻塞的socket,所有操作都会立即返回,如果没有得到结果,返回wsaewouldblock,表示是一个阻塞操作。
  csocket建立在casyncsocket的基础上,是casyncsocket的派生类。也就是缺省情况下使用该类创建的socket是非阻塞的socket,但是csocket的网络i/o是阻塞的,它在完成任务之后才返回。csocket的阻塞不是建立在“阻塞”socket的基础上,而是在“非阻塞”socket上实现的阻塞操作,在阻塞期间,csocket实现了本线程的消息循环,因此,虽然是阻塞操作,但是并不影响消息循环,即用户仍然可以和程序交互。
  casyncsocket
  casyncsocket封装了低层的winsock api,其成员变量m_hsocket保存其对应的socket句柄。使用casyncsocket的方法如下:
  首先,在堆或者栈中构造一个casyncsocket对象,例如:
  casyncsocket sock;或者
  casyncsocket *psock = new casyncsocket;
  其次,调用create创建socket,例如:
  使用缺省参数创建一个面向连接的socket
  sock.create()
  指定参数参数创建一个使用数据报的socket,本地端口为30
  psocket.create(30, sock_dgrm);
  其三,如果是客户程序,使用connect连接到远地;如果是服务程序,使用listen监听远地的连接请求。
  其四,使用成员函数进行网络i/o。
  最后,销毁casyncsocket,析构函数调用close成员函数关闭socket。
  下面,分析casyncsocket的几个函数,从中可以看到它是如何封装低层的winsock api,简化有关操作的;还可以看到它是如何实现非阻塞的socket和非阻塞操作。
  socket对象的创建和捆绑
  (1)create函数
  首先,讨论create函数,分析socket句柄如何被创建并和casyncsocket对象关联。create的实现如下:
  bool casyncsocket::create(uint nsocketport, int nsockettype,
  long levent, lpctstr lpszsocketaddress)
  {
  if (socket(nsockettype, levent))
  {
  if (bind(nsocketport,lpszsocketaddress))
  return true;
  int nresult = getlasterror();
  close();
  wsasetlasterror(nresult);
  }
  return false;
  }
  其中:
  参数1表示本socket的端口,缺省是0,如果要创建数据报的socket,则必须指定一个端口号。
  参数2表示本socket的类型,缺省是sock_stream,表示面向连接类型。
  参数3是屏蔽位,表示希望对本socket监测的事件,缺省是fd_read | fd_write | fd_oob | fd_accept | fd_connect | fd_close。
  参数4表示本socket的ip地址字符串,缺省是null。
  create调用socket函数创建一个socket,并把它捆绑在this所指对象上,监测指定的网络事件。参数2和3被传递给socket函数,如果希望创建数据报的socket,不要使用缺省参数,指定参数2是sock_dgrm。
  如果上一步骤成功,则调用bind给新的socket分配端口和ip地址。
  (2)socket函数
  接着,分析socket函数,其实现如下:
  bool casyncsocket::socket(int nsockettype, long levent,
  int nprotocoltype, int naddressformat)
  {
  assert(m_hsocket == invalid_socket);
  m_hsocket = socket(naddressformat,nsockettype,nprotocoltype);
  if (m_hsocket != invalid_socket)
  {
  casyncsocket::attachhandle(m_hsocket, this, false);
  return async_select_(levent);
  }
  return false;
  }
  其中:
  参数1表示socket类型,缺省值是sock_stream。
  参数2表示希望监测的网络事件,缺省值同create,指定了全部事件。
  参数3表示使用的协议,缺省是0。实际上,sock_stream类型的socket使用tcp协议,sock_dgrm的socket则使用udp协议。
  参数4表示地址族(地址格式),缺省值是pf_inet(等同于af_inet)。对于tcp/ip来说,协议族和地址族是同值的。
  在socket没有被创建之前,成员变量m_hsocket是一个无效的socket句柄。socket函数把协议族、socket类型、使用的协议等信息传递给winsock api函数socket,创建一个socket。如果创建成功,则把它捆绑在this所指对象。
  (3)捆绑(attatch)
  捆绑过程类似于其他windows对象,将在模块线程状态的winsock映射中添加一对新的映射:this所指对象和新创建的socket对象的映射。
  另外,如果本模块线程状态的“socket窗口”没有创建,则创建一个,该窗口在异步操作时用来接收winsock的通知消息,窗口句柄保存到模块线程状态的m_hsocketwindow变量中。函数async_select_将指定该窗口为网络事件消息的接收窗口。
  函数attachhandle的实现在此不列举了。
  (4)指定要监测的网络事件
  在捆绑完成之后,调用async_select_指定新创建的socket将监测的网络事件。async_select_实现如下:
  bool casyncsocket::async_select_(long levent)
  {
  assert(m_hsocket != invalid_socket);
  _afx_sock_thread_state* pstate = _afxsockthreadstate;
  assert(pstate->m_hsocketwindow != null);
  return wsaasync_select_(m_hsocket, pstate->m_hsocketwindow,
  wm_socket_notify, levent) != socket_error;
  }
  函数参数levent表示希望监视的网络事件。
  _ afxsockthreadstate得到的是当前的模块线程状态,m_ hsocketwindow是本模块在当前线程的“socket窗口”,指定监视m_hsocket的网络事件,如指定事件发生,给窗口m_hsocketwindow发送wm_socket_notify消息。
  被指定的网络事件对应的网络i/o将是异步操作,是非阻塞操作。例如:指定fr_read导致receive是一个异步操作,如果不能立即读到数据,则返回一个错误wsaewouldblock。在数据到达之后,winsock通知窗口m_hsocketwindow,导致onreceive被调用。
  指定fr_write导致send是一个异步操作,即使数据没有送出也返回一个错误wsaewouldblock。在数据可以发送之后,winsock通知窗口m_hsocketwindow,导致onsend被调用。
  指定fr_connect导致connect是一个异步操作,还没有连接上就返回错误信息wsaewouldblock,在连接完成之后,winsock通知窗口m_hsocketwindow,导致onconnect被调用。
  对于其他网络事件,就不一一解释了。
  所以,使用casyncsocket时,如果使用create缺省创建socket,则所有网络i/o都是异步操作,进行有关网络i/o时则必须覆盖以下的相关函数:
  onaccept、onclose、onconnect、onoutofbanddata、onreceive、onsend。
  (5)bind函数
  经过上述过程,socket创建完毕,下面,调用bind函数给m_hsocket指定本地端口和ip地址。bind的实现如下:
  bool casyncsocket::bind(uint nsocketport, lpctstr lpszsocketaddress)
  {
  uses_conversion;
  //使用winsock的地址结构构造地址信息
  sockaddr_in sockaddr;
  memset(&sockaddr,0,sizeof(sockaddr));
  //得到地址参数的值
  lpstr lpszascii = t2a((lptstr)lpszsocketaddress);
  //指定是internet地址类型
  sockaddr.sin_family = af_inet;
  if (lpszascii == null)
  //没有指定地址,则自动得到一个本地ip地址
  //把32比特的数据从主机字节序转换成网络字节序
  sockaddr.sin_addr.s_addr = htonl(inaddr_any);
  else
  {
  //得到地址
  dword lresult = inet_addr(lpszascii);
  if (lresult == inaddr_none)
  {
  wsasetlasterror(wsaeinval);
  return false;
  }
  sockaddr.sin_addr.s_addr = lresult;
  }
  //如果端口为0,则winsock分配一个端口(1024—5000)
  //把16比特的数据从主机字节序转换成网络字节序
  sockaddr.sin_port = htons((u_short)nsocketport);
  //bind调用winsock api函数bind
  return bind((sockaddr*)&sockaddr, sizeof(sockaddr));
  }
  其中:函数参数1指定了端口;参数2指定了一个包含本地地址的字符串,缺省是null。
  函数bind首先使用结构sockaddr_in构造地址信息。该结构的域sin_family表示地址格式(tcp/ip同协议族),赋值为af_inet(internet地址格式);域sin_port表示端口,如果参数1为0,则winsock分配一个端口给它,范围在1024和5000之间;域sin_addr是表示地址信息,它是一个联合体,其中s_addr表示如下形式的字符串,“28.56.22.8”。如果参数没有指定地址,则winsock自动地得到本地ip地址(如果有几个网卡,则使用其中一个的地址)。
  (6)总结create的过程
  首先,调用socket函数创建一个socket;然后把创建的socket对象映射到casyncsocket对象(捆绑在一起),指定本socket要通知的网络事件,并创建一个“socket窗口”来接收网络事件消息,最后,指定socket的本地信息。
  下一步,是使用成员函数connect连接远地主机,配置socket的远地信息。函数connect类似于bind,把指定的远地地址转换成sockaddr_in对象表示的地址信息(包括网络字节序的转换),然后调用winsock函数connect连接远地主机,配置socket的远地端口和远地ip地址。
No. 2
  MFC提供了两个类CAsyncSocket和CSocket来封装WinSock API,这给程序员提供了一个更简单的网络编程接口。
  CAsyncSocket在较低层次上封装了WinSock API,缺省情况下,使用该类创建的socket是非阻塞的socket,所有操作都会立即返回,如果没有得到结果,返回WSAEWOULDBLOCK,表示是一个阻塞操作。
  CSocket建立在CAsyncSocket的基础上,是CAsyncSocket的派生类。也就是缺省情况下使用该类创建的socket是非阻塞的socket,但是CSocket的网络I/O是阻塞的,它在完成任务之后才返回。CSocket的阻塞不是建立在“阻塞”socket的基础上,而是在“非阻塞”socket上实现的阻塞操作,在阻塞期间,CSocket实现了本线程的消息循环,因此,虽然是阻塞操作,但是并不影响消息循环,即用户仍然可以和程序交互。
  CAsyncSocket
  CAsyncSocket封装了低层的WinSock API,其成员变量m_hSocket保存其对应的socket句柄。使用CAsyncSocket的方法如下:
  首先,在堆或者栈中构造一个CAsyncSocket对象,例如:
  CAsyncSocket sock;或者
  CAsyncSocket *pSock = new CAsyncSocket;
  其次,调用Create创建socket,例如:
  使用缺省参数创建一个面向连接的socket
  sock.Create()
  指定参数参数创建一个使用数据报的socket,本地端口为30
  pSocket.Create(30, SOCK_DGRM);
  其三,如果是客户程序,使用Connect连接到远地;如果是服务程序,使用Listen监听远地的连接请求。
  其四,使用成员函数进行网络I/O。
  最后,销毁CAsyncSocket,析构函数调用Close成员函数关闭socket。
  下面,分析CAsyncSocket的几个函数,从中可以看到它是如何封装低层的WinSock API,简化有关操作的;还可以看到它是如何实现非阻塞的socket和非阻塞操作。
  socket对象的创建和捆绑
  (1)Create函数
  首先,讨论Create函数,分析socket句柄如何被创建并和CAsyncSocket对象关联。Create的实现如下:
  BOOL CAsyncSocket::Create(UINT nSocketPort, int nSocketType,
  long lEvent, LPCTSTR lpszSocketAddress)
  {
  if (Socket(nSocketType, lEvent))
  {
  if (Bind(nSocketPort,lpszSocketAddress))
  return TRUE;
  int nResult = GetLastError();
  Close();
  WSASetLastError(nResult);
  }
  return FALSE;
  }
  其中:
  参数1表示本socket的端口,缺省是0,如果要创建数据报的socket,则必须指定一个端口号。
  参数2表示本socket的类型,缺省是SOCK_STREAM,表示面向连接类型。
  参数3是屏蔽位,表示希望对本socket监测的事件,缺省是FD_READ | FD_WRITE | FD_OOB | FD_ACCEPT | FD_CONNECT | FD_CLOSE。
  参数4表示本socket的IP地址字符串,缺省是NULL。
  Create调用Socket函数创建一个socket,并把它捆绑在this所指对象上,监测指定的网络事件。参数2和3被传递给Socket函数,如果希望创建数据报的socket,不要使用缺省参数,指定参数2是SOCK_DGRM。
  如果上一步骤成功,则调用bind给新的socket分配端口和IP地址。
  (2)Socket函数
  接着,分析Socket函数,其实现如下:
  BOOL CAsyncSocket::Socket(int nSocketType, long lEvent,
  int nProtocolType, int nAddressFormat)
  {
  ASSERT(m_hSocket == INVALID_SOCKET);
  m_hSocket = socket(nAddressFormat,nSocketType,nProtocolType);
  if (m_hSocket != INVALID_SOCKET)
  {
  CAsyncSocket::AttachHandle(m_hSocket, this, FALSE);
  return AsyncSelect(lEvent);
  }
  return FALSE;
  }
  其中:
  参数1表示Socket类型,缺省值是SOCK_STREAM。
  参数2表示希望监测的网络事件,缺省值同Create,指定了全部事件。
  参数3表示使用的协议,缺省是0。实际上,SOCK_STREAM类型的socket使用TCP协议,SOCK_DGRM的socket则使用UDP协议。
  参数4表示地址族(地址格式),缺省值是PF_INET(等同于AF_INET)。对于TCP/IP来说,协议族和地址族是同值的。
  在socket没有被创建之前,成员变量m_hSocket是一个无效的socket句柄。Socket函数把协议族、socket类型、使用的协议等信息传递给WinSock API函数socket,创建一个socket。如果创建成功,则把它捆绑在this所指对象。
  (3)捆绑(Attatch)
  捆绑过程类似于其他Windows对象,将在模块线程状态的WinSock映射中添加一对新的映射:this所指对象和新创建的socket对象的映射。
  另外,如果本模块线程状态的“socket窗口”没有创建,则创建一个,该窗口在异步操作时用来接收WinSock的通知消息,窗口句柄保存到模块线程状态的m_hSocketWindow变量中。函数AsyncSelect将指定该窗口为网络事件消息的接收窗口。
  函数AttachHandle的实现在此不列举了。
  (4)指定要监测的网络事件
  在捆绑完成之后,调用AsyncSelect指定新创建的socket将监测的网络事件。AsyncSelect实现如下:
  BOOL CAsyncSocket::AsyncSelect(long lEvent)
  {
  ASSERT(m_hSocket != INVALID_SOCKET);
  _AFX_SOCK_THREAD_STATE* pState = _afxSockThreadState;
  ASSERT(pState->m_hSocketWindow != NULL);
  return WSAAsyncSelect(m_hSocket, pState->m_hSocketWindow,
  WM_SOCKET_NOTIFY, lEvent) != SOCKET_ERROR;
  }
  函数参数lEvent表示希望监视的网络事件。
  _ afxSockThreadState得到的是当前的模块线程状态,m_ hSocketWindow是本模块在当前线程的“socket窗口”,指定监视m_hSocket的网络事件,如指定事件发生,给窗口m_hSocketWindow发送WM_SOCKET_NOTIFY消息。
  被指定的网络事件对应的网络I/O将是异步操作,是非阻塞操作。例如:指定FR_READ导致Receive是一个异步操作,如果不能立即读到数据,则返回一个错误WSAEWOULDBLOCK。在数据到达之后,WinSock通知窗口m_hSocketWindow,导致OnReceive被调用。
  指定FR_WRITE导致Send是一个异步操作,即使数据没有送出也返回一个错误WSAEWOULDBLOCK。在数据可以发送之后,WinSock通知窗口m_hSocketWindow,导致OnSend被调用。
  指定FR_CONNECT导致Connect是一个异步操作,还没有连接上就返回错误信息WSAEWOULDBLOCK,在连接完成之后,WinSock通知窗口m_hSocketWindow,导致OnConnect被调用。
  对于其他网络事件,就不一一解释了。
  所以,使用CAsyncSocket时,如果使用Create缺省创建socket,则所有网络I/O都是异步操作,进行有关网络I/O时则必须覆盖以下的相关函数:
  OnAccept、OnClose、OnConnect、OnOutOfBandData、OnReceive、OnSend。
  (5)Bind函数
  经过上述过程,socket创建完毕,下面,调用Bind函数给m_hSocket指定本地端口和IP地址。Bind的实现如下:
  BOOL CAsyncSocket::Bind(UINT nSocketPort, LPCTSTR lpszSocketAddress)
  {
  USES_CONVERSION;
  //使用WinSock的地址结构构造地址信息
  SOCKADDR_IN sockAddr;
  memset(&sockAddr,0,sizeof(sockAddr));
  //得到地址参数的值
  LPSTR lpszAscii = T2A((LPTSTR)lpszSocketAddress);
  //指定是Internet地址类型
  sockAddr.sin_family = AF_INET;
  if (lpszAscii == NULL)
  //没有指定地址,则自动得到一个本地IP地址
  //把32比特的数据从主机字节序转换成网络字节序
  sockAddr.sin_addr.s_addr = htonl(INADDR_ANY);
  else
  {
  //得到地址
  DWORD lResult = inet_addr(lpszAscii);
  if (lResult == INADDR_NONE)
  {
  WSASetLastError(WSAEINVAL);
  return FALSE;
  }
  sockAddr.sin_addr.s_addr = lResult;
  }
  //如果端口为0,则WinSock分配一个端口(1024—5000)
  //把16比特的数据从主机字节序转换成网络字节序
  sockAddr.sin_port = htons((u_short)nSocketPort);
  //Bind调用WinSock API函数bind
  return Bind((SOCKADDR*)&sockAddr, sizeof(sockAddr));
  }
  其中:函数参数1指定了端口;参数2指定了一个包含本地地址的字符串,缺省是NULL。
  函数Bind首先使用结构SOCKADDR_IN构造地址信息。该结构的域sin_family表示地址格式(TCP/IP同协议族),赋值为AF_INET(Internet地址格式);域sin_port表示端口,如果参数1为0,则WinSock分配一个端口给它,范围在1024和5000之间;域sin_addr是表示地址信息,它是一个联合体,其中s_addr表示如下形式的字符串,“28.56.22.8”。如果参数没有指定地址,则WinSock自动地得到本地IP地址(如果有几个网卡,则使用其中一个的地址)。
  (6)总结Create的过程
  首先,调用socket函数创建一个socket;然后把创建的socket对象映射到CAsyncSocket对象(捆绑在一起),指定本socket要通知的网络事件,并创建一个“socket窗口”来接收网络事件消息,最后,指定socket的本地信息。
  下一步,是使用成员函数Connect连接远地主机,配置socket的远地信息。函数Connect类似于Bind,把指定的远地地址转换成SOCKADDR_IN对象表示的地址信息(包括网络字节序的转换),然后调用WinSock函数Connect连接远地主机,配置socket的远地端口和远地IP地址。