上一节已经完成了对用户的身份验证了,既然有了验证,那么接下来就能对不同的客户端进行区分了,所以这一节讲实现私聊功能。就是通过服务器对客户端的数据进行转发到特定的用户上,

  实现私聊功能的聊天程序

  实现的技术细节是:对客户端发送的数据增加一个标识头,由于我们处理的是纯文本,所以为了讲解的方便就把标识头加到聊天信息的前面,然后在服务器中判断。如果是要在做成产品的话,因为要考虑传送纯文本,图片,文件,特定的结构体等等其他非纯文本信息,那么我们可以对一次聊天信息,发送两次数据,第一次用TCP发送一个结构体,该结构体包含接下来要接收的信息的格式,大小等信息,然后第二次就发送真正的数据块。

  关于第二次发送为什么要用UDP呢?这个是学腾讯qq的,因为标识结构体比较小,而且是必须要有的(为什么?)所以使用TCP,而信息那一部分,往往是数据比较大的,都用tcp传的话,会占用更多的资源。所以我们聊天的时候,有时候会出现这样一条信息,“由于网络问题,该信息可能发送失败”。想想,如果是tcp传输,那么就只有成功和失败,没有什么可能的问题,注意传文件是两个客户端进行tcp连接的,不然怎么确保正确性呢,哎,其实还是很复杂的。(真的吗?求辟谣!)

  回到我们的程序中来吧,我的处理办法是在服务器里判断第一个单词是不是simple,如果是就读取第二个单词,为用户名,然后根据用户名从fd_C中查找,看fd_C对应fd_A的socket号码,然后根据这个fd号码进行转发,而不是进行群发。(如果要增加什么功能,如传文件的话那么,道理一样,判断第一个单词是不是file,如果是第二个单词是文件名什么的,具体就是这样做的。如果是做有界面的客户端,就可以进行选择要聊天的用户,然后在后台生成simple这个标识号了。就对用户友好一点。)

  好了,到了激动人心的时刻了,下面是代码讲解。

  client.c 基本不变

  server.c

  ...
#include <mysql.h>//用于mysql连接
  ... struct user
{
    ...
29 }; int MAX(int a,int b)
    ...

void print_time(char * ch,time_t *now)
{
    ...
} int mysql_check_login(struct user su)
{
    ...
91 return ;
} //根据用户名返回该用户名在fd_A中的位置
//fd=-1,表示没有该用户 //fd>0 正常返回
int fd_ctoa(char fd_C[][],char *ch)
{
int i,j;
int fd=-;
for(i=;i<BACKLOG;i++)
{
if(strcmp(fd_C[i],ch)==)
{
fd=i;
break;
}
}
return fd;
} int main(int argc,char *argv[])
{
    ...
while()
{
FD_ZERO(&servfd);//清空所有server的fd
FD_ZERO(&recvfd);//清空所有client的fd
FD_SET(sockfd,&servfd);
//timeout.tv_sec=30;//可以减少判断的次数
switch(select(max_servfd+,&servfd,NULL,NULL,&timeout))
{
        ... ...
}
//FD_COPY(recvfd,servfd);
for(i=;i<MAX_CON_NO;i++)//最大队列进行判断,优化的话,可以使用链表
{
        ...
} switch(select(max_recvfd+,&recvfd,NULL,NULL,&timeout))
{
case -:
//select error
break;
case :
//timeout
break;
default:
for(i=;i<conn_amount;i++)
{
if(FD_ISSET(fd_A[i],&recvfd))
{
/*receive datas from client*/
if((recvSize=recv(fd_A[i],recvBuf,MAX_DATA_SIZE,))==- || recvSize==)
{
                  ...
}
else//客户端发送数据过来,然后这里进行转发
{
/*send datas to client*/
/*下面是私聊代码,为了方便讲解所以写在这里*/
sscanf(recvBuf,"%s%s",ch,username);
if(strcmp(ch,"simple")==)//判断第一个单词是不是simple私聊的标识符
{
printf("私聊信息处理: %s\n",recvBuf);
for(j=;j<strlen(recvBuf);j++)//为了方便我规定聊天信息中以#符号后面为发送的聊天文本前面为标识符号(一切都是为了方便 ^v^)
{
if(recvBuf[j]=='#')
{
j++;
break;
}
}
if(j<strlen(recvBuf))
{
printf("%s对%s私聊说:%s\n",fd_C[i],username,&recvBuf[j]);//打印在服务器控制台方便调试
fd=fd_ctoa(fd_C,username);//根据用户名得到该用户名所对应的fd_A中的位置
printf("fd=%d\n",fd);//打印描述符号,用于调试
if(fd>=)//表示找到对应的用户名
{
strcpy(sendBuf,fd_C[i]);
strcat(sendBuf," 对您私聊 ");
print_time(ch,&now);
strcat(sendBuf,ch);//加个时间戳
strcat(sendBuf,"\t\t");
strcat(sendBuf,&recvBuf[j]);
//sendSize=send(fd_A[fd],&recvBuf[j],strlen(recvBuf)-j,0);
sendSize=send(fd_A[fd],sendBuf,strlen(sendBuf),);//发往指定的客户端
}
else
{
strcpy(ch,"私聊信息发送失败,可能是没有该用户");
sendSize=send(fd_A[i],ch,strlen(ch),); }
}
else
{
strcpy(ch,"私聊信息发送失败,可能是没有 # 符号");
sendSize=send(fd_A[i],ch,strlen(ch),); }
break;
}
else
{
}
//其他else就是其他命令了,为了方便就不支持其他命令了 ...
}
}
}
break;
}//end-switch
}//end-while(1)
return ;
}

  照例给个运行时的截图,提提神。

  好了,我们已经完成群聊和私聊的功能了,作为一个聊天程序实现这两个基本功能也就差不多啦。

  一点小小的补充:

