利用select异步I/O模型实现群聊

来源:互联网 发布:iea数据公布 编辑:程序博客网 时间:2024/06/14 09:27

之前所有写的socket程序都是“同步阻塞”的,这里的“同步”是指,应用中的函数调用与相应的操作系统内核中的函数是同步的,“阻塞”指的是当accept,recv,send等函数还没有确认/接收/发送时,相应的线程处于等待状态,无法继续往下执行。

“同步阻塞”虽然易于理解与实现,但是是一种效率很低的模式,因为当阻塞的时候,这个线程是不能干任何事情的,因此,“异步非阻塞”是一种效率更高的方式。

利用

int iMode = 1;
retVal = ioctlsocket(sHost, FIONBIO, (u_long FAR*) &iMode);可以将sHost这个套接字设置成非阻塞模式。

设置成非阻塞模式以后,需要将accept等阻塞函数放在一个死循环里面,因为非阻塞状态下这些函数一旦执行,不管成功与否都会接着往下执行,如果还没有到相应的条件(如recv函数的条件是收到了字符串)就会返回一个错误值WSAWOULDBLOCK,如果返回的是这个错误值的话,说明函数本没有出错,只是依然“被阻塞”,条件还没到达而已,所以需要继续执行循环,如果返回了错误,而且不是WSAWOULDBLOCK,则说明是真的出错了。

select模型是一种异步I/O模型,使用fd_set来管理套接字池。

typedef struct fd_set {  u_int fd_count;   SOCKET fd_array[FD_SETSIZE]; } fd_set;
select函数可以将指定的fd_set中处于就绪状态的套接字筛选出来。

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

参数说明如下:
nfds,只为与Berkeley套接字相兼容而保留此参数,在执行函数时会被忽略。
readfds,用于检测可读性的套接字集合。返回的fd_set已经是经过筛选的就绪套接字了
writefds,用于检测可写性的套接字集合。同上
exceptfds,用于检测存在错误的套接字集合。同上
timeout,select()函数等待的最长时间。如果是阻塞模式的操作,则将此参数设置为null,表示永不超时。

返回值是就绪套接字数目的总和,返回处于就绪状态并且已经包含在fd_set结构中的描述字总数;如果超时则返回0;否则的话,返回SOCKET_ERROR错误。

套接字集合操作宏:
FD_CLR(s, *set):从集合中删除指定的套接字。
FD_ISSET(s, *set):如果参数s是集合中的成员,则返回非0值,否则返回0。
FD_SET(s, *set):向集合中添加套接字。
FD_ZERO(s, *set):将集合初始化为空集合。

