TCP网络编程之chat聊天室

这一节我们再讲一个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 10

int 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        9878
char 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_chat3
Socket successful!
Bind successful!
Listening.....
i == 0
before accept i == 0
please enter 'Q' to quit server!
Accept successful!
connect to client 0 : 192.168.65.1:44408 
before recv
after recv
j == 0
i == 0
i == 1
before accept i == 1
in pthread_recv,index = 0,connfd = 4
Accept successful!
connect to client 0 : 192.168.65.1:44409 
before recv
after recv
j == 0
j == 1
i == 0
i == 1
i == 2
before accept i == 2
in 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 xiaoming
name is :xiaoming
Socket successful!
Connect successful!

------------------Welecom come char------------------------

wait for thread_recv 
xiaoming enter chat....
xiaohong enter chat....
hello xiaohong
2017/12/06 16:14:27 
        昵称 ->xiaoming:
                  hello xiaohong

2017/12/06 16:14:37 
        昵称 ->xiaohong:
                  hello xiaoming

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

ubuntu:~/test/1214-test/chat3.0$ ./client_chat3 192.168.65.1 xiaohong
name is :xiaohong
Socket successful!
Connect successful!
wait for thread_recv 

------------------Welecom come char------------------------

xiaohong enter chat....
2017/12/06 16:14:27 
        昵称 ->xiaoming:
                  hello xiaohong

hello xiaoming
2017/12/06 16:14:37 
        昵称 ->xiaohong:
                  hello xiaoming

猜你喜欢

转载自blog.csdn.net/u014530704/article/details/78731827
今日推荐