这个程序,客户端们通过服务器进行群聊。
主要讲讲两点:1.怎么弄非阻塞模式 。 2.select的粗略讲解(心急的可以跳过,直接看后面代码)
首先,看看这个程序服务端设计的基本逻辑,其实非常简单,就在一个while(1)循环里面不停地轮询 accept 和 select函数。
有人可能问,accept不是会阻塞,直到有客户端连接进来的吗?
其实当你的socket套接字设置成非阻塞模式,那么accept也不会阻塞。
1.那怎么弄非阻塞呢?
这就涉及到 fcntl函数。fcntl能改变文件的属性。看看他的原型:int fcntl(int fd, int cmd);失败返回-1(当然这个函数还有很多用途,但这里只谈谈他如何实现非阻塞)
三行代码:
long val = fcntl(sockfd,F_GETFL); //把sockfd套接字的属性拿出来给val
val|=O_NONBLOCK; //把非阻塞的属性O_NONBLOCK加进去,用或运算
fcntl(sockfd,F_SETFL,val); //再把做好的属性val,再加到sockfd中去
这时候 把sockfd 作为参数给 accept,accept就不会再阻塞。
2.select()函数:select函数能够同时监听多个文件描述符,若其中一个或多个文件描述符有反应(读或写),select就会返回。
原型:
int select(nfds, readfds, writefds, errfds, timeout)
fd_set *readfds, *writefds, *errfds; //第2,3,4个参数都是文件描述符集,对Socket编程比较有用的是 readfds。
struct timeval *timeout; //控制select()如何返回,是非阻塞,还是阻塞一定时间返回,还是直接有文件描述符有响应才返回。
struct timeval *timeout; //控制select()如何返回,是非阻塞,还是阻塞一定时间返回,还是直接有文件描述符有响应才返回。
函数参数:
1.nfs:最大的文件描述符+1,这个不能错,若不能确定,写一个算法找出来,下面提供的代码有
2.可读文件描述符集。什么意思呢,就是这个集中的文件描述符随时可能给服务器写数据,那作为服务器,就要监测这些文件描述符,若不关心可读,填NULL。
3.可写文件描述符集。这里不用到,就不说了。若不关心,可填NULL
4.异常文件描述符集。若不关心,可填NULL。
5.时间控制结构体。这个结构体里面有2个成员。一个代表秒,一个代表毫秒。两个都设成0表示select将会非阻塞返回。
对此,还有一些列的宏提供给select用:
1.FD_SET(); 用来把文件描述符加到文件描述符集中
2.FD_ZERO(); 清空文件描述符集中的所有描述符
3.FD_ISSET();判断某个文件描述符有没有响应。
注意:select每返回一次后,都要重新清空文件描述符集,和重新把文件描述符加到文件描述符集中。
下面给出服务端的代码:
//TCP服务端 #include"myhead.h" struct client_list { int sock; struct client_list *next; }; struct client_list *head = NULL; struct client_list *init_list(struct client_list*head) { head = malloc(sizeof(struct client_list)); head->sock = -1; head->next = NULL; return head; } //新的客户端加到客户端队列中 int add_sock(struct client_list*head,int new_sock) { struct client_list *p = head; struct client_list *new_node = malloc(sizeof(struct client_list)); new_node->sock = new_sock; new_node->next = NULL; while(p->next!=NULL) { p = p->next; } p->next = new_node; return 0; } //找出最大的文件描述符 int find_max(struct client_list*head) { struct client_list *p = head->next; if(p==NULL) return 0; int max_sd = p->sock; for(p;p!=NULL;p=p->next) { if(max_sd < p->sock) max_sd = p->sock; } return max_sd; } //信息转发给其他客户端 int write_to_client(struct client_list*head,char *wbuf,int size) { struct client_list *p=head; for(p=head->next;p!=NULL;p=p->next) { write(p->sock,wbuf,size); } return 0; } //当有新的客户端作为新的文件描述符加进来时,显示客户端列表中的所有客户端文件描述符 void show_client_list(struct client_list*head) { struct client_list *p = head; if(p->next == NULL) { printf("IS A EMPTY LIST!\n"); return ; } else { puts("client_list is :"); for(p =head->next; p!=NULL;p = p->next) { printf("%d ",p->sock); } printf("\n"); } }
//取消退出客户端的结点。 int del_node(struct client_list*head,int sock) { struct client_list *p = head->next; struct client_list *q = head; while(p!=NULL) { if(p->sock == sock) { q->next = p->next; free(p); p = NULL; } else { p = p->next; q = q->next; } } return 0; } int main(int argc, char const *argv[]) { char rbuf[50]={0}; char wbuf[50]={0}; int sockfd,size,on=1; int new_sock; int max_sd; struct client_list *pos; struct timeval timeout = {0,0}; //设置select为非阻塞返回 fd_set fdset; long val; head = init_list(head); //初始化客户端链表。 pos = head; struct sockaddr_in saddr; struct sockaddr_in caddr; size = sizeof(struct sockaddr_in); bzero(&saddr,size); saddr.sin_family = AF_INET; saddr.sin_port = htons(8888); saddr.sin_addr.s_addr = htonl(INADDR_ANY); sockfd = socket(AF_INET,SOCK_STREAM,0); setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));//设置socket套接字为复用,不设也可以 //把sockfd设置为非阻塞 val = fcntl(sockfd,F_GETFL); val|=O_NONBLOCK; fcntl(sockfd,F_SETFL,val); bind(sockfd,(struct sockaddr*)&saddr,size); listen(sockfd,10); while(1) { new_sock = accept(sockfd,(struct sockaddr*)&caddr,&size);//循环接受新连接的客户端 if (new_sock!= -1) { puts("new node come!\n"); printf("new_sock = %d\n",new_sock); add_sock(head,new_sock); show_client_list(head); } max_sd = find_max(head); //从客户端队列中,找出最大的文件描述符 FD_ZERO(&fdset); //清空文件描述符集 pos = head; //把每个套接字加入到集合中 if(pos->next != NULL) //若套接字列表不是空 { for(pos=head->next;pos!=NULL;pos=pos->next) { FD_SET(pos->sock,&fdset); } } select(max_sd+1,&fdset,NULL,NULL,&timeout); //等待描述符 for(pos=head->next;pos!=NULL;pos = pos->next) //检查哪个套接字有响应 { if(FD_ISSET(pos->sock,&fdset)) //判断pos->sock这个文件描述符指向的客户端有没有数据写过来 { bzero(rbuf,50); read(pos->sock,rbuf,50); printf("%s\n",rbuf); if(strcmp(rbuf,"quit")==0) //若客户端发来的信息为quit,则取消这个客户端的结点。 { del_node(head,pos->sock); } write_to_client(head,rbuf,50); //把写过来的信息转发给队列中的其他客户端。 } } } return 0; }
客户端代码:
//客户端
#include"myhead.h"
int main(int argc, char const *argv[])
{
int sockfd;
char rbuf[50]={0};
char wbuf[50]={0};
char ipbuf[50]={0};
int port;
int max_sd;
int size,on=1;
int ret;
fd_set fdset;
struct sockaddr_in saddr;
size = sizeof(struct sockaddr_in);
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8888);
saddr.sin_addr.s_addr = inet_addr("192.168.152.128");
sockfd = socket(AF_INET,SOCK_STREAM,0);
setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
ret = connect(sockfd,(struct sockaddr*)&saddr,size);
if(ret ==0)
{
printf("connect sucess\n");
inet_ntop(AF_INET,(void*)&saddr.sin_addr.s_addr,ipbuf,50);
port = ntohs(saddr.sin_port);
printf("ip:%s,port:%d\n",ipbuf,port);
}
else if(ret == -1)
{
printf("failed to connect\n");
return -1;
}
while(1)
{
FD_ZERO(&fdset);
FD_SET(sockfd,&fdset); //把监测服务端的描述符集放到集合中
FD_SET(STDIN_FILENO,&fdset); //STDIN_FILENO这个文件描述符用于监测标准输入(键盘)
max_sd = sockfd>STDIN_FILENO?sockfd:STDIN_FILENO;
select(max_sd+1,&fdset,NULL,NULL,NULL); //这里的select是设置为一直阻塞到有文件描述符发生响应
if(FD_ISSET(sockfd,&fdset)) //判断是否服务端有数据发过来
{
bzero(rbuf,50);
read(sockfd,rbuf,50);
printf("%s\n",rbuf);
}
if(FD_ISSET(STDIN_FILENO,&fdset)) //判断键盘是否有数据传过来
{
bzero(wbuf,50);
scanf("%s",wbuf);
write(sockfd,wbuf,50); //把键盘传过来的数据发给服务端
if(strcmp(wbuf,"quit")==0) //若键盘过来的数据为quit,则关闭这个客户端
{
printf("quit!\n");
return 0;
}
}
}
return 0;
}