---项目分析)
1.我的思路要做一个聊天室,我觉得就是要实现客户端与服务端之间的通信,也就是要在客户端和服务端之间建立连接;总的来说就是下面这幅图,来实现客户端服务器的交互,也就是最简单的cs模型;2.信息的存储和xx管理系统类似,我们用户的信息,用户的数据,用户的聊天记录什么的都需要存储起来,而且我们需要一个东西,作为用户的唯一标示,例如每个人的DNA是独一无二的,我们每个用户也需要一个独一无二的东西来找到这个用户,可以是账号,或者昵称;最后我选择了用账号作为用户的唯一标示,仿照QQ账号不是你自己设置的,而是在你创建好以后系统发给你的;2.1用什么存在数据库和文件中我选择了用数据库进行数据的存储,数据库存储相对于文件的好处就在于查找的方便,例如想要查找一个用户的数据,通过一个函数就可以搞定,而在文件中还要把文件中的数据一个个拿出来,然后慢慢比较;但是对于数据库的操作我仅限于增删改查,对于主键,外键的设置我并不是很了解;2.2系统怎么发放账号因为主键外键还有数据库中的递增不熟悉,所以我采用了一种比较麻烦的方式,就是把最后一次的账号存到文件中,然后下次注册时取出来加一就是该用户的账号;2.3数据表我一共建立了张数据表;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;inti;BOX*tmpbox_head;MYSQL mysql;mysqlaccept_mysql();pack(PACK*)recv_pack;switch(pack-type){}}这个函数就是epoll检测到事件后开启的线程,用这个线程处理事件,通过switch(pack-type)来判断相应的事件进入相应的函数;3.2epoll模板#includemysql/mysql.h#includestdio.h#includeunistd.h#includestring.h#includesys/epoll.h#includepthread.h#includefcntl.h#includenetinet/in.h#includearpa/inet.h#includesys/socket.h#includemy_friends.h#includemy_deal.h#includemy_mysql.h#includemy_socket.h#includemy_err.h#includemy_pack.h#defineMAXEPOLL 1024intmain(){inti;intsock_fd;intconn_fd;intsocklen;intacceptcont0;intkdpfd;intcurfds;intnfds;charneed[MAXIN];MYSQL mysql;structsockaddr_in cli;structepoll_event ev;structepoll_event events[MAXEPOLL];PACK recv_pack;PACK*pack;pthread_t pid;MYSQL_RES*result;pthread_mutex_init(mutex,NULL);socklensizeof(structsockaddr_in);mysqlaccept_mysql();sock_fdmy_accept_seve();kdpfdepoll_create(MAXEPOLL);ev.eventsEPOLLIN|EPOLLET;ev.data.fdsock_fd;if(epoll_ctl(kdpfd,EPOLL_CTL_ADD,sock_fd,ev)0){my_err(epoll_ctl,__LINE__);}curfds1;while(1){if((nfdsepoll_wait(kdpfd,events,curfds,-1))0){my_err(epoll_wait,__LINE__);}for(i0;infds;i){if(events[i].data.fdsock_fd){if((conn_fdaccept(sock_fd,(structsockaddr*)cli,socklen))0){my_err(accept,__LINE__);}printf(连接成功,套接字编号%d\n,conn_fd);acceptcont;ev.eventsEPOLLIN|EPOLLET;ev.data.fdconn_fd;if(epoll_ctl(kdpfd,EPOLL_CTL_ADD,conn_fd,ev)0){my_err(epoll_ctl,__LINE__);}curfds;continue;}elseif(events[i].eventsEPOLLIN){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.typeEXIT){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.typeLOGIN){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);resultmysql_store_result(mysql);if(!mysql_fetch_row(result)){recv_pack.typeACCOUNT_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_fdevents[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客户端主函数intmain(){intsock_fd;pthread_t pid1;pthread_t pid2;structsockaddr_in seve;sing0;pthread_mutex_init(mutex_cli,NULL);pthread_cond_init(cond_cli,NULL);sock_fdmy_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);return0;}关于屏蔽掉的ctrlc信号,当时想的是客户端ctrlc以后服务端就下线这个用户就好了,但是最后因为没办法给函数传进去参数,作废了;因为当时已经写完了,如果在把套接字什么的改成全局变量怕会出错,也就是单纯的屏蔽掉了ctrlc信号;thread_read:负责接收从服务端发回来的数据包;thread_write:负责客户端向服务端发包;这两个线程通过条件变量同步;4.3 thread_read函数这是我写的客户端接收服务端发回来的数据包的函数;void*thread_write(void*sock_fd){pthread_t pid;intret;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-have0;while(1){memset(recv_pack,0,sizeof(PACK));if((retrecv(*(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_accountsend_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);}elseif(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_accountsend_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_accountrecv_pack-data.send_account;strcpy(file-send_nickname,recv_pack-data.send_user);strcpy(file-filename,recv_pack-data.write_buff);file-have1;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密码的隐藏#includestdio.h#includestdlib.h#includetermios.h#includeunistd.hintmain(){structtermios old,new;charpassword[8]{0};charch;inti0;tcgetattr(0,old);newold;new.c_lflag~(ECHO|ICANON);printf(请输入密码....\n);while(1){tcsetattr(0,TCSANOW,new);scanf(%c,ch);tcsetattr(0,TCSANOW,old);if(i8||ch\n){break;}password[i]ch;printf(*);i;}return0;}5.关于聊天5.1双方在线一对一的聊天相对比较简单,就是利用服务器作为转发器,客户端A先发消息给服务端,服务端在在数据库中查找到客户端B的套接字编号,然后转发给客户端B;5.2离线消息这个就是客户端A先发给服务端,服务端检测到客户端B不在线,然后服务端将这个消息内容,谁发的保存在服务端,然后当客户端B登录以后发送给客户端B;5.3加好友加好友和单聊类似,也是服务端充当转发器的角色,所以没什么好说的就是要人性化一点,消息只能好友之间发送;5.4关于群聊群聊就是高级的单聊,使用while循环遍历群成员,在线的查找套接字把消息发过去,不在线的服务端存起来,等到用户上线以后发过去;还有一些人性化的设计比如说向qq一样一个左边一个右边那种就要靠自己设计了;6.发文件这个应该是聊天室里面最难的一部分了这是我想出来的一种发文件的方法,客户端先把文件发送给服务端,等到文件发送完毕,服务端保存好,向客户端B发送文件的一些信息,客户端决定要不要接受这个文件,如果选择接受,客户端就不断请求,每次服务端从文件中取出1023个字节发给客户端B,文件发送完了以后服务端给客户端B发送一个完结的包,客户端停止请求;我的这种方法的好处就在于,在客户端A给客户端B发文件时,并不会影响客户端和客户端之间交流;