在项目10中,采用多线程技术实现了TCP协议的一对多通信,但如果客户端过多,就会导致服务器端的线程数量膨胀,使得服务器的资源占用过大。能不能让TCP程序在一个线程中同时与多个客户端进行通信呢?答案是可以的,这需要用到I/O复用模型,I/O复用模型的核心是select()函数,select()函数可以管理多个套接字,使服务器端在单个线程中仍然能够处理多个套接字的I/O事件,达到跟多线程操作类似的效果。

虽然用ioctlsocket()函数把socket设置成非阻塞的,然后用循环逐个socket查看当前套接字是否有数据到来,轮询进行,也能实现TCP的一对多通信,但这种方法需要不停地查看是否有数据到达,浪费CPU资源。

11.1 Select模型基础

Select模型又称为I/O复用模型,因为使用select()函数来管理I/O而得名。select()函数可以管理很多个套接字,但其数量仍然是有限的,在WinSock 2中套接字集合中的元素最多只能是64个。如果需要管理更多的套接字,可以将select()函数与多线程技术相结合,每当套接字数量是64的倍数时,就新开一个线程。

11.1.1 Select模型的集合与事件

为了实现一对多通信,需要在服务器端使用套接字集合,套接字集合中的每个套接字都可以与一个客户端单独进行通信。select()函数使用套接字集合fd_set来管理多个套接字,fd_set是一个结构体,用于保存一组套接字,它的定义如下:

typedef struct fd_set{

unsigned int  fd_count;

SOCKET fd_array[FD_SETSIZE];

} fd_set;

其中,fd_count用来保存集合中套接字的个数,而fd_array(套接字数组)用于存储集合中所有套接字的描述符。FD_SETSIZE是一个常量,在WinSock2.h中定义,其值为64。

为了方便编程,Select模型提供了如下4个宏来对套接字集合进行操作。

  1. FD_ZERO(*set):用来初始化set为空集合。套接字集合在使用前总是必须清空。
  2. FD_CLR(s,*set):从set集合中移除套接字s。
  3. FD_ISSET(s,*set):检查s是不是set的成员,如果是则返回TRUE。
  4. FD_SET(s,*set):添加新的套接字到集合。

下面介绍select()函数的使用,该函数的原型如下:

int select(

int nfds,                       //一般为0,仅为与Berkeley套接字兼容

fd_set * readfds,                 //一个套接字集合,用于检查可读性

fd_set * writefds,                 //一个套接字集合,用于检查可写性

fd_set * exceptfds,                     //一个套接字集合,用于检查错误

const struct timeval * timeout             /*指定此函数等待的最长时间,若为NULL,则最长时间为无限大*/                 )

返回值:负值表示select()函数执行出错;正值表示某些套接字可读写或出错;0表示timeout指定的时间内没有可读写或出错误的套接字。

select()函数中间的三个参数指向的三个套接字集合分别用来保存要检查可读性(readfds)、可写性(writefds)和是否出错(exceptfds)的套接字。

Select()返回时,如果有下列事件发生,对应的套接字不会被删除。

对于readfds,主要有以下事件:

  1. 数据可读;
  2. 连接已经关闭、重启或中断;
  3. listen已经调用,并且有一个连接请求到达,accept函数将成功;

对于writefds,主要有以下事件:

  1. 数据能够发送;
  2. 如果一个非阻塞连接调用正在被处理,连接已经成功;

对于exceptfds,主要有以下事件:

  1. 如果一个非阻塞连接调用正在被处理,连接企图失败;
  2. OOB数据可读。

可见,Select模型的优势在于,可以同时等待多个套接字,当某个或者多个套接字满足可读可写时,通知应用程序调用输入或者输出函数进行读写。

Select()函数就好像是一个消息中心,当消息到来时,通知应用程序接收和发送数据。应该看到Select模型完成一次I/O操作时需经历2次Windows Sockets函数的调用。例如,当接收对方数据时,第一步,调用Select()函数等待该套接字满足条件;第二步,调用recv()函数接收数据。

因此,使用Select函数的socket程序,其效率肯定会受到损失。因为,每一次Socket I/O调用都会经过该函数,因而会导致严重的CPU额外负担。在Socket连接数不多的情况下,这种效率损失是可接受的,但是当Socket连接数很多时,该模型肯定会产生问题。

11.1.2 select模型编程的步骤

使用Select模型编程的基本步骤如下:

①用FD_ZERO宏来初始化需要的fd_set;

