上一节已经完成了对用户的身份验证了,既然有了验证,那么接下来就能对不同的客户端进行区分了,所以这一节讲实现私聊功能。就是通过服务器对客户端的数据进行转发到特定的用户上,
实现私聊功能的聊天程序
实现的技术细节是:对客户端发送的数据增加一个标识头,由于我们处理的是纯文本,所以为了讲解的方便就把标识头加到聊天信息的前面,然后在服务器中判断。如果是要在做成产品的话,因为要考虑传送纯文本,图片,文件,特定的结构体等等其他非纯文本信息,那么我们可以对一次聊天信息,发送两次数据,第一次用TCP发送一个结构体,该结构体包含接下来要接收的信息的格式,大小等信息,然后第二次就发送真正的数据块。
关于第二次发送为什么要用UDP呢?这个是学腾讯qq的,因为标识结构体比较小,而且是必须要有的(为什么?)所以使用TCP,而信息那一部分,往往是数据比较大的,都用tcp传的话,会占用更多的资源。所以我们聊天的时候,有时候会出现这样一条信息,“由于网络问题,该信息可能发送失败”。想想,如果是tcp传输,那么就只有成功和失败,没有什么可能的问题,注意传文件是两个客户端进行tcp连接的,不然怎么确保正确性呢,哎,其实还是很复杂的。(真的吗?求辟谣!)
回到我们的程序中来吧,我的处理办法是在服务器里判断第一个单词是不是simple,如果是就读取第二个单词,为用户名,然后根据用户名从fd_C中查找,看fd_C对应fd_A的socket号码,然后根据这个fd号码进行转发,而不是进行群发。(如果要增加什么功能,如传文件的话那么,道理一样,判断第一个单词是不是file,如果是第二个单词是文件名什么的,具体就是这样做的。如果是做有界面的客户端,就可以进行选择要聊天的用户,然后在后台生成simple这个标识号了。就对用户友好一点。)
好了,到了激动人心的时刻了,下面是代码讲解。
client.c 基本不变
server.c
... 17 #include <mysql.h>//用于mysql连接 ... 24 25 struct user 26 { ...
29 }; 30 31 int MAX(int a,int b) ...
37 38 void print_time(char * ch,time_t *now) 39 { ...
43 } 44 45 46 int mysql_check_login(struct user su) 47 { ...
91 return 0; 92 } 93 94 //根据用户名返回该用户名在fd_A中的位置 95 //fd=-1,表示没有该用户 //fd>0 正常返回 96 int fd_ctoa(char fd_C[][32],char *ch) 97 { 98 int i,j; 99 int fd=-1; 100 for(i=0;i<BACKLOG;i++) 101 { 102 if(strcmp(fd_C[i],ch)==0) 103 { 104 fd=i; 105 break; 106 } 107 } 108 return fd; 109 } 110 111 int main(int argc,char *argv[]) 112 { ... 187 while(1) 188 { 189 FD_ZERO(&servfd);//清空所有server的fd 190 FD_ZERO(&recvfd);//清空所有client的fd 191 FD_SET(sockfd,&servfd); 192 //timeout.tv_sec=30;//可以减少判断的次数 193 switch(select(max_servfd+1,&servfd,NULL,NULL,&timeout)) 194 {
... ...
242 } 243 //FD_COPY(recvfd,servfd); 244 for(i=0;i<MAX_CON_NO;i++)//最大队列进行判断,优化的话,可以使用链表 245 { ...
250 } 251 252 switch(select(max_recvfd+1,&recvfd,NULL,NULL,&timeout)) 253 { 254 case -1: 255 //select error 256 break; 257 case 0: 258 //timeout 259 break; 260 default: 261 for(i=0;i<conn_amount;i++) 262 { 263 if(FD_ISSET(fd_A[i],&recvfd)) 264 { 265 /*receive datas from client*/ 266 if((recvSize=recv(fd_A[i],recvBuf,MAX_DATA_SIZE,0))==-1 || recvSize==0) 267 { ...
273 } 274 else//客户端发送数据过来,然后这里进行转发 275 { 276 /*send datas to client*/ 277 /*下面是私聊代码,为了方便讲解所以写在这里*/ 278 sscanf(recvBuf,"%s%s",ch,username); 279 if(strcmp(ch,"simple")==0)//判断第一个单词是不是simple私聊的标识符 280 { 281 printf("私聊信息处理: %s\n",recvBuf); 282 for(j=0;j<strlen(recvBuf);j++)//为了方便我规定聊天信息中以#符号后面为发送的聊天文本前面为标识符号(一切都是为了方便 ^v^) 283 { 284 if(recvBuf[j]=='#') 285 { 286 j++; 287 break; 288 } 289 } 290 if(j<strlen(recvBuf)) 291 { 292 printf("%s对%s私聊说:%s\n",fd_C[i],username,&recvBuf[j]);//打印在服务器控制台方便调试 293 fd=fd_ctoa(fd_C,username);//根据用户名得到该用户名所对应的fd_A中的位置 294 printf("fd=%d\n",fd);//打印描述符号,用于调试 295 if(fd>=0)//表示找到对应的用户名 296 { 297 strcpy(sendBuf,fd_C[i]); 298 strcat(sendBuf," 对您私聊 "); 299 print_time(ch,&now); 300 strcat(sendBuf,ch);//加个时间戳 301 strcat(sendBuf,"\t\t"); 302 strcat(sendBuf,&recvBuf[j]); 303 //sendSize=send(fd_A[fd],&recvBuf[j],strlen(recvBuf)-j,0); 304 sendSize=send(fd_A[fd],sendBuf,strlen(sendBuf),0);//发往指定的客户端 305 } 306 else 307 { 308 strcpy(ch,"私聊信息发送失败,可能是没有该用户"); 309 sendSize=send(fd_A[i],ch,strlen(ch),0); 310 311 } 312 } 313 else 314 { 315 strcpy(ch,"私聊信息发送失败,可能是没有 # 符号"); 316 sendSize=send(fd_A[i],ch,strlen(ch),0); 317 318 } 319 break; 320 } 321 else 322 { 323 } 324 //其他else就是其他命令了,为了方便就不支持其他命令了 325 ...
352 } 353 } 354 } 355 break; 356 }//end-switch 357 }//end-while(1) 358 return 0; 359 }
照例给个运行时的截图,提提神。
好了,我们已经完成群聊和私聊的功能了,作为一个聊天程序实现这两个基本功能也就差不多啦。
一点小小的补充:
//上一节忘了说mysql怎么设置开机启动了,指令如下,root用户执行 chkconfig mysqld on service mysqld start
另一个知识点的补充,也是今天才注意到的。以前我们每登陆一个客户端都会分配一个文件描述符fd,而服务器中对每个连接产生的fd号是从3开始,连一个就加一个。而现在分配的ID(fd)号是从4开始的不说,还每次增加2。这就奇怪了。
1 [myuser@localhost client-server]$ ./server ser 2 username:ser 3 Success to establish a socket... 4 Success to bind the socket... 5 Success to accpet a connection request... 6 >>>>>> 127.0.0.1:54880 join in! ID(fd):4 7 加入的时间是:06:39:04 8 9 客户端发来的用户名是:user3,密码:123456 10 查询的sql:select * from clients where username="user3" and password="123456"; 11 验证成功! 12 Success to accpet a connection request... 13 >>>>>> 127.0.0.1:54881 join in! ID(fd):6 14 加入的时间是:06:39:04 15 16 客户端发来的用户名是:user1,密码:123456 17 查询的sql:select * from clients where username="user1" and password="123456"; 18 验证成功! 19 Success to accpet a connection request... 20 >>>>>> 127.0.0.1:54882 join in! ID(fd):8 21 加入的时间是:06:39:04 22 23 客户端发来的用户名是:user2,密码:123456 24 查询的sql:select * from clients where username="user2" and password="123456"; 25 验证成功! 26 数据是:user2 06:39:04
就是那几个大红色标出来的fd号,连接3个客户端居然是分配到4,6,8。而不是3,4,5
还好我们的代码每次都增加不多,可以很快就知道为什么?因为有了数据库的连接。
解释:文件描述符0,1,2这三个默认分配给stdin,stdout,stderr,然后接下来就按需分配了。3号是服务器用于接收客户端请求而创建的sockfd,在一开始就创建了。4号就是client1了,5号就是client1下连接数据库而创建的。由于我们的服务器对每个连接都要有一次访问数据库,所以对应单数的那些fd都是用在数据库连接上了。(什么是文件描述符?自己上网查咯)
一些小总结,其实网络编程还是很有趣的,了解后就会发现很多看起来很叼的技术,其内部底层还是很简单的实现的。就我们常常听到的下面这些技术 防火墙,远程控制,远程SHELL,VPN,内网穿透等等看起来很厉害的技术,都基本上都是使用服务器,实现一对一的转发而已。只不过特定的功能还要靠特定的优化办法(如一些特定的IO操作,算法,安全性等)处理而已,也就是优化处理速度与安全性。如果是一般的使用,那我们其实都是可以实现的。所以别看一个小小的聊天程序的一个私聊功能,其实还是很多高级应用的基础(麻雀虽小,五脏俱全)。(由于本人技术问题,本博只提供思路,想法和一个小小的入门级程序。)
参考资料
关于标识符包头的详解: http://blog.csdn.net/jia162/article/details/1926576