TCP网络编程之chat聊天室

来源:互联网 发布:ubuntu更新软件命令 编辑:程序博客网 时间:2024/06/05 09:40

这一节我们再讲一个tcp长连接的例子,实现网络聊天室的基本功能。
聊天室的基本原理:采用Client/Server TCP架构,客户端发送消息给服务器,服务器再把消息转发给所有的客户端。

一、需求分析

聊天室功能清单,总结的很好,来自博客:
http://blog.csdn.net/ccj2020/article/details/7838910
一个在Linux下可以使用的聊天软件,要求至少实现如下功能:

  1. 采用Client/Server架构
  2. Client A 登陆聊天服务器前,需要注册自己的ID和密码
  3. 注册成功后,Client A 就可以通过自己的ID和密码登陆聊天服务器
  4. 多个Client X 可以同时登陆聊天服务器之后,与其他用户进行通讯聊天
  5. Client A成功登陆后可以查看当前聊天室内其他在线用户Client x
  6. Client A可以选择发消息给某个特定的Client X,即”悄悄话”功能
  7. Client A 可以选择发消息全部的在线用户,即”群发消息”功能
  8. Client A 在退出时需要保存聊天记录
  9. Server端维护一个所有登陆用户的聊天会的记录文件,以便备查

可以选择实现的附加功能:

  1. Server可以内建一个特殊权限的账号admin,用于管理聊天室
  2. Admin可以将某个Client X “提出聊天室”
  3. Admin可以将某个Client X ”设为只能旁听,不能发言”
  4. Client 端发言增加表情符号,可以设置某些自定义的特殊组合来表达感情.如输入:),则会自动发送”XXX向大家做了个笑脸”
  5. Client段增加某些常用话语,可以对其中某些部分进行”姓名替换”,例如,输入/ClientA/welcome,则会自动发送”ClientA 大侠,欢迎你来到咱们的聊天室”

附加功能:

  1. 文件传输

这里我只完成了最基本的功能4,多个客户同时聊天,这也是聊天室的核心功能,其它功能以后再一一实现。

二、chat服务器实现

程序的实现是采用Client/Server TCP架构,服务器负责监听客户端的连接。
当有客户端连接上服务器时,服务器会专门为连接上的客户端开一个线程,用来接收客户端发送过来的消息并把此消息转发给所有的客户端。此外,程序还开了一个线程专门处理关闭服务器的线程,当我们在终端输入字符’Q’时,服务器将关闭所有的连接并退出进程。
程序基本架构:

  1. 主线程:监听来自客户端的连接,如果没有连接,则阻塞在accept函数。
  2. pthread_handle线程处理函数:接收客户发来的消息并群发出去。
  3. quit线程处理函数:可实现通过终端随时关闭服务器。

实现代码:

