windows socket网络编程二:select模型

分析

我们思考一下前面的程序有什么缺点?
我们在调试程序的时候可以发现我们服务器在启动后,如果不打开客户端就一直卡着。
乍一看可能没什么问题,因为我们只有一个客户端,如果我们有多个客户端,由于accept、recv是傻等阻塞的,做其中一件事,另外一件事就做不了,在傻傻的等着,服务器完全无法正常工作。

解决方法:
我们可以给操作系统一组SOCKET,让系统帮我们监视着SOCKET的动向,哪个SOCKET有请求,请求类型(accept、recv、send)是什么。这样我们就可以明确知道要处理什么了。
也就是select模型,select就是挑选的意思,它把请求的套接字给我们选出来,我们直接就去处理这些套接字。

服务器

直到开始监听都是一模一样的

定义一个装socket的结构

#ifndef FD_SETSIZE
#define FD_SETSIZE      64
#endif /* FD_SETSIZE */

typedef struct fd_set {
  u_int  fd_count;
  SOCKET fd_array[FD_SETSIZE];
} fd_set, FD_SET, *PFD_SET, *LPFD_SET;

用来存放一组socket,投递给系统。
成员:
fd_count — socket使用数量
fd_array — socket数组(通过修改FD_SETSIZE的定义就能修改存放socket的最大数量)

对应的宏操作:

宏操作操作
FD_ZERO集合清0(将fd_count置零)
FD_SET当数量不足FD_SETSIZE,并且集合中不存在的时候向集合中添加一个socket
FD_CLR集合中删除指定socket
FD_ISSET判断一个socket是否在集合中,不在返回0,在返回非0

代码:

fd_set allSockets;
// 清零
FD_ZERO(&allSockets);
// 添加服务器socket
FD_SET(socketServer, &allSockets);

select

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

功能:监视socket集合,如果某个socket发生事件(链接或者收发数据),通过返回值以及参数告诉我们
参数:

  • nfds — 忽略,填0,这个参数仅仅是为了兼容Berkeley sockets。
  • readfds — 检查是否有可读的socket,系统将有事件发生的socket再赋值回来,调用后,这个参数就只剩下有请求的socket。
  • writefds — 检查是否有可写的socket,系统将可写的socket再赋值回来,调用后,这个参数就只剩下可以被send数据的客户端socket。(只要链接成功建立起来了,那该客户端套接字就是可写的)
  • exceptfds — 检查套接字上的异常错误,系统将有错误的socket再赋值回来,调用后,这个参数就只剩下有错误的socket。
  • timeout — 最大等待时间,NULL为完全阻塞。

返回值

  • 返回0,则在等待的时间内socket没有反应
  • socket已经准备就绪的数量
  • SOCKET_ERROR,则发生错误

代码:

// 时间段
struct timeval st;
st.tv_sec = 3;
st.tv_usec = 0;

fd_set readSockets = allSockets;		// recv
fd_set writeSockets = allSockets;		// send
FD_CLR(socketServer, &writeSockets);	// 可以有也可以没有
fd_set errorSockets = allSockets;		// 错误

int nRes = select(0, &readSockets, &writeSockets, &errorSockets, &st);

处理错误

int WSAAPI getsockopt(
  SOCKET s,
  int    level,
  int    optname,
  char   *optval,
  int    *optlen
);

功能:检索套接字选项
参数:

  • s — socket
  • level — 选项的级别
  • optname — 套接字选项
  • optval — 指向缓冲区的指针
  • optlen — optval指向缓冲区的大小(以字节为单位)

返回值:

  • 如果成功,返回0。
  • 如果失败,返回SOCKET_ERROR。

代码:

// 处理错误
for (u_int i = 0; i < errorSockets.fd_count; ++i)
{
	char str[100] = { 0 };
	int len = 99;
	if (SOCKET_ERROR == getsockopt(errorSockets.fd_array[i], SOL_SOCKET, SO_ERROR, str, &len))
	{
		printf("getsockopt 失败 error:%d\n", WSAGetLastError());
	}
	else
	{
		printf("客户端(socket:%d)发生错误:%s\n", errorSockets.fd_array[i], str);
	}
}

