基于Linux下的多人聊天室
1.涉及知识点
Linux、C语言、TCP通信、epoll、SQL
2.整体架构流程
服务器:
1.搭建TCP连接客户端
2.链接数据库
3.使用epoll
4.处理各种客户端消息的接收与发送
客户端:
1.搭建TCP连接服务器
2.建立菜单处理各种消息的发送与接收
3.核心功能展示
1.登录界面
用户端:
服务器端(反馈):
进行注册:
登录:
私聊:
群聊
数据库:
4.详细代码
- 服务器端
#include <stdio.h>
#include <mysql/mysql.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <time.h>
#define SERVER_IP "192.168.43.113"
#define SERVER_PORT 1111
int skd; // 服务器套接子
int epfd;
struct epoll_event ev; // 这个结构体仅仅创建一次,后续都可使用
int clinum; // 用来标记在线人数
int num; // 用来保存异动套接子的个数
char buf[1024]; // 数据库传数据的
enum MessageType
{
// menu1
REGISTRATION,
LOGIN,
// menu2
QUN,
SI,
DELETE_ID,
LOG_out,
Ke_out
// 此处添加其他消息类型
};
typedef struct // 打包本地数据
{
int chat_mode;
int UID;
char nickname[50];
char password[20];
int target_id;
char send_message[1024];
int socket;
int log_flag;
enum MessageType type;
} User;
User user[1024];
User user1;
User user2;
MYSQL *sql;
void *client_register_func(void *arg);
void *client_LOG_IN_func(void *arg);
void look_time();
int main(void)
{
memset(&user1, 0, sizeof(user1));
skd = socket(AF_INET, SOCK_STREAM, 0); // 套接字类型为流式套接字(SOCK_STREAM)
struct sockaddr_in info; // 用于存储服务器的地址信息
info.sin_addr.s_addr = inet_addr(SERVER_IP); // 设置服务器的IP地址
info.sin_family = AF_INET; // 指定地址族为IPv4(AF_INET)
info.sin_port = htons(SERVER_PORT); // 设置服务器的端口号,htons是将主机字节顺序统一为大端(主机字节顺序大小段模式不确定)
// 允许快速重用端口,并支持多个套接字绑定到同一个端口
int opt = 1;
setsockopt(skd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
setsockopt(skd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
// 将套接字与指定的本地地址和端口号相关联
int ret = bind(skd, (struct sockaddr *)&info, sizeof(info));
if (ret < 0)
{
perror("bind");
return -1;
}
printf("服务器IP绑定成功\n");
ret = listen(skd, 1024);
if (ret < 0)
{
perror("listen");
return -1;
}
printf("服务器已启动,等待连接...\n");
char buf[128];
// 1.创建句柄 --- 只创建一次
epfd = epoll_create(1);
if (epfd <= 0)
{
perror("epoll_create");
return -1;
}
// 2.在句柄中加入监听套接子
ev.data.fd = skd;
ev.events = EPOLLIN;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, skd, &ev);
if (ret < 0)
{
perror("epoll_ctl");
return -1;
}
struct epoll_event retevn[1024];
// 链接数据库
// 1.初始化数据库
sql = mysql_init(NULL);
// 2.连接数据库
sql = mysql_real_connect(sql, "localhost", "root", "1", "mysql", 0, NULL, 0);
if (sql == NULL)
{
perror("mysql_real_connect");
return -1;
}
printf("数据库连接成功\n");
// 链接需要使用的数据库
sprintf(buf, "%s", "use Chat_data");
// 在终端创建数据表:users_info(UID int,昵称 char(50),密码 char(20))
ret = mysql_query(sql, buf);
if (ret != 0)
{
perror("mysql_query");
return -1;
}
printf("更改数据库成功\n");
memset(buf, 0, sizeof(buf));
int i = 0;
while (1)
{
printf("开始阻塞\n");
//-1表示单次阻塞的时间足够长
num = epoll_wait(epfd, retevn, 1024, -1);
if (num <= 0)
{
perror("epoll_wait");
continue;
}
printf("解除阻塞\n");
for (i = 0; i < num; i++) // 异动套接子可能不止一个
{
if (retevn[i].data.fd == skd) // 说明服务器套接字有动静
{
if (retevn[i].events == EPOLLIN)
{
// 说明有客户端上线
struct sockaddr client_info;
socklen_t len = sizeof(client_info);
// 将上线的文件描述符单独保存
int client_socket = accept(skd, &client_info, &len);
if (client_socket < 0)
{
perror("accept");
// 处理接受错误
continue; // 继续处理其他事件
}
// 将上线的套接字加入到epoll的句柄
ev.data.fd = client_socket;
ev.events = EPOLLIN;
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, client_socket, &ev);
if (ret < 0)
{
perror("epoll_ctl");
// 处理epoll控制错误
close(client_socket); // 关闭套接字
continue; // 继续处理其他事件
}
printf("new client connected: %d\n", client_socket);
user[clinum].socket = client_socket; // 将新客户端信息保存到数组中
printf("clinum = %d\n", clinum);
printf("user[clinum].socket = %d\n", user[clinum].socket);
clinum++;
}
}
else // 说明客户端有动静
{
for (int j = 0; j < clinum; j++)
{
if (user[j].socket == retevn[i].data.fd)
{
if (read(retevn[i].data.fd, &user1, sizeof(user1)) == 0)
{
printf("socket:%d 断开\n", user[j].UID);
close(user[j].socket);
// 移除断开连接的套接字
for (int k = j; k < clinum - 1; k++)
{
user[k] = user[k + 1];
}
clinum--;
break;
}
switch (user1.type)
{
case REGISTRATION:
{
pthread_t pd1 = 0;
pthread_create(&pd1, NULL, client_register_func, (void *)retevn[i].data.fd);
break;
}
case LOGIN:
{
user1.socket = user[j].socket; // 异动套接子 --- 发送方套接子
pthread_t pd2 = 0;
pthread_create(&pd2, NULL, client_LOG_IN_func, (void *)retevn[i].data.fd);
break;
}
case QUN:
{
user1.socket = user[j].socket; // 异动套接子 --- 发送方套接子
printf("%d %d %s\n", user1.socket, user1.UID, user1.send_message);
printf("%s\n", user1.send_message);
printf("user1.socket%d\n", user1.socket);
for (int k = 0; k < clinum; k++)
{
if (user[k].socket != user1.socket)
{
printf("user1.socket%d\n", user1.socket);
printf("user1.nick%s\n", user1.nickname);
printf("user[k].socket %d\n", user[k].socket);
sprintf(buf, "%s", "select * from users_info");
mysql_query(sql, buf);
MYSQL_RES *retval = mysql_store_result(sql);
// 8.1.获取行数 --- 判断数据表中是否存在相关数据
// 判断这个人是否存在,找到对应人的对应值
unsigned long rownum = mysql_num_rows(retval);
unsigned long fieldnum = mysql_num_fields(retval);
// 8.2获取每一行中的内容
int a = 0, b = 0;
MYSQL_ROW rowval;
for (a = 0; a < rownum; a++) // 打印数据表中的所有内容
{
rowval = mysql_fetch_row(retval);
if (atoi(rowval[3]) == user1.socket)
{
strcpy(user1.nickname, rowval[1]);
}
}
write(user[k].socket, &user1, sizeof(user1));
}
}
break;
}
case SI:
{
user2=user1;
printf("%d:%d\n",user1.socket,user1.UID);
sprintf(buf, "%s", "select * from users_info");
mysql_query(sql, buf);
MYSQL_RES *retval = mysql_store_result(sql);
// 8.1.获取行数 --- 判断数据表中是否存在相关数据
// 判断这个人是否存在,找到对应人的对应值
unsigned long rownum = mysql_num_rows(retval);
unsigned long fieldnum = mysql_num_fields(retval);
// 8.2获取每一行中的内容
int a = 0, b = 0;
MYSQL_ROW rowval;
for (a = 0; a < rownum; a++) // 打印数据表中的所有内容
{
rowval = mysql_fetch_row(retval);
if (atoi(rowval[3]) == user1.socket)
{
strcpy(user2.nickname,rowval[1]);
}
if (atoi(rowval[0]) == user2.UID)
{
user2.socket = atoi(rowval[3]);
}
}
printf("接收方套接子为 is %d\n", user2.socket);
write(user2.socket, &user2, sizeof(user2));
break;
}
case LOG_out:
{
printf("UID:%d %s 退出登陆\n", user1.UID, user1.nickname);
sprintf(buf, "UPDATE users_info SET socket = 0 WHERE socket = %d", user[j].socket);
mysql_query(sql, buf);
// UPDATE users_info SET socket = 0 WHERE UID = '50663392'
break;
}
case Ke_out:
{
printf("socket:%d 客户端退出\n", user[j].socket);
close(user[j].socket);
// 移除断开连接的套接字
for (int k = j; k < clinum - 1; k++)
{
user[k] = user[k + 1];
}
clinum--;
break;
}
case DELETE_ID:
{
printf("UID:%d 账号已注销\n", user1.UID);
sprintf(buf, "delete from users_info where UID=%d", user1.UID);
mysql_query(sql, buf);
}
default:
break;
}
}
}
}
}
}
return 0;
}
void *client_register_func(void *arg)
{
srand(time(0));
int fd = (int)arg;
int i = 0;
for (i = 0; i < clinum; i++)
{
if (user[i].socket == fd)
{
int number;
do
{
number = rand() % 90000000 + 10000000;
} while (number == 0);
// 检查随机数是否已经存在
int exists = 0;
for (int j = 0; j < clinum; j++)
{
if (user[j].UID == number)
{
exists = 1;
break;
}
}
if (exists)
{
// 如果随机数已存在,重新生成
i--;
continue;
}
strcpy(user[i].nickname, user1.nickname);
strcpy(user[i].password, user1.password);
user[i].UID = number;
// 写到数据库中
sprintf(buf, "insert into users_info value(%d,'%s','%s',%d)", user[i].UID, user[i].nickname, user[i].password, user[i].socket);
mysql_query(sql, buf);
write(fd, &user[i], sizeof(user[i]));
}
}
return NULL;
}
void *client_LOG_IN_func(void *arg)
{
int fd = (int)arg;
sprintf(buf, "%s", "select * from users_info");
mysql_query(sql, buf);
MYSQL_RES *retval = mysql_store_result(sql);
// 8.1.获取行数 --- 判断数据表中是否存在相关数据
// 判断这个人是否存在,找到对应人的对应值
unsigned long rownum = mysql_num_rows(retval);
unsigned long fieldnum = mysql_num_fields(retval);
// 8.2获取每一行中的内容
int a = 0, j = 0;
MYSQL_ROW rowval;
for (a = 0; a < rownum; a++) // 打印数据表中的所有内容
{
rowval = mysql_fetch_row(retval);
if (atoi(rowval[0]) == user1.UID)
{
for (j = 0; j < fieldnum; j++)
{
if (strcmp(rowval[j], user1.password) == 0)
{
user1.log_flag = 1;
if (user1.log_flag == 1)
{
printf("UID:%d 成功上线\n", user1.UID);
sprintf(buf, "UPDATE users_info SET socket = %d WHERE UID = %d", fd, user1.UID);
mysql_query(sql, buf);
write(user1.socket, &user1, sizeof(user1));
}
}
}
}
}
if (user1.log_flag == 0)
{
write(user1.socket, &user1, sizeof(user1));
printf("账号或密码错误\n");
}
return NULL;
}
- 客户端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <time.h>
#define SERVER_IP "192.168.43.113"
#define SERVER_PORT 1111
int ke_socket; // 套接字
int ret;
char rbuf[1024];
int si_id;
int n; // 菜单选择
enum MessageType
{
// menu1
REGISTRATION,
LOGIN,
// menu2
QUN,
SI,
DELETE_ID,
LOG_out,
Ke_out
// 此处添加其他消息类型
};
typedef struct // 打包本地数据
{
int chat_mode;
int UID;
char nickname[50];
char password[20];
int target_id;
char send_message[1024];
int socket;
int log_flag;
enum MessageType type;
} User;
User user;
void si_chat(int ke_socket);
void look_time();
void qun_chat(int ke_socket);
void registers(int ke_socket);
void menu2(int ke_socket);
void LOG_in(int ke_socket);
void *my_qun_threadfunc(void *arg);
void *my_sichat_func(void *arg);
void myfunc(int signum) // 重定义2号信号
{
if (signum == 2) // 收到 SIGINT 信号
{
printf("客户端已退出\n");
close(ke_socket);
exit(0);
}
}
void *mypthreadfunc(void *arg) // 客户端发送
{
int client_socket = *(int *)arg;
while (1)
{
printf("client send:\n");
scanf("%s", user.send_message);
write(client_socket, &user, sizeof(user));
}
return NULL;
}
int main(void)
{
ke_socket = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in info;
info.sin_addr.s_addr = inet_addr(SERVER_IP);
info.sin_family = AF_INET;
info.sin_port = htons(SERVER_PORT);
int opt = 1;
setsockopt(ke_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
setsockopt(ke_socket, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
ret = connect(ke_socket, (struct sockaddr *)&info, sizeof(info));
if (ret < 0)
{
perror("connect");
return -1;
}
printf("成功链接智慧树\n");
signal(SIGINT, myfunc);
while (1)
{
printf("*****欢迎使用虚空终端*****\n");
printf("1.登陆 2.注册 3.退出\n");
printf("请选择:\n");
int a;
scanf("%d", &a);
switch (a)
{
case 1:
LOG_in(ke_socket);
break;
case 2:
registers(ke_socket);
break;
case 3:
printf("退出\n");
user.log_flag = 0;
user.socket = ke_socket;
user.type = Ke_out;
write(ke_socket, &user, sizeof(user));
return 0;
default:
printf("输入有误,请重新输入!\n");
break;
}
}
close(ke_socket);
return 0;
}
void registers(int ke_socket)
{
while (1)
{
user.type = REGISTRATION;
srand(time(0));
printf("请输入想要注册的昵称:");
scanf("%s", user.nickname);
printf("请输入密码:");
scanf("%s", user.password);
printf("请确认密码:");
char remm[20];
scanf("%s", remm);
if (strcmp(user.password, remm) == 0)
{
write(ke_socket, &user, sizeof(user)); // 写入用户信息
read(ke_socket, &user, sizeof(user)); // 读取服务器返回的用户信息
printf("注册成功,请牢记个人信息:\n");
printf("昵称:%s\n", user.nickname);
printf("UID:%d\n", user.UID);
break;
}
else
{
printf("注册失败,两次密码不一致!\n");
printf("请返回菜单,重新注册!\n");
}
}
}
void LOG_in(int ke_socket)
{
int count = 0;
while (count < 3)
{
user.type = LOGIN;
printf("请输入您的UID:\n");
scanf("%d", &user.UID);
printf("请输入密码:\n");
getchar();
scanf("%s", user.password);
write(ke_socket, &user, sizeof(user));
usleep(1000);
read(ke_socket, &user, sizeof(user));
if (user.log_flag == 1)
{
printf("登陆成功!\n");
menu2(ke_socket);
return;
}
else
{
count++;
if (count < 3)
{
printf("帐号或密码错误,请重新输入!\n");
}
else
{
printf("输入错误次数达到上限,返回菜单\n");
return;
}
}
}
}
void menu2(int ke_socket)
{
while (1)
{
printf("****登陆成功****\n");
printf("1.私聊 2.群聊 3.注销 4.登出\n");
printf("请选择:");
int n = 0;
scanf("%d", &n);
switch (n)
{
case 1:
si_chat(ke_socket);
break;
case 2:
qun_chat(ke_socket);
break;
case 3:
printf("注销\n");
user.log_flag = 0;
user.type = DELETE_ID;
write(ke_socket, &user, sizeof(user));
break;
case 4:
printf("登出\n");
user.log_flag = 0;
user.type = LOG_out;
write(ke_socket, &user, sizeof(user));
return;
default:
printf("输入有误!\n");
continue;
}
}
}
void qun_chat(int ke_socket)
{
// 创建子线程接收数据
pthread_t pd;
pthread_create(&pd, NULL, my_qun_threadfunc, &ke_socket);
user.type=QUN;
while (1)
{
printf("发给群聊:\n");
scanf("%s", user.send_message);
if (strcmp(user.send_message, "quit") == 0)
{
printf("用户已退出群聊\n");
pthread_cancel(pd);
break;
}
write(ke_socket, &user, sizeof(user));
}
return;
}
void look_time()
{
time_t now = time(NULL);
struct tm *tm_info = localtime(&now);
char time_str[20];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", tm_info);
printf("[%s]\n", time_str);
}
void *my_qun_threadfunc(void *arg) // 发送给客户端
{
int client_socket = *(int *)arg;
user.type = QUN;
while (1)
{
read(client_socket, &user, sizeof(user));
look_time();
printf("%s向群聊发送:%s\n", user.nickname,user.send_message);
}
return NULL;
}
void si_chat(int ke_socket)
{
// 创建子线程接收私聊信息
user.type = SI;
pthread_t pd;
ret = pthread_create(&pd, NULL, my_sichat_func, &ke_socket);
printf("请输入想要私聊的UID:\n");
scanf("%d", &user.UID);
int account = user.UID;
while (1)
{
printf("私聊发送:\n");
scanf("%s", user.send_message);
if (strcmp(user.send_message, "quit") == 0)
{
printf("已退出群聊\n");
pthread_cancel(pd);
break;
}
user.UID = account;
write(ke_socket, &user, sizeof(user));
}
return;
}
void *my_sichat_func(void *arg) // 私聊接收
{
int client_socket = *(int *)arg;
while (1)
{
read(ke_socket, &user, sizeof(user));
look_time();
printf("%s:%s\n", user.nickname, user.send_message);
}
return NULL;
}
5.复盘总结
1.TCP搭建流程不够熟悉。
2.对epoll的理解不够深入。
3.功能不够丰富。