//server_chat3.c#include<stdio.h>   #include<stdlib.h>#include<sys/types.h> #include<sys/stat.h>#include<netinet/in.h>  #include<sys/socket.h> #include<string.h>#include<unistd.h>#include<signal.h>#include<sys/ipc.h>#include<errno.h>#include<sys/shm.h>#include<time.h>#include<pthread.h>#include <arpa/inet.h>#define PORT 9878#define SIZE 1024#define SIZE_SHMADD 2048#define LISTEN_MAX 10int listenfd;int connfd[LISTEN_MAX];//套接字描述符int get_sockfd(){    struct sockaddr_in server_addr;    if((listenfd=socket(AF_INET,SOCK_STREAM,0))==-1)    {        perror("socket");        exit(-1);    }    printf("Socket successful!\n");    //sockaddr结构     bzero(&server_addr,sizeof(struct sockaddr_in));     server_addr.sin_family=AF_INET;                    server_addr.sin_addr.s_addr=htonl(INADDR_ANY);     server_addr.sin_port=htons(PORT);      // 设置套接字选项避免地址使用错误,为了允许地址重用,我设置整型参数(on)为 1 (不然,可以设为 0 来禁止地址重用)    int on=1;      if((setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)))<0)      {          perror("setsockopt failed");          exit(-1);      }    //绑定服务器的ip和服务器端口号    if(bind(listenfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)         {              perror("bind");        exit(-1);       }    printf("Bind successful!\n");    //设置允许连接的最大客户端数         if(listen(listenfd,LISTEN_MAX)==-1)         {        perror("bind");        exit(-1);     }       printf("Listening.....\n");    return listenfd;}void* pthread_handle(void * arg){    int index,i;    index = *(int *)arg;    printf("in pthread_recv,index = %d,connfd = %d\n",index,connfd[index]);    char buffer[SIZE];    while(1)    {        //用于接收信息        memset(buffer,0,SIZE);        if((recv(connfd[index],buffer,SIZE,0)) <= 0)        {            close(connfd[index]);            pthread_exit(0);        }        printf(" %s\n",buffer);        for(i = 0; i < LISTEN_MAX ; i++)          {            if(connfd[i] != -1)              {                if(send(connfd[i],buffer,strlen(buffer),0) == -1)                {                    perror("send");                    pthread_exit(0);                }             }          }      }}void quit()  {      char msg[10];    int i = 0;    while(1)      {        printf("please enter 'Q' to quit server!\n");        scanf("%s",msg);          if(strcmp("Q",msg)==0)          {              printf("now close server\n");              close(listenfd);             for(i = 0; i < LISTEN_MAX ; i++)              {                  if(connfd[i] != -1)                  {                    close(connfd[i]);                 }              }                   exit(0);          }      }  }  int main(int argc, char **argv){    struct sockaddr_in client_addr;    int sin_size;    pid_t ppid,pid;    int num = 0,i = 0,ret;    //线程标识号    pthread_t thread_server_close,thread_handle;    //unsigned char buffer[SIZE];    char buffer[SIZE];    //创建套接字描述符    int listenfd = get_sockfd();    //记录空闲的客户端的套接字描述符(-1为空闲)      for(i = 0 ; i < LISTEN_MAX; i++)      {          connfd[i]=-1;      }    //创建一个线程,对服务器程序进行管理(关闭)      ret = pthread_create(&thread_server_close,NULL,(void*)(&quit),NULL);     if(ret != 0)    {        perror("Create pthread_handle fail!");        exit(-1);    }    while(1)    {        for(i=0;i < LISTEN_MAX;i++)          {              printf("i == %d\n",i);            if(connfd[i]==-1)//表示套接字容器空闲,可用            {                  break;              }        }         printf("before accept i == %d\n",i);        //服务器阻塞,直到客户程序建立连接         sin_size=sizeof(struct sockaddr_in);        if((connfd[i]=accept(listenfd,(struct sockaddr *)(&client_addr),&sin_size))==-1)                 {            perror("accept");            exit(-1);//要continue还是exit,再考虑        }        printf("Accept successful!\n");        printf("connect to client %d : %s:%d \n",num , inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));        //把界面发送给客户端        memset(buffer,0,SIZE);        strcpy(buffer,"\n------------------Welecom come char------------------------\n");        send(connfd[i],buffer,SIZE,0);        //将加入的新客户发送给所有在线的客户端/        printf("before recv\n");        recv(connfd[i],buffer,SIZE,0);        printf("after recv\n");        strcat( buffer," enter chat....");         int j;        for(j = 0; j < LISTEN_MAX; j++)        {            if(connfd[j] != -1)            {                printf("j == %d\n",j);                send(connfd[j],buffer,strlen(buffer),0);              }        }        int socked_index = i;//这里避免线程还未创建完成,i的值可能会被while循环修改        //创建线程行读写操作        ret = pthread_create(&thread_handle, NULL, pthread_handle, &socked_index);//用于接收信息        if(ret != 0)        {            perror("Create pthread_handle fail!");            exit(-1);        }   }   return 0;}

这里需要注意的点:
原来程序写成了下面这样,把变量 i 作为参数传递给线程函数thread_handle。

ret = pthread_create(&thread_handle, NULL, pthread_handle, &i);if(ret != 0){    perror("Create pthread_handle fail!");    exit(-1);}

后来调试程序的时候发现问题了,就改成了先把i变量赋值给socked_index,然后再把socked_index传递给线程函数thread_handle。
这样做有什么区别呢?读者可以思考一下。

int socked_index = i;ret = pthread_create(&thread_handle, NULL, pthread_handle, &socked_index);//用于接收信息if(ret != 0){    perror("Create pthread_handle fail!");    exit(-1);}

原因是,加入我们用变量i传入线程函数,那么在主线程while循环中,在变量i还未传入的时候,变量i被修改了,造成程序出错。可以理解成线程创建需要一定的时间,但是此时变量 i 会被主线程修改。

三、chat客户端实现

客户端程序的基本框架:
主线程主动和服务器建立连接,然后创建两个线程,一个用于发送消息,一个用于接收消息。
pthread_send线程处理函数:
—–获取客户的输入和当前时间,如果输入是’Q’字符,则退出,否则就把消息发送给服务器。
pthread_recv线程处理函数:
—–接收来自服务器的消息,并打印到终端。

代码实现:

//client_chat3.c#include<stdio.h>#include<netinet/in.h>  #include<sys/socket.h> #include<sys/types.h>#include<string.h>#include<stdlib.h>#include<netdb.h>#include<unistd.h>#include<signal.h>#include<errno.h>#include<time.h>#include<pthread.h>#define SIZE 1024#define SERV_PORT        9878char name[32];void* pthread_recv(void * arg){    char buffer[SIZE];    int sockfd = *(int *)arg;    while(1)    {        //用于接收信息        memset(buffer,0,SIZE);        if(sockfd > 0)        {            if((recv(sockfd,buffer,SIZE,0)) <= 0)            {                close(sockfd);                exit(1);            }            printf("%s\n",buffer);        }    }}void* pthread_send(void * arg){    //时间函数    char buffer[SIZE],buf[SIZE];    int sockfd = *(int *)arg;    struct tm *p_curtime;    time_t timep;    while(1)    {        memset(buf,0,SIZE);        fgets(buf,SIZE,stdin);//获取用户输入的信息        memset(buffer,0,SIZE);        time(&timep);        p_curtime = localtime(&timep);        strftime(buffer, sizeof(buffer), "%Y/%m/%d %H:%M:%S", p_curtime);        /*输出时间和客户端的名字*/        strcat(buffer," \n\t昵称 ->");        strcat(buffer,name);        strcat(buffer,":\n\t\t  ");        /*对客户端程序进行管理*/        if(strncmp("Q",buf,1)==0)        {            printf("该客户端下线...\n");            strcat(buffer,"退出聊天室!");            if((send(sockfd,buffer,SIZE,0)) <= 0)            {                perror("error send");            }            close(sockfd);            sockfd = -1;            exit(0);        }        else        {            strncat(buffer,buf,strlen(buf)-1);            strcat(buffer,"\n");                        if((send(sockfd,buffer,SIZE,0)) <= 0)            {                 perror("send");            }        }    }}int main(int argc, char **argv){    pid_t pid;    int sockfd,confd;    char buffer[SIZE],buf[SIZE];     struct sockaddr_in server_addr;    struct sockaddr_in client_addr;    struct hostent *host;    short port;    //线程标识号    pthread_t thread_recv,thread_send;    void *status;    int ret;    //四个参数    if(argc!=3)     {         fprintf(stderr,"Usage:%s hostname name\a\n",argv[0]);         exit(1);     }     //使用hostname查询host 名字     if((host=gethostbyname(argv[1]))==NULL)     {         fprintf(stderr,"Gethostname error\n");         exit(1);     }     //port=atoi(argv[2]);    strcpy(name,argv[2]);    printf("name is :%s\n",name);    /*客户程序开始建立 sockfd描述符 */     if((sockfd=socket(AF_INET,SOCK_STREAM,0)) < 0)     {         perror("socket");        exit(-1);    }     printf("Socket successful!\n");    /*客户程序填充服务端的资料 */     bzero(&server_addr,sizeof(server_addr)); // 初始化,置0    server_addr.sin_family=AF_INET;          // IPV4    server_addr.sin_port=htons(SERV_PORT);  // (将本机器上的short数据转化为网络上的short数据)端口号    server_addr.sin_addr=*((struct in_addr *)host->h_addr); // IP地址    /* 客户程序发起连接请求 */     if(connect(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr)) < 0)     {        perror("connect");        exit(-1);     }    printf("Connect successful!\n");    /*将客户端的名字发送到服务器端*/    send(sockfd,name,20,0);    //创建线程行读写操作/    ret = pthread_create(&thread_recv, NULL, pthread_recv, &sockfd);//用于接收信息    if(ret != 0)    {        perror("Create thread_recv fail!");        exit(-1);    }    ret = pthread_create(&thread_send, NULL, pthread_send, &sockfd);//用于发送信息    if(ret != 0)    {        perror("Create thread_send fail!");        exit(-1);    }    printf("wait for thread_recv \n");    pthread_join(thread_recv, &status);    printf("wait for thread_send \n");    pthread_join(thread_send, &status);    printf("close sockfd \n");    close(sockfd);    return 0;    }

实验结果:
启动服务器

ubuntu:~/test/1214-test/chat3.0$ ./server_chat3Socket successful!Bind successful!Listening.....i == 0before accept i == 0please enter 'Q' to quit server!Accept successful!connect to client 0 : 192.168.65.1:44408 before recvafter recvj == 0i == 0i == 1before accept i == 1in pthread_recv,index = 0,connfd = 4Accept successful!connect to client 0 : 192.168.65.1:44409 before recvafter recvj == 0j == 1i == 0i == 1i == 2before accept i == 2in pthread_recv,index = 1,connfd = 5 2017/12/06 16:14:27         昵称 ->xiaoming:                  hello xiaohong 2017/12/06 16:14:37         昵称 ->xiaohong:                  hello xiaoming

启动客户端,并且在串口输入hello xiaohong

ubuntu:~/test/1214-test/chat3.0$ ./client_chat3 192.168.65.1 xiaomingname is :xiaomingSocket successful!Connect successful!------------------Welecom come char------------------------wait for thread_recv xiaoming enter chat....xiaohong enter chat....hello xiaohong2017/12/06 16:14:27         昵称 ->xiaoming:                  hello xiaohong2017/12/06 16:14:37         昵称 ->xiaohong:                  hello xiaoming

启动客户端,在串口输入hello xiaoming

ubuntu:~/test/1214-test/chat3.0$ ./client_chat3 192.168.65.1 xiaohongname is :xiaohongSocket successful!Connect successful!wait for thread_recv ------------------Welecom come char------------------------xiaohong enter chat....2017/12/06 16:14:27         昵称 ->xiaoming:                  hello xiaohonghello xiaoming2017/12/06 16:14:37         昵称 ->xiaohong:                  hello xiaoming
原创粉丝点击