②用FD_SET宏将套接字句柄分配给相应的fd_set,例如,如果要检查一个套接字是否有需要接收的数据,则可用FD_SET宏把该套接字的描述符加入可读性检测套接字集合中(第2个参数指向的套接字集合);

③调用select()函数,该函数将会阻塞直到满足返回条件,返回时,各集合中无网络I/O事件发生的套接字将被删除。例如,对可读性检查集合readfds中的套接字,如果select()函数返回时接收缓冲区中没有数据需要接收,select()函数则会把该套接字从集合中删除掉;

④用FD_ISSET对套接字句柄进行检查,如果被检查的套接字仍然在开始分配的那个fd_set里,则说明马上可以对该套接字进行相应的I/O操 作。例如,一个分配给可读性检测套接字集合readfds的套接字,在select()函数返回后仍然在该集合中,则说明该套接字有数据已经到来, 马上调用recv函数就可以读取成功。

实际上,一般的应用程序通常不会只有一次网络I/O,因此不会只有一次select()函数调用,而应该是上述过程的一个循环,因此应把select()函数的调用放到一个while循环里。

套接字创建后,在Select模型下,当发生网络I/O时,程序的执行过程是:向Select函数注册等待I/O操作的套接字,循环执行Select系统调用,阻塞等待,直到网络事件发生或超时返回,对返回的结果进行判断,针对不同的等待套接字进行对应的网络处理。

11.2 群聊软件

使用Select模型可以用很简洁的代码实现一个群聊软件。群聊软件分为服务器端和客户端,一个服务器端通过Select模型可连接多个客户端,服务器可以接收任何一个客户端发来的消息,然后把这个消息转发给其他客户端,该软件的运行效果如图11-1所示(启动了3个相同的客户端)。Select模型仅用在服务器端,客户端使用的仍然是6.3.2节中制作的TCP通信客户端。

图11-1 群聊软件运行效果

11.2.1 群聊软件的实现原理

该群聊软件服务器端程序的实现原理是:首先将监听套接字加入到套接字集合FD_SET中,然后将与每个客户端通信的通信套接字逐个加入到套接字集合中,因此套接字集合FD_SET中的套接字如图11-2所示。

监听套接字

通信套接字1

通信套接字2

通信套接字n

……

FD_SET集合

图11-2 套接字集合FD_SET示意图

服务器端程序的核心是在一个新开的线程中调用Select函数管理套接字集合,由于Select模型从程序启动开始,就要一直等待各个套接字的连接并通信,因此在Windows程序中,为了不阻塞主界面线程,必须把Select函数放到一个单独的线程中。该线程的伪代码如下:

while(true)      {            //让Select函数一直工作

FD_ZERO(&fdread);            //初始化fdread

fdread=p->fdsock;        //将fdsock中的所有套接字添加到fdread中

if(select(0, &fdread, NULL, NULL, NULL)>0)    {     //管理可读事件

for(int i=0;i<p->fdsock.fd_count;i++)  {//分别管理套接字集合中各个套接字

//如果有数据可读或连接到达事件

if (FD_ISSET(p->fdsock.fd_array[i], &fdread)) {

if(p->fdsock.fd_array[i]==p->sock_server) //如果是监听套接字,则表示是连接到达事件

{     ……       //有客户连接请求到达,接受连接,并将返回的套接字加入到套接字集合

newsock=accept (p->sock_server, (struct sockaddr *) &client_addr, &addr_len);

FD_SET(newsock, &p->fdsock);        //将新套接字加入fdsock

}

else  {     //说明不是监听套接字,则表示有客户发来数据,接收数据

int size=recv(p->fdsock.fd_array[i],msgbuffer,sizeof(msgbuffer),0);

if(size<0) {}          //表示接收信息失败

else if(size==0){}          //表示对方关闭了连接

else{                     //size>0,表示接收到了信息

p->c_recvbuf.AddString( msgbuffer ); //将信息显示到列表框中

}}  }}}}

因此,群聊软件服务器端程序的流程如图11-3所示。

添加监听套接字到套接字集合fdsock中

将fdsock添加到检查可读性集合readfds中

调用select()函数

如果存在可读事件fdread

如果是监听套接字

如果是通信套接字

用accept()接受连接

将返回的套接字加入到fdsock集合中

用recv()接收数据

判断recv()的返回值

>0,表示接收到了数据

=0,表示收到了断开连接信息

<0,表示接收信息失败

分别对套接字集合中的每个套接字

可以用send()发送数据

图11-3 群聊软件服务器端程序流程图

11.2.2 服务器端程序的制作步骤

