在Windows环境下用C语言实现CS模型中我们详细介绍了网络通信中需要使用的几个函数并指出了最基本的CS模型存在的缺点,为了克服这些缺点,人们提出了select模型,select模型和CS模型相比有以下几个优势:
- 解决了基本CS模型中accept与recv函数等待连接和等待消息时程序的阻塞。
- 实现多个客户端连接,能够接收多个客户端的消息
- select模型仅用于服务器端
1. select模型原理
- 每个客户端都有一个socket,服务器也有自己的socket,将所有的socket放进一个数据结构里。
- 通过select函数遍历装有socket数组的数据结构,当某个select有响应时,select就会通过其相应的参数值反馈出来。
- 在这里需要对返回值进行判断,如果返回值是服务器的socket,则是客户端请求连接,这时需要调用accept函数接收连接;如果返回值是客户端的socket,则是客户端请求通信,调用send函数或recv函数收发消息。
2. select模型用法
2.1 fd_set结构体
首先我们使用一个结构体用来装客户端的socket,系统已经为我们定义了一个fd_set结构体,结构体原型如下所示:
typedef struct fd_set {
u_int fd_count; /* how many are SET? */
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set;
结构体中第一个成员fd_count是结构体成员的个数,第二个成员fd_array是一个socket类型的数组,系统将FD_SETSIZE利用宏定义定义为64,代表该数组最多有64个成员,也就是最多有64个客户端连接。
同时系统为我们定义了四个fd_set的参数宏:
- FD_ZERO,将结构体清零,FD_ZERO;
- FD_SET,向结构体中添加一个socket,添加前会检查数组中元素是否超过64和数组中是否已经存在该元素;
- FD_CLR:删除数组中指定的socket,从集合中删除一个socket后一定要closesocket,否则会造成内存泄露;
- FD_ISSET:判断一个socket是否在集合中,若不存在返回0,若存在返回非零;
2.2 select函数使用方法
select函数原型如下所示:
int WSAAPI select{
int nfds;
fd_set *readfds;
fd_set *writefds;
fd_set *exceptfds;
const timeval *timeout;
};
- 参数1:忽略(填0即可),这个参数仅仅是为了兼容Berkeley sockets;
- 参数2:检查是否有可读的socket,即客户端发来了消息,该socket就会被设置。 原理:它开始时包含所有的socket,通过select函数全部投递给系统,系统将有事件发生的socket再重新赋值给参数2,这样参数2就包含了所有有事件发生的socket了。
- 参数3:检查是否有可写的socket,就是可以给哪些客户端套接字发消息,即send,只要连接成功建立起来了,那该客户端套接字就是可写的(不一定非要在参数3中使用send方法)。 原理:它开始时包含所有的socket,通过select全部投放给系统,系统将可写的socket再赋值回来,调用后这个参数就是装着可以被send信息的客户端socket上。
- 检查套接字上的异常错误,用法跟参数2/3一样,将有异常错误的套接字重新装进来,反馈给我们。
- 参数5:最大等待时间,当客户端没有请求时,那么select函数可以等一会,如果我们设置的最大等待时间过后还没有请求,那就继续执行select下面的语句;如果在最大等待时间之内有请求,则立刻执行下面的语句。
对于参数5系统也为我们定义了一个结构体如下所示:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* and microseconds */
};
我们可以利用结构体中的两个成员为参数5赋值,第一个参数tv_sec代表等待的秒数,第二个参数tv_usec代表等待的微秒数。如果两个参数均设置为0则select函数不会等待,如果参数5直接填NULL则select将完全阻塞,直至有事件响应才会向下执行(一般不要填NULL)。
6. 返回值
- 如果客户端在等待时间内没有反应,返回0;
- 如果有客户端请求交流,返回一个大于零的数;
- 如果发生了错误则返回SOCKET_ERROR,通过WSAGetLastError()得到错误码。
2.3 实现程序
服务端使用的程序listen函数以前的程序与基本CS模型是一样的,select模型优化CS模型主要体现在select函数和fd_set结构体的使用。
由于select模型优化主要体现在服务端,因此客户端使用的程序和CS模型客户端的程序是一样的,在此我们将客户端中的send函数注释掉,因为这个模型中我们只通过客户端向服务器发信息。
程序运行注意事项:我们需要首先运行起服务器,然后到客户端工程目录下,找到Debug文件夹下的exe文件,多次双击可以打开多个客户端,多个客户端都可以向服务器发信息。
//服务端
#define _CRT_SECURE_NO_WARNINGS
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
#include<stdio.h>
#include<string.h>
#include<stdbool.h>
fd_set all_Sockets;
BOOL WINAPI over(DWORD dwCtrlType)
{
switch (dwCtrlType)
{
case CTRL_CLOSE_EVENT:
//释放所有socket
for (u_int i = 0; i < all_Sockets.fd_count; i++)
{
closesocket(all_Sockets.fd_array[i]);
}
//清理网络库
WSACleanup();
}
return 0;
}
int main()
{
SetConsoleCtrlHandler(over, TRUE);//这个函数的作用是当点击运行框右上角叉号关闭时,执行上面的over函数
WORD wdVersion = MAKEWORD(2, 2);//使用网络库的版本
WSADATA wdSockMsg; //系统通过这个参数给我们一些配置信息
int nRes = WSAStartup(wdVersion, &wdSockMsg);//打开/启动网络库,只有启动了库,这个库里的函数才能使用
if (0 != nRes)
{
switch (nRes)
{
case WSASYSNOTREADY:
printf("可以重启电脑,或检查网络库");
break;
case WSAVERNOTSUPPORTED:
printf("请更新网络库");
break;
case WSAEINPROGRESS:
printf("Please reboot this software");
break;
case WSAEPROCLIM:
printf("请关闭不必要的软件,以为当前网络提供充足资源");
break;
case WSAEFAULT:
printf("参数错误");
break;
}
}
//版本校验
if (2 != HIBYTE(wdSockMsg.wVersion) || 2 != LOBYTE(wdSockMsg.wVersion))
{
//版本打开错误
WSACleanup(); //关闭网络库
return 0;
}
SOCKET socketSever = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//socket函数三个参数分别为地址类型(IPV4),套接字类型()和协议类型(TCP)
//如果执行失败则返回INVALID_SOCKET
if (INVALID_SOCKET == socketSever)
{
int a = WSAGetLastError(); //如果socket调用失败,返回错误码(工具 -> 错误查找 可以查询具体错误)
WSACleanup(); //关闭网络库
return 0;
}
struct sockaddr_in si;
si.sin_family = AF_INET; //地址类型
si.sin_port = htons(12345); //端口号
si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //IP地址
int bres = bind(socketSever, (const struct sockaddr*)&si, sizeof(si));
/*参数1:前面创建的socket
参数2:是一个结构体sockaddr(包含地址类型、端口号和IP地址)地址,官方给出结构体sockaddr不方便赋值,因此我们定义sockaddr_in
分别赋值地址类型、端口号和IP地址后,强制类型转换为sockaddr
参数3:参数2类型的大小 */
if (SOCKET_ERROR == bres)
{
//bind函数出错
int a = WSAGetLastError(); //返回错误码
closesocket(socketSever); //关闭socket
WSACleanup(); //关闭网络库
return 0;
}
//开始监听
int a = listen(socketSever, SOMAXCONN);
if (SOCKET_ERROR == a)
{
//listen 函数出错
int a = WSAGetLastError(); //返回错误码
closesocket(socketSever); //关闭socket
WSACleanup(); //关闭网络库
return 0;
}
FD_ZERO(&all_Sockets); //清零
FD_SET(socketSever, &all_Sockets);//添加服务器socket
while (1)
{
fd_set readSockets = all_Sockets;
fd_set writeSockets = all_Sockets;
fd_set errorSockets = all_Sockets;
//时间段
struct timeval timeval_a;//给参数5赋值等待时间
timeval_a.tv_sec = 3;
timeval_a.tv_usec = 0;
int select_a = select(0, &readSockets, &writeSockets, &errorSockets, &timeval_a);
//第二个参数测试recv和accept,第三个参数测试send,第四个参数测试错误
if(0 == select_a)
{
//没有响应
continue;
}
else if (select_a > 0)//有响应
{
//遍历参数4,查看select函数是否有错误返回
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))//调用getsockopt函数获取错误信息
//参数1:我们要操作的socket,参数2:socket上的情况,参数4:代表一段空间,返回的错误信息装在里面,参数5:参数4的长度
{
printf("无法得到错误信息\n");
}
printf("%s\n", str);
}
//遍历参数3,寻找找出可以给哪些客户端socket发消息
for (u_int i = 0; i < writeSockets.fd_count; i++)
{
//printf("服务器:%d,%d可写\n", socketSever, writeSockets.fd_array[i]);
if (SOCKET_ERROR == send(writeSockets.fd_array[i], "OK", 2, 0))
{
int a = WSAGetLastError();
}
}
for (u_int i = 0; i < readSockets.fd_count; i++)
{
//遍历参数2中(有响应)的socket,在这里响应的socket只可能是服务器socket和客户端socket两种可能
if (readSockets.fd_array[i] == socketSever)//如果有响应的socket是服务器socket,则是客户端请求连接,需要调用accept函数
{
//accept
SOCKET socketClient = accept(socketSever, NULL, NULL);
if (INVALID_SOCKET == socketClient)
{
continue;
}
FD_SET(socketClient, &all_Sockets); // 将刚返回的socket添加到socket数组中
}
else //如果是客户端socket则需要接收消息
{
char buf[1500] = { 0 };
int recv_a = recv(readSockets.fd_array[i], buf, 1500, 0);
if (0 == recv_a)
{
printf("客户端下线\n");
SOCKET socket_temp = readSockets.fd_array[i];
//从集合中拿掉
FD_CLR(readSockets.fd_array[i],&all_Sockets);
//释放
closesocket(socket_temp);
}
else if (0 < recv_a)
{
//接收成功
printf("%s\n", buf);
}
else
{
//recv函数出错
int a = WSAGetLastError();
}
}
}
}
else
{
printf("错误码2:%d\n", WSAGetLastError());
}
}
//释放所有socket
for (u_int i = 0; i < all_Sockets.fd_count; i++)
{
closesocket(all_Sockets.fd_array[i]);
}
WSACleanup(); //关闭网络库
return 0;
}
//客户端
#define _CRT_SECURE_NO_WARNINGS
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<stdio.h>
#include<winsock2.h>
#pragma comment(lib,"Ws2_32.lib")
//#include<string.h>
int main()
{
WORD wdVersion = MAKEWORD(2, 2); //使用网络库的版本
WSADATA wdSockMsg; //系统通过这个参数给我们一些配置信息
int nRes = WSAStartup(wdVersion, &wdSockMsg);
if (0 != nRes)
{
switch (nRes)
{
case WSASYSNOTREADY:
printf("可以重启电脑,或检查网络库");
break;
case WSAVERNOTSUPPORTED:
printf("请更新网络库");
break;
case WSAEINPROGRESS:
printf("Please reboot this software");
break;
case WSAEPROCLIM:
printf("请关闭不必要的软件,以为当前网络提供充足资源");
break;
case WSAEFAULT:
printf("参数错误");
break;
}
return 0;
}
//版本校验
if (2 != HIBYTE(wdSockMsg.wVersion) || 2 != LOBYTE(wdSockMsg.wVersion))
{
//版本打开错误
WSACleanup(); //关闭网络库
return 0;
}
//服务器的socket
SOCKET socketSever = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//这三个参数分别为地址类型(IPV4),套接字类型和协议类型(TCP)
//如果执行失败则返回INVALID_SOCKET
if (INVALID_SOCKET == socketSever)
{
int a = WSAGetLastError(); //如果socket调用失败,返回错误码(工具 -> 错误查找)
WSACleanup(); //关闭网络库
return 0;
}
struct sockaddr_in si;
si.sin_family = AF_INET;
si.sin_port = htons(12345);
si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
int connect_a = connect(socketSever, (const struct sockaddr*)&si, sizeof(si));
if (SOCKET_ERROR == connect_a)
{
int a = WSAGetLastError(); //如果socket调用失败,返回错误码(工具 -> 错误查找)
closesocket(socketSever); //关闭socket
WSACleanup(); //关闭网络库
return 0;
}
send(socketSever, "连接成功", strlen("连接成功"), 0);//如果连接成功,向服务端发送“连接成功”
while (1)
{
/*char buf[1500] = { 0 };
int res = recv(socketSever, buf, 1499, 0);
if (0 == res)
{
printf("连接中断,客户端下线\n");
}
else if (SOCKET_ERROR == res)
{
printf("错误码:%d\n", WSAGetLastError());
}
else
{
printf("%d,%s\n", res, buf);
}*/
//发送函数
char buf[1500] = { 0 };
scanf("%s", buf);
if ('0' == buf[0])//输入0时,退出循环,客户端下线
{
break;
}
int send_a = send(socketSever, buf, strlen(buf), 0);
if (SOCKET_ERROR == send_a)
{
//出现错误
int a = WSAGetLastError();
}
}
//关闭socket
closesocket(socketSever);
//清理网络库
WSACleanup();
return 0;
}
select模型与CS模型相比性能有了一定的提高,但是select模型也有缺陷,当select函数投递一组socket给操作系统时,操作系统将有信号的socket装进fe_set中并返回,这一过程是阻塞的,为了进一步提高执行效率,人们提出了事件选择模型,我们将在后续的博文中介绍事件选择模型。