一、功能介绍
这是基于Linux下命令行设计的一个简单的群聊天程序。
这个例子可以学习、巩固Linux下网络编程相关知识点
练习Linux下socket、TCP编程
练习Linux下pthread、线程编程
练习Linux下多路IO检测、select函数使用
练习C语言链表使用
练习线程间同步与互斥、互斥锁mutex的使用
群聊程序分为客户端和服务器两个程序
服务器端: 运行整个例子要先运行服务器, 服务器主要用于接收客户端的消息,再转发给其他在线的客户端。服务器里采用多线程的形式,每连接上一个客户端就创建一个子线程单独处理;用了一个全局链表存放已经连接上来的客户端,当一个客户端发来消息后,就逐个转发给其他客户端,客户端断开连接下线后,就删除对应的节点;链表添加节点、删除节点采用互斥锁保护。
客户端: 客户端相当于一个用户,客户端代码可以同时运行多个,连接到服务器之后,互相发送消息进行聊天。发送的消息采用一个结构体封装,里面包含了 用户名、状态、消息本身。
功能总结: 支持好友上线提醒、好友下线提醒、当前在线总人数提示、聊天消息文本转发。
好友上线通知、正常聊天效果:
好友下线提示:
二、select函数功能、参数介绍
在linux命令行可以直接man查看select函数的原型、头文件、帮助、例子 相关信息。
select函数可以同时监听多个文件描述符的状态,在socket编程里,可以用来监听客户端或者服务器有没有发来消息。
Linux下监听文件描述符状态的函数有3个:select、poll、epoll,这3个函数都可以用在socket网络编程里监听客户端、服务器的状态。 这篇文章的例子里使用的是select,后面文章会继续介绍poll、epoll函数的使用例子。
select函数原型、参数介绍
#include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout); 函数功能: 监听指定数量的文件描述符的状态。 函数参数: int nfds : 监听最大的文件描述符+1的值 fd_set *readfds :监听读事件的文件描述符集合,不想监听读事件这里可以填NULL fd_set *writefds :监听写事件的文件描述符集合,不想监听事件这里可以填NULL fd_set *exceptfds :监听其他事件的文件描述符集合,不想监听事件这里可以填NULL struct timeval *timeout : 指定等待的时间。 如果填NULL表示永久等待,直到任意一个文件描述符产生事件再返回。 如果填正常时间,如果在等待的时间内没有事件产生,也会返回。 struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */ }; 返回值: 表示产生事件文件描述符数量。 ==0表示没有事件产生。 >0表示事件数量 <0表示错误。 void FD_CLR(int fd, fd_set *set); //清除某个集合里的指定文件描述符 int FD_ISSET(int fd, fd_set *set); //判断指定集合里的指定文件描述符是否产生了某个事件。 为真就表示产生了事件 void FD_SET(int fd, fd_set *set); //将指定的文件描述符添加到指定的集合 void FD_ZERO(fd_set *set); //清空整个集合。
三、聊天程序代码
#include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <dirent.h> #include <stdlib.h> #include <pthread.h> #include <semaphore.h> #include <signal.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <pthread.h> #include <sys/select.h> #include <sys/time.h> //消息结构体 struct MSG_DATA { char type; //消息类型. 0表示有聊天的消息数据 1表示好友上线 2表示好友下线 char name[50]; //好友名称 int number; //在线人数的数量 unsigned char buff[100]; //发送的聊天数据消息 }; struct MSG_DATA msg_data; //文件接收端 int main(int argc,char **argv) { if(argc!=4) { printf("./app <IP地址> <端口号> <名称>\n"); return 0; } int sockfd; //忽略 SIGPIPE 信号--方式服务器向无效的套接字写数据导致进程退出 signal(SIGPIPE,SIG_IGN); /*1. 创建socket套接字*/ sockfd=socket(AF_INET,SOCK_STREAM,0); /*2. 连接服务器*/ struct sockaddr_in addr; addr.sin_family=AF_INET; addr.sin_port=htons(atoi(argv[2])); // 端口号0~65535 addr.sin_addr.s_addr=inet_addr(argv[1]); //IP地址 if(connect(sockfd,(const struct sockaddr *)&addr,sizeof(struct sockaddr_in))!=0) { printf("客户端:服务器连接失败.\n"); return 0; } /*3. 发送消息表示上线*/ msg_data.type=1; strcpy(msg_data.name,argv[3]); write(sockfd,&msg_data,sizeof(struct MSG_DATA)); int cnt; fd_set readfds; while(1) { FD_ZERO(&readfds); //清空集合 FD_SET(sockfd,&readfds); //添加要监听的文件描述符---可以多次调用 FD_SET(0,&readfds); //添加要监听的文件描述符---可以多次调用 // 0表示读 1写 2错误 //监听读事件 cnt=select(sockfd+1,&readfds,NULL,NULL,NULL); if(cnt) { if(FD_ISSET(sockfd,&readfds)) //判断收到服务器的消息 { cnt=read(sockfd,&msg_data,sizeof(struct MSG_DATA)); if(cnt<=0) //判断服务器是否断开了连接 { printf("服务器已经退出.\n"); break; } else if(cnt>0) { if(msg_data.type==0) { printf("%s:%s 在线人数:%d\n",msg_data.name,msg_data.buff,msg_data.number); } else if(msg_data.type==1) { printf("%s 好友上线. 在线人数:%d\n",msg_data.name,msg_data.number); } else if(msg_data.type==2) { printf("%s 好友下线. 在线人数:%d\n",msg_data.name,msg_data.number); } } } if(FD_ISSET(0,&readfds)) //判断键盘上有输入 { gets(msg_data.buff); //读取键盘上的消息 msg_data.type=0; //表示正常消息 strcpy(msg_data.name,argv[3]); //名称 write(sockfd,&msg_data,sizeof(struct MSG_DATA)); } } } close(sockfd); return 0; }
3.2 select.c 服务器端代码
#include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <dirent.h> #include <stdlib.h> #include <pthread.h> #include <semaphore.h> #include <signal.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #include <pthread.h> #include <sys/select.h> #include <sys/time.h> pthread_mutex_t mutex; //定义互斥锁 int sockfd; //消息结构体 struct MSG_DATA { char type; //消息类型. 0表示有聊天的消息数据 1表示好友上线 2表示好友下线 char name[50]; //好友名称 int number; //在线人数的数量 unsigned char buff[100]; //发送的聊天数据消息 }; //存放当前服务器连接的客户端套接字 struct CLIENT_FD { int fd; struct CLIENT_FD *next; }; //定义链表头 struct CLIENT_FD *list_head=NULL; struct CLIENT_FD *List_CreateHead(struct CLIENT_FD *list_head); void List_AddNode(struct CLIENT_FD *list_head,int fd); void List_DelNode(struct CLIENT_FD *list_head,int fd); int List_GetNodeCnt(struct CLIENT_FD *list_head); void Server_SendMsgData(struct CLIENT_FD *list_head,struct MSG_DATA *msg_data,int client_fd); /* 线程工作函数 */ void *thread_work_func(void *argv) { int client_fd=*(int*)argv; free(argv); struct MSG_DATA msg_data; //1. 将新的客户端套接字添加到链表中 List_AddNode(list_head,client_fd); //2. 接收客户端信息 fd_set readfds; int cnt; while(1) { FD_ZERO(&readfds); //清空整个集合。 FD_SET(client_fd,&readfds); //添加要监听的描述符 cnt=select(client_fd+1,&readfds,NULL,NULL,NULL); if(cnt>0) { //读取客户端发送的消息 cnt=read(client_fd,&msg_data,sizeof(struct MSG_DATA)); if(cnt<=0) //表示当前客户端断开了连接 { List_DelNode(list_head,client_fd); //删除节点 msg_data.type=2; } //转发消息给其他好友 msg_data.number=List_GetNodeCnt(list_head); //当前在线好友人数 Server_SendMsgData(list_head,&msg_data,client_fd); if(msg_data.type==2)break; } } close(client_fd); } /* 信号工作函数 */ void signal_work_func(int sig) { //销毁互斥锁 pthread_mutex_destroy(&mutex); close(sockfd); exit(0); //结束进程 } int main(int argc,char **argv) { if(argc!=2) { printf("./app <端口号>\n"); return 0; } //初始化互斥锁 pthread_mutex_init(&mutex,NULL); signal(SIGPIPE,SIG_IGN); //忽略 SIGPIPE 信号--防止服务器异常退出 signal(SIGINT,signal_work_func); //创建链表头 list_head=List_CreateHead(list_head); /*1. 创建socket套接字*/ sockfd=socket(AF_INET,SOCK_STREAM,0); //设置端口号的复用功能 int on = 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); /*2. 绑定端口号与IP地址*/ struct sockaddr_in addr; addr.sin_family=AF_INET; addr.sin_port=htons(atoi(argv[1])); // 端口号0~65535 addr.sin_addr.s_addr=INADDR_ANY; //inet_addr("0.0.0.0"); //IP地址 if(bind(sockfd,(const struct sockaddr *)&addr,sizeof(struct sockaddr))!=0) { printf("服务器:端口号绑定失败.\n"); } /*3. 设置监听的数量*/ listen(sockfd,20); /*4. 等待客户端连接*/ int *client_fd; struct sockaddr_in client_addr; socklen_t addrlen; pthread_t thread_id; while(1) { addrlen=sizeof(struct sockaddr_in); client_fd=malloc(sizeof(int)); *client_fd=accept(sockfd,(struct sockaddr *)&client_addr,&addrlen); if(*client_fd<0) { printf("客户端连接失败.\n"); return 0; } printf("连接的客户端IP地址:%s\n",inet_ntoa(client_addr.sin_addr)); printf("连接的客户端端口号:%d\n",ntohs(client_addr.sin_port)); /*创建线程*/ if(pthread_create(&thread_id,NULL,thread_work_func,client_fd)) { printf("线程创建失败.\n"); break; } /*设置线程的分离属性*/ pthread_detach(thread_id); } //退出进程 signal_work_func(0); return 0; } /* 函数功能: 创建链表头 */ struct CLIENT_FD *List_CreateHead(struct CLIENT_FD *list_head) { if(list_head==NULL) { list_head=malloc(sizeof(struct CLIENT_FD)); list_head->next=NULL; } return list_head; } /* 函数功能: 添加节点 */ void List_AddNode(struct CLIENT_FD *list_head,int fd) { struct CLIENT_FD *p=list_head; struct CLIENT_FD *new_p; pthread_mutex_lock(&mutex); while(p->next!=NULL) { p=p->next; } new_p=malloc(sizeof(struct CLIENT_FD)); new_p->next=NULL; new_p->fd=fd; p->next=new_p; pthread_mutex_unlock(&mutex); } /* 函数功能: 删除节点 */ void List_DelNode(struct CLIENT_FD *list_head,int fd) { struct CLIENT_FD *p=list_head; struct CLIENT_FD *tmp; pthread_mutex_lock(&mutex); while(p->next!=NULL) { tmp=p; p=p->next; if(p->fd==fd) //找到了要删除的节点 { tmp->next=p->next; free(p); break; } } pthread_mutex_unlock(&mutex); } /* 函数功能: 获取当前链表中有多少个节点 */ int List_GetNodeCnt(struct CLIENT_FD *list_head) { int cnt=0; struct CLIENT_FD *p=list_head; pthread_mutex_lock(&mutex); while(p->next!=NULL) { p=p->next; cnt++; } pthread_mutex_unlock(&mutex); return cnt; } /* 函数功能: 转发消息 */ void Server_SendMsgData(struct CLIENT_FD *list_head,struct MSG_DATA *msg_data,int client_fd) { struct CLIENT_FD *p=list_head; pthread_mutex_lock(&mutex); while(p->next!=NULL) { p=p->next; if(p->fd!=client_fd) { write(p->fd,msg_data,sizeof(struct MSG_DATA)); } } pthread_mutex_unlock(&mutex); }