暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

windows下C++实现socket通信服务器(中)

小懵白生活小趣谈 2021-06-20
1416

一、Background

在上一节windows下C++实现socket通信服务器(上)文章中,给大家简单介绍了部署在服务器中的一对一通信socket服务端建立
接下来呢,将为大家实现IOCP异步方式的小并发socket服务端建立,此知识适用于小白学生小型项目制作。

二、Why try IOCP?

socket在TCP通信的时候,往往是一对一的通信。无论是采用多线程还是线程池,其最终结果都是一个线程对应连接一个socket客户端。
这就造成了很多线程并行地运行在系统,导致CPU大量的时间上进行线程的上下文切换,造成了效率的低下。
而IOCP是基于proactor模式的,是一个异步I/O操作端口的模型,与重叠I/O技术有关
其工作原理大体是:

其优势表现为:

解决了"one-thread-per-client"的缺点;

完成端口会充分利用Windows内核来进行I/O的调度;

消除了无谓的线程上下文切换,实现了高并发性能;


三、IOCP设计
实现步骤如下:
1完成端口的创建;
(2)绑定socket和完成端口的关联;
(3)调用输入输出函数,发起重叠IO操作;

(4)在服务线程中,等待完成端口重叠IO操作结果;

1、创建完成端口

其所调用的函数为如下:

    HANDLE WINAPI CreateIoCompletionPort(
    _In_ HANDLE FileHandle, 已经打开的文件句柄或者空句柄,一般是客户端的句柄
    _In_opt_ HANDLE ExistingCompletionPort, 已经存在的IOCP句柄
    _In_ ULONG_PTR CompletionKey, 完成键,包含了指定I/O完成包的指定文件
    _In_ DWORD NumberOfConcurrentThreads 真正并发同时执行最大线程数,一般推介是CPU核心数*2
    );

    需要注意的是该函数还有一个作用:

    把一个IO句柄和完成端口关联起来。区别在于里面的参数不同。
    创建完成端口语句如下:
      HANDLE m_hIOCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL00 );

      2、建立Worker线程

      一般来说线程的数量设置在CPU*2,这样可以更加充分利用CPU资源。

          SYSTEM_INFO msi;  确定处理器的核心数量

        GetSystemInfo(&msi);

        for (int i = 0; i < msi.dwNumberOfProcessors*2; i++){

        HANDLE hThread;
        DWORD Thread_ID;
        hThread=CreateThread(NULL, 0, _WorkerThread, 0,0,&Thread_ID);
        CloseHandle(hThread);
        }

        其中,ServerWorkThread是自行定义的Worker线程的线程函数。

        3、创建socket

            WSADATA wsaData;

          WSAStartup(MAKEWORD(2, 2), &wsaData); //MAKEWORD(2, 2)版本号

          SOCKET _socket=WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);

          SOCKADDR_IN addrServer;

          addrServer.sin_addr.S_un.S_addr = htonl(INADDR_ANY);//inet_addr("0.0.0.0");
            addrServer.sin_family = AF_INET;
            addrServer.sin_port = htons(81);

            bind(_socket, (SOCKADDR*)&addrServer, sizeof(SOCKADDR)); //绑定  

            listen(_socket, 2);//监听  
          4、完成端口绑定

          每当有客户端连入的时候,我们还需调用:

          CreateIoCompletionPort()函数,与完成端口进行绑定。

            int len = sizeof(SOCKADDR);   
            SOCKADDR_IN addr;


            SOCKET Clietnt=accept(_socket,(SOCKADDR*)&addr,&len);


            PerHandleData = (LPPER_HANDLE_DATA)GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA)); // 在堆中为这个PerHandleData申请指定大小的内存
            PerHandleData -> socket = Clietnt;
            memcpy (&PerHandleData -> ClientAddr, &addr, len);


            if(CreateIoCompletionPort((HANDLE)Clietnt,m_hIOCompletionPort,(ULONG_PTR)PerHandleData,0)==NULL){
                  cout<<"客户端绑定失败"<<endl;      
            closesocket(Clietnt);
            }

            需要注意的是:把上面的代码放进while循环里,每当得到一个新的socket,继续调用函数进行绑定。

            而此时却不是新建立完成端口了,而是把新连入的Socket,与目前的完成端口绑定在一起。

            至此,我们就完成了完成端口的设定了,接下来就是实现各种操作了,比如说使用WSARecv()函数来接收数据,发起重叠IO操作

            5、worker线程函数定义

            在这个线程函数里,我们需要使用以下函数:

            GetQueuedCompletionStatus() 来监控完成端口

              BOOL WINAPI GetQueuedCompletionStatus(
              __in HANDLE CompletionPort, // 这个就是我们建立的那个唯一的完成端口
              __out LPDWORD lpNumberOfBytes, //这个是操作完成后返回的字节数
              __out PULONG_PTR lpCompletionKey, // 这个是返回我们建立完成端口的时候绑定的那个自定义结构体参数
              __out LPOVERLAPPED *lpOverlapped, // 这个是返回我们在连入Socket的时候一起建立的那个重叠结构
              __in DWORD dwMilliseconds // 等待完成端口的超时时间,如果线程不需要做其他的事情,那就INFINITE就行了
              );
              简单来说,是让worker线程进入不占用CPU的睡眠状态,直到完成端口上出现了需要处理的网络请求——读取数据,发送数据等
              就将这个请求从完成端口的队列中取回来,继续执行本线程中后面的处理代码。

              不过我们可以看到,该函数里有多个参数是我们需要用来接收值的,因此为了方便,定义这些参数成结构体使用:

                struct _IO_DATA{  
                int _Bytes;
                IO_OPERATION opCode;
                WSABUF wsabuf;
                OVERLAPPED m_Overlapped; //必要的,其余看你自己定义
                char buff[2048];
                };


                typedef struct //此结构体用来保存客户端的socket
                {
                  SOCKET socket;    //必要的
                  SOCKADDR_STORAGE ClientAddr;
                  
                }PER_HANDLE_DATA, *LPPER_HANDLE_DATA;

                代码实现如下:

                    HANDLE g_hIocp=(HANDLE)WordThreadcontext;  //线程函数参数——端口

                  DWORD dwIoSize=0;

                  LPPER_HANDLE_DATA lpCompletionKey=NULL;

                  LPOVERLAPPED lpOverlapped=NULL;

                  _IO_DATA* _IO_Context=NULL;

                  GetQueuedCompletionStatus(g_hIocp,&dwIoSize,(PULONG_PTR)&lpCompletionKey,(LPOVERLAPPED*)&lpOverlapped,INFINITE)

                  实现完后,接着就是打印接收到信息了,然后为下一个重叠调用建立单I/O操作数据。

                  比如说实现接收客户端发送过来的数据:WSARecv()
                    int WSARecv(
                    SOCKET s, // 当然是投递这个操作的套接字
                    LPWSABUF lpBuffers, // 接收缓冲区
                    // 这里需要一个由WSABUF结构构成的数组
                    DWORD dwBufferCount, // 数组中WSABUF结构的数量,设置为1即可
                    LPDWORD lpNumberOfBytesRecvd, // 如果接收操作立即完成,这里会返回函数调用所接收到的字节数
                    LPDWORD lpFlags, // 说来话长了,我们这里设置为0 即可
                    LPWSAOVERLAPPED lpOverlapped, // 这个Socket对应的重叠结构
                    NULL // 这个参数只有完成例程模式才会用到,
                    // 完成端口中我们设置为NULL即可
                    );


                    这里呢,为了大家按照IOCP实现的理解思路,我这里呢就先不封装了。

                    mian函数如下:

                      int main(){

                      HANDLE m_hIOCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0 );

                      SYSTEM_INFO msi; //确定处理器的核心数量

                      GetSystemInfo(&msi);

                      for (int i = 0; i < msi.dwNumberOfProcessors*2; i++){

                      HANDLE hThread=CreateThread(NULL, 0, _WorkerThread,m_hIOCompletionPort ,0,NULL);
                      CloseHandle(hThread);
                      }

                      WSADATA wsaData;

                      WSAStartup(MAKEWORD(2, 2), &wsaData); //MAKEWORD(2, 2)版本号

                      SOCKET _socket=WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);

                      SOCKADDR_IN addrServer;

                      addrServer.sin_addr.S_un.S_addr = htonl(INADDR_ANY);//inet_addr("0.0.0.0");
                        addrServer.sin_family = AF_INET;
                        addrServer.sin_port = htons(81);

                        bind(_socket, (SOCKADDR*)&addrServer, sizeof(SOCKADDR)); //绑定  

                        listen(_socket, 2);//监听  

                        cout<<"等待连接....."<<endl;

                        while(1){

                          int len = sizeof(SOCKADDR);

                          SOCKADDR_IN addr;

                          PER_HANDLE_DATA * PerHandleData = NULL;
                            
                          SOCKET Clietnt=accept(_socket,(SOCKADDR*)&addr,&len);

                      cout<<"客户端连接成功......."<<endl;

                      PerHandleData = (LPPER_HANDLE_DATA)GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA)); // 在堆中为这个PerHandleData申请指定大小的内存
                      PerHandleData -> socket = Clietnt;
                      memcpy (&PerHandleData -> ClientAddr, &addr, len);

                      if(CreateIoCompletionPort((HANDLE)Clietnt,m_hIOCompletionPort,(ULONG_PTR)PerHandleData,0)==NULL){

                      cout<<"客户端绑定失败"<<endl;

                      closesocket(Clietnt);
                      }

                      else{

                      _IO_DATA* data=new _IO_DATA;

                      data = (_IO_DATA*)GlobalAlloc(GPTR, sizeof(_IO_DATA));

                      memset(&data->m_Overlapped,0,sizeof(data->m_Overlapped));

                      data->opCode=IO_READ;

                      data->wsabuf.buf=data->buff;
                      data->wsabuf.len=2048;

                      DWORD nByte,dwFalgs=0;

                      int iRet=WSARecv(Clietnt,&data->wsabuf,1,&nByte,&dwFalgs,&data->m_Overlapped,NULL);

                      if(iRet==SOCKET_ERROR&&(GetLastError()!=ERROR_IO_PENDING)){
                      cout<<"接受失败"<<endl;
                      closesocket(Clietnt);
                      delete data;
                      continue;
                      }
                      }
                      }

                      closesocket(_socket);

                      WSACleanup();

                      return 0;
                      }

                      因为细节方面忽略了很多,可能存在bug,所以先上main函数的代码了,下一期补充完全代码。

                      最终的效果呢就是这样,客户端:

                      服务器里面的服务端:



                      下一期:按照IOCP原理流程,一步一步实现代码!



                      点击上方蓝字关注我们



                      文章转载自小懵白生活小趣谈,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                      评论