天天看點

Linux下基于TCP協定的群聊系統設計(多線程+select)

一、功能介紹

這是基于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 *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);
}
           

繼續閱讀