该群聊软件服务器端程序制作的步骤如下:

1)创建一个MFC工程:新建工程,选择“MFC APPWizard(exe)”,输入工程名(如Selwins),单击“下一步”,在步骤1选择“基本对话框”,单击“完成”按钮。

2)在左侧“工作空间”中找到ResourceView选项卡,找到Dialog下的“IDD_SELWINS_DIALOG”,设置对话框的界面及各控件ID如图11-4所示。

图11-4 服务器端程序的界面及控件ID

3)按“Ctrl+W”键“建立类向导”,打开“MFC类向导”对话框,在“Member Variables”选项卡中为控件设置成员变量如图11-5所示。

图11-5 设置成员变量

4)初始化对话框界面,打开*dlg.cpp文件,在OnInitDialog()函数中加入如下代码:

BOOL CSelwinsDlg::OnInitDialog()

{……

m_ip=CString("127.0.0.1");                //默认的本机ip地址

m_port=CString("5566");                 //默认的本机端口号

UpdateData(FALSE);                 //变量的值传到界面上

return TRUE;        }

5)在*dlg.h文件中,添加如下引用头文件和定义端口号常量的代码。

#include "winsock2.h"

#pragma comment(lib,"ws2_32.lib")

#define PORT 5566         //定义端口号常量

6)在*dlg.h文件中,声明套接字变量和线程函数,以及管理套接字集合的变量,代码如下。

class CSelwinsDlg : public CDialog      {

public:

CSelwinsDlg(CWnd* pParent = NULL);      // standard constructor

SOCKET sock_server,newsock;          //定义监听套接字和临时已连接套接字变量

fd_set fdsock;                      //保存所有套接字的集合

fd_set fdread;                      //select要检测的可读套接字集合

struct sockaddr_in addr;              //存放本地地址的sockaddr_in结构变量

static UINT selectThread(LPVOID a);         //声明线程函数

……       }

7)双击“启动”按钮,为该按钮编写创建套接字并监听的代码。

char msgbuffer[100],sendbuf[130],Climsg[40];   //定义用于接收客户端信息的缓冲区

void CSelwinsDlg::OnCreate() {

c_BTNCreate.EnableWindow(FALSE);        //将启动按钮设置为无效

WSADATA wsaData;

if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0)    {

c_recvbuf.AddString("加载winsock.dll失败!\n");   }

if ((sock_server = socket(AF_INET,SOCK_STREAM,0))<0) {         //创建套接字

c_recvbuf.AddString("创建套接字失败!\n");

WSACleanup();      }

int addr_len = sizeof(struct sockaddr_in);

memset((void *)&addr,0,addr_len);

addr.sin_family =AF_INET;

addr.sin_port = htons(PORT);

addr.sin_addr.s_addr = htonl(INADDR_ANY);    //允许套接字使用本机的任何IP

if(bind(sock_server,( struct sockaddr *)&addr,sizeof(addr))!=0)  {

c_recvbuf.AddString("地址绑定失败!\n");

closesocket(sock_server);

WSACleanup();      }

if(listen(sock_server,5)==0)

c_recvbuf.AddString("等待客户端连接......\n");

FD_ZERO(&fdsock);           //初始化fdsock

FD_SET(sock_server, &fdsock);         //将监听套接字加入到套接字集合fdsock

AfxBeginThread(&CSelwinsDlg::selectThread,(LPVOID)this); //创建线程

}

8)编写线程函数selectThread(),该函数主要功能是管理套接字集合,其功能分为三大块,即:① 接受连接;② 接收数据;③ 发送数据。其中接受连接和接收数据都是对fdread集合进行判断,如果该集合中的套接字为监听套接字,则接受连接,而如果该集合中的套接字为通信套接字,则接收数据。如果接收数据成功,则表明也可发送数据,此时使用for循环向fdsock中的所有其他套接字发送数据。代码如下:

UINT  CSelwinsDlg::selectThread(LPVOID a){

fd_set fdread;              //select要检测的可读套接字集合

fd_set writefds;            //select要检测的可写套接字集合

SOCKET newsock;                     //声明通信套接字

struct sockaddr_in  client_addr;          //存放客户端地址的sockaddr_in变量

CSelwinsDlg*p;                                 //获得窗口的句柄

int addr_len = sizeof(struct sockaddr_in);

p=( CSelwinsDlg*)a;

while(true) {                        //循环:接收连接请求并收发数据

FD_ZERO(&fdread);            //初始化fdread

fdread=p->fdsock;               //将fdsock中的所有套接字添加到fdread中

writefds=p->fdsock;

if(select(0, &fdread, NULL, NULL, NULL)>0){  //管理fdread集合

for(int i=0;i<p->fdsock.fd_count;i++)  {

if (FD_ISSET(p->fdsock.fd_array[i], &fdread))        {

if(p->fdsock.fd_array[i]==p->sock_server) //如果是监听套接字

{     //有客户连接请求到达,接收连接请求

newsock=accept (p->sock_server, (struct sockaddr *) &client_addr, &addr_len);

if(newsock==INVALID_SOCKET) {  //accept出错则终止所有通信

p->c_recvbuf.AddString("accept函数调用失败!\n");

for(int j=0;j<p->fdsock.fd_count;j++)

closesocket(p->fdsock.fd_array[j]);    //关闭所有套接字

WSACleanup();      }     //注销WinSock动态链接库

else  {                          //接受客户端连接成功

sprintf(Climsg,"客户端%s:%d连接成功",inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

p->c_recvbuf.AddString(Climsg);                      //提示连接成功

send(newsock,Climsg,strlen(Climsg)+1,0) ;        //发送提示信息

FD_SET(newsock, &p->fdsock);               //将新套接字加入fdsock

}}

else  {     //有客户发来数据,接收数据

memset((void *) msgbuffer,0, sizeof(msgbuffer));             //缓冲区清零

int size=recv(p->fdsock.fd_array[i],msgbuffer,sizeof(msgbuffer),0);

if(size<0)              //接收信息

p->c_recvbuf.AddString("接收信息失败!");

else if(size==0)

p->c_recvbuf.AddString("对方已关闭!\n");

else  {           //显示收到信息

//获取对方IP地址

getpeername(p->fdsock.fd_array[i], (struct sockaddr *)&client_addr, &addr_len);

sprintf(sendbuf,"%s说:%s",inet_ntoa(client_addr.sin_addr),msgbuffer);

p->c_recvbuf.AddString(sendbuf );

for(int j=0;j<p->fdsock.fd_count;j++)  {     //群发消息的代码

//去掉监听套接字和发送消息的套接字

if(p->fdsock.fd_array[j]!=p->sock_server && j!=i)

send(p->fdsock.fd_array[j],msgbuffer,strlen(msgbuffer)+1,0) ;//向所有成员转发收到的信息

}

break;            }

closesocket(p->fdsock.fd_array[i]);   //关闭套接字

FD_CLR(p->fdsock.fd_array[i],&(p->fdsock));//清除已关闭套接字

}     }     }     }

else  {

p->c_recvbuf.AddString("Select调用失败!");

break;            //终止循环退出程序

}     }

return 0;  }

本例中,发送数据前也可对writefds集合进行判断,如果该集合不为空,则表示可以发送数据。因此,上述代码也可改写成如下形式:

if(select(0, &fdread, &writefds, NULL, NULL)>0) {

for(int i=0;i<p->fdsock.fd_count;i++)  {

……

if (FD_ISSET(p->fdsock.fd_array[i], &writefds)) { //对writefds集合进行判断

for(int j=0;j<p->fdsock.fd_count;j++)  {     //群发消息代码

if(p->fdsock.fd_array[j]!=p->sock_server && j!=i)

send(p->fdsock.fd_array[j],msgbuffer,strlen(msgbuffer)+1,0) ;

}     }

}}

习题

1. 以下哪一项不会触发select()函数中的可读事件                                     (             )

A. 有数据可接收                B. 有连接请求到达

C. 有连接断开                    D. 有数据可发送

2. 以下哪个宏可用来将一个套接字加入到select()函数的集合中                (           )

A. FD_ZERO               B. FD_SET                  C. FD_CLR                 D. FD_ISSET

3. select()函数有 个参数                                                                   (         )

A. 3                             B. 4                             C. 5                      D. 6

4. select()函数可以管理的套接字集合有 , , 。

5. select()函数的返回值等于0表示    。

6. 简述使用select模型实现TCP一对多通信的步骤。

7.(实验)将9.3.2节的网络用户登录程序服务器端用Select模型改写成一对多通信的。

糖儿飞教你学C++ Socket网络编程——28. 使用select模型实现一对多通信相关推荐

  1. 糖儿飞教你学C++ Socket网络编程——2.本书目录

    项目1 网络编程的实现原理... 1 1.1 网络程序的类型与应用领域... 1 1.1.1 网络程序的类型... 1 1.1.2 网络程序的应用领域... 2 1.2 套接字及其种类... 4 1. ...

  2. 糖儿飞教你学C++ Socket网络编程——5.2 TCP通信程序的函数及流程总结

    TCP服务器端程序流程 监听套接字=socket(AF_INET, 套接字的类型, 0) bind(监听套接字, 本地地址, 地址长度) 通信套接字=accept(监听套接字, 对方地址, 地址长度的 ...

  3. 糖儿飞教你学C++ Socket网络编程——5.套接字编程步骤与函数

    TCP是一个面向连接的传输层协议,提供高可靠性的字节流传输服务,主要用于一次传输要交换大量报文的情形.为了维护传输的可靠性,TCP增加了许多开销:例如确认.流量控制.计时器以及连接管理等.TCP协议的 ...

  4. 糖儿飞教你学C++ Socket网络编程——6.控制台版的TCP通信程序

    根据图2-1的TCP通信程序的流程,下面编程实现一个控制台版的TCP通信程序,程序分为服务器端和客户端,双方可以相互发送消息,运行效果如图2-4所示. 图2-4 控制台版的TCP通信程序(左图为服务器 ...

  5. 糖儿飞教你学C++ Socket网络编程——18. MFC WinSock版的TCP通信程序

    在4.2节中使用Win32 API方法制作了一个TCP异步通信的程序,本节将4.2节的程序用MFC框架重新编写,改写后程序的界面如图6-15所示,功能与4.2节的程序完全相同. 图6-15 MFC版T ...

  6. 5.3linux下C语言socket网络编程简例

    原创文章,转载请注明转载字样和出处,谢谢! 这里给出在Linux下的简单socket网络编程的实例,使用tcp协议进行通信,服务端进行监听,在收到客户端的连接后,发送数据给客户端:客户端在接受到数据后 ...

  7. linux下C语言socket网络编程简例

    转自博文:http://blog.csdn.net/kikilizhm/article/details/7858405 在练习写网络编程时,该例给了我帮助,在写服务器时,我把while逻辑位置想法错了 ...

  8. Python面向对象进阶和socket网络编程

    写在前面 为什么坚持?想一想当初: 一.面向对象进阶 - 1.反射补充 - 通过字符串去操作一个对象的属性,称之为反射: - 示例1: class Chinese:def __init__(self, ...

  9. python网络编程讲解_详解Python Socket网络编程

    Socket 是进程间通信的一种方式,它与其他进程间通信的一个主要不同是:它能实现不同主机间的进程间通信,我们网络上各种各样的服务大多都是基于 Socket 来完成通信的,例如我们每天浏览网页.QQ ...

最新文章

  1. Rails安全导读【一】
  2. rpm mysql 更改目录_rpm形式安装的MySQL服务 并 修改数据文件目录( red hat)_mysql...
  3. Hbase 2.x Region in transition (永久RIT) 异常解决
  4. 为了建设我们的飞鸽传书2011
  5. 读书笔记—《发现你的行为模式(钻石版)》-DiSC测试
  6. 微软官方Microsoft文档地址
  7. php7 phpunit,Make phpunit catch php7 TypeError
  8. 11-4实战上色及修复照片
  9. qtp xml联合xsl输出html报表,通过xml和xsl实现数据和页面展示模板的解耦(简单完整网站代码示例)...
  10. VS2010/MFC编程入门之前言
  11. Java后台入坑二:renrenfast后台打包和前端打包运行
  12. 第十八篇_Class文件
  13. dpp-enrollee配网
  14. 【leetcode】189.旋转数组 (四种方法开阔思路,java实现!)
  15. linux c 获取usb vid,Linux如何使用libudev获取USB设备VID及PID
  16. JavaScript 数组方法every()
  17. tar包安装vsftpd
  18. consul服务发现入门篇
  19. 万豪旅享家推出“一日通”“入住通”和“游玩通”三项权益
  20. mysql创建单表只读访问用户及过程问题处理:如mysql: command not found ///GRANT command denied to user

热门文章

  1. 使用火炬之光UI .
  2. Blender-剪辑
  3. nodo.js nodemon
  4. 一种基于双MCU协同的多功能押解脚环
  5. JavaScript算法——快速排序
  6. 数据可视化分析教学课件——FineBI实验册节选====医药产品分析
  7. cs计算机科学 文书,牛人申请美国计算机CS专业的文书
  8. [HZWER NOIP模拟题][杂题][防骗题]数列
  9. K8S环境的Jenkin性能问题处理
  10. PHP CURL 应用日记1--验证登录再调用API