分析
我们思考一下前面的程序有什么缺点?
我们在调试程序的时候可以发现我们服务器在启动后,如果不打开客户端就一直卡着。
乍一看可能没什么问题,因为我们只有一个客户端,如果我们有多个客户端,由于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