//上一节忘了说mysql怎么设置开机启动了,指令如下,root用户执行
chkconfig mysqld on
service mysqld start

  另一个知识点的补充,也是今天才注意到的。以前我们每登陆一个客户端都会分配一个文件描述符fd,而服务器中对每个连接产生的fd号是从3开始,连一个就加一个。而现在分配的ID(fd)号是从4开始的不说,还每次增加2。这就奇怪了。

 [myuser@localhost client-server]$ ./server ser
username:ser
Success to establish a socket...
Success to bind the socket...
Success to accpet a connection request...
>>>>>> 127.0.0.1: join in! ID(fd):4
加入的时间是::: 客户端发来的用户名是:user3,密码:
查询的sql:select * from clients where username="user3" and password="";
验证成功!
Success to accpet a connection request...
>>>>>> 127.0.0.1: join in! ID(fd):6
加入的时间是::: 客户端发来的用户名是:user1,密码:
查询的sql:select * from clients where username="user1" and password="";
验证成功!
Success to accpet a connection request...
>>>>>> 127.0.0.1: join in! ID(fd):8
加入的时间是::: 客户端发来的用户名是:user2,密码:
查询的sql:select * from clients where username="user2" and password="";
验证成功!
数据是:user2 ::

  就是那几个大红色标出来的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

  本文地址: http://www.cnblogs.com/wunaozai/p/3878374.html

