这个程序,客户端们通过服务器进行群聊。
主要讲讲两点: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;
}