1.我的思路
要做一个聊天室,我觉得就是要实现客户端与服务端之间的通信,也就是要在客户端和服务端之间建立连接;总的来说就是下面这幅图,来实现客户端服务器的交互,也就是最简单的cs模型;
2.信息的存储
和xx管理系统类似,我们用户的信息,用户的数据,用户的聊天记录什么的都需要存储起来,而且我们需要一个东西,作为用户的唯一标示,例如每个人的DNA是独一无二的,我们每个用户也需要一个独一无二的东西来找到这个用户,可以是账号,或者昵称;最后我选择了用账号作为用户的唯一标示,仿照QQ账号不是你自己设置的,而是在你创建好以后系统发给你的;
2.1用什么存
在数据库和文件中我选择了用数据库进行数据的存储,数据库存储相对于文件的好处就在于查找的方便,例如想要查找一个用户的数据,通过一个函数就可以搞定,而在文件中还要把文件中的数据一个个拿出来,然后慢慢比较;但是对于数据库的操作我仅限于增删改查,对于主键,外键的设置我并不是很了解;
2.2系统怎么发放账号
因为主键外键还有数据库中的递增不熟悉,所以我采用了一种比较麻烦的方式,就是把最后一次的账号存到文件中,然后下次注册时取出来加一就是该用户的账号;
2.3数据表
我一共建立了5张数据表;
chat_messages:聊天记录
friends:好友列表
group_members:群成员
groups:群信息
user_data:用户信息
account
账号,nickname
昵称,password
密码,user_state
用户状态(是否在线),user_socket
用户套接字;
group_account
群号,group_name
群名,group_meber)numebr
群成员数量
group_account
群号,group_name
群名,group_member_account
群成员账号,group_member_nickname
群成员昵称,group_state
群成员群地位(群主管理员普通群员)
user
当前用户账号,friend_user
该用户好友账号,realtion
两人的关系(特别关心,黑名单,普通)
send_user
发送者的账号,recv_user
接收者的账号,messages
消息内容,send_can_look
发送者是否能查看,recv_can_look
接收者是否能查看;
后两个是用于查看聊天记录
2.4关于数据库操作时是否加锁
这一点上我选择添加了互斥锁,但是数据库自己是有锁机制的,这一点上我不是很清楚,以防万一我选择了加锁;
3.服务端的设计
3.1服务端大体框架
举个栗子:
void *deal(void *recv_pack) {
pthread_detach(pthread_self());
PACK *pack;
int i;
BOX *tmp = box_head;
MYSQL mysql;
mysql = accept_mysql();
pack = (PACK*)recv_pack;
switch(pack->type){
}
}
这个函数就是epoll
检测到事件后开启的线程,用这个线程处理事件,通过switch(pack->type)
来判断相应的事件进入相应的函数;
3.2epoll模板
#include <mysql/mysql.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include "my_friends.h"
#include "my_deal.h"
#include "my_mysql.h"
#include "my_socket.h"
#include "my_err.h"
#include "my_pack.h"
#define MAXEPOLL 1024
int main() {
int i;
int sock_fd;
int conn_fd;
int socklen;
int acceptcont = 0;
int kdpfd;
int curfds;
int nfds;
char need[MAXIN];
MYSQL mysql;
struct sockaddr_in cli;
struct epoll_event ev;
struct epoll_event events[MAXEPOLL];
PACK recv_pack;
PACK *pack;
pthread_t pid;
MYSQL_RES *result;
pthread_mutex_init(&mutex, NULL);
socklen = sizeof(struct sockaddr_in);
mysql = accept_mysql();
sock_fd = my_accept_seve();
kdpfd = epoll_create(MAXEPOLL);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = sock_fd;
if(epoll_ctl(kdpfd, EPOLL_CTL_ADD, sock_fd, &ev) < 0) {
my_err("epoll_ctl", __LINE__);
}
curfds = 1;
while(1) {
if((nfds = epoll_wait(kdpfd, events, curfds, -1)) < 0){
my_err("epoll_wait", __LINE__);
}
for (i = 0; i < nfds; i++) {
if (events[i].data.fd == sock_fd) {
if ((conn_fd = accept(sock_fd, (struct sockaddr*)&cli, &socklen)) < 0) {
my_err("accept", __LINE__);
}
printf("连接成功,套接字编号%d\n", conn_fd);
acceptcont++;
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_fd;
if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, conn_fd, &ev) < 0) {
my_err("epoll_ctl", __LINE__);
}
curfds++;
continue;
} else if (events[i].events & EPOLLIN) {
memset(&recv_pack, 0, sizeof(PACK));
if (recv(events[i].data.fd, &recv_pack, sizeof(PACK), MSG_WAITALL) < 0) {
close(events[i].data.fd);
perror("recv");
continue;
}
if (recv_pack.type == EXIT) {
if (send(events[i].data.fd, &recv_pack, sizeof(PACK), 0) < 0) {
my_err("send", __LINE__);
}
memset(need, 0, sizeof(need));
sprintf(need, "update user_data set user_state = 0 where user_state = 1 and user_socket = %d", events[i].data.fd);
mysql_query(&mysql, need);
epoll_ctl(kdpfd, EPOLL_CTL_DEL, events[i].data.fd, &ev);
curfds--;
continue;
}
if (recv_pack.type == LOGIN) {
memset(need, 0, sizeof(need));
sprintf(need, "select *from user_data where account = %d", recv_pack.data.send_account);
pthread_mutex_lock(&mutex);
mysql_query(&mysql, need);
result = mysql_store_result(&mysql);
if (!mysql_fetch_row(result)) {
recv_pack.type = ACCOUNT_ERROR;
memset(recv_pack.data.write_buff, 0, sizeof(recv_pack.data.write_buff));
printf("$$sad\n");
strcpy(recv_pack.data.write_buff, "password error");
if (send(events[i].data.fd, &recv_pack, sizeof(PACK), 0) < 0) {
my_err("send", __LINE__);
}
pthread_mutex_unlock(&mutex);
continue;
}
memset(need, 0, sizeof(need));
sprintf(need, "update user_data set user_socket = %d where account = %d", events[i].data.fd, recv_pack.data.send_account);
mysql_query(&mysql, need);
pthread_mutex_unlock(&mutex);
}
recv_pack.data.recv_fd = events[i].data.fd;
pack = (PACK*)malloc(sizeof(PACK));
memcpy(pack, &recv_pack, sizeof(PACK));
pthread_create(&pid, NULL, deal, (void*)pack);
}
}
}
}
3.3recv时的一些小陷阱
万一服务端收包的时候没有收齐,怎么办?
这里因为我每次发的是一个定长包,所以我把recv()
的最后一个参数设置为了,MSG_WAITALL
,这个参数就是把服务端改成了一个阻塞的模式,就是一定会收完在返回;
但是这个有一个坏处就是万一有一个收包收的慢了点,那服务器就阻塞住了,就卡住了,别的客户端就用不了了;
正确的服务端设计应该是有两个buffer
,一个读buffer,一个写buffer,客户端要先发来一个包长,然后如果在所给的时间片内没有受够就先存到读buffer中然后再下一次继续收,然后再开线程;
但是这种两个buffer的确实还不会写,所以就写了一个这种阻塞式的;
以上就是我服务端的设计
4.客户端设计
4.1客户端框架
使用条件变量控制发送线程和接收线程之间的同步,确保客户端收到服务端发回的确认包以后,客户端发送线程在进行下一步操作;
举个栗子: 客户端发送登录信息,给服务端把账号密码发过去以后,就开始pthread_cond_wait()
,当服务端发回数据包后,可能携带的登陆成功,或者登录失败的信息后,接收线程pthread_cond_signal()
发送信号唤醒发送线程,在这里为了防止虚假唤醒,特地while
循环等待信号;这里就不解释什么是虚假唤醒了,这种情况确实很少见;
4.2客户端主函数
int main() {
int sock_fd;
pthread_t pid1;
pthread_t pid2;
struct sockaddr_in seve;
sing = 0;
pthread_mutex_init(&mutex_cli, NULL);
pthread_cond_init(&cond_cli, NULL);
sock_fd = my_accept_cli();
// signal(SIGINT,mask_ctrl_c);
pthread_create(&pid1, NULL, thread_read, (void *)&sock_fd);
pthread_create(&pid2, NULL, thread_write, (void *)&sock_fd);
pthread_join(pid1, NULL);
pthread_join(pid2, NULL);
return 0;
}
关于屏蔽掉的ctrl+c
信号,当时想的是客户端ctrl+c
以后服务端就下线这个用户就好了,但是最后因为没办法给函数传进去参数,作废了;
因为当时已经写完了,如果在把套接字什么的改成全局变量怕会出错,也就是单纯的屏蔽掉了ctrl+c
信号;
thread_read
:负责接收从服务端发回来的数据包;
thread_write
:负责客户端向服务端发包;
这两个线程通过条件变量同步;
4.3 thread_read函数
这是我写的客户端接收服务端发回来的数据包的函数;
void *thread_write(void *sock_fd) {
pthread_t pid;
int ret;
group_list = (GROUP_G *)malloc(sizeof(GROUP_G));
member_list = (GROUP *)malloc(sizeof(GROUP));
list = (FRIEND *)malloc(sizeof(FRIEND));
box = (BOX *)malloc(sizeof(BOX));
recv_pack = (PACK*)malloc(sizeof(PACK));
message = (MESSAGE *)malloc(sizeof(MESSAGE));
group_message = (GROUP_MESSAGE *)malloc(sizeof(GROUP_MESSAGE));
file = (FLE *)malloc(sizeof(FLE));
file->have = 0;
while (1) {
memset(recv_pack, 0, sizeof(PACK));
if ((ret = recv(*(int *)sock_fd, recv_pack, sizeof(PACK), MSG_WAITALL)) < 0) {
my_err("recv", __LINE__);
}
switch(recv_pack->type) {
}
}
那么问题来了,这个函数只能收PACK
这一种类型的数据包,那服务端如果要发送别的数据类型的包怎么办?
那就在相应的事件下再开一个线程,同时pthread_join()
使thread_read这个线程函数阻塞住;
void *thread_box(void *sock_fd) {
if (recv(*(int *)sock_fd, box, sizeof(BOX), MSG_WAITALL) < 0) {
my_err("recv", __LINE__);
}
pthread_exit(0);
}
void *thread_list(void *sock_fd) {
memset(list, 0, sizeof(FRIEND));
if (recv(*(int *)sock_fd, list, sizeof(FRIEND), MSG_WAITALL) < 0) {
my_err("recv", __LINE__);
}
pthread_exit(0);
}
void *thread_recv_fmes(void *sock_fd) {
if (recv_pack->data.send_account == send_pack->data.recv_account) {
printf("账号为%d昵称为%s的好友说:\t%s\n", recv_pack->data.send_account, recv_pack->data.send_user, recv_pack->data.read_buff);
} else if(strcmp(recv_pack->data.write_buff, "ohyeah") == 0){
printf("来自特别关心%d昵称%s的好友说:\t%s\n", recv_pack->data.send_account, recv_pack->data.send_user, recv_pack->data.read_buff);
} else {
box->send_account[box->talk_number] = recv_pack->data.send_account;
strcpy(box->read_buff[box->talk_number++], recv_pack->data.read_buff);
printf("消息盒子里来了一条好友消息!\n");
}
pthread_exit(0);
}
void *thread_recv_gmes(void *sock_fd) {
if (recv_pack->data.recv_account == send_pack->data.recv_account) {
printf("群号%d 群名%s 账号%d 昵称%s:\t%s\n", recv_pack->data.recv_account, recv_pack->data.recv_user, recv_pack->data.send_account, recv_pack->data.send_user, recv_pack->data.read_buff);
} else {
printf("消息盒子里来了一条群消息!!\n");
box->group_account[box->number] = recv_pack->data.recv_account;
box->send_account1[box->number] = recv_pack->data.send_account;
strcpy(box->message[box->number++], recv_pack->data.read_buff);
}
}
void *thread_recv_file(void *sock_fd) {
memset(file, 0, sizeof(file));
file->send_account = recv_pack->data.send_account;
strcpy(file->send_nickname, recv_pack->data.send_user);
strcpy(file->filename, recv_pack->data.write_buff);
file->have = 1;
printf("账号%d\t昵称%s\t的好友给你发送了一个%s文件快去接收吧\n", file->send_account, file->send_nickname, file->filename);
pthread_exit(0);
}
void *thread_read_message(void *sock_fd) {
if (recv(*(int *)sock_fd, message, sizeof(MESSAGE), MSG_WAITALL) < 0) {
my_err("recv", __LINE__);
}
pthread_exit(0);
}
void *thread_member(void *sock_fd) {
memset(member_list, 0, sizeof(GROUP));
if (recv(*(int *)sock_fd, member_list, sizeof(GROUP), MSG_WAITALL) < 0) {
my_err("recv", __LINE__);
}
pthread_exit(0);
}
void *thread_group_list(void *sock_fd) {
memset(group_list, 0, sizeof(GROUP_G));
if (recv(*(int *)sock_fd, group_list, sizeof(GROUP_G), MSG_WAITALL) < 0) {
my_err("recv", __LINE__);
}
pthread_exit(0);
}
这些函数就是我用来接收别的数据包的线程函数;
4.5密码的隐藏
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
int main()
{
struct termios old,new;
char password[8] = {
0};
char ch;
int i = 0;
tcgetattr(0,&old);
new = old;
new.c_lflag &= ~(ECHO | ICANON);
printf("请输入密码....\n");
while(1)
{
tcsetattr(0,TCSANOW,&new);
scanf("%c",&ch);
tcsetattr(0,TCSANOW,&old);
if(i == 8 || ch == '\n')
{
break;
}
password[i] = ch;
printf("*");
i++;
}
return 0;
}
5.关于聊天
5.1双方在线
一对一的聊天相对比较简单,就是利用服务器作为转发器,客户端A先发消息给服务端,服务端在在数据库中查找到客户端B的套接字编号,然后转发给客户端B;
5.2离线消息
这个就是客户端A先发给服务端,服务端检测到客户端B不在线,然后服务端将这个消息内容,谁发的保存在服务端,然后当客户端B登录以后发送给客户端B;
5.3加好友
加好友和单聊类似,也是服务端充当转发器的角色,所以没什么好说的就是要人性化一点,消息只能好友之间发送;
5.4关于群聊
群聊就是高级的单聊,使用while
循环遍历群成员,在线的查找套接字把消息发过去,不在线的服务端存起来,等到用户上线以后发过去;
还有一些人性化的设计比如说向qq一样一个左边一个右边那种就要靠自己设计了;
6.发文件
这个应该是聊天室里面最难的一部分了
这是我想出来的一种发文件的方法,客户端A先把文件发送给服务端,等到文件发送完毕,服务端保存好,向客户端B发送文件的一些信息,客户端B决定要不要接受这个文件,如果选择接受,客户端B就不断请求,每次服务端从文件中取出1023个字节发给客户端B,文件发送完了以后服务端给客户端B发送一个完结的包,客户端B停止请求;
我的这种方法的好处就在于,在客户端A给客户端B发文件时,并不会影响客户端C和客户端D之间交流;