Socket 详细介绍

本文详细介绍了 WinSock 提供的五种 I/O 模型:Select 模型、WSAAsyncSelect 模型、WSAEventSelect 模型、Overlapped I/O 模型和 Completionport 模型。重点讲解了 Select 模型和 WSAAsyncSelect 模型的使用方法及优缺点,包括如何防止阻塞和提高多线程应用的扩展性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

套接字模式:分为阻塞套接字(block)和非阻塞套接字(non-block),或者为同步套接字(synchrony)和异步套接字(asynchrony)

套接字模型:描述如何对套接字的I/O进行管理。

WinSock 提供五种套接字I/O模型:Select模型、WSAAsyncSelect模型、WSAEventSelect模型、OverLapped I/O模型和Completion port模型。

1:select模型

先看一下下面的这句代码:

阻塞socket

int iResult = recv(s, buffer,1024);

   这是用来接收数据的,在默认的阻塞模式下的套接字里,recv会阻塞在那里,直到套接字连接上有数据可读,把数据读到buffer里后recv函数才会返 回,不然就会一直阻塞在那里。

   在单线程的程序里出现这种情况会导致主线程(单线程程序里只有一个默认的主线程)被阻塞,这样整个程序被锁死在这里,如果永远没数据发送过来,那么程序就会被永远锁死。这个问题可以用多线程解决,但是在有多个套接字连接的情况下,这不是一个好的选择,扩展性很差。

非阻塞 socket:

再看代码:
int iResult = ioctlsocket(s, FIOBIO, (unsigned long *)&ul);
iResult = recv(s, buffer,1024);

//-------------------------
// Initialize Winsock
WSADATA wsaData;
int iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
if (iResult != NO_ERROR)
  printf("Error at WSAStartup()\n");

//-------------------------
// Create a SOCKET object.
SOCKET m_socket;
m_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (m_socket == INVALID_SOCKET) {
  printf("Error at socket(): %ld\n", WSAGetLastError());
  WSACleanup();
  return;
}

//-------------------------
// Set the socket I/O mode: In this case FIONBIO
// enables or disables the blocking mode for the 
// socket based on the numerical value of iMode.
// If iMode = 0, blocking is enabled; 
// If iMode != 0, non-blocking mode is enabled.
int iMode = 0;
ioctlsocket(m_socket, FIONBIO, (u_long FAR*) &iMode);


 

这一次recv的调用不管套接字连接上有没有数据可以接收都会马上返回。原因就在于我们用ioctlsocket把套接字设置为非阻塞模式了。不过你跟踪 一下就会发现,在没有数据的情况下,recv确实是马上返回了,但是也返回了一个错误:WSAEWOULDBLOCK,意思就是请求的操作没有成功完成。看到这里很多人可能会说,那么就重复调用recv并检查返回值,直到成功为止,但是这样做效率很成问题,开销太大

 

多线程来解决使用阻塞套接字存在的问题:

  多线程来解决阻塞套接字的方法是为阻塞套接字的IO操作创建单独的线程,阻塞的套接字IO操作放在单独的线程中,而不会因为套接字IO操作的阻塞造成整个主线程的阻塞,但是这样也会造成一定的问题:

1) 如果是多个套接字的场合通过多线程来解决主线程阻塞就会显得不合适了,server端创建一个监听socket来负责监听连接,而为accept函数

   为每个client端连接创建一个套接字,这样就会创建很多的套接字。如果是创建不同的套接字则应该创建多个线程,而每个线程的线程函数是不同的,这样就造成了所谓的扩展性很差。

2)如果不是每个连接创建一个套接字的话,多线程方法比较直观,程序非常简单而且可移植性好,但是不能利用平台相关的特性。例如,如果连接数增多的时候(成千上万的连接),那么线程数成倍增长,操作系统忙于频繁的线程间切换,而且大部分线程在其生命周期内都是处于非活动状态的,这大大浪费了系统的资源。所以,如果你已经知道你的代码只会运行在Windows平台上,建议采用Winsock I/O模型。

 

微软提供了select函数来解决这个问题

 int select(
              int nfds,
              fd_set FAR *readfds,
              fd_set FAR *writefds,
              fd_set FAR *exceptfds,
              const struct timeval FAR *timeout
 );

第一个参数不要管,会被系统忽略的。第二个参数是用来检查套接字可读性,也就说检查套接字上是否有数据可读,同样,第三个参数用来检查数据是否可以发出。最后一个是检查是否有带外数据可读取。

 

