TCP网络编程之chat聊天室
来源:互联网 发布:ubuntu更新软件命令 编辑:程序博客网 时间:2024/06/05 09:40
这一节我们再讲一个tcp长连接的例子,实现网络聊天室的基本功能。
聊天室的基本原理:采用Client/Server TCP架构,客户端发送消息给服务器,服务器再把消息转发给所有的客户端。
一、需求分析
聊天室功能清单,总结的很好,来自博客:
http://blog.csdn.net/ccj2020/article/details/7838910
一个在Linux下可以使用的聊天软件,要求至少实现如下功能:
- 采用Client/Server架构
- Client A 登陆聊天服务器前,需要注册自己的ID和密码
- 注册成功后,Client A 就可以通过自己的ID和密码登陆聊天服务器
- 多个Client X 可以同时登陆聊天服务器之后,与其他用户进行通讯聊天
- Client A成功登陆后可以查看当前聊天室内其他在线用户Client x
- Client A可以选择发消息给某个特定的Client X,即”悄悄话”功能
- Client A 可以选择发消息全部的在线用户,即”群发消息”功能
- Client A 在退出时需要保存聊天记录
- Server端维护一个所有登陆用户的聊天会的记录文件,以便备查
可以选择实现的附加功能:
- Server可以内建一个特殊权限的账号admin,用于管理聊天室
- Admin可以将某个Client X “提出聊天室”
- Admin可以将某个Client X ”设为只能旁听,不能发言”
- Client 端发言增加表情符号,可以设置某些自定义的特殊组合来表达感情.如输入:),则会自动发送”XXX向大家做了个笑脸”
- Client段增加某些常用话语,可以对其中某些部分进行”姓名替换”,例如,输入/ClientA/welcome,则会自动发送”ClientA 大侠,欢迎你来到咱们的聊天室”
附加功能:
- 文件传输
这里我只完成了最基本的功能4,多个客户同时聊天,这也是聊天室的核心功能,其它功能以后再一一实现。
二、chat服务器实现
程序的实现是采用Client/Server TCP架构,服务器负责监听客户端的连接。
当有客户端连接上服务器时,服务器会专门为连接上的客户端开一个线程,用来接收客户端发送过来的消息并把此消息转发给所有的客户端。此外,程序还开了一个线程专门处理关闭服务器的线程,当我们在终端输入字符’Q’时,服务器将关闭所有的连接并退出进程。
程序基本架构:
- 主线程:监听来自客户端的连接,如果没有连接,则阻塞在accept函数。
- pthread_handle线程处理函数:接收客户发来的消息并群发出去。
- 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
- TCP网络编程之chat聊天室
- 网络编程TCP协议-聊天室
- TCP之(chat)
- 14 QT TCP网络编程与网络聊天室的实现
- java网络编程之聊天室客户端(二)
- 【网络编程】之七、select聊天室
- android 4.0网络编程之socket聊天室
- 【网络编程】之七、select聊天室
- Java的网络编程之聊天室功能
- 网络编程之TCP
- 网络编程之TCP
- linux网络编程练习---聊天室(TCP/IP实现)
- Erlang TCP编程:聊天室
- 网络编程之TCP编程
- 网络编程 简易聊天室
- c#网络编程之TCP
- java网络编程之TCP
- 网络编程之TCP通信
- linux:bash shell
- Python中解决Gensim找不到模块的问题
- poj 3181
- try、catch、finally相关问题
- C++ STL一一map存储小实例
- TCP网络编程之chat聊天室
- mybatis一对一 一对多关联关联
- Windows上安装MySQL
- Java删除字符串中指定开始到指定结束之间的内容
- C++ explicit关键字详解
- Webservice工作原理及实例
- SWIG之为C/C++的API生成Python调用接口基础
- https://jingyan.baidu.com/album/d169e186620b60436711d860.html?picindex=4
- php 日志类