处理可写

我们可以在处理这边send,不过只要链接成功建立起来了,那该客户端套接字就是可写的,所以我们也可以在其他地方send。

代码:

// 可发送
for (u_int i = 0; i < writeSockets.fd_count; ++i)
{
	// 可send
}

处理可读

因为我们投递给系统的socket有服务器和客户端两种,所以要分类处理,服务器是accept,客户端是recv。

代码:

// 有响应
for (u_int i = 0; i < readSockets.fd_count; ++i)
{
	if (readSockets.fd_array[i] == socketServer)	// 服务器
	{
		// accept
		SOCKET socketClient = accept(socketServer, NULL, NULL);
		if (INVALID_SOCKET == socketClient)	// 链接出错
		{
			printf("accept 失败 error:%d\n", WSAGetLastError());
			continue;
		}
		printf("客户端连接 socket:%d\n", socketClient);

		// 添加到待监听
		FD_SET(socketClient, &allSockets);
	}
	else	// 客户端
	{
		// 接收消息
		char szRecvBuffer[1500] = { 0 };
		int result = recv(readSockets.fd_array[i], szRecvBuffer, 1500, 0);
		if (0 == result)	// 客户端正常关闭
		{
			// 从集合中拿掉
			SOCKET socketTemp = readSockets.fd_array[i];
			FD_CLR(readSockets.fd_array[i], &allSockets);
			closesocket(socketTemp);	// 释放socket
			printf("客户端正常下线\n");
		}
		else if (SOCKET_ERROR == result)	// recv出错
		{
			int error = WSAGetLastError();
			switch (error)
			{
				case 10054:	// 强制下线
				{
					SOCKET socketTemp = readSockets.fd_array[i];
					FD_CLR(readSockets.fd_array[i], &allSockets);
					closesocket(socketTemp);	// 释放socket
					printf("客户端异常下线\n");
					break;
				}
				default:
					printf("recv 失败 error:%d\n", error);
					break;
			}
		}
		else
		{
			// 接收到客户端消息 
			printf("Client Data : %s \n", szRecvBuffer);

			// 给客户回信
			if (SOCKET_ERROR == send(readSockets.fd_array[i], "ok", strlen("ok") + 1, 0))
			{
				printf("send 失败 error:%d\n", WSAGetLastError());
				break;
			}
		}
	}
}

关闭服务器

程序中只有select出错服务器才会退出,所以我们只能按窗口的x退出,在退出前需要做socket的释放。

BOOL WINAPI SetConsoleCtrlHandler(
  _In_opt_ PHANDLER_ROUTINE HandlerRoutine,
  _In_     BOOL             Add
);

功能:从调用过程的处理程序函数列表中添加或删除应用程序定义的HandlerRoutine函数。
参数:

  • HandlerRoutine — 指向要添加或删除的应用程序定义的HandlerRoutine函数的指针。此参数可以为NULL。
  • Add — 如果此参数为TRUE,则添加处理程序;否则为false。如果为FALSE,则删除处理程序。

代码:

BOOL WINAPI fun(DWORD dwCtrlType)
{
	switch (dwCtrlType)
	{
	case CTRL_CLOSE_EVENT:
		// 释放所有socket
		for (u_int i = 0; i < allSockets.fd_count; ++i)
		{
			closesocket(allSockets.fd_array[i]);
		}
		// 清理网络库
		WSACleanup();
	}
	return TRUE;
}

// 退出释放内存
SetConsoleCtrlHandler(fun, TRUE);

运行结果

在这里插入图片描述

模型流程图

在这里插入图片描述

源码链接

百度云链接:https://pan.baidu.com/s/1xBOiSADlAG2gO1TC6BBO_A
提取码:sxbd

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值