最后一个参数是用来设置select等待多久的,是个结构:
struct timeval {
            long tv_sec; // seconds
            long tv_usec; // and microseconds
};
如果将这个结构设置为(0,0),那么select函数会马上返回。

 说了这么久,select的作用到底是什么?

他的作用就是:

1)防止在在阻塞模式的套接字里被锁死。

2)避免在非阻塞套接字里重复检查WSAEWOULDBLOCK错误。

 

他的工作流程如下:

1:用FD_ZERO宏来初始化我们感兴趣的fd_set,也就是select函数的第二三四个参数。
2:用FD_SET宏来将套接字句柄分配给相应的fd_set。
3:调用select函数。
4:用FD_ISSET对套接字句柄进行检查,如果我们所关注的那个套接字句柄仍然在开始分配的那个fd_set里,那么说明马上可以进行相应的IO操 作。比如一个分配给select第一个参数的套接字句柄在select返回后仍然在select第一个参数的fd_set里,那么说明当前数据已经来了, 马上可以读取成功而不会被阻塞

#include "stdafx.h"
 #include <iostream>
 #include <winsock2.h>
 #include <windows.h>
 
 #define TRACE ATLTrace //必须要加上这个宏定义,否则在WIN32的控制台程序中是不能直接用的
 
 #define InternetAddr "127.0.0.1"
 #define iPort 5055
 
 #pragma comment(lib, "ws2_32.lib")
 
 int _tmain(int argc, _TCHAR* argv[])
 {
     WSADATA wsa;
     WORD wVersionRequested;
     int err;
 
    wVersionRequested = MAKEWORD( 2, 2 );
     err = WSAStartup( wVersionRequested, &wsa);
     if ( err != 0 ) {
     //Tell the user that we could not find a usable 
     //WinSock DLL.     
     TRACE("你忘记添加WinSock DLL了\n");
     WSACleanup();
     return 1;
      }
 
    // Create a SOCKET for listening for  incoming connection requests
     SOCKET fdServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    
     sockaddr_in server;
 
  server.sin_family = AF_INET;
  server.sin_addr.s_addr = inet_addr(InternetAddr);
  server.sin_port = htons(iPort);
  //Bind the socket.
     int ret = bind(fdServer, (sockaddr*)&server, sizeof(server));
     ret = listen(fdServer, 4);
 
     SOCKET AcceptSocket;
     fd_set     fdread;
  timeval    tv;
  int nSize;
    //其实也算是轮训,那么对阻塞socket用select和对使用非阻塞socket的优点在哪?
   //可能的优点就是避免在非阻塞套接字里重复检查WSAEWOULDBLOCK错误。
     while(1)
   {
                  
          FD_ZERO(&fdread);//初始化fd_set
          FD_SET(fdServer, &fdread);//分配套接字句柄到相应的fd_set
                              
         tv.tv_sec = 2;//这里我们打算让select等待两秒后返回,避免被锁死,也避免马上返回
         tv.tv_usec = 0;
                                                  
         select(0, &fdread, NULL, NULL, &tv);
                                                          
         nSize = sizeof(server);
         //先判断fdServer是否还在fd_set内来判断是否可以读,这样就避免因为 accept在等待
         //时造成的阻塞
         if (FD_ISSET(fdServer, &fdread))
             //如果套接字句柄还在fd_set里,说明客户端已经有connect的请求发过来了,
             //马上可以accept成功
          {
              AcceptSocket = accept(fdServer,( sockaddr*) &server, &nSize);
              break;
            }                                             
         else
         //还没有客户端的connect请求,我们可以去做别的事,避免像没有用select方式
         //的阻塞套接字程序被锁死的情况,如果没用select,当程序运行到accept的时候客户
         //端恰好没有connect请求,那么程序就会被锁死,做不了任何事情
             {
             //do something
                MessageBox(NULL, "waiting", "recv", MB_ICONINFORMATION);
         //别的事做完后,继续去检查是否有客户端连接请求
             }
    }
 
    char buffer[128];
       ZeroMemory(buffer, 128);
 
          ret = recv(AcceptSocket,buffer,128,0);//这里同样可以用select,用法和上面一样
 
          MessageBox(NULL, buffer, "recv", MB_ICONINFORMATION);
 
         closesocket(AcceptSocket);
         WSACleanup();
         return 0;
 }

select函数的返回值 :

函数失败的返回值:调用失败返回SOCKET_ERROR,超时返回0。

