聊天室简介
本聊天室基于LinuxC进行编写,使用到的有tcp协议、多线程、互斥量、条件变量等知识,实现一个最大可接入20个用户的群聊聊天室;服务端运行后,用户运行用户端接入,输入用户名即可接入;用新用户接入或者下线时,均会群发消息提醒其他在线用户;运行效果如下所示:
由于时间仓促,本聊天室为1.0版本,仍有许多地方需要优化,如并发机制、接入客户端数量、客户端下线后编号的复用(可考虑用链表代替数组)、代码简洁性等问题,后续会再优化更新;
聊天室源码
服务端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#define BUFSIZE 512
#define ClientMax 20
int s_fd;
int c_fd[ClientMax] = {
0}; //最大可接收20个客户端的套接字
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t w_msg = PTHREAD_COND_INITIALIZER;
char sendBuf[BUFSIZE] = {
0};
//构造一个结构体用于存储客户端用户的名字及套接字编号
struct Userinfo
{
int num;
char username[20];
};
//用于发送信息的线程函数
void *sendmsg_func(void *p)
{
int i;
printf("启动信息发送线程:\n");
while(1)
{
pthread_mutex_lock(&lock);
//利用条件变量,当收到任一客户端的信息后,就群发该信息到全体客户端中
pthread_cond_wait(&w_msg,&lock);
//给所有在线的客户端发送信息
for(i = 0;c_fd[i] != 0 && i < ClientMax;i++)
{
if (c_fd[i] == -1)
{
continue; //如果是已退出的客户端,则不发送信息
}
else
{
if(write(c_fd[i],sendBuf,BUFSIZE) < 0 )
{
perror("write");
exit(-1);
}
}
}
pthread_mutex_unlock(&lock);
}
}
//用于接收客户端信息的函数
void *recv_func(void *p)
{
//将传递进来的用户姓名、socket编号存到局部变量中,方便使用
int tmp_cnt = ((struct Userinfo *)p)->num;
char tmp_username[20] = {
0};
strcpy(tmp_username,((struct Userinfo *)p)->username);
char readBuf[BUFSIZE] = {
0};
int n_read = 0;
printf("启动%d号线程用于接收信息\n",tmp_cnt);
//通知聊天室内所有用户有新用户上线
pthread_mutex_lock(&lock);
memset(sendBuf,0,BUFSIZE);
sprintf(sendBuf,"%s上线了\n",tmp_username);
pthread_cond_signal(&w_msg);
pthread_mutex_unlock(&lock);
//不断接收对应套接字用户发来的信息
while(1)
{
memset(readBuf,0,BUFSIZE);
n_read = read(c_fd[tmp_cnt],readBuf,sizeof(readBuf));
if(n_read == -1)
{
perror("read");
exit(1);
}
else if(n_read == 0)
{
//用户下线了,群发信息告知其他用户
pthread_mutex_lock(&lock);
memset(sendBuf,0,BUFSIZE);
sprintf(sendBuf,"%s下线了\n",tmp_username);
pthread_cond_signal(&w_msg);
pthread_mutex_unlock(&lock);
c_fd[tmp_cnt] = -1; //如果对应的客户端退出,则令对应的c_fd的值为-1,表示掉线
pthread_exit(NULL); //如果对方关闭,结束线程
}
else
{
printf("#%s\n",readBuf); //将用户发送的信息打印在服务端,若有数据库,这里可以将聊天记录存在数据库
}
//给群发信息的线程发送信号,群发用户发送进来的消息
pthread_mutex_lock(&lock);
memset(sendBuf,0,BUFSIZE);
strcpy(sendBuf,readBuf);
pthread_cond_signal(&w_msg);
pthread_mutex_unlock(&lock);
}
}
//信号中断函数,若本程序被外来信号打断,可以及时清理现场,释放资源
void int_handler(int s)
{
int i;
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&w_msg);
for(i=0;c_fd[i] != 0 && i < ClientMax;i++)
{
if(c_fd[i] == -1)
continue;
else
close(c_fd[i]);
}
close(s_fd);
exit(0);
}
void main()
{
struct sockaddr_in s_addr;
struct sockaddr_in c_addr[ClientMax];
pthread_t tid[ClientMax] = {
0};
int addr_len = sizeof(struct sockaddr_in);
int err;
int c_cnt = 0;
struct Userinfo userinfo[ClientMax] = {
0};
//创建socket
s_fd = socket(AF_INET,SOCK_STREAM,0);
if (s_fd < 0)
{
perror("socket");
exit(1);
}
//取消关闭socket后的wait等待
int val =1;
if (setsockopt(s_fd,SOL_SOCKET,SO_REUSEADDR,&val,sizeof(val)) < 0)
{
perror("setsockopt");
exit(1);
}
//bind()
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(8899); //host to net short
inet_aton("0.0.0.0",&s_addr.sin_addr); //sin_addr即为in_addr格式
if(bind(s_fd,(struct sockaddr*)&s_addr,sizeof(s_addr)) == -1)
{
perror("bind");
exit(-1);
}
//listen()
if(listen(s_fd,200) < 0) //暴露s_fd指向的socket给客户端连接,最多接受200个连接
{
perror("listen");
exit(-1);
}
//创建一个线程用来发送消息给所有客户端
pthread_t send_tid;
err = pthread_create(&send_tid,NULL,sendmsg_func,NULL);
if(err)
{
fprintf(stderr,"Create pthread fail:%s\n",strerror(err));
exit(1);
}
sleep(1); //让发送信息线程有足够时间加锁
signal(SIGINT,int_handler);
//不断等待是否有新客户端接入
while(1)
{
userinfo[c_cnt].num = c_cnt; //给新接入的客户端套接字分配一个编号
//等待新客户端接入
c_fd[c_cnt] = accept(s_fd,(struct sockaddr*)(c_addr+c_cnt),&addr_len); //接收客户端的连接
if (c_fd[c_cnt] < 0)
{
perror("accept()");
exit(-1);
}
printf("get connect %s\n",inet_ntoa(c_addr[c_cnt].sin_addr));
//将客户端发送来的姓名存入userinfo结构体中
err = read(c_fd[c_cnt],userinfo[c_cnt].username,sizeof(userinfo[c_cnt].username));
if(err == -1)
{
perror("read");
exit(1);
}
//创建一个新线程用来发送信息
err = pthread_create((tid+c_cnt),NULL,recv_func,userinfo+c_cnt);
if (err)
{
fprintf(stderr,"Create pthread fail:%s\n",strerror(err));
exit(1);
}
c_cnt++; //客户端计数器+1
}
//close()
//回收资源
close(s_fd);
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&w_msg);
exit(0);
}
客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <time.h>
#define BUFSIZE 512
int c_fd;
int n_read;
char readBuf[BUFSIZE] = {
0};
//用来发送信息的线程函数
void *rcvmsg_func(void *p)
{
while(1)
{
memset(readBuf,0,BUFSIZE); //清空readBuf
//读取来自服务端的信息
n_read = read(c_fd,readBuf,sizeof(readBuf));
if(n_read == -1)
{
perror("read");
exit(1);
}
else if(n_read == 0)
{
pthread_exit(NULL); //如果对方或者自己关闭套接字,则退出
}
else
{
printf("#%s\n",readBuf);
}
}
}
//该函数用于组合待发送的信息
void msg_merge(char *sendBuf,char *username,char *time,char *str)
{
char title[20] = {
"骚气聊天室 "};
char say[10] = {
"说:"};
strcpy(sendBuf,title);
strcat(sendBuf,time);
strcat(sendBuf,username);
strcat(sendBuf,say);
strcat(sendBuf,str);
}
void main()
{
struct sockaddr_in c_addr;
int addr_len = sizeof(struct sockaddr_in);
char str[BUFSIZE] = {
0}; //存放输入的信息
char sendBuf[BUFSIZE] = {
0};
int err;
char username[20] = {
0};
struct tm *timeptr; //关于日期的一个结构体指针
time_t timeval; //关于时间的一个结构体变量
char tm[50];
//创建socket
c_fd = socket(AF_INET,SOCK_STREAM,0);
if (c_fd < 0)
{
perror("socket");
exit(1);
}
c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(8899);
inet_aton("127.0.0.1",&c_addr.sin_addr);
//输入用户姓名
printf("欢迎来到骚气聊天室,输入你的英文名:\n");
scanf("%s",username);
getchar(); //吸收掉scanf的换行符
//connect()
if((connect(c_fd,(struct sockaddr*)&c_addr,sizeof(c_addr))) == -1)
{
perror("connect");
exit(-1);
}
//创建一个线程用于接收信息
pthread_t tid;
err = pthread_create(&tid,NULL,rcvmsg_func,NULL);
if (err)
{
fprintf(stderr,"Create pthread fail:%s\n",strerror(err));
exit(1);
}
//send you name to server
if((err = write(c_fd,username,sizeof(username))) < 0 )
{
perror("write");
exit(1);
}
printf("连接成功,退出聊天室请输入“quit”\n");
printf("聊天可直接输入信息:\n");
//while循环用于发送信息
while(1)
{
//清空buf中的信息
memset(str,0,BUFSIZE);
memset(sendBuf,0,BUFSIZE);
//获取当前时间
(void)time(&timeval);
strcpy(tm,ctime(&timeval));
fgets(str,BUFSIZE,stdin); //接收用户输入的信息
//判断是否为退出命令
if(strcmp(str,"quit\n") == 0)
{
printf("退出聊天室");
break;
}
msg_merge(sendBuf,username,tm,str); //将姓名、时间、发送的信息组合到sendBuf中
//发送信息到socket中
if((err = write(c_fd,sendBuf,BUFSIZE)) < 0 )
{
perror("write");
exit(1);
}
else if (err == 0)
{
break;
}
}
//close()
close(c_fd);
exit(0);
}