linux下基于tcp协议的群聊系统设计(多线程+select)(代码片段)

DS小龙哥 DS小龙哥     2023-02-04     511

关键词:

一、功能介绍

这是基于Linux下命令行设计的一个简单的群聊天程序。

这个例子可以学习、巩固Linux下网络编程相关知识点

  1. 练习Linux下socket、TCP编程
  2. 练习Linux下pthread、线程编程
  3. 练习Linux下多路IO检测、select函数使用
  4. 练习C语言链表使用
  5. 练习线程间同步与互斥、互斥锁mutex的使用

群聊程序分为客户端和服务器两个程序

  1. 服务器端: 运行整个例子要先运行服务器, 服务器主要用于接收客户端的消息,再转发给其他在线的客户端。服务器里采用多线程的形式,每连接上一个客户端就创建一个子线程单独处理;用了一个全局链表存放已经连接上来的客户端,当一个客户端发来消息后,就逐个转发给其他客户端,客户端断开连接下线后,就删除对应的节点;链表添加节点、删除节点采用互斥锁保护。

  2. 客户端: 客户端相当于一个用户,客户端代码可以同时运行多个,连接到服务器之后,互相发送消息进行聊天。发送的消息采用一个结构体封装,里面包含了 用户名、状态、消息本身。

功能总结: 支持好友上线提醒、好友下线提醒、当前在线总人数提示、聊天消息文本转发。

好友上线通知、正常聊天效果:

好友下线提示:

二、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);  //清空整个集合。

三、聊天程序代码

3.1 client.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/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 *

4.基于nio的群聊系统(代码片段)

【README】1.本文总结自B站《netty-尚硅谷》,很不错;2.文末有错误及解决方法;【1】群聊需求1)编写一个NIO群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)2)实现多人群聊;3... 查看详情

linux下命令

...Linux是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX和UNIX的多用户、多任务、支持多线程和多CPU的操作系统。它能运行主要的UNIX工具软件、应用程序和网络协议。它支持32位和64位硬件。Linux继承了Unix以网络为核心... 查看详情

linux系统下基础命令介绍

...Linux是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX和UNIX的多用户、多任务、支持多线程和多CPU的操作系统。它能运行主要的UNIX工具软件、应用程序和网络协议。它支持32位和64位硬件。Linux继承了Unix以网络为核心... 查看详情

linux系统下基础命令介绍

...是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX和UNIX的多用户、多任务、支持多线程和多CPU的操作系统。它能运行主要的UNIX工具软件、应用程序和网络协议。它支持32位和64位硬件。Linux继承了Unix以网络为核心... 查看详情

linux系统下基础命令介绍

...是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX和UNIX的多用户、多任务、支持多线程和多CPU的操作系统。它能运行主要的UNIX工具软件、应用程序和网络协议。它支持32位和64位硬件。Linux继承了Unix以网络为核心... 查看详情

2017-2018-120155307信息安全系统设计基础》实验五通信协议设计

...议设计实验要求:任务一:Linux下OpenSSL的安装与使用1、基于Socket实现TCP通信,一人实现服务器,一人实现客户端2、研究OpenSSL算法,测试对称算法中的AES,非对称算法中的RSA,Hash算法中的MD53、选用合适的算法,基于混合密码系... 查看详情

linux篇第十九篇——网络套接字编程(tcp套接字的编写+多进程版本+多线程版本+线程池版本)(代码片段)

...️本篇博客开始给大家介绍网络编程中的套接字编程——基于UDP协议的套接字和基于TCP的套接字,这篇博客主要介绍基于UDP协议套接字,下一篇介绍基于TCP协议的套接字。在介绍套接字编程之前,我会先给大家介绍一... 查看详情

20165304信息安全系统设计基础》实验五通信协议设计(代码片段)

...议设计实验要求:任务一:Linux下OpenSSL的安装与使用1、基于Socket实现TCP通信,一人实现服务器,一人实现客户端2、研究OpenSSL算法,测试对称算法中的AES,非对称算法中的RSA,Hash算法中的MD53、选用合适的算法,基于混合密码系... 查看详情

基于tcp协议的多线程服务器

功能:客户端发送消息给服务端,服务端回显消息给客户端。server端:650)this.width=650;"src="http://s5.51cto.com/wyfs02/M01/87/FA/wKiom1flNeKwy2gWAAUHuAlkdTk745.jpg"title="tcp_server.jpg"alt="wKiom1flNeKwy2gWAAUHuAlkdTk745.jpg"/>Clie 查看详情

2017-2018-120155315《信息安全系统设计基础》实验五通讯协议设计

实验内容基于Socket实现TCP通信,一人实现服务器,一人实现客户端研究OpenSSL算法,测试对称算法中的AES,非对称算法中的RSA,Hash算法中的MD5选用合适的算法,基于混合密码系统实现对TCP通信进行机密性、完整性保护。实验知识... 查看详情

第13章tcp编程_基于自定义协议的多线程模型

7.基于自定义协议的多线程模型(1)服务端编程  ①主线程负责调用accept与客户端连接  ②当接受客户端连接后,创建子线程来服务客户端,以处理多客户端的并发访问。  ③服务端接到的客户端信息后,回显给客户端(... 查看详情

网络协议分析课程设计:基于tcp协议网上聊天程序

...。最好是整个课程设计报告。急需!先谢过各位大触了!基于TCP协议网上聊天程序有的参考技术A写作攻略网上很多。说实话一下是教不会的。多查阅资料。或帮你处理。 参考技术B以前弄过一个不过,硬盘坏了就丢失了。无能为... 查看详情

网易云音乐为啥没有linux版本

...Linux是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX和UNIX的多用户、多任务、支持多线程和多CPU的操作系统。它能运行主要的UNIX工具软件、应用程序和网络协议。它支持32位和64位硬件。Linux继承了Unix以网络为核心... 查看详情

linux系统简史

...Linux是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX和UNIX的多用户、多任务、支持多线程和多CPU的操作系统。它能运行主要的UNIX工具软件、应用程序和网络协议。它支持32位和64位硬件。Linux继承了Unix以网络为核心... 查看详情

linux篇第十九篇——网络套接字编程(tcp套接字的编写+多进程版本+多线程版本+线程池版本)(代码片段)

...️本篇博客开始给大家介绍网络编程中的套接字编程——基于UDP协议的套接字和基于TCP的套接字,这篇博客主要介绍基于UDP协议套接字,下一篇介绍基于TCP协议的套接字。在介绍套接字编程之前,我会先给大家介绍一... 查看详情

linux篇第十九篇——网络套接字编程(tcp套接字的编写+多进程版本+多线程版本+线程池版本)(代码片段)

...️本篇博客开始给大家介绍网络编程中的套接字编程——基于UDP协议的套接字和基于TCP的套接字,这篇博客主要介绍基于UDP协议套接字,下一篇介绍基于TCP协议的套接字。在介绍套接字编程之前,我会先给大家介绍一... 查看详情

2017-2018-120155338信息安全系统设计基础》实验五通信协议设计

...ttp://www.cnblogs.com/rocedu/p/5087623.html)中的作业两人一组:1、基于Socket实现TCP通信,一人实现服务器,一人实现客户端2、研究OpenSSL算法,测试对称算法中的AES,非对称算法中的RS 查看详情

linux下如何删除一个文件?

...Linux是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX和UNIX的多用户、多任务、支持多线程和多CPU的操作系统。它能运行主要的UNIX工具软件、应用程序和网络协议。它支持32位和64位硬件。Linux继承了Unix以网络为核心... 查看详情