效果展示

step1.服务器启动!端口号3006

1024多人激情在线聊天室---select函数的使用-LMLPHP

step2.1号客户端启动!jack加入了群聊

1024多人激情在线聊天室---select函数的使用-LMLPHP

step3.2号客户端启动!kelly加入了群聊

1024多人激情在线聊天室---select函数的使用-LMLPHP

step4.3号客户端启动!zwj加入了群聊

1024多人激情在线聊天室---select函数的使用-LMLPHP

step5.开始聊天吧!zwj发出问候

1024多人激情在线聊天室---select函数的使用-LMLPHP

step6.Kelly尴尬回应,并询问jack情况

1024多人激情在线聊天室---select函数的使用-LMLPHP

step7.jack回到

1024多人激情在线聊天室---select函数的使用-LMLPHP

具体代码实现(客户端)

在客户端,存在“收到消息”和“发送消息”这两个事件。其中“收到消息”是随机发生的。“收到消息”取决于其他客户端什么时候发送消息,而“发送消息”取决于用户什么时候键入数据。正是因为“收到消息”这个事件随机发生的,因此不能让程序被采集键盘数据所阻塞掉,这样会使得某个一直不发送消息的用户,无法及时收到其他用户的对话。因此需要使用select函数,对文件描述符监听。哪一个就就绪,就先处理哪一个事件。下面是客户端的核心代码段。

 if(severfd > ){
printf("connction estabilshed\n"); FD_ZERO(&read_set);
FD_SET(,&read_set);
FD_SET(severfd,&read_set); while(){
ready_set = read_set;
Select(severfd+,&ready_set,NULL,NULL,NULL);
if(FD_ISSET(,&ready_set)){
read(, sbuf, sizeof(sbuf));//send data to server
mergeMessage(name,sbuf,Message);
write(severfd,Message,StrLen(Message));
}
if(FD_ISSET(severfd,&ready_set)){
read(severfd, rbuf, sizeof(rbuf));
printf("%s",rbuf);
}
}
}else{
printf("fail to estabilsh connction\n");
}

第3行的ready_set和read_set是 fd_set类型结构体变量,在进入循环之前,已经为read_set做好了初始化。第5行将标准输入0,放置于fd_set中,第6行将从服务器返回的serverfd置于fd_set。在第10行使用select函数,监听0号和serverfd文件描述符,此时进程被挂起。0和serverfd谁先就绪,就会唤醒进程,继续处理下面两个if语句。如果是0号先就绪,那么从标准输入中读取数据,并将输入数据和nickname封装成Messgae,写至serverfd去。如果是serverfd先就绪,那么就从serverfd中读出数据,并打印至控制台显示。客户端代码较容易,逻辑简单,接下来讲述服务器端代码如何实现。

具体代码实现(服务器端)

在编写服务器代码时,也存在“接受新客户端的链接”和“推送消息”这两个事件。这两个事件都是随机发生的。“接受新客户端的链接”取决于客户端什么时候发起请求链接,向其他客户端“推送消息”取决于是否有新的消息被发送至服务器段。因此既不能让“推送消息”事件阻塞服务器,也不能让“接受新客户端的链接”事件阻塞服务器,而应是,哪一个事件先就绪,就先处理哪一个事件。否则的话,服务器要么就一直等待处理链接请求,要么就一直等待推送消息。服务器与客户端代码还有不同的一点是,服务器应该记录下所有客户发送的消息和客户端信息,然后才能将该客户发送的消息推送至其他客户端。因此服务器,应该设置一个数据结构,用于存储和记录所有客户端的信息。

服务器数据结构

服务器段用到的数据结构,该数据结构用于记录所有访问服务器的客户链接请求。其中nready和maxi用于减少遍历循环时的次数,每一次有客户端链接到服务器,就更新maxi的值,因为文件描述符在linux系统下,是从3开始递增(其中0,1,2为标准输入输出,2为标准错误输出)。clientfd是一个int数组用于记录所有客户端的文件描述符。clientfbuf是一个指针数组,每一个指针都指向客户端专属的缓冲器。服务器用此结构,记录每一个客户端的输入数据。

 typedef struct {
fd_set read_set;
fd_set ready_set;
int nready;
int maxi;
int clientfd[FD_SETSIZE];
char* clientbuf[FD_SETSIZE];
} pool;

服务器核心代码

服务器在第4行使用select函数,将使得服务器被挂起,一旦当发生了请求链接事件,服务器将处理该链接,并将该请求添加至pool中,用于推送消息。

     while () {
pool.ready_set = pool.read_set;
printf("waiting \n");
pool.nready = Select(pool.maxfd+, &pool.ready_set, NULL, NULL, NULL); if (FD_ISSET(listenfd, &pool.ready_set)) {
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
add_client(connfd, &pool);
} push_message(&pool);
}

下面是添加客户端的代码,每当有一个客户端成功和服务器成功建立起链接,就把该客户端的文件描述符存入结构中,并为其初始化buf。在第12行,把该文件描述符,加入到服务器的fd_set中,让服务器的select函数监听该客户端。

 void add_client(int connfd, pool *p)
{
int i;
p->nready--;
for (i = ; i < FD_SETSIZE; i++)
if (p->clientfd[i] < ) { p->clientfd[i] = connfd; initBuf(&p->clientbuf[i]); FD_SET(connfd, &p->read_set); if (i > p->maxi)
p->maxi = i;
break;
}
}

下面是推送消息的代码,循环遍历pool,并从中取出文件描述符,和它对应的buf。然后并读出它所发送的数据,并在第16行将消息转发至除它以外所有的其他客户端。

 void push_messgae(pool *p)
{
int i, connfd, n, j, cfd;
char *buf; for (i = ; (i <= p->maxi) && (p->nready > ); i++) {
connfd = p->clientfd[i];
buf = p-> clientbuf[i]; if ((connfd > ) && (FD_ISSET(connfd, &p->ready_set))) {
p->nready--;
if ((n = read(connfd, buf, sizeof(buf))) != ) {
printf("i am in,maxi = %d \n",p->maxi);
for(j=;j <= p->maxi;j++){
cfd = p->clientfd[j];
if(cfd == connfd) continue;
printf("transfering to %d\n",cfd);
write(cfd , buf, n);
}
}
else {
Close(connfd);
FD_CLR(connfd, &p->read_set);
p->clientfd[i] = -;
}
}
}
}
05-11 09:32