前言
暑假把linux下的高级编程和网络编程学习了一遍,学习很重要,但是也得有个练手的地方,所以必须做做项目来认识下自己所学习的知识。
能够找到小伙伴一起做项目也是一件很快乐的事情的,很幸运的有两个小伙伴一起做这个项目,而我正好负责整个客户端模块,她两负责编写服务器的模块。
开始吧:
项目具体描述:做一个可以实现单个服务器响应多客户端私聊和群聊的聊天工具。具体功能越丰富越好。
下面我以我们做项目的形式来讲讲我们的项目的实现。
项目策划
虽然只是私底下做项目,但是我们还是做了个小小的项目时间和项目实现上的规划,相当于一个实现的计划。
几个问题
这个项目有几个核心的东西必须讨论清楚:
(1)用户信息存储的数据库表
(2)自己定制的信息协议
自己定制协议,这样可以很大程度上放开很多东西,有了自己的协议我可以把所有的东西都变成一条消息,比如在实现私聊,群聊,离线消息,加好友等,都可以设置为一条请求消息和一条对应的回复消息的协议直接进行消息的打包,传输和解析操作。
下面看看外面自己定制的协议:
1 #ifndef AGREEMENT 2 #define AGREEMENT 3 4 /* 5 注册:101+用户名长度+用户名+密码长度+密码+密保问题+密保答案长度+密保答案 6 登录:102+用户名长度+用户名+密码长度+密码 7 改密:103+用户名长度+用户名+密保问题+密保答案长度+密保答案+密码长度+新密码 8 退出:104+用户名长度+用户名 9 加入群聊:105+用户名长度+用户名 10 回复: 11 111+回复类型+消息长度+消息 1:成功,2:用户名重复失败 12 112+回复类型+消息长度+消息 1:成功 2:失败(用户名,密码) 13 113+回复类型+消息长度+消息 1:成功 2:失败(用户名,密保) 14 15 16 201+发送者用户名长度+发送者用户名+接受者用户名长度+接受者用户名+消息长度+消息 17 202+发送者用户名长度+发送者用户名+消息长度+消息 18 203+被禁言用户名长度+被禁言用户名+禁言时间消息(一个字节) 19 204+被删除用户名长度+被删除用户名 20 205+指定用户名长度+指定用户名 21 206+发送者用户名长度+发送者用户名+接受者用户名长度+接受者用户名+消息长度+消息 22 207+发送者用户名长度+发送者用户名+接受者用户名长度+接受者用户名+文件消息长度+文件消息 23 208+用户数量+用户名长度+用户名 24 */ 25 //枚举: 26 /*消息类型*/ 27 enum{REGISTER,LOGIN,CHPASSWD,QUIT,JOIN,PMSG,QMSG,GAGMSG,DELMSG,SETMSG,LATEMSG,FILEMSG,USERLIST,REG_ACK,LOG_ACK,CHPW_ACK}; 28 29 /*101解析*/ 30 enum{REG_TYPE,REG_USERLEN,REG_USER,REG_PWLEN,REG_PW,REG_QUE,REG_ANSLEN,REG_ANS}; 31 /*102解析*/ 32 enum{LOG_TYPE,LOG_USERLEN,LOG_USER,LOG_PWLEN,LOG_PW}; 33 /*103解析*/ 34 enum{CHPW_TYPE,CHPW_USERLEN,CHPW_USER,CHPW_QUE,CHPW_ANSLEN,CHPW_ANS,CHPW_PWLEN,CHPW_PW}; 35 /*104解析*/ 36 enum{QUIT_TYPE,QUIT_USERLEN,QUIT_USER}; 37 /*105解析*/ 38 enum{JOIN_TYPE,JOIN_USERLEN,JOIN_USER}; 39 40 /*201解析*/ 41 enum{PRI_TYPE,PRI_FROMLEN,PRI_FROM,PRI_TOLEN,PRI_TO,PRI_MSGLEN,PRI_MSG}; 42 /*202解析*/ 43 enum{QM_TYPE,QM_FROMLEN,QM_FROM,QM_MSGLEN,QM_MSG}; 44 /*203解析*/ 45 enum{GAG_TYPE,GAG_USERLEN,GAG_USER,GAG_TIME}; 46 /*204解析*/ 47 enum{DEL_TYPE,DEL_USERLEN,DEL_USER}; 48 /*205解析*/ 49 enum{SET_TYPE,SET_USERLEN,SET_USER}; 50 /*206解析*/ 51 enum{LATE_TYPE,LATE_FROMLEN,LATE_FROM,LATE_TOLEN,LATE_TO,LATE_MSGLEN,LATE_MSG}; 52 /*207解析*/ 53 enum{FILE_TYPE,FILE_FROMLEN,FILE_FROM,FILE_TOLEN,FILE_TO,FILE_MSGLEN,FILE_MSG}; 54 /*208解析*/ 55 enum{USERL_TYPE,USERL_USERLEN,USERL_USER}; 56 57 /*111解析*/ 58 enum{REG_ACK_TYPE,REG_ACK_REPLY,REG_ACK_MSGLEN,REG_ACK_MSG}; 59 /*112解析*/ 60 enum{LOG_ACK_TYPE,LOG_ACK_REPLY,LOG_ACK_MSGLEN,LOG_ACK_MSG}; 61 /*113解析*/ 62 enum{CHPW_ACK_TYPE,CHPW_ACK_REPLY,CHPW_ACK_MSGLEN,CHPW_ACK_MSG}; 63 64 #include <QString> 65 #include <QCryptographicHash> 66 67 QString md5(QString str); 68 69 #endif // AGREEMENT
其中enum(枚举)的定义主要是为了后面的消息解析的连续性。
并且看的出我们的协议是分为两个大组的,最重要的是可扩展性很强,你可以随时的加上一些协议,删除一些协议。灵活性是比较强的。
数据库我们采用的是sqlite本地数据库,数据库表主要是必须定下来防止后续的改动,不能轻易改动。
做项目必须让头脑时刻清醒着,比较好的是在之前建立项目的框架图:
这是我们第一次讨论的框架图,并讨论了几个重要的问题
总的来说,框架很简单,主要就是服务器跟数据库的对话,还有客户端跟服务器的对话,服务器本身可以作为一个管理员的身份。
我主要负责客户端的编写,客户端就是一些信息的解析和发送,并且这次做的是界面的客户端,所以主要还是客户端的界面上的功能的实现。而作为客户端的框架也需要提前想好,这里的主要原因是我用的是Qt界面编程,使用信号与槽的机制进行界面类之间的通信,这里主要的关键在于你的socket只有一个,因为你只能连接一次服务器,就不断开的,所以你的socket在类之间不好传递,所以你就需要在一个界面类中发送所有的socket消息。具体看下下面的一张图界面关系:
通信层的socket在”登陆后的主界面“上,接受的数据包都在这里。
刚开始的时候我把登录界面作为主界面,后来发现这是错误的,因为私聊界面和群聊界面是依托在登陆后的界面上,也就是说如果我以登录界面为socket通信层,那么私聊界面离登录界面有两层的距离,这样通信起来会极大的不方便。
所以写代码前的考虑显得很重要,我因为这个问题整个项目的修改了两次,这在项目中是大忌,因为那个时候你会觉得崩溃的感觉。
下面是我的解析包的代码:
1 void MyTcpSocket::readyReadSlot() 2 { 3 qDebug()<<"have data"; 4 /*read first byte to get msgType*/ 5 while(this->bytesAvailable() > 0) 6 { 7 unsigned char oneByte; 8 if(this->bytesAvailable()>=1 && currType == -1) 9 { 10 this->read((char*)&oneByte,1); 11 qDebug()<<"onebyte:"<<oneByte; 12 13 switch(oneByte) 14 { 15 case 111: 16 msgType = REG_ACK; 17 currType = REG_ACK_TYPE; 18 break; 19 case 112: 20 msgType = LOG_ACK; 21 currType = LOG_ACK_TYPE; 22 break; 23 case 113: 24 msgType = CHPW_ACK; 25 currType = CHPW_ACK_TYPE; 26 break; 27 case 201: 28 msgType = PMSG; 29 currType = PRI_TYPE; 30 break; 31 case 202: 32 msgType = QMSG; 33 currType = QM_TYPE; 34 break; 35 case 203: 36 msgType = GAGMSG; 37 currType = GAG_TYPE; 38 break; 39 case 204: 40 msgType = DELMSG; 41 currType = DEL_TYPE; 42 break; 43 case 205: 44 msgType = SETMSG; 45 currType = SET_TYPE; 46 break; 47 case 208: 48 msgType = USERLIST; 49 currType = USERL_TYPE; 50 break; 51 case 209: 52 msgType = BROAD; 53 currType = BROAD_TYPE; 54 break; 55 56 57 default: 58 qDebug()<<"UNKNOW msgType"; 59 break; 60 } 61 62 } 63 64 /*read next data*/ 65 unsigned char ackType,msgLen; 66 QByteArray msg; 67 switch(msgType) 68 { 69 case REG_ACK: 70 { 71 72 if(this->bytesAvailable()>=1 && currType==REG_ACK_TYPE) 73 { 74 this->read((char*)&ackType,1); 75 currType = REG_ACK_REPLY; 76 qDebug()<<"ackType"<<ackType; 77 } 78 if(this->bytesAvailable()>=1 && currType==REG_ACK_REPLY) 79 { 80 this->read((char*)&msgLen,1); 81 currType = REG_ACK_MSGLEN; 82 qDebug()<<"msgLen:"<<msgLen; 83 } 84 if(this->bytesAvailable()>=msgLen && currType==REG_ACK_MSGLEN) 85 { 86 msg.resize(msgLen); 87 this->read(msg.data(),msg.size()); 88 currType = -1; 89 msgType = -1; 90 qDebug()<<"msg:"<<QString(msg); 91 emit registerAckSignal(ackType,msg); 92 qDebug()<<"send register signal"; 93 ackType = -1; 94 msg.clear(); 95 } 96 break; 97 } 98 case LOG_ACK: 99 { 100 101 if(this->bytesAvailable()>=1 && currType==LOG_ACK_TYPE) 102 { 103 this->read((char*)&ackType,1); 104 currType = LOG_ACK_REPLY; 105 qDebug()<<"ackType"<<ackType; 106 } 107 if(this->bytesAvailable()>=1 && currType==LOG_ACK_REPLY) 108 { 109 this->read((char*)&msgLen,1); 110 currType = LOG_ACK_MSGLEN; 111 qDebug()<<"msgLen:"<<msgLen; 112 } 113 if(this->bytesAvailable()>=msgLen && currType==LOG_ACK_MSGLEN) 114 { 115 msg.resize(msgLen); 116 this->read(msg.data(),msg.size()); 117 currType = -1; 118 msgType = -1; 119 qDebug()<<"msg:"<<QString(msg); 120 qDebug()<<"login send signal"; 121 emit loginAckSignal(ackType,msg); 122 123 ackType = -1; 124 msg.clear(); 125 } 126 127 break; 128 } 129 case CHPW_ACK: 130 { 131 132 if(this->bytesAvailable()>=1 && currType==CHPW_ACK_TYPE) 133 { 134 this->read((char*)&ackType,1); 135 currType = CHPW_ACK_REPLY; 136 qDebug()<<"ackType"<<ackType; 137 } 138 if(this->bytesAvailable()>=1 && currType==CHPW_ACK_REPLY) 139 { 140 this->read((char*)&msgLen,1); 141 currType = CHPW_ACK_MSGLEN; 142 qDebug()<<"msgLen:"<<msgLen; 143 } 144 if(this->bytesAvailable()>=msgLen && currType==CHPW_ACK_MSGLEN) 145 { 146 msg.resize(msgLen); 147 this->read(msg.data(),msg.size()); 148 currType = -1; 149 msgType = -1; 150 qDebug()<<"msg:"<<QString(msg); 151 emit chpasswdAckSignal(ackType,msg); 152 qDebug()<<"chpasswd signal send"; 153 ackType = -1; 154 msg.clear(); 155 } 156 157 break; 158 } 159 /*204read del msg*/ 160 case DELMSG: 161 { 162 char userLen; 163 QByteArray user; 164 if(this->bytesAvailable()>=1 && currType == DEL_TYPE) 165 { 166 this->read((char*)&userLen,1); 167 currType = DEL_USERLEN; 168 qDebug()<<"userLen:"<<userLen; 169 } 170 if(this->bytesAvailable()>=userLen && currType == DEL_USERLEN) 171 { 172 user.resize(userLen); 173 this->read(user.data(),user.size()); 174 currType = -1; 175 msgType = -1; 176 //QMessageBox::information(this,"del","you have been deleted,beacause you a choubi!!!"); 177 qDebug()<<"user:"<<user; 178 exit(0); 179 } 180 break; 181 } 182 case SETMSG: 183 { 184 QString user; 185 unsigned char userLen; 186 if(this->bytesAvailable()>=1 && currType == SET_TYPE) 187 { 188 this->read((char*)&userLen,1); 189 currType = SET_USERLEN; 190 } 191 if(this->bytesAvailable()>=userLen && currType == SET_USERLEN) 192 { 193 QByteArray ba; 194 ba.resize(userLen); 195 this->read(ba.data(),ba.size()); 196 currType = -1; 197 msgType = -1; 198 199 user = QString(ba); 200 201 emit setAdminSignal(user); 202 } 203 break; 204 } 205 206 207 /*208read users list*/ 208 case USERLIST: 209 { 210 unsigned char actType,userLen; 211 QByteArray user; 212 if(this->bytesAvailable()>=1 && currType == USERL_TYPE) 213 { 214 this->read((char*)&actType,1); 215 currType = USERL_ACT; 216 qDebug()<<"actType:"<<actType; 217 } 218 if(this->bytesAvailable()>=1 && currType == USERL_ACT) 219 { 220 this->read((char*)&userLen,1); 221 currType = USERL_USERLEN; 222 qDebug()<<"userLen:"<<userLen; 223 } 224 if(this->bytesAvailable()>=userLen && currType == USERL_USERLEN) 225 { 226 user.resize(userLen); 227 this->read(user.data(),user.size()); 228 currType = -1; 229 msgType = -1; 230 qDebug()<<"user:"<<QString(user); 231 //emit signal to add user in listWidget 232 emit addUserSignal(actType,QString(user)); 233 } 234 break; 235 } 236 case PMSG: 237 { 238 QString send,rec,time,msg; 239 unsigned char sendLen,recLen,msgLen; 240 if(this->bytesAvailable()>=1 && currType == PRI_TYPE) 241 { 242 this->read((char*)&sendLen,1); 243 currType = PRI_FROMLEN; 244 qDebug()<<"sendLen:"<<sendLen; 245 } 246 if(this->bytesAvailable()>=sendLen && currType == PRI_FROMLEN) 247 { 248 QByteArray ba; 249 ba.resize(sendLen); 250 this->read(ba.data(),ba.size()); 251 send = QString(ba); 252 currType = PRI_FROM; 253 qDebug()<<"send:"<<send; 254 } 255 if(this->bytesAvailable()>=1 && currType == PRI_FROM) 256 { 257 this->read((char*)&recLen,1); 258 currType = PRI_TOLEN; 259 qDebug()<<"recLen:"<<recLen; 260 } 261 if(this->bytesAvailable()>=recLen && currType == PRI_TOLEN) 262 { 263 QByteArray ba1; 264 ba1.resize(recLen); 265 this->read(ba1.data(),ba1.size()); 266 rec = QString(ba1); 267 currType = PRI_TO; 268 qDebug()<<"rec:"<<rec; 269 } 270 if(this->bytesAvailable()>=1 && currType == PRI_TO) 271 { 272 this->read((char*)&msgLen,1); 273 currType = PRI_MSGLEN; 274 qDebug()<<"msgLen:"<<msgLen; 275 } 276 if(this->bytesAvailable()>=msgLen && currType == PRI_MSGLEN) 277 { 278 QByteArray ba2; 279 ba2.resize(msgLen); 280 this->read(ba2.data(),ba2.size()); 281 time = QString(ba2).mid(0,18); 282 msg = QString(ba2).mid(19); 283 qDebug()<<"time:"<<time<<"msg:"<<msg; 284 285 currType = -1; 286 msgType = -1; 287 288 emit priChatMsgSignal(send,send+" "+time+" "+msg); 289 } 290 break; 291 } 292 case QMSG: 293 { 294 QString from,time,msg; 295 unsigned char fromLen,msgLen; 296 if(this->bytesAvailable()>=1 && currType == QM_TYPE) 297 { 298 this->read((char*)&fromLen,1); 299 currType = QM_FROMLEN; 300 qDebug()<<"fromLen:"<<fromLen; 301 } 302 if(this->bytesAvailable()>=fromLen && currType == QM_FROMLEN) 303 { 304 QByteArray ba; 305 ba.resize(fromLen); 306 this->read(ba.data(),ba.size()); 307 from = QString(ba); 308 currType = QM_FROM; 309 qDebug()<<"from:"<<from; 310 } 311 if(this->bytesAvailable()>=1 && currType == QM_FROM) 312 { 313 this->read((char*)&msgLen,1); 314 currType = QM_MSGLEN; 315 qDebug()<<"msgLen"<<msgLen; 316 } 317 if(this->bytesAvailable()>=msgLen && currType == QM_MSGLEN) 318 { 319 QByteArray ba; 320 ba.resize(msgLen); 321 this->read(ba.data(),ba.size()); 322 time = QString(ba).mid(0,18); 323 msg = QString(ba).mid(19); 324 currType = -1; 325 msgType = -1; 326 327 qDebug()<<"msg:"<<time<<" "<<msg; 328 emit groupChatMsgSignal(from+" "+time+" "+msg); 329 } 330 break; 331 } 332 case BROAD: 333 { 334 QString msg; 335 unsigned char msgLen; 336 if(this->bytesAvailable()>=1 && currType == BROAD_TYPE) 337 { 338 this->read((char*)&msgLen,1); 339 currType = BROAD_MSGLEN; 340 qDebug()<<"msgLen:"<<msgLen; 341 } 342 if(this->bytesAvailable()>=msgLen && currType == BROAD_MSGLEN) 343 { 344 QByteArray ba; 345 ba.resize(msgLen); 346 this->read(ba.data(),ba.size()); 347 currType = -1; 348 msgType = -1; 349 msg = QString(ba); 350 emit broadMsgSignal(msg); 351 } 352 break; 353 } 354 case GAGMSG: 355 { 356 QString user; 357 unsigned char userLen; 358 char time; 359 if(this->bytesAvailable()>=1 && currType == GAG_TYPE) 360 { 361 this->read((char*)&userLen,1); 362 currType = GAG_USERLEN; 363 qDebug()<<"userLen:"<<userLen; 364 } 365 if(this->bytesAvailable()>=userLen && currType == GAG_USERLEN) 366 { 367 QByteArray ba; 368 ba.resize(userLen); 369 this->read(ba.data(),ba.size()); 370 currType = GAG_USER; 371 user = QString(ba); 372 qDebug()<<"user:"<<user; 373 } 374 if(this->bytesAvailable()>=1 && currType == GAG_USER) 375 { 376 this->read((char*)&time,1); 377 currType = -1; 378 msgType = -1; 379 qDebug()<<"time:"<<time; 380 emit gagMsgSignal(user,time); 381 } 382 break; 383 } 384 385 386 default: 387 qDebug()<<"UNKNOW msgType2"; 388 break; 389 }//switch 390 }//while 391 }//readData
基本上是一个模式的解析,这里用上了之前定于的枚举解析,很方便的防止包不完整的情况的发生
总结
我们用了四天做完了这个项目,做完后的感觉是,一些基础的知识很重要,这个项目的基础知识你必须手到擒来。在写代码前一定要把很多东西考虑清楚,想清楚之后去写代码就只是做码农的工作了,只是客户端的逻辑复杂一点点。在协议,框架等东西都很确定的情况下,你就不会晕,而是给自己一个模块一个模块的去写出来,所以,框架才是最重要的。