Select 模型的使用,简单聊天室的实现
select模型是一种非阻塞的I/O 模型,他主要是使用select来同时管理多个套接字,如果没有网络事件发生,它边进入等待状态,以便执行同步IO;它的好处在于一个线程可以同时管理多个连接的套接字,这样避免了线程的膨胀。
要使用selcet模型的步骤:
1. 创建socket
2. socket 通过bind绑定本地地址
3. socket 监听
到这里,都是跟阻塞模式的一模一样,下面是不同的
4.设置套接字的模式为阻塞
u_long value=1;
iRet=ioctlsocket(socket_listen,FIONBIO ,&value);
5. 调用select 函数 int select(
int nfds,
fd_set* readfds,
fd_set* writefds,
fd_set* exceptfds,
const struct timeval* timeout
);
第一个参数是设为0,不管。第二,三,四个参数都是一个套接字的集合,第二个参数是检测套接字的可读性,第三个检测可写性,第四个检查错误。 第五个设置超时时间,如果为NULL,那么永远等待。第二,三,四个参数不能全为NULL,可以允许其中两个为NULL.
失败返回SOCKET_ERROR也就是-1,如果超时返回0
所以,判断是否成功,只要判断返回值是否>0就OK。
select 方法返回后,会在相应的套接字集合fd_set中移除没有网络事件发生的套接字 。
readfds等待的网络事件:
1.如果是监听套接字,那么说明有一个连接未决,需要调用accept
2.如果是普通套接字,有数据可读,需要调用recv
3.如果是普通套接字,连接关闭,重启,或者 中断
writefds等待的网络事件:
1.数据能够发送
2.如果一个非阻塞连接调用正在处理,连接已经成功
exceptfds等待的网络事件:
1.如果一个非阻塞连接调用正在处理,连接失败
2.OOB 数据可读
下面是一个通过Selcet模型来写的类似聊天室的小程序:
服务器端代码:
int iRet=bind(socket_listen,(sockaddr *)&sin,sizeof(sin));
if (iRet!=0)
{
cout<<"bind 绑定本地地址失败"<<endl;
return false;
}
else
{
cout<<"bind 绑定本地地址成功"<<endl;
}
iRet=listen(socket_listen,SOMAXCONN);
if (iRet!=0)
{
cout<<"服务器 listen 失败"<<endl;
return false;
}
else
{
cout<<"服务器 listen 成功"<<endl;
}
u_long value=1;
iRet=ioctlsocket(socket_listen,FIONBIO ,&value);
if (iRet==0)
{
cout<<"设置 服务器 非阻塞模式成功"<<endl;
}
else
{
cout<<"设置 服务器 非阻塞模式失败"<<endl;
return false;
}
SOCKET client_socket;
sockaddr_in client_addr;
int addr_len=sizeof(sockaddr_in);
//所有要检测的socket,用来跟selcet后的进行对比
fd_set set_server;
FD_ZERO(&set_server);
FD_SET(socket_listen,&set_server);
fd_set set_read;
while (true)
{
cout<<endl<<endl;
//cout<<"进入 select循环"<<endl;
//将set_server复制给set_read ,这样才能每次都检查所有的socket
set_read=set_server;
if (select(0,&set_read,NULL,NULL,NULL)>0)
{
//cout<<"select函数成功返回"<<endl;
//循环处理selcet的结果
for (int i=0;i<set_read.fd_count;i++)
{
//处理listen套接字
if (socket_listen==set_read.fd_array[i])
{
//说明有一个未处理的连接
cout<<"服务器受到一个未处理的连接"<<endl;
if (set_server.fd_count<FD_SETSIZE)
{
cout<<"select 套接字未到达上限,接受连接"<<endl;
client_socket=accept(socket_listen,(sockaddr *)&client_addr,&addr_len);
if (INVALID_SOCKET ==client_socket)
{
cout<<"accept接受连接失败"<<endl;
}
else
{
cout<<"accept接受连接成功,连接来自:" <<endl;
cout<<inet_ntoa(client_addr.sin_addr)<<endl;
cout<<"端口号:"<<ntohs(client_addr.sin_port)<<endl;
//将这个新的socket添加到set_server中去
//这样以后就可以用selcet检测到 来自客户端的数据了。
FD_SET(client_socket,&set_server);
string strTemp=inet_ntoa(client_addr.sin_addr);
strTemp.append("::");
char szFormat[20];
strTemp.append( ltoa( ntohs(client_addr.sin_port),szFormat,10));
m_addr_name_map[client_socket]=strTemp;
}
}
else
{
cout<<"select 套接字到达上限,拒绝连接"<<endl;
}
}
else
{
char dataBuffer[MAX_PATH];
memset(dataBuffer,0,MAX_PATH);
if (recv(set_read.fd_array[i],dataBuffer,MAX_PATH,0))
{
//cout<<"收到消息:"<<dataBuffer<<endl;
string strTemp=dataBuffer;
if (strTemp.find("SetName:")==0)
{
strTemp=strTemp.substr(8);
cout<<"设置名称:"<<strTemp.c_str()<<endl;
m_addr_name_map[set_read.fd_array[i]]=strTemp;
}
else
{
string strOut=m_addr_name_map[set_read.fd_array[i]];
strOut.append("说:");
strOut.append(strTemp);
//在服务器上面输出
cout<<strOut.c_str()<<endl;
//遍历列表,发送给其他的clients
DispatchToAllClients(set_read.fd_array[i],strOut);
}
}
else
{
cout<<"收到连接断开消息"<<endl;
FD_CLR(set_read.fd_array[i],&set_server);
}
}
}
}
else
{
cout<<"select函数失败,返回"<<endl;
break;
}
}
return true;
}
为了实现IP:端口号 跟用户名的对应,定义了一个map<SOCKET,string>来表示对应关系。
同时,为了实现聊天室的效果,也就是一个人发送消息到服务器,服务器应该分发他的这个消息到所有的其他的客户端,所以增加了一个DispatchToAllClients函数
代码如下:
for (;iter_begin!=iter_end;++iter_begin)
{
if (iter_begin->first!=self)
{
//如果不是发送消息的客户端本身,那么发送消息
send(iter_begin->first,strMessage.c_str(),strMessage.length(),0);
}
}
return true;
}
下面是客户端的实现:
客户端通过一个循环来获得用户的输入,如果有输入,并且不是"quit"和"exit",那么就发送消息到服务器。
同时为了接受服务器转发的其他Clients发送的消息,创建了一个线程,也是用select模型来等待服务器的read网络事件,如果等待到了,就recv,实现了对服务器的消息的接收已经本身消息的发送的同时进行。
客户端代码:
if (m_socket_client==INVALID_SOCKET)
{
cout<<"客户端 创建 socket失败"<<endl;
return false;
}
else
{
cout<<"客户端 创建 socket成功"<<endl;
}
int iRet=connect(m_socket_client,(sockaddr *)&addr,sizeof(sockaddr_in));
if (iRet!=0)
{
cout<<"客户端 连接服务器 失败"<<endl;
return false;
}
else
{
cout<<"客户端连接服务器成功"<<endl;
}
u_long value=1;
iRet=ioctlsocket(m_socket_client,FIONBIO ,&value);
if (iRet==0)
{
cout<<"设置 服务器 非阻塞模式成功"<<endl;
}
else
{
cout<<"设置 服务器 非阻塞模式失败"<<endl;
return false;
}
string str;
char buffer[MAX_PATH];
cout<<"请输入您的称呼:"<<endl;
gets(buffer);
str=buffer;
while (str.empty())
{
cout<<"请重新输入您的称呼"<<endl;
gets(buffer);
}
string strSend="SetName:";
strSend.append(str);
send(m_socket_client,(char *)strSend.c_str(),strSend.length(),0);
//单独创建一个线程来显示其他用户的发言
_beginthread(ThreadFunc,NULL,(void*)this);
while (true)
{
//发送消息
gets(buffer);
str=buffer;
if ( (strcmp(str.c_str(),"quit")==0) || (strcmp(str.c_str(),"exit")==0) )
{
break;
}
else
{
SendMessageToServer(str);
}
}
CloseClient();
return false;
}
发送消息到服务器的函数SendMessageToServer实现
其中最后退出时候的CloseClient函数,就是断开跟服务器的连接
线程函数的实现:
fd_set set_client;
FD_ZERO(&set_client);
FD_SET(lp->m_socket_client,&set_client);
fd_set set_read;
//接收消息
char buffer[MAX_PATH];
while (true)
{
set_read=set_client;
if (select(0,&set_read,NULL,NULL,NULL)>0)
{
for (int i=0;i<set_read.fd_count;i++)
{
ZeroMemory(buffer,sizeof(buffer));
if (recv(set_read.fd_array[i],buffer,MAX_PATH,0)>0)
{
cout<<buffer<<endl;
}
}
}
}
}