大家都知道Qt是一款很强大的面相对象的程序设计开发工具,包含图形界面,支持WP、IOS以及Android。经过学习了一段时间以后,用Qt设计客户端,用LINUX设计服务端,实现了这个小项目。
在这次项目中,实现了以下内容:
【服务端】
1、新用户注册,老用户验证登录,管理员可以对用户实行管理,管理功能有:删除用户并永远不能再注册、对用户进行禁言。
2、点对点私聊、群聊。
3、聊天内容保存和记录。
4、关键词过滤。
5、离线消息。
6、守护进程
【客户端】
1、注册、登录、改密。
2、私聊群聊。
3、添加组,添加好友。
4、接收离线消息。
5、删除、禁言提示。
**********************************************************************************************************
【项目开始准备】
本环节是至关重要的,特别是一个团队共同实现一个项目的时候,做好项目的规划能让团队产生1+1>2的效果。
在这个项目中会产生大量的数据传递(同时也是醉主要的一部分),因此必须要准备好通用的数据传递和数据解析时采用的枚举,以保证数据交流的过程中产生不必要的BUG。
一部分枚举定义如下:
**********************************************************************************************************
//type
enum{REGISTER, FORGET,LOGIN,CHANGE,TXT,FILES,ADDFRI,ADDGRP,EXIT,UPDATE,FORBID,ALLOW,REJECT,DELETE,BROADCAST,KEY};
**********************************************************************************************************
上述的枚举是项目中最重要的枚举,它代表了每个数据传输的目的。其他枚举就不一一列举了,但在定义枚举的时候要注意不要让枚举所代表的值重复,容易出错。
除了枚举之外还要定义好数据传递时的协议,我们在项目中使用的协议模式是:Type+messagelen+message。
例如添加好友时我们传递的信息是:ADDFIR+usernamelen+uesrname+friendnamelen+friendname。
要注意因为是Qt和LINUX之间传输数据,所以我们采用char类型传输,因此在传递“ADDFIR”这个int型枚举时,我们要注意转换。
除了协议我们还要搭好服务器和客户端的结构,这在接下来会介绍。
数据库的建立:
****************************************************************************************************************
CREATE TABLE usrinfo(usrno CHAR PRIMARY KEY,usrname VARCHAR(20) NOT NULL,passwd VARCHAR(20) NOT NULL,reject CHAR,speak CHAR,online CHAR,sockfd CHAR,question CHAR,answer VARCHAR(20),friends VARCHAR(50),crowd VARCHAR(50),ismanager CHAR);
CREATE TABLE grpinfo(grpno CHAR PRIMARY KEY,grpname VARCHAR(20) NOT NULL,usrnum VARCHAR(50) NOT NULL);
****************************************************************************************************************
【服务端实现思路】
服务端在最初的设计上没有把界面考虑在内,因为我们只需要服务器做好数据的解析、转发以及记录的功能就好,不需要在界面上操作,所以选择了再LINUX系统上开发。并且在服务端逻辑处理上没有那么复杂,所以主要根据所需要的功能来进行文件的划分,再进行适当的调用即可:
add.c //主要处理添加好友和添加组功能
communicate.c //处理聊天
login.c //登陆
main.c
register.c //注册和密码找回
sqlite.c //数据库操作
sqlitetest.c //数据存储
其实数据处理的部分还是简单的,这里的难点主要是在数据库方面,如果涉及到数据库方面的程序能好好琢磨,会使你的服务端健壮不少,可惜这方面是我的弱项。
当服务器解析出收到的信息以后,服务器需要进行相应的操作,以添加好友为例,当服务器收到添加好友的信息时,服务器需要先检测数据库列表中是否存在你要添加的friendname,如果存在,需要调用两个id数据库里的“friend”列,在该列中添加相应的信息。程序部分代码如下:
if(sqlite.result==0){
retmsg=FRI_ERRUSR+'0';
wtlen=write(clifd,&retmsg,1);
}
else{
sqlite.tag=SQL_GETLIST;
sprintf(sql,"SELECT friends FROM usrinfo WHERE usrname='%s'",myname);
judgeSQL(&sqlite);
sqlite.tag=SQL_GETNO;
sprintf(sql,"SELECT usrno FROM usrinfo WHERE usrname='%s'",friname);
judgeSQL(&sqlite);
sqlite.list[strlen(sqlite.list)]=sqlite.usrno;
sprintf(sql,"UPDATE usrinfo SET friends='%s' WHERE usrname='%s'",sqlite.list,myname);
execSQL();
retmsg=FRI_OK+'0';
wtlen=write(clifd,&retmsg,1);
}
void execSQL(){
char *errmsg;
if(sqlite3_exec(db,sql,NULL,NULL,&errmsg)!=SQLITE_OK){
sqlite3_close(db);
printf("%s\n",errmsg);
sqlite3_free(errmsg);
return ;
}
}
我们再看注册用户的处理(略过判断ID重复之类的检测代码):
else if(sqlite.result==0){
sqlite.tag=SQL_SUM;
sprintf(sql,"SELECT reject FROM usrinfo");
judgeSQL(&sqlite);
member.usrno=(++sqlite.sum)+'0';
sprintf(sql,"INSERT INTO usrinfo VALUES('%c','%s','%s','%c','%c',\
'%c','%s','%c','%s','%s','%s','%c')",member.usrno,member.usrname,member.passwd,\
member.reject,member.speak,member.online,member.sockfd,member.question,member.answer,\
member.friends,member.crowd,member.ismanager);
printf("sql:%s\n",sql);
execSQL();
retmsg=REG_OK+'0';
wtlen=write(clifd,&retmsg,1);
}
从上述两段代码上可以看出大部分的代码处理都涉及到数据库。所以我们大可以这样认为,服务端的主要任务就是数据处理和数据库的运行和维护。
还有一点要提的是我们将每个用户的聊天记录全部保存在文件里,文件名就是用户的ID。这样可以减轻数据库的压力,同时查询时也方便,下面附部分代码:
filefd=open(filename,O_RDWR|O_CREAT,0666);
if(filefd==-1){
perror("open");
return ;
}
err=lseek(filefd,0,SEEK_END);
if(err==-1){
perror("lseek");
return ;
}
retmsg='\n';
write(filefd,&retmsg,1);
retmsg='#';
write(filefd,&retmsg,1);
if(online){
retmsg='1';
write(filefd,&retmsg,1);
}
else{
retmsg='0';
write(filefd,&retmsg,1);
}
write(filefd,timeval,strlen(timeval));
retmsg='#';
write(filefd,&retmsg,1);
write(filefd,&tag,1);
write(filefd,&retmsg,1);
write(filefd,myname,strlen(myname));
write(filefd,&retmsg,1);
write(filefd,txt,strlen(txt));
close(filefd);
只要能将数据部分处理好,服务端的逻辑处理并不难。最后附上服务端函数逻辑:
**************************************************************************************************************
【客户端实现思路】
客户端是用Qt实现的。Qt的使用让整个服务端的设计更具有层次感,因此对逻辑处理有更高的要求。如果结构设计不好就会使整个Qt程序的数据传递变得比较麻烦,同时也会让程序看起来比较拖沓(本人在这个项目实现过程中深有体会)。由于数据处理方面在服务端部分已经做过阐述,因此这部分主要介绍客户端的结构。
这里提供两种设计思路:
(1)主要用信号传递数据
这个方法的核心就是在一个文件里集中处理从服务端发送回来的信号,利用枚举的层次性做出相应的判断,再通过信号将数据传送给不同的界面做出相应的相应。这个方法的优点是逻辑处理相对简单,所有界面都处于同一层,整个数据传递的过程集中、清晰。如果上层结构对下层结构用信号进行数据传递会产生很多BUG,这个结构可以很好的规避这一点。
结构图如下:
由上图可以看出,所有的功能界面处于同一级,每当有一个“pushbutton”被按下时就向“client”界面发送一个请求,所有的信息交互由“client”界面和服务器进行交互,同时在“client”中解析所有从服务器中传递来的信息,再通过相应的函数传输给下面的功能界面。因为有枚举,所以不用担心数据传递的出错。这种方法总体来说结构和思路都很清晰,而且方便添加新的功能,唯一的缺点应该就是客户端内的数据传递比较多。
优先推荐这种设计方法。
(2)在相应的功能界面传递数据
这个方法核心就是在每个功能界面都有一个“connect”用来与服务端进行信息交互。结构图如下:
从上图可以看出,和第一种方面明显的区别是这里的每个功能窗口并不是同级的,因此并不适合用signal进行信号传递。当用户登录成功后,我们便将第一个界面建立的socket通过函数传给下一个界面,这样整个程序可以共用一个socket。通过在每个界面中用socket和服务器进行数据交互,这样就达到了在相应的功能界面和服务器进行相应的数据交互的目的。这种结构的优点是可以自行选择什么时候和服务器进行连接。如上图,当你打开登录界面的时候并没有和服务器建立连接,只有当你申请登录、注册、找回密码时才进行数据交互。对服务器的负荷相对较小,同时客户端内信息交互较少。当然缺点也是比较明显的,添加功能不如第一种方法方便,逻辑设计较第一种稍难。
接下来附上一段服务端ADDFRIEND的代码:
QString friendname;
QString type;
if(ui->friendnumlineEdit->text().isEmpty()){
QMessageBox::warning(this,"warning","no friend username");
return;
}
type.setNum(ADDFRI);
friendname+=type;
char mynameLength = username.toLatin1().length()+'0';
friendname+=mynameLength;
friendname+=username;
char friendnameLength = ui->friendnumlineEdit->text().toLatin1().length()+'0';
friendname+=friendnameLength;
friendname += ui->friendnumlineEdit->text().toLatin1();
QByteArray cd;
char *pf;
cd=friendname.toLatin1();
pf=cd.data();qDebug()<<pf;
if(socket->state()==QAbstractSocket::ConnectedState)
socket->write(pf);
在本段代码中涉及到数据类型的转化和数据的传递。要注意我们和服务端交互信息采用的是char型。还有一点要注意的就是将ADDFRI转化为QString类型的时候,在本段代码中因为ADDFRI的枚举值小于10所以没有出错,若枚举值大于10,则需要采用type = ADDFRI+'0'
这种方式。
【结语】
服务器和客户端的设计思路有所不同,侧重点也有所不同,这个项目对知识的掌握还是有很大的帮助的。等以后能力更上一层楼以后,说不定看现在的设计都是缺点,但这是成长的必经之路。用此文纪念我的学习生涯。