zoukankan      html  css  js  c++  java
  • [Linux环境编程] TCP通信与多线程编程实现“多人在线聊天室”

    [linux环境编程] TCP通信与多线程编程实现“多人在线聊天室”

    一、基本概念

    1、TCP通信

      TCP(Transmission Control Protocol)就是传输控制通讯协议,是TCP/IP体系结构中最主要的传输协议。其“三次握手”提供了可靠的传送,高可靠性保证了数据传输不会出现丢失与乱序,再加之TCP连接两端设有缓存用来临时存放双向通信的数据,所以可以支持全双工传输。非常贴合“多人在线聊天室”对数据传输的需求。

    2、多线程编程

      多线程是指从软件或者硬件上实现多个线程并发执行的技术。简单来说就是能够在同一时间执行多于一个线程,每个线程处理各自独立的任务,进而简化处理异步事件的代码,改善响应时间,提升整体处理性。

      一个进程的所有信息对该进程的所有线程都是共享的,包括可执行代码、程序的全局内存和堆内存、栈以及文件描述符。因此,相比进程与进程之间,线程之间的通信免去了繁杂的规则,但同时也面临着线程同步的问题,需要多加注意。

    二、程序实现效果

    1、功能介绍

      服务端:可限制聊天室在线最大人数与等待进入最大人数。当聊天室人数达到上限,申请进入聊天室的用户将会排队,聊天室内任意用户退出时,排队用户会按照顺序自动加入聊天室。

      客户端:实时显示聊天室内成员所发消息以及成员昵称,可主动退出聊天室,当聊天室有成员变动时会有系统提示。

    2、测试流程:

    2.1  编译

      -std=gnu99:以GNU99标准编译代码;

      -o output_filename:将输出文件的名称命名为output_filename,同时这个名称不能和源文件同名。如果不给出这个选项,就会生成系统默认的“a.out”可执行文件,易被覆盖。

      -lpthread:在编译的链接阶段自动加载pthread库。

            gcc -std=gnu99 server_tcp.c -o server -lpthread
            gcc -std=gnu99 client_tcp.c -o client -lpthread

      

    2.2  开启server服务端与三个客户端(client1/2/3)

      Ubuntu可通过Ctrl+Shift+T开启多个终端标签页,分别执行以下四条指令。其中第一个参数为可执行文件的路径;第二个参数为通讯的串口号,1~1024已被系统使用,一般情况下大于1024即可;第三个参数为通讯的IP地址,“127.0.0.1”会自动转化为本机的IP地址。

            ./server 2333 127.0.0.1
            ./client 2333 127.0.0.1
            ./client 2333 127.0.0.1
            ./client 2333 127.0.0.1

    3、测试效果

      服务端设置:最大在线人数2人,排队最大人数1人(实际中至少要设置5个,排队人数上限太低则无实际意义,这里设置1人仅做演示)。开启服务器后依次连接三个客户端,输入用户名后申请进入聊天室,前两个用户进入聊天室,第三个用户排队等候。输入字符‘q’退出聊天室。

    server(服务器):

     

    client1(用户123):

     

    client2(用户456):

    client3(用户789):

    三、代码分析

    1、客户端(server_tcp.c)

    1.1  main 主函数

     1 #include <stdio.h>
     2 #include <sys/socket.h>
     3 #include <stdbool.h>
     4 #include <arpa/inet.h>
     5 #include <sys/types.h>
     6 #include <unistd.h>
     7 #include <string.h>
     8 #include <stdlib.h>
     9 #include <netinet/in.h>
    10 #include <pthread.h>
    11 
    12 // 定义消息结构体,用于信息的传输
    13 typedef struct Msg
    14 {
    15     char m_name[31];
    16     char m_buf[255];
    17 }Msg;
    18 
    19 Msg msg = {};
    20 
    21 bool quit = false;  // 该程序中暂无实际意义,用于后期拓展
    22 int  sockfd = 0; // socket标识符
    23 int main(int argc,char** argv)
    24 {
    25     pthread_t ptid[2] = {};
    26 
    27     // 创建socket对象
    28     sockfd = socket(AF_INET,SOCK_STREAM,0);
    29     if(0 > sockfd)
    30     {
    31         perror("socket");
    32         return -1;
    33     }
    34 
    35     // 准备通信地址
    36     struct sockaddr_in addr = {AF_INET};
    37     addr.sin_port = htons(atoi(argv[1]));
    38     addr.sin_addr.s_addr = inet_addr(argv[2]);
    39     
    40     /*//等待连接
    41     struct sockaddr_in src_addr = {};
    42     socklen_t addr_len = sizeof(src_addr);*/
    43 
    44     // 连接
    45     int ret = connect(sockfd,(struct sockaddr*)&addr,sizeof(addr));
    46     if(0 > ret)
    47     {
    48         perror("connect");
    49         return -1;
    50     }
    51 
    52     printf("请输入您的昵称:");
    53     scanf("%s",msg.m_name);
    54 
    55     // 创建读数据进程
    56     ret = pthread_create(&ptid[0],NULL,pthread_read,NULL);
    57     if(0 > ret)
    58     {
    59         perror("pthread_create");
    60         return -1;
    61     }
    62 
    63     // 创建写数据进程
    64     ret = pthread_create(&ptid[1],NULL,pthread_write,NULL);
    65     if(0 > ret)
    66     {
    67         perror("pthread_create");
    68         return -1;
    69     }
    70 
    71     while(!quit);
    72     close(sockfd);
    73 }

       客户端主函数主要用于建立起客户端和服务器的连接,客户端在申请连接后可能出现“进入聊天室(num<nmax_chat)”、“等待进入聊天室(num<nmax_chat+nmax_wait)”和“连接被拒(num>=nmax_chat+nmax_wait)”三种情况。客户端在获取用户的用户名后将会建立起读、写数据两个线程,实时接收和发送数据。

    1.2  pthread_read 读线程

     1 void* pthread_read(void* arg)
     2 {
     3     while(1)
     4     {
     5         bzero(&msg.m_buf,sizeof(msg.m_buf));
     6         int ret = recv(sockfd,&msg,sizeof(msg),0);
     7         if (0 < strlen(msg.m_buf))
     8         {
     9             printf("%s:%s
    ",msg.m_name,msg.m_buf);
    10         }
    11         /*if(!strcmp("q",msg))
    12         {
    13             break;
    14         }*/
    15     }
    16     close(sockfd);
    17 }

      读线程实时接收数据并显示数据发送者的用户名及其所发送的数据。 

    1.3  pthread_write 写线程

     1 void* pthread_write(void* arg)
     2 {
     3     while(1)
     4     {
     5         gets(msg.m_buf);
     6         //sprintf(msg,"%s:%s",name,msg); // name贴进去时msg已经改变
     7         printf("33[1A");
     8         printf("
                                       
    ");
     9         fflush(stdout);
    10         send(sockfd,&msg,sizeof(msg),0);
    11         if(!strcmp("q",msg.m_buf))
    12         {
    13             quit = true;
    14             break;
    15         }        
    16     }
    17 }

      写线程与读线程之间存在互相干扰,因为二者都必须保证实时性,所以无法采用互斥锁来保护数据,这里将全局变量msg改为局部变量即可解决问题。而主函数中的msg.m_name则可以通过线程创建函数传递给写线程,希望代码更加严谨的话可以自行更改。

      7~9行代码组合实现了“消除己方残留在终端显示界面的所发送的数据”。

      第7行代码:将光标上移一行,即残留显示数据的行列;

      第8行代码:将光标移至该行行首,再输出一段空格覆盖原有数据,最后将光标移回行首;

      第9行代码:刷新标准输出缓冲区,把输出缓冲区里的东西打印到标准输出设备上(显示终端)。目的是使7、8行代码立即生效。

    2、服务端

    2.1  main 主函数

     1 #include <stdio.h>
     2 #include <sys/socket.h>
     3 #include <stdbool.h>
     4 #include <arpa/inet.h>
     5 #include <sys/types.h>
     6 #include <unistd.h>
     7 #include <string.h>
     8 #include <stdlib.h>
     9 #include <netinet/in.h>
    10 #include <pthread.h>
    11 
    12 typedef struct Msg
    13 {
    14     char m_name[31];
    15     char m_buf[255];
    16 }Msg;
    17 
    18 typedef struct Client
    19 {
    20     int  c_fd;
    21     bool c_flag;
    22 }Client;
    23 
    24 const int nmax_chat = 2;
    25 const int nmax_wait = 1;
    26 int num_chat = 0;
    27 int num_wait = 0;
    28 Client client[4] = {}; // nmax_wait + nmax_chat + 1;
    29 
    30 bool quit = false;
    31 int  sockfd = 0;
    32 pthread_t ptid[3] = {};
    33 
    34 int main(int argc,char** argv)
    35 {
    36     // 创建socket对象
    37     sockfd = socket(AF_INET,SOCK_STREAM,0);
    38     if(0 > sockfd)
    39     {
    40         perror("socket");
    41         return -1;
    42     }
    43 
    44     // 准备通信地址
    45     struct sockaddr_in addr = {AF_INET};
    46     addr.sin_port = htons(atoi(argv[1]));
    47     addr.sin_addr.s_addr = inet_addr(argv[2]);
    48 
    49     // 绑定对象与地址
    50     int ret = bind(sockfd,(struct sockaddr*)&addr,sizeof(addr));
    51     if(0 > ret)
    52     {
    53         perror("bind");
    54         return -1;
    55     }
    56 
    57     // 设置排队数量
    58     listen(sockfd,num_wait);
    59     
    60     // 创建线程
    61     ret = pthread_create(&ptid[0],NULL,pthread_accept,NULL);
    62     if(0 > ret)
    63     {
    64         perror("pthread_create");
    65         return -1;
    66     }
    67 
    68     while(!quit);
    69     // 关闭连接
    70     close(sockfd);
    71 }

      功能与客户端主函数类似,主要用于建立起客户端和服务器的连接的准备工作。客户端在设立监听数量(等待连接最大数量nmax_wait)后将会建立起接收线程。

    2.2  pthread_accep 接收线程

     1 void* pthread_accept(void* arg)
     2 {    
     3     // 等待连接
     4     struct sockaddr_in src_addr = {};
     5     socklen_t addr_len = sizeof(src_addr);
     6 
     7     //printf("聊天室已创建!
    ");
     8     while(1)
     9     {
    10         if (nmax_chat > num_chat)
    11         {
    12             int i = 0;
    13             for ( i = 0; i < nmax_chat; i++)
    14             {
    15                 if (false == client[i].c_flag)
    16                 {
    17                     client[i].c_fd = accept(sockfd,(struct sockaddr*)&src_addr,&addr_len);
    18                     client[i].c_flag = true;
    19                     break;
    20                 }
    21             }    
    22             num_chat++;
    23             // 创建线程
    24             //printf("chat[%d] fd:%d flag:%d num_chat:%d
    ",i,client[i].c_fd,client[i].c_flag,num_chat);
    25             int ret = pthread_create(&ptid[1],NULL,pthread_com,&client[i]);
    26             if(0 > ret)
    27             {
    28                 perror("pthread_create");
    29                 pthread_exit(NULL);
    30             }
    31         }
    32     }
    33     pthread_exit(NULL);
    34 }

       接收线程可以实时接收申请连接服务器的客户端,建立起服务器和客户端的连接,通过for循环与标识符c_flag来判断是否连接申请的客户端。打个比方,就好像你去饭店吃饭,当你想进入饭店时,门口店员会先环视店内(for循环)、确认是否有座(num_chat<nmax_chat?)、有无被预定(client[i].flag),有则安排进店,无则确认店外的等候座椅是否还有空位(num_wait<nmax_wait?),有则安排座位在店外等候(listen(nmax_wait)),无则无法安排。对于可以进店的用户,店员会安排好座位(client[i].c_fd)并递上菜单(client[i].c_flag),点好菜后准备上菜(pthread_com)。

    2.3  pthread_com 信息传输线程

    void* pthread_com(void* arg)
    { 
        Client* client2 = arg;
        Msg msg = {}; // 发送该线程对应用户消息及进出聊天室情况
        char name[31] = {}; // 记录、发送该线程对应用户昵称
    
        int ret = recv(client2->c_fd,&msg,sizeof(msg),0);
        strcpy(name,msg.m_name);
        //printf("name:%s fd:%d flag:%d num_chat:%d
    ",name,client2->c_fd,client2->c_flag,num_chat);
    
        char buf[34] = "***"; // sizeof(buf) = sizeof(name) + 3
        bzero(&msg,sizeof(msg));
        strcpy(msg.m_name,strcat(buf,name));
        sprintf(msg.m_buf,"已进入聊天室[%d人]***",num_chat);        
        for (int i = 0; i < nmax_chat; i++)
        {
            if(client[i].c_flag == true)
                send(client[i].c_fd,&msg,sizeof(msg),0);
        }
    
        //通信
        while(1)
        {
            bzero(&msg,sizeof(msg));
            int ret = recv(client2->c_fd,&msg,sizeof(msg),0);
            if (0 < strlen(msg.m_buf))
            {
                if(!strcmp("q",msg.m_buf))
                {
                    num_chat--;     
                    client2->c_flag = false;
                    //printf("name:%s fd:%d flag:%d num_chat:%d
    ",name,client2->c_fd,client2->c_flag,num_chat);
                    char buf[34] = "***"; // sizeof(buf) = sizeof(name) + 3
                    bzero(&msg,sizeof(msg));
                    strcpy(msg.m_name,strcat(buf,name));
                    sprintf(msg.m_buf,"已退出聊天室[%d人]***",num_chat);        
                    for (int i = 0; i < nmax_chat; i++)
                    {
                        if(client[i].c_flag == true)
                            send(client[i].c_fd,&msg,sizeof(msg),0);
                    }    
                    break;
                }    
                else
                {
                    for (int i = 0; i < nmax_chat; i++)
                    {
                        //printf("返回了数据:%s
    ",msg.m_buf);
                        strcpy(msg.m_name,name);
                        if(client[i].c_flag == true)
                            send(client[i].c_fd,&msg,sizeof(msg),0);
                    }
                }
            }
        }
        pthread_exit(NULL);
    }

       信息传输线程主要承担起邮局的功能,将各个客户端投递到邮局的信件(msg)配送至收件人(client n)手中,同时将一些意外事件(人员变动)转达给寄件人收件人(client n)。不过相比真正的邮局还是有很大差别的,这家邮局不仅会复制你的信件(strcpy),还有可能夹杂私货、更改内容(敏感词屏蔽(未实现))。

    四、总结

      总的来说,最近这次编程让我意识到一个很大的问题。以前总是在做之前想太多,总是希望能够一次性设计好整个架构,然而这是建立在一定的项目经验上的。就我目前而言暂不具备这样的水平,所以在编写程序时就很容易导致代码臃肿、逻辑混乱,从而导致难以调试。

      所以这次编程更改了思路:先将项目拆分成一个个小的功能模块,底层搭好后再按照功能层级逐个实现,一个一个拼接、一块砖一块砖的搭建,边搭建边微调框架,最终完成程序的编写。这种编程思路在这次练习中起到了很大的作用。整个编程过程思路清晰、编写流畅,代码的调试也轻松许多。

      以上便是我这次练习的总结,发布博客以供记录、总结,博客中如有纰漏欢迎指出,欢迎讨论、共同进步。

  • 相关阅读:
    从开心网的奴隶安抚与折磨想到员工积极性与人力成本的问题
    悲剧的做网站的,我们都没有认真的前行
    如何做生意
    Android Market中产品图标设计原则
    控制UpdataPanel中的GridView模板列控件同步刷新
    DataTable筛选
    IE6,7,8,FF兼容总结
    DevPress Grid 设置行样式
    SQL分组查询
    DevexPress checkedit 多选解决方案(原创)
  • 原文地址:https://www.cnblogs.com/usingnamespace-caoliu/p/9411083.html
Copyright © 2011-2022 走看看