在Windows环境下用C语言实现CS模型中我们详细介绍了网络通信中需要使用的几个函数并指出了最基本的CS模型存在的缺点,为了克服这些缺点,人们提出了select模型,select模型和CS模型相比有以下几个优势:

  • 解决了基本CS模型中accept与recv函数等待连接和等待消息时程序的阻塞。
  • 实现多个客户端连接,能够接收多个客户端的消息
  • select模型仅用于服务器端

1. select模型原理

  1. 每个客户端都有一个socket,服务器也有自己的socket,将所有的socket放进一个数据结构里。
  2. 通过select函数遍历装有socket数组的数据结构,当某个select有响应时,select就会通过其相应的参数值反馈出来。
  3. 在这里需要对返回值进行判断,如果返回值是服务器的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的参数宏:

  1. FD_ZERO,将结构体清零,FD_ZERO;
  2. FD_SET,向结构体中添加一个socket,添加前会检查数组中元素是否超过64和数组中是否已经存在该元素;
  3. FD_CLR:删除数组中指定的socket,从集合中删除一个socket后一定要closesocket,否则会造成内存泄露
  4. 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. 参数1:忽略(填0即可),这个参数仅仅是为了兼容Berkeley sockets;
  2. 参数2:检查是否有可读的socket,即客户端发来了消息,该socket就会被设置。 原理:它开始时包含所有的socket,通过select函数全部投递给系统,系统将有事件发生的socket再重新赋值给参数2,这样参数2就包含了所有有事件发生的socket了。
  3. 参数3:检查是否有可写的socket,就是可以给哪些客户端套接字发消息,即send,只要连接成功建立起来了,那该客户端套接字就是可写的(不一定非要在参数3中使用send方法)。 原理:它开始时包含所有的socket,通过select全部投放给系统,系统将可写的socket再赋值回来,调用后这个参数就是装着可以被send信息的客户端socket上。
  4. 检查套接字上的异常错误,用法跟参数2/3一样,将有异常错误的套接字重新装进来,反馈给我们。
  5. 参数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://释放所有socketfor (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_SOCKETif (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);        //关闭socketWSACleanup();                 //关闭网络库return 0;}//开始监听int a = listen(socketSever, SOMAXCONN);if (SOCKET_ERROR == a){//listen 函数出错int a = WSAGetLastError();        //返回错误码closesocket(socketSever);        //关闭socketWSACleanup();                 //关闭网络库return 0;}FD_ZERO(&all_Sockets); //清零FD_SET(socketSever, &all_Sockets);//添加服务器socketwhile (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函数{//acceptSOCKET 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());}}//释放所有socketfor (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;}//服务器的socketSOCKET socketSever = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//这三个参数分别为地址类型(IPV4),套接字类型和协议类型(TCP)//如果执行失败则返回INVALID_SOCKETif (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);               //关闭socketWSACleanup();                         //关闭网络库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();}}//关闭socketclosesocket(socketSever);//清理网络库WSACleanup();return 0;
}

  select模型与CS模型相比性能有了一定的提高,但是select模型也有缺陷,当select函数投递一组socket给操作系统时,操作系统将有信号的socket装进fe_set中并返回,这一过程是阻塞的,为了进一步提高执行效率,人们提出了事件选择模型,我们将在后续的博文中介绍事件选择模型。

