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

之前所有写的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 9990

SOCKET      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 Socket
	if (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;
}



多机测试也是通过的。




已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页