分享

Windows Socket 异步编程(非阻塞)

 myallmy 2023-02-03 发布于北京
                     使用Select异步模式来实现返送示例。服务器启动并监听9999端口,并将收到的客户端信息打印并返送给客户端。

重点理解的是:一个套接字是否是可读、可写状态。当服务器端socket在Accept成功之后,便是可读状态,接收客户端发送数据。当客户端发送recv函数时,这个socket便成为可写状态,服务器端便知道这个客户端可写,然后根据自己的定义发送给客户端内容。如果客户端不发送recv函数,即下面Client中的recv函数的话,服务器端保存的客户端这个socket便没有进入可写状态的时候,也就不会有回送的情况发生。

服务端:

  1. #include <WINSOCK2.H>
  2. #include <iostream>
  3. #pragma comment(lib,"WS2_32.lib")
  4. using namespace std;
  5. #define PORT 9999
  6. #define DATA_BUFSIZE 8192
  7. // 定义套接字信息
  8. typedef struct _SOCKET_INFORMATION {
  9. CHAR Buffer[DATA_BUFSIZE]; // 发送和接收数据的缓冲区
  10. WSABUF DataBuf; // 定义发送和接收数据缓冲区的结构体,包括缓冲区的长度和内容
  11. SOCKET Socket; // 与客户端进行通信的套接字
  12. DWORD BytesSEND; // 保存套接字发送的字节数
  13. DWORD BytesRECV; // 保存套接字接收的字节数
  14. } SOCKET_INFORMATION, * LPSOCKET_INFORMATION;
  15. DWORD TotalSockets = 0; // 记录正在使用的套接字总数量
  16. LPSOCKET_INFORMATION SocketArray[FD_SETSIZE]; // 保存Socket信息对象的数组,FD_SETSIZE表示SELECT模型中允许的最大套接字数量
  17. // 创建SOCKET信息
  18. BOOL CreateSocketInformation(SOCKET s)
  19. {
  20. LPSOCKET_INFORMATION SI; // 用于保存套接字的信息
  21. // printf("Accepted socket number %d\n", s); // 打开已接受的套接字编号
  22. // 为SI分配内存空间
  23. if ((SI = (LPSOCKET_INFORMATION) GlobalAlloc(GPTR, sizeof(SOCKET_INFORMATION))) == NULL)
  24. {
  25. printf("GlobalAlloc() failed with error %d\n", GetLastError());
  26. return FALSE;
  27. }
  28. // 初始化SI的值
  29. SI->Socket = s;
  30. SI->BytesSEND = 0;
  31. SI->BytesRECV = 0;
  32. // 在SocketArray数组中增加一个新元素,用于保存SI对象
  33. SocketArray[TotalSockets] = SI;
  34. TotalSockets++; // 增加套接字数量
  35. return(TRUE);
  36. }
  37. // 从数组SocketArray中删除指定的LPSOCKET_INFORMATION对象
  38. void FreeSocketInformation(DWORD Index)
  39. {
  40. LPSOCKET_INFORMATION SI = SocketArray[Index]; // 获取指定索引对应的LPSOCKET_INFORMATION对象
  41. DWORD i;
  42. closesocket(SI->Socket); // 关闭套接字
  43. GlobalFree(SI); // 释放指定LPSOCKET_INFORMATION对象资源
  44. // 将数组中index索引后面的元素前移
  45. if (Index != (TotalSockets-1))
  46. {
  47. for (i = Index; i < TotalSockets; i++)
  48. {
  49. SocketArray[i] = SocketArray[i+1];
  50. }
  51. }
  52. TotalSockets--; // 套接字总数减1
  53. }
  54. int main()
  55. {
  56. SOCKET ListenSocket; // 监听套接字
  57. SOCKET AcceptSocket; // 与客户端进行通信的套接字
  58. SOCKADDR_IN InternetAddr; // 服务器的地址
  59. WSADATA wsaData; // 用于初始化套接字环境
  60. INT Ret; // WinSock API的返回值
  61. FD_SET WriteSet; // 获取可写性的套接字集合
  62. FD_SET ReadSet; // 获取可读性的套接字集合
  63. DWORD Total = 0; // 处于就绪状态的套接字数量
  64. DWORD SendBytes; // 发送的字节数
  65. DWORD RecvBytes; // 接收的字节数
  66. // 初始化WinSock环境
  67. if ((Ret = WSAStartup(MAKEWORD(2,2),&wsaData)) != 0)
  68. {
  69. printf("WSAStartup() failed with error %d\n", Ret);
  70. WSACleanup();
  71. return -1;
  72. }
  73. // 创建用于监听的套接字
  74. if ((ListenSocket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET)
  75. {
  76. printf("WSASocket() failed with error %d\n", WSAGetLastError());
  77. return -1;
  78. }
  79. // 设置监听地址和端口号
  80. InternetAddr.sin_family = AF_INET;
  81. InternetAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
  82. InternetAddr.sin_port = htons(PORT);
  83. // 绑定监听套接字到本地地址和端口
  84. if(bind(ListenSocket, (PSOCKADDR)&InternetAddr, sizeof(InternetAddr)) == SOCKET_ERROR)
  85. {
  86. printf("bind() failed with error %d\n", WSAGetLastError());
  87. return -1;
  88. }
  89. // 开始监听
  90. if (listen(ListenSocket, 5))
  91. {
  92. printf("listen() failed with error %d\n", WSAGetLastError());
  93. return -1;
  94. }
  95. // 设置为非阻塞模式
  96. ULONG NonBlock = 1;
  97. if(ioctlsocket(ListenSocket, FIONBIO, &NonBlock) == SOCKET_ERROR)
  98. {
  99. printf("ioctlsocket() failed with error %d\n", WSAGetLastError());
  100. return -1;
  101. }
  102. CreateSocketInformation(ListenSocket);// 为ListenSocket套接字创建对应的SOCKET_INFORMATION,把ListenSocket添加到SocketArray数组中
  103. while(TRUE)
  104. {
  105. FD_ZERO(&ReadSet);// 准备用于网络I/O通知的读/写套接字集合
  106. FD_ZERO(&WriteSet);
  107. FD_SET(ListenSocket, &ReadSet);// 向ReadSet集合中添加监听套接字ListenSocket
  108. // 将SocketArray数组中的所有套接字添加到WriteSet和ReadSet集合中,SocketArray数组中保存着监听套接字和所有与客户端进行通信的套接字
  109. // 这样就可以使用select()判断哪个套接字有接入数据或者读取/写入数据
  110. for (DWORD i=0; i<TotalSockets; i++)
  111. {
  112. LPSOCKET_INFORMATION SocketInfo = SocketArray[i];
  113. FD_SET(SocketInfo->Socket, &ReadSet);//这说明该socket有读操作。而读操作是客户端发起的
  114. FD_SET(SocketInfo->Socket, &WriteSet);//这说明该socket有写操作。
  115. }
  116. // 判断读/写套接字集合中就绪的套接字
  117. if((Total = select(0, &ReadSet, &WriteSet, NULL, NULL)) == SOCKET_ERROR)//将NULL以形参传入Timeout,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止.服务器会停到这里等待客户端相应
  118. {
  119. printf("select() returned with error %d\n", WSAGetLastError());
  120. return -1;
  121. }
  122. // 依次处理所有套接字。本服务器是一个回应服务器,即将从客户端收到的字符串再发回到客户端。
  123. for (DWORD i=0; i<TotalSockets; i++)
  124. {
  125. LPSOCKET_INFORMATION SocketInfo = SocketArray[i]; // SocketInfo为当前要处理的套接字信息
  126. // 判断当前套接字的可读性,即是否有接入的连接请求或者可以接收数据
  127. if (FD_ISSET(SocketInfo->Socket, &ReadSet))
  128. {
  129. if(SocketInfo->Socket == ListenSocket) // 对于监听套接字来说,可读表示有新的连接请求
  130. {
  131. Total--; // 就绪的套接字减1
  132. // 接受连接请求,得到与客户端进行通信的套接字AcceptSocket
  133. if((AcceptSocket = accept(ListenSocket, NULL, NULL)) != INVALID_SOCKET)
  134. {
  135. // 设置套接字AcceptSocket为非阻塞模式
  136. // 这样服务器在调用WSASend()函数发送数据时就不会被阻塞
  137. NonBlock = 1;
  138. if(ioctlsocket(AcceptSocket, FIONBIO, &NonBlock) == SOCKET_ERROR)
  139. {
  140. printf("ioctlsocket() failed with error %d\n", WSAGetLastError());
  141. return -1;
  142. }
  143. // 创建套接字信息,初始化LPSOCKET_INFORMATION结构体数据,将AcceptSocket添加到SocketArray数组中
  144. if(CreateSocketInformation(AcceptSocket) == FALSE)
  145. return -1;
  146. }
  147. else
  148. {
  149. if(WSAGetLastError() != WSAEWOULDBLOCK)
  150. {
  151. printf("accept() failed with error %d\n", WSAGetLastError());
  152. return -1;
  153. }
  154. }
  155. }
  156. else // 接收数据
  157. {
  158. Total--; // 减少一个处于就绪状态的套接字
  159. memset(SocketInfo->Buffer, ' ', DATA_BUFSIZE); // 初始化缓冲区
  160. SocketInfo->DataBuf.buf = SocketInfo->Buffer; // 初始化缓冲区位置
  161. SocketInfo->DataBuf.len = DATA_BUFSIZE; // 初始化缓冲区长度
  162. // 接收数据
  163. DWORD Flags = 0;
  164. if(WSARecv(SocketInfo->Socket, &(SocketInfo->DataBuf), 1, &RecvBytes, &Flags,NULL, NULL) == SOCKET_ERROR)
  165. {
  166. // 错误编码等于WSAEWOULDBLOCK表示暂没有数据,否则表示出现异常
  167. if(WSAGetLastError() != WSAEWOULDBLOCK)
  168. {
  169. printf("WSARecv() failed with error %d\n", WSAGetLastError());
  170. FreeSocketInformation(i); // 释放套接字信息
  171. }
  172. continue;
  173. }
  174. else // 接收数据
  175. {
  176. SocketInfo->BytesRECV = RecvBytes; // 记录接收数据的字节数
  177. SocketInfo->DataBuf.buf[RecvBytes] = '\0';
  178. if(RecvBytes == 0) // 如果接收到0个字节,则表示对方关闭连接
  179. {
  180. FreeSocketInformation(i);
  181. continue;
  182. }
  183. else
  184. {
  185. cout << SocketInfo->DataBuf.buf << endl;// 如果成功接收数据,则打印收到的数据
  186. }
  187. }
  188. }
  189. }
  190. else
  191. {
  192. // 如果当前套接字在WriteSet集合中,则表明该套接字的内部数据缓冲区中有数据可以发送
  193. if(FD_ISSET(SocketInfo->Socket, &WriteSet))
  194. {
  195. Total--; // 减少一个处于就绪状态的套接字
  196. SocketInfo->DataBuf.buf = SocketInfo->Buffer + SocketInfo->BytesSEND; // 初始化缓冲区位置
  197. SocketInfo->DataBuf.len = SocketInfo->BytesRECV - SocketInfo->BytesSEND; // 初始化缓冲区长度
  198. if(SocketInfo->DataBuf.len > 0) // 如果有需要发送的数据,则发送数据
  199. {
  200. if(WSASend(SocketInfo->Socket, &(SocketInfo->DataBuf), 1, &SendBytes, 0, NULL, NULL) == SOCKET_ERROR)
  201. {
  202. // 错误编码等于WSAEWOULDBLOCK表示暂没有数据,否则表示出现异常
  203. if(WSAGetLastError() != WSAEWOULDBLOCK)
  204. {
  205. printf("WSASend() failed with error %d\n", WSAGetLastError());
  206. FreeSocketInformation(i); // 释放套接字信息
  207. }
  208. continue;
  209. }
  210. else
  211. {
  212. SocketInfo->BytesSEND += SendBytes; // 记录发送数据的字节数
  213. // 如果从客户端接收到的数据都已经发回到客户端,则将发送和接收的字节数量设置为0
  214. if (SocketInfo->BytesSEND == SocketInfo->BytesRECV)
  215. {
  216. SocketInfo->BytesSEND = 0;
  217. SocketInfo->BytesRECV = 0;
  218. }
  219. }
  220. }
  221. }
  222. } // 如果ListenSocket未就绪,并且返回的错误不是WSAEWOULDBLOCK(该错误表示没有接收的连接请求),则出现异常
  223. }
  224. }
  225. system("pause");
  226. return 0;
  227. }
客户端:
  1. #include <Winsock.h>
  2. #include <string>
  3. #include <iostream>
  4. #pragma comment(lib, "ws2_32.lib")
  5. using namespace std;
  6. #define BUFSIZE 64
  7. #define PORT 9999
  8. int main()
  9. {
  10. WSAData wsaData;
  11. SOCKET sHost;
  12. sockaddr_in addrServ;
  13. char buf[BUFSIZE];
  14. int retVal;
  15. if(WSAStartup(MAKEWORD(2,2), &wsaData) != 0)
  16. {
  17. cout << "WSAStartup失败!" << endl;
  18. return -1;
  19. }
  20. sHost = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  21. if (INVALID_SOCKET == sHost)
  22. {
  23. cout << "socket() 错误!" << endl;
  24. WSACleanup();
  25. return -1;
  26. }
  27. addrServ.sin_family = AF_INET;
  28. addrServ.sin_port = htons(PORT);
  29. addrServ.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
  30. retVal = connect(sHost, (LPSOCKADDR)&addrServ, sizeof(addrServ));
  31. if (SOCKET_ERROR == retVal)
  32. {
  33. cout << "connect 错误!" << endl;
  34. closesocket(sHost);
  35. WSACleanup();
  36. return -1;
  37. }
  38. while (true)
  39. {
  40. cout << "输入要发给服务器的内容" << endl;
  41. // string msg;
  42. // getline(cin, msg);
  43. char msg[BUFSIZE];
  44. cin.getline(msg, BUFSIZE);
  45. ZeroMemory(buf, BUFSIZE);
  46. strcpy(buf, msg);
  47. retVal = send(sHost, buf, strlen(buf), 0);
  48. if (SOCKET_ERROR == retVal)
  49. {
  50. cout << "发送失败" << endl;
  51. closesocket(sHost);
  52. WSACleanup();
  53. return -1;
  54. }
  55. retVal = recv(sHost, buf, sizeof(buf)+1, 0);
  56. cout << "从服务器端接收:" << buf << endl;
  57. if (strcmp(buf, "quit") == 0)
  58. {
  59. cout << "quit" << endl;
  60. break;
  61. }
  62. }
  63. closesocket(sHost);
  64. WSACleanup();
  65. return 0;
  66. }



    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多