5种Windows网络模型之select模型相关推荐

  1. [Windows]7种网络编程I/O模型代码实现实例

    From: http://blog.csdn.net/woshinia/article/details/8585930 部分代码参考<[WINDOWS网络与通信程序设计].王艳平>,网络中 ...

  2. Windows网络编程系列教程之四:Select模型

    讲一下套接字模式和套接字I/O模型的区别.先说明一下,只针对Winsock,如果你要骨头里挑鸡蛋把UNIX下的套接字概念来往这里套,那就不关我的事. 套接字模式:阻塞套接字和非阻塞套接字.或者叫同步套 ...

  3. Windows下select模型(以及EAGAIN、EWOULDBLOCK、EINTR)

    在这里记录一下,以前都是新项目用到了就从旧项目中拷贝. 自从将博客当作记事本,发现自己多了一个好习惯. Windows下select模型_程序员攻略-CSDN博客 套接字IO超时设置和使用select ...

  4. Windows环境下IOCP和SELECT模型性能比较

    在大量客户端连接的情况下,IOCP模型应该是具有先天优势的,首先是每次调用时不需要传入socket列表,其次是他在通知时就已经完成了IO操作,节省了系统调用. 道理是这么个道理,然而在实际应用过程当中 ...

  5. Windows套接字I/O模型(2) -- Select模型

    一.Select模型介绍 套接字I/O Select模型的"中心思想"便是利用select函数,实现对I/O的管理.利用select函数判断套接字(一个或多个)上是否存在数据,或者 ...

  6. java socket编程 select_windows socket编程select模型使用

    int select( int nfds,            //忽略 fd_ser* readfds,    //指向一个套接字集合,用来检测其可读性 fd_set* writefds,   / ...

  7. socket select模型

    由于socket recv()方法是堵塞式的,当多个客户端连接服务器时,其中一个socket的recv调用时,会产生堵塞,使其他连接不能继续. 如果想改变这种一直等下去的焦急状态,可以多线程来实现(不 ...

  8. socket编程的select模型

    在掌握了socket相关的一些函数后,套接字编程还是比较简单的,日常工作中碰到很多的问题就是客户端/服务器模型中,如何让服务端在同一时间高效的处理多个客户端的连接,我们的处理办法可能会是在服务端不停的 ...

  9. 了解select模型的六大注意点

    Select模型出现的目的::模型的出现是为了解决"一个客户端一线程"的问题,为了WINDOWS的线程切换不要太频繁.   select函数 int select( int max ...

  10. 详细解析SELECT模型

    先看一下下面的这句代码: int iResult = recv(s, buffer,1024); 这是用来接收数据的,在默认的阻塞模式下的套接字里,recv会阻塞在那里,直到套接字连接上有数据可读,把 ...

最新文章

  1. Linux修改/etc/profile配置错误command is not found自救方法
  2. 安装python的redis模块
  3. C#方法重载(overload)方法重写(override)隐藏(new)
  4. struts2--java.lang.IllegalAccessException: Class ognl.OgnlRuntime can not access a member of
  5. 可穿戴在线展持续升温:聚焦产业热点 畅谈核心技术发展
  6. 解决Mac无法编辑 .bash_profile文件与使用sudo时permission denied报错
  7. ip8plus多重_【苹果 iPhone 8 Plus 手机使用总结】容量|处理器|手感_摘要频道_什么值得买...
  8. swift 展示html富文本,Swift HTML富文本显示
  9. bat批处理文件的简单解密方法(乱码)
  10. HTML5 标签audio添加网页背景音乐代码
  11. 广和通率先启动基于联发科技 T830 5G平台的5G模组开发,加速全球运营商5G FWA部署
  12. Thunder9(迅雷9)去掉右侧浏览器广告的方法
  13. 产品 • B端和C端产品经理有什么区别?
  14. day0---docker容器的dockerfile知识(5)
  15. pyqt5以及pyqtgraph(pyqt界面设计绘图)
  16. 蚂蚁金服开放平台-支付宝新版接口的参数设置
  17. Keil自定义关键字、快捷键···
  18. 蒸米ROP_X86学习总结
  19. IE 8 中 JS 调用 adobe reader 打印 PDF 文档
  20. 病毒先生,这很好玩吗??!!

热门文章

  1. Scratch软件编程等级考试一级——20210320
  2. 关于计算机教学的论文,关于计算机教学论文.docx
  3. java根据经纬度得出中心点的经纬度
  4. node状态管理cookie,session,token的各自特点和使用方法还有hash算法加密
  5. MySQL基本操作四:数据的查询
  6. Java 三个枪手游戏
  7. uni-app打包成Android Apk 全程详解
  8. python三国演义人物 统计分析前20个_python爬取三国演义文本,统计三国演义中出场次数前30的人物,并生成词云、图表...
  9. 三国演义人名爬取与处理
  10. 访问本地环境时出现The requested URL / was not found on this server.