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