暑假项目聊天室(2)---项目分析

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之间交流;

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值