int ret;
        if((ret=select(0,&fdread,NULL,NULL,NULL))==SOCKET_ERROR)
        {
            //Error Condition
        }
        if(ret > 0)//ret>0这个ret值表示满足条件的socket的数量,不止一个socket满足IO操作的条件
        {
            if(FD_ISSET(fdServer,&fdread))
            {
                //A read event has occured on socket fdServer
            }
        }

2:WSAAsyncSelect模型

WSAAsynSelect模型也是一个常用的异步I/O模型。应用程序可以在一个套接字上接收以WINDOWS消息为基础的网络事件通知。该模型的实现方法是通过调用WSAAsynSelect函数自动将套接字设置(转变)为非阻塞模式,并向WINDOWS注册一个或多个网络事件,并提供一个通知时使用的窗口句柄。当注册的事件发生时,对应的窗口将收到一个基于消息的通知。

#include <winsock.h>
 #include <tchar.h>
 
 #define PORT         5150
 #define MSGSIZE      1024
 #define WM_SOCKET WM_USER+0
 
 #pragma comment(lib, "ws2_32.lib")
 
 LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
 
 int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
 {
      static TCHAR szAppName[] = _T("AsyncSelect Model");
      HWND            hwnd ;
      MSG             msg ;
      WNDCLASS        wndclass ;
 
      wndclass.style            = CS_HREDRAW | CS_VREDRAW ;
      wndclass.lpfnWndProc      = WndProc ;
      wndclass.cbClsExtra       = 0 ;
      wndclass.cbWndExtra       = 0 ;
      wndclass.hInstance        = hInstance ;
      wndclass.hIcon            = LoadIcon (NULL, IDI_APPLICATION) ;
      wndclass.hCursor          = LoadCursor (NULL, IDC_ARROW) ;
      wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
      wndclass.lpszMenuName     = NULL ;
      wndclass.lpszClassName = szAppName ;
 
      if (!RegisterClass(&wndclass))
      {
        MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ;
        return 0 ;
      }
 
      hwnd = CreateWindow (szAppName,                     // window class name
                           TEXT ("AsyncSelect Model"), // window caption
                           WS_OVERLAPPEDWINDOW,           // window style
                           CW_USEDEFAULT,                 // initial x position
                           CW_USEDEFAULT,                 // initial y position
                           CW_USEDEFAULT,                 // initial x size
                           CW_USEDEFAULT,                 // initial y size
                           NULL,                          // parent window handle
                           NULL,                          // window menu handle
                           hInstance,                     // program instance handle
                           NULL) ;                        // creation parameters
 
      ShowWindow(hwnd, iCmdShow);
      UpdateWindow(hwnd);
 
      while (GetMessage(&msg, NULL, 0, 0))
      {
        TranslateMessage(&msg) ;
        DispatchMessage(&msg) ;
      }
   
      return msg.wParam;
 }
 
 LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
 {
      WSADATA          wsd;
      static SOCKET sListen;
      SOCKET           sClient;
      SOCKADDR_IN      local, client;
      int              ret, iAddrSize = sizeof(client);
      char             szMessage[MSGSIZE];
 
      switch (message)
      {
 case WM_CREATE:
        // Initialize Windows Socket library
      WSAStartup(0x0202, &wsd);
   
      // Create listening socket
        sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
     
      // Bind
        local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
      local.sin_family = AF_INET;
      local.sin_port = htons(PORT);
      bind(sListen, (struct sockaddr *)&local, sizeof(local));
   
      // Listen
        listen(sListen, 3);
 
        // Associate listening socket with FD_ACCEPT event
      WSAAsyncSelect(sListen, hwnd, WM_SOCKET, FD_ACCEPT);
      return 0;
 
      case WM_DESTROY:
        closesocket(sListen);
        WSACleanup();
        PostQuitMessage(0);
        return 0;
   
      case WM_SOCKET:
        if (WSAGETSELECTERROR(lParam))//lParam的高字节包含了可能出现的任何的错误代码
        {
          closesocket(wParam);
          break;
        }
     
        switch (WSAGETSELECTEVENT(lParam)) //lParam的低字节指定已经发生的网络事件
        {
        case FD_ACCEPT:
          // Accept a connection from client
          sClient = accept(wParam, (struct sockaddr *)&client, &iAddrSize);
       
          // Associate client socket with FD_READ and FD_CLOSE event
          WSAAsyncSelect(sClient, hwnd, WM_SOCKET, FD_READ | FD_CLOSE);
          break;
 
        case FD_READ:
          ret = recv(wParam, szMessage, MSGSIZE, 0);
 
          if (ret == 0 || ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET)
          {
            closesocket(wParam);
          }
          else
          {
            szMessage[ret] = '\0';
            send(wParam, szMessage, strlen(szMessage), 0);
          }
          break;
       
        case FD_CLOSE:
          closesocket(wParam);      
          break;
        }
        return 0;
      }
   
      return DefWindowProc(hwnd, message, wParam, lParam);
 }


 