基于此模型,可以简单实现一个多人群聊
不断将所有套接字放进读就绪套接字集合和写就绪套接字集合中,然后一次select筛选出真正就绪的套接字,如果是读套接字的话,判断是不是监听套接字(监听套接字只需要 一个),如果是的话说明有连接请求,accept就行了,如果不是监听套接字说明是有数据可读,读出来,更新这个套接字的DataBuf和Buffer即可。如果是写套接字,则查看DataBuf和Buffer是否为空,不为空则需要发送数据,为空则略过。
使用SOCKET_INFORMATION保存套接字信息,接收到字符串以后存在Buffer和DataBuf中,如果DataBuf.len>0的话,说明接收到了字符串,需要转发给其它除了监听socket 和本socket以外的所有已连接的socket。发送完毕以后Buffer和DataBuf清零,表示这个socket没有数据需要发送了。
服务器端代码如下:
// select_test.cpp : 定义控制台应用程序的入口点。//#include "stdafx.h"#include <stdio.h>#include <winsock2.h>#pragma comment(lib,"WS2_32.lib")   #define DATA_BUFSIZE 1024#define MAX_SOCKET_NUM 1024#define SERVER_PORT 9990// 定义套接字信息typedef   struct   _SOCKET_INFORMATION   {CHAR   Buffer[DATA_BUFSIZE];// 发送和接收数据的缓冲区WSABUF   DataBuf;// 定义发送和接收数据缓冲区的结构体,包括缓冲区的长度和内容SOCKET   Socket;// 与客户端进行通信的套接字DWORD   BytesSEND;// 保存套接字发送的字节数DWORD   BytesRECV;// 保存套接字接收的字节数} SOCKET_INFORMATION, *LPSOCKET_INFORMATION;int TotalSockets;LPSOCKET_INFORMATION SocketArray[MAX_SOCKET_NUM];// 从数组SocketArray中删除指定的LPSOCKET_INFORMATION对象void   FreeSocketInformation(DWORD   Index){LPSOCKET_INFORMATION SI = SocketArray[Index];// 获取指定索引对应的LPSOCKET_INFORMATION对象DWORD   i;// 关闭套接字closesocket(SI->Socket);GlobalFree(SI);// 将数组中index索引后面的元素前移for (i = Index; i < TotalSockets; i++){SocketArray[i] = SocketArray[i + 1];}TotalSockets--;// 套接字总数减1}BOOL   CreateSocketInformation(SOCKET   s){LPSOCKET_INFORMATION   SI;if ((SI = (LPSOCKET_INFORMATION)GlobalAlloc(GPTR, sizeof(SOCKET_INFORMATION))) == NULL){printf("GlobalAlloc()   failed   with   error   %d\n", GetLastError());return   FALSE;}// 初始化SI的值    SI->Socket = s;SI->BytesSEND = 0;SI->BytesRECV = 0;// 在SocketArray数组中增加一个新元素,用于保存SI对象 SocketArray[TotalSockets] = SI;TotalSockets++;// 增加套接字数量return(TRUE);}int main(int argc, char* argv[]){int Ret;SOCKET AcceptSocket;WSADATA wsaData;DWORD recvbytes, sendbytes; //接收和发送的字节数// 初始化WinSock环境if ((Ret = WSAStartup(0x0202, &wsaData)) != 0){printf("WSAStartup()   failed   with   error   %d\n", Ret);WSACleanup();return -1;}SOCKET ListenSocket;// 创建用于监听的套接字 if ((ListenSocket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0,WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET){printf("WSASocket()   failed   with   error   %d\n", WSAGetLastError());return -1;}sockaddr_in InternetAddr;// 设置监听地址和端口号InternetAddr.sin_family = AF_INET;InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);InternetAddr.sin_port = htons(SERVER_PORT);// 绑定监听套接字到本地地址和端口if (bind(ListenSocket, (PSOCKADDR)&InternetAddr, sizeof(InternetAddr)) == SOCKET_ERROR){printf("bind()   failed   with   error   %d\n", WSAGetLastError());return -1;}// 开始监听if (listen(ListenSocket, 5)){printf("listen()   failed   with   error   %d\n", WSAGetLastError());return -1;}// 设置为非阻塞模式ULONG NonBlock = 1;if (ioctlsocket(ListenSocket, FIONBIO, &NonBlock) == SOCKET_ERROR){printf("ioctlsocket() failed with error %d\n", WSAGetLastError());return -1;}CreateSocketInformation(ListenSocket);while (1){fd_set ReadSet, WriteSet;FD_ZERO(&ReadSet);FD_ZERO(&WriteSet);FD_SET(ListenSocket, &ReadSet);for (int i = 0; i <= TotalSockets - 1; i++){LPSOCKET_INFORMATION psi = SocketArray[i];FD_SET(SocketArray[i]->Socket, &ReadSet);FD_SET(SocketArray[i]->Socket, &WriteSet);}int TotalReady;TotalReady = select(0, &ReadSet, &WriteSet, NULL, NULL);if (TotalReady == SOCKET_ERROR){printf("select error with %d\n", WSAGetLastError());return -1;}for (int i = 0; i <= TotalSockets - 1; i++){LPSOCKET_INFORMATION psi = SocketArray[i];if (FD_ISSET(psi->Socket, &ReadSet)){if (psi->Socket == ListenSocket){TotalReady--;AcceptSocket = accept(ListenSocket, NULL, NULL);if (AcceptSocket == INVALID_SOCKET&&WSAGetLastError() != WSAEWOULDBLOCK){printf("accept failed\n");return -1;}else if (AcceptSocket != INVALID_SOCKET){DWORD NonBlock = 1;Ret = ioctlsocket(AcceptSocket, FIONBIO, &NonBlock);if (Ret == SOCKET_ERROR){printf("ioctlsocket failed\n");return -1;}if (CreateSocketInformation(AcceptSocket) == false){printf("create socket information failed\n");return -1;}}}else{if (FD_ISSET(psi->Socket, &ReadSet)){TotalReady--;memset(psi->Buffer, 0, sizeof(psi->Buffer));psi->DataBuf.buf = psi->Buffer;psi->DataBuf.len = DATA_BUFSIZE;DWORD flag = 0;Ret = WSARecv(psi->Socket, &(psi->DataBuf), 1, &recvbytes, &flag, NULL, NULL);if (Ret == SOCKET_ERROR){if (WSAGetLastError() != WSAEWOULDBLOCK){printf("WSARecv failed\n");FreeSocketInformation(i);}continue;}else{psi->BytesRECV = recvbytes;if (recvbytes == 0){FreeSocketInformation(i);continue;}else{psi->DataBuf.buf[recvbytes] = '\0';printf("%s\n", psi->DataBuf.buf);}}}}}else{if (FD_ISSET(psi->Socket, &WriteSet)){TotalReady--;psi->DataBuf.buf = psi->Buffer + psi->BytesSEND;psi->DataBuf.len = psi->BytesRECV - psi->BytesSEND;if (psi->DataBuf.len > 0)//这个socket有还数据,需要转发出去{for (int j = 0; j <= TotalSockets - 1 ; j++)//挨个转发给其它socket{LPSOCKET_INFORMATION tpsi;tpsi = SocketArray[j];if (tpsi->Socket == ListenSocket||i==j)//转发给除了监听socket和本socket以外的所有连接的socket{continue;}Ret = WSASend(tpsi->Socket, &(psi->DataBuf), 1, &sendbytes, 0, NULL, NULL);if (Ret == SOCKET_ERROR&&Ret != WSAEWOULDBLOCK){printf("WSASend failed %d\n",WSAGetLastError());FreeSocketInformation(j);}else if (Ret == SOCKET_ERROR&&Ret == WSAEWOULDBLOCK){continue;}else if (Ret != SOCKET_ERROR){printf("send succeed\n");}}psi->BytesSEND = psi->BytesSEND + sendbytes;//已经发送了的字节数,怕字节数过多,一次发送不完if (psi->BytesSEND == psi->BytesRECV)//转发完毕,socket缓冲区清零{psi->BytesRECV = 0;psi->BytesSEND = 0;}}}}}}system("pause");return 0;}
客户端依然是开两个线程,一个发送一个接收,注意套接字设置为非阻塞后要将部分语句放在死循环里面判断
客户端代码如下:
// TcpClient.cpp : 定义控制台应用程序的入口点。//#include "stdafx.h"#include <Winsock2.H>   #include <string>#include <iostream>#include <windows.h>#pragma comment(lib,"WS2_32.lib")   #define BUF_SIZE    64          // 缓冲区大小  #define SERVER_IP "10.21.38.14"#define SERVER_PORT 9990SOCKET      sHost;// 与服务器进行通信的套接字   DWORD WINAPI send(LPVOID p){char buf[BUF_SIZE];// 用于接受数据缓冲区int retVal;while (1){// 向服务器发送数据   // 接收输入的数据std::string str;std::getline(std::cin, str);// 将用户输入的数据复制到buf中ZeroMemory(buf, BUF_SIZE);strcpy(buf, str.c_str());// 循环等待while (true){// 向服务器发送数据retVal = send(sHost, buf, strlen(buf), 0);if (SOCKET_ERROR == retVal){int err = WSAGetLastError();if (err == WSAEWOULDBLOCK)// 无法立即完成非阻塞套接字上的操作{Sleep(500);continue;}else{printf("send failed !\n");closesocket(sHost);WSACleanup();return -1;}}break;}}return 0;}DWORD WINAPI recv(LPVOID){char buf[BUF_SIZE];int retVal;while (1){while (true){ZeroMemory(buf, BUF_SIZE);// 清空接收数据的缓冲区retVal = recv(sHost, buf, sizeof(buf) + 1, 0);   // 接收服务器回传的数据   if (SOCKET_ERROR == retVal){int err = WSAGetLastError();// 获取错误编码if (err == WSAEWOULDBLOCK)// 接收数据缓冲区暂无数据{Sleep(100);continue;}else if (err == WSAETIMEDOUT || err == WSAENETDOWN){printf("recv failed !\n");closesocket(sHost);WSACleanup();return -1;}break;}break;}printf("Recv From Server: %s\n", buf);}}int main(){WSADATA     wsd;// 用于初始化Windows Socket   SOCKADDR_IN servAddr;// 服务器地址   char        buf[BUF_SIZE];// 用于接受数据缓冲区   int         retVal;// 调用各种Socket函数的返回值   // 初始化Windows Socketif (WSAStartup(MAKEWORD(2, 2), &wsd) != 0){printf("WSAStartup failed !\n");return 1;}// 创建套接字   sHost = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);if (INVALID_SOCKET == sHost){printf("socket failed !\n");WSACleanup();return -1;}// 设置套接字为非阻塞模式int iMode = 1;retVal = ioctlsocket(sHost, FIONBIO, (u_long FAR*) &iMode);if (retVal == SOCKET_ERROR){printf("ioctlsocket failed !\n");WSACleanup();return -1;}// 设置服务器地址   servAddr.sin_family = AF_INET;servAddr.sin_addr.S_un.S_addr = inet_addr(SERVER_IP);servAddr.sin_port = htons(SERVER_PORT);// 在实际应用中,建议将服务器的IP地址和端口号保存在配置文件中int sServerAddlen = sizeof(servAddr);// 计算地址的长度       // 循环等待while (true){// 连接服务器   Sleep(200);retVal = connect(sHost, (LPSOCKADDR)&servAddr, sizeof(servAddr));Sleep(200);if (SOCKET_ERROR == retVal){int err = WSAGetLastError();if (err == WSAEWOULDBLOCK || err == WSAEINVAL)// 无法立即完成非阻塞套接字上的操作{//Sleep(500);continue;}else if (err == WSAEISCONN)// 已建立连接{break;}else{continue;}}}printf("连接群聊服务器成功\n");DWORD send_id, recv_id;CreateThread(NULL, 0, send, 0, 0, &send_id);CreateThread(NULL, 0, recv, 0, 0, &recv_id);while (1){}closesocket(sHost);WSACleanup();// 暂停,按任意键继续system("pause");return 0;}


多机测试也是通过的。




0 0