Socket网络编程--聊天程序(8)的更多相关文章

  1. Socket网络编程--聊天程序(9)

    这一节应该是聊天程序的最后一节了,现在回顾我们的聊天程序,看起来还有很多功能没有实现,但是不管怎么说,都还是不错的.这一节我们将讲多服务器问题(高大上的说法就是负载问题了.)至于聊天程序的文件发送(也 ...

  2. Socket网络编程--聊天程序(1)

    很早的一段时间,看了APUE和UNPv1了解了网络编程,但是但是只是看而已,没有具体的实践,趁现在没有什么事做,就来实践了解一下网络编程.写博客保存下来,方便以后用到的时候可以查到. 此次的聊天程序是 ...

  3. Socket网络编程--聊天程序(6)

    这一小节将增加一个用户的结构体,用于保存用户的用户名和密码,然后发给服务器,然后在服务器进行判断验证.这里就有一个问题,以前讲的就是发送字符串是使用char类型进行传输,然后在服务器进行用同样是字符串 ...

  4. Socket网络编程--聊天程序(7)

    接上一小节,本来是计划这一节用来讲数据库的增删改查,但是在实现的过程中,出现了一点小问题,也不是技术的问题,就是在字符界面上比较不好操作.比如要注册一个帐号,就需要弄个字符界面提示,然后输入数字表示选 ...

  5. Socket网络编程--聊天程序(3)

    上一小节,已经讲到可以每个人多说话,而且还没有限制,简单的来说,我们已经完成了聊天的功能了,那么接下来我们要实现什么功能呢?一个聊天程序至少应该支持一对多的通讯吧,接下来就实现多个客户端往服务器发送数 ...

  6. Socket网络编程--聊天程序(4)

    上一小节讲到可以实现多客户端与服务器进行通讯,对于每一个客户端的连接请求,服务器都要分配一个进程进行处理.对于多用户连接时,服务器会受不了的,而且还很消耗资源.据说有个select函数可以用,好像还很 ...

  7. Socket网络编程--聊天程序(5)

    上一小节我们讲了使用select来避免使用多进程的资源浪费问题.上次只是实现了从多个客户端发送数据给服务器端,接下来就要实现从服务器端发送数据给各个客户端. 使用select多路转换处理聊天程序2 c ...

  8. Socket网络编程--聊天程序(2)

    上一节简单如何通过Socket创建一个连接,然后进行通信.只是每个人只能说一句话.而且还是必须说完才会接收到信息,总之是很不方便的事情.所以这一小节我们将对上一次的程序进行修改,修改成每个人可以多说话 ...

  9. Socket网络编程--小小网盘程序(5)

    各位好呀!这一小节应该就是这个小小网盘程序的最后一小节了,这一节将实现最后的三个功能,即列出用户在服务器中的文件列表,还有删除用户在服务器中的文件,最后的可以共享文件给好友. 列出用户在服务器中的文件 ...

随机推荐

  1. 试安装pyQt5+eric6+python安装

    1.先安装pip最新版 安装之前把sit-packages----pip旧版本删掉 然后再cmd输入pip install --user update pip 2.安装pyqt5 pip instal ...

  2. 潭州课堂25班:Ph201805201 第八课:函数基础和函数参数 (课堂笔记)

    1, 函数定义 def fun(): print('测试函数') fun() #调用函数 return 运行函数返回值 def fun(): name = [1,3,4,5] return name[ ...

  3. angularJS常用的内置方法

    angular.lowercase(aaa); angular.uppercase(bbb); angular.isString(); angular.isNumber(); angular.isAr ...

  4. android TextView如何换行?

    具体代码是: rb_zuotian.setText("第一行"+"\n"+"第二行"); rb_jintian.setText(DateUt ...

  5. javascript中用正则表达式判断是否为汉字及常用的判断

    a.判断是否为汉字: 1.汉字为任意长度时: var han = /^[\u4e00-\u9fa5]+$/; 例如: var han = /^[\u4e00-\u9fa5]+$/; var vals= ...

  6. PHP Redis 对象方法手册

    redis(Remote Dictionary Server)是一种Nosql技术,它是一个开源的高级kv存储和数据结构存储系统. redis不仅仅是能够存储key和value这种简单的键值对,还能存 ...

  7. python 数字

    python数字: 本文主要讲解常用的数字类型转换, 数字函数和随机数字函数. # coding:utf-8 # python数字类型转换 def data_conversion(): "& ...

  8. JDBC(3)—ResultSet结果集

    简介:ResultSet:结果集.封装了使用JDBC进行查询的结果.Statement只能进行更新操作,所以使用ResultSet进行查询操作. 1.调用Statement对象的executeQuer ...

  9. SpringMVC加载配置Properties文件的几种方式

    最近开发的项目使用了SpringMVC的框架,用下来感觉SpringMVC的代码实现的非常优雅,功能也非常强大, 网上介绍Controller参数绑定.URL映射的文章都很多了,写这篇博客主要总结一下 ...

  10. Java LinkedList工作原理及实现

    1. 概述 以双向链表实现.链表无容量限制,但双向链表本身使用了更多空间,也需要额外的链表指针操作. 按下标访问元素—get(i)/set(i,e) 要悲剧的遍历链表将指针移动到位(如果i>数组 ...