WSAAsyncSelect是最简单的一种Winsock I/O模型(之所以说它简单是因为一个主线程就搞定了)。使用Raw Windows API写过窗口类应用程序的人应该都能看得懂。这里,我们需要做的仅仅是:
1.在WM_CREATE消息处理函数中,初始化Windows Socket library,创建监听套接字,绑定,监听,并且调用WSAAsyncSelect函数表示我们关心在监听套接字上发生的FD_ACCEPT事件
2.自定义一个消息WM_SOCKET,一旦在我们所关心的套接字(监听套接字客户端套接字)上发生了某个事件,系统发送消息(WM_SOCKET)给hWnd指向的窗体,而WndProc函数处理所有发往窗体的消息并且message参数被设置为WM_SOCKET
3.在WM_SOCKET的消息处理中,分别对FD_ACCEPT、FD_READ和FD_CLOSE事件进行处理;

4.在窗口销毁消息(WM_DESTROY)的处理函数中,我们关闭监听套接字,清除Windows Socket library。

 

下面这张用于WSAAsyncSelect函数的网络事件类型表可以让你对各个网络事件有更清楚的认识:
表1

FD_READ 应用程序想要接收有关是否可读的通知,以便读入数据
FD_WRITE 应用程序想要接收有关是否可写的通知,以便写入数据
FD_OOB 应用程序想接收是否有带外(OOB)数据抵达的通知
FD_ACCEPT 应用程序想接收与进入连接有关的通知
FD_CONNECT 应用程序想接收与一次连接或者多点join操作完成的通知
FD_CLOSE 应用程序想接收与套接字关闭有关的通知
FD_QOS 应用程序想接收套接字“服务质量”(QoS)发生更改的通知
FD_GROUP_QOS     应用程序想接收套接字组“服务质量”发生更改的通知(现在没什么用处,为未来套接字组的使用保留)
FD_ROUTING_INTERFACE_CHANGE 应用程序想接收在指定的方向上,与路由接口发生变化的通知
FD_ADDRESS_LIST_CHANGE     应用程序想接收针对套接字的协议家族,本地地址列表发生变化的通知

3:WSAEventSelect模型

WSAEventSelect模型类似WSAAsynSelect模型,但最主要的区别是网络事件发生时会被发送到一个事件对象句柄,而不是发送到一个窗口。这样可能更加的好,对于服务器端的程序来说。

使用步骤如下:

a、 创建事件对象来接收网络事件:

WSAEVENT WSACreateEvent( void );

该函数的返回值为一个事件对象句柄,它具有两种工作状态:已传信(signaled)和未传信(nonsignaled)以及两种工作模式:人工重设(manual reset)和自动重设(auto reset)。默认未未传信的工作状态和人工重设模式。

 

b、将事件对象与套接字关联,同时注册事件,使事件对象的工作状态从未传信转变未已传信。

int WSAEventSelect( SOCKET s,WSAEVENT hEventObject,long lNetworkEvents );
s为套接字
hEventObject为刚才创建的事件对象句柄

lNetworkEvents为掩码,定义如上面所述

 

c、I/O处理后,设置事件对象为未传信
BOOL WSAResetEvent( WSAEVENT hEvent );
Hevent为事件对象

成功返回TRUE,失败返回FALSE。

 

d、等待网络事件来触发事件句柄的工作状态:

DWORD WSAWaitForMultipleEvents( DWORD cEvents,const WSAEVENT FAR * lphEvents, BOOL fWaitAll,DWORD dwTimeout, BOOL fAlertable );
lpEvent为事件句柄数组的指针
cEvent为为事件句柄的数目,其最大值为WSA_MAXIMUM_WAIT_EVENTS
fWaitAll指定等待类型:TRUE:当lphEvent数组重所有事件对象同时有信号时返回;
FALSE:任一事件有信号就返回。
dwTimeout为等待超时(毫秒)

fAlertable为指定函数返回时是否执行完成例程

 

nIndex=WSAWaitForMultipleEvents(…);

MyEvent=EventArray[Index- WSA_WAIT_EVENT_0]; 

 

