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

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

小懵白生活小趣谈 2021-06-21
1218
话说在上一节windows下C++实现socket通信服务器(中),为大家简要地概况如何实现IOCP。
今天呢,我就不讲理论知识了,直接上手教你如何简单实现基于IOCP 的socket server。
大体上的实现过程如下图所示:(图里拼错名字了,不改了)

这里面需要注意:有多个细节没有体现出来,只是将重要的部分展示给大家,方便快速入门IOCP。
从上图可以看得出,在windows平台下IOCP的使用是通过调用API方式实现的,相对来说容易实现。
它的原理表现为:

1、创建完成端口:CreateIoCompletionPort(...);
    HANDLE WINAPI CreateIoCompletionPort(
    _In_ HANDLE FileHandle, //已经打开的文件句柄或者空句柄,一般是客户端的句柄
    _In_opt_ HANDLE ExistingCompletionPort, //已经存在的IOCP句柄
    _In_ ULONG_PTR CompletionKey, //完成键,包含了指定I/O完成包的指定文件
    _In_ DWORD NumberOfConcurrentThreads //真正并发同时执行最大线程数,一般推介是CPU核心数*2
    );
    这里的话不需要我们引入定义变量参数,直接调用使用。
      HANDLE m_hIOCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0 );

      2、socket与完成端口绑定:CreateIoCompletionPort(...);

        CreateIoCompletionPort((HANDLE)Clietnt,m_hIOCompletionPort,(ULONG_PTR)PerHandleData,0)
        此时需要引入三个我们自己定义的变量参数,第一个参数是accept()之后的socket_client,第二个是我们创建的完成端口。
        第三个是我们自定义的结构体变量,用来存储socket套接字信息的。
        3、IO队列获取:GetQueuedCompletionStatus();
          BOOL WINAPI GetQueuedCompletionStatus(
          __in HANDLE CompletionPort, // 这个就是我们建立的那个唯一的完成端口
          __out LPDWORD lpNumberOfBytes, //这个是操作完成后返回的字节数
          __out PULONG_PTR lpCompletionKey, // 这个是返回我们建立完成端口的时候绑定的那个自定义结构体参数
          __out LPOVERLAPPED *lpOverlapped, // 这个是返回我们在连入Socket的时候一起建立的那个重叠结构
          __in DWORD dwMilliseconds // 等待完成端口的超时时间,如果线程不需要做其他的事情,那就INFINITE就行了
          );
          还是一样,需要传入我们自定义的变量作为参数获取值。其中,这里有两个参数是比较重要的。
          分别是:lpCompletionKey,lpOverlapped
          这两个传值回来,使用到WSARecv()、WSASend()里面使用。
          为此,定义两个结构体变量:
            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;

            当然了还有:PostQueuedCompletionStatus(),该函数是向完成队列里面发送一个完成包,用来结束线程的也,还可以作为WASSend()来发送数据的。



            大体上,按照流程图实现就没啥问题了。大家可以封装一个类,三个流程可以对应三个类函数。

            这里的话要求大家自己去动手实现,这里不在多述了。

            worker线程函数如下:

              DWORD WINAPI _WorkerThread(LPVOID WordThreadPort){

              HANDLE g_hIocp=(HANDLE)WordThreadPort; //建立好的完成端口

              DWORD dwIoSize=0;

              LPPER_HANDLE_DATA lpCompletionKey=NULL; //结构体1变量

              LPOVERLAPPED lpOverlapped=NULL;

              _IO_DATA* _IO_Context=NULL; // 结构体2变量

              DWORD dwFlages=0;
              DWORD nBytes;

              int nRet=0;

              while(1){

              if(GetQueuedCompletionStatus(g_hIocp,&dwIoSize,(PULONG_PTR)&lpCompletionKey,(LPOVERLAPPED*)&lpOverlapped,INFINITE)!=true)
              cout<<"error"<<endl;

              _IO_Context=(_IO_DATA*)CONTAINING_RECORD(lpOverlapped,_IO_DATA,m_Overlapped);

              if(dwIoSize==0){
              cout<<"客户端断开连接....."<<endl;
              closesocket(lpCompletionKey->socket);
              delete _IO_Context;
              continue;
              }

              WaitForSingleObject(hMutex,INFINITE);

              cout<<"_IO_Context data:"<<_IO_Context->wsabuf.buf<<endl;

              ReleaseMutex(hMutex);

              memset(_IO_Context->wsabuf.buf,'\0',sizeof(_IO_Context->wsabuf.buf));

              ZeroMemory(&(_IO_Context->m_Overlapped),sizeof(OVERLAPPED));

              _IO_Context->wsabuf.len=2048;

              _IO_Context->wsabuf.buf=_IO_Context->buff;

              nRet=WSARecv(lpCompletionKey->socket,& _IO_Context->wsabuf,1,&nBytes,&dwFlages,&_IO_Context->m_Overlapped,NULL);

              if(nRet==SOCKET_ERROR&&(ERROR_IO_PENDING!=WSAGetLastError())){

              cout<<"失败:"<<WSAGetLastError()<<endl;
              closesocket(lpCompletionKey->socket);
              delete _IO_Context;
              continue;
                    }
              }

              return 0;
              }
              代码补充说明:
                #include <iostream>  头文件
                #include <Winsock2.h>
                #include <windows.h>


                using namespace std;


                HANDLE hMutex = CreateMutex(NULLFALSENULL);  //用于打印Client发送过来的数据
                enum IO_OPERATION{IO_WRITE,IO_READ}; //读写状态枚举


                struct _IO_DATA{
                int _Bytes;
                  IO_OPERATION opCode;  //自行在代码里添加补充什么时候实现发送与接收的操作
                WSABUF wsabuf;
                OVERLAPPED m_Overlapped;
                char buff[2048];
                };


                typedef struct
                {
                SOCKET socket;
                SOCKADDR_STORAGE ClientAddr;

                }PER_HANDLE_DATA, *LPPER_HANDLE_DATA;


                DWORD WINAPI _WorkerThread(LPVOID WordThreadPort);


                main函数:

                  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;
                  }
                  里面还有很多细节自己去实现了,比如说报错什么的,不过功能是可以实现的。


                  最后的话,就是考虑实现MySQL数据库的添加使用了,作为客户端需要保存的数据,像QQ一样。保存聊天记录,用户信息等。
                  大体上实现就是先建立连接本地MySQL:
                    bool QQ_Users::Init_MySQL(){

                    mysql_init(&user);

                    if(NULL == mysql_real_connect(&user, "localhost", "root", password_.c_str(), "MYSQL", 3306, NULL, 0))

                    return false;

                    else return true;
                    }

                    建立好以后,接着创建数据库和表了:

                      bool QQ_Users::create_database_table(const string sql){


                      if (mysql_query(&user, sql.c_str())){

                      cout << "line: " << __LINE__ << ";"<< mysql_error(&user) << mysql_errno(&user) << endl;

                      return false;
                      }

                      cout<<"successfully established"<<endl<<endl;

                      return true;
                      }

                      最后,就是插入数据和读取数据了。

                      关于IOCP的socket服务端就介绍到这里了,由于云服务器是一核的,所以高效的线程数量建议为2个worker线程。
                      也就是说也只是个轻量级的高并发,适用于小型学生项目了。
                      下一期为大家提供如何模拟QQ聊天系统服务端的实现思路,MySQL数据库的设计。


                      点击上方蓝字关注我们



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

                      评论