事件选择模型也比较简单,实现起来也不是太复杂,它的基本思想是将每个套接字都和一个WSAEVENT对象对应起来,并且在关联的时候指定需要关注的哪些网 络事件。一旦在某个套接字上发生了我们关注的事件(FD_READ和FD_CLOSE),与之相关联的WSAEVENT对象被Signaled。程序定义 了两个全局数组,一个套接字数组,一个WSAEVENT对象数组,其大小都是MAXIMUM_WAIT_OBJECTS(64),两个数组中的元素一一对 应。
同样的,这里的程序没有考虑两个问题,一是不能无条件的调用accept,因为我们支持的并发连接数有限。解决方法是将套接字按 MAXIMUM_WAIT_OBJECTS分组,每MAXIMUM_WAIT_OBJECTS个套接字一组,每一组分配一个工作者线程;或者采用WSAAccept代替accept,并回调自己定义的Condition Function。第二个问题是没有对连接数为0的情形做特殊处理,程序在连接数为0的时候CPU占用率为100%。

SOCKET       Socket[WSA_MAXIMUM_WAIT_EVENTS];
 WSAEVENT   Event[WSA_MAXINUM_WAIT_EVENTS];
 SOCKET    Accept, Listen;
 DWORD     EventTotal = 0;
 DWORD     Index;
 
 //Set up a TCP socket for listening on port 5150
 Listen = socket(PF_INET,SOCK_STREAM,0);
 
 InternetAddr.sin_family      = AF_INET;
 InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);
 InternetAddr.sin_port        = htons(5150);
 
 bind(Listen,(PSOCKADDR) &InternetAddr,sizeof(InternetAddr));
 
 NewEvent = WSACreateEvent();
 
 WSAEventSelect(Listen,NewEvnet,FD_ACCEPT|FD_CLOSE);
 
 listen(Listen,5);
 
 Socket[EventTotal] = Listen;
 Event[EventTotal] = NewEvent;
 EventTotal++;
 
 while (TRUE)
 {
     //Wait for network events on all sockets
     Index = WSAWaitForMultipleEvents(EventTotal,EventArray,FALSE,WSA_INFINITE,FALSE);
 
     WSAEnumNewWorkEvents(SocketArray[Index-WSA_WAIT_EVENT_0],
         EventArray[Index-WSA_WAIT_EVENT_0],
         &NetworkEvents);
     //Check for FD_ACCEPT messages
     if (NetworkEvents.lNetworkEvents & FD_ACCEPT)
     {
         if (NetworkEvents.iErrorCode[FD_ACCEPT_BIT] !=0)
         {
             //Error
             break;
         }
         //Accept a new connection and add it to the socket and event lists
         Accept = accept(SocketArray[Index-WSA_WAIT_EVENT_0],NULL,NULL);
 
         //We cannot process more than WSA_MAXIMUM_WAIT_EVENTS sockets ,
         //so close the accepted socket
         if (EventTotal > WSA_MAXIMUM_WAIT_EVENTS)
         {
             printf("..");
             closesocket (Accept);
             break;
         }
         NewEvent = WSACreateEvent();
 
         WSAEventSelect(Accept,NewEvent,FD_READ|FD_WRITE|FD_CLOSE);
 
         Event[EventTotal] = NewEvent;
         Socket[EventTotal]= Accept;
         EventTotal++;
         prinrt("Socket %d connect\n",Accept);
     }
     //Process FD_READ notification
     if (NetworkEvents.lNetwoAD)rkEvents & FD_RE
     {
         if (NetworkEvents.iErrorCode[FD_READ_BIT !=0])
         {
             //Error
             break;
         }
 
         //Read data from the socket
         recv(Socket[Index-WSA_WAIT_EVENT_0],buffer,sizeof(buffer),0);
     }
     //process FD_WRITE notitication
     if (NetworkEvents.lNetworkEvents & FD_WRITE)
     {
         if (NetworkEvents.iErrorCode[FD_WRITE_BIT] !=0)
         {
             //Error
             break;
         }
         send(Socket[Index-WSA_WAIT_EVENT_0],buffer,sizeof(buffer),0);
     }
     if (NetworkEvents.lNetworkEvents & FD_CLOSE)
     {
         if(NetworkEvents.iErrorCode[FD_CLOSE_BIT] !=0)
         {
             //Error
             break;
         }
         closesocket (Socket[Index-WSA_WAIT_EVENT_0]);
         //Remove socket and associated event from the Socket and Event arrays and
         //decrement eventTotal
         CompressArrays(Event,Socket,& EventTotal);
     }
 }



 


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值