Python之路【第十八篇】Django小项目webQQ实现
WEBQQ的实现的几种方式
1、HTTP协议特点
首先这里要知道HTTP协议的特点:短链接、无状态!
在不考虑本地缓存的情况举例来说:咱们在连接博客园的时候,当tcp连接后,我会把我自己的http头发给博客园服务器,服务器端就会看到我请求的URL,server端就会根据URL分发到相应的视图处理(Django的views里)。最后给我返回这个页面,当返回之后连接就断开了。
短连接:
服务器为什么要断开?很多情况下我还会打开页面,我请求一次连接断开了为什么会这样?为什么不建立长期的连接?这个是HTTP设计的考虑,在大并发的情况下,如果连接不断开,我不知道你什么时候点,你可能立刻就点有可能10分钟1个小时或者其他时间点,那么你就会占着这个连接(是很浪费的,并且连接是有限的),所以当返回后server端就会断开这个连接。
无状态:
服务器不保存客户端的任何状态,每一次客户端连接服务器的时候都要把相关的信息发给客户端告诉客户端你是谁,服务端不会保存你是谁?
那么问题来了,为什么我们在登录京东之后登录一次之后,服务器就不会让咱们在登录了,根据咱们之前的博客的Session和Cookie。服务器端会在用户登录的时候,在服务器端生成一个sessionID(有有效期)并且返回给客户。客户端会把这个seesionID存到Cookie里。这样登录之后就不需要再输入密码!
2、WEBqq通信实现
首先看下面的图
根据WEBQQ的工作来看下,首先C1要发送一条数据给C2首先得通过WEB Server进行中转,首先咱们这知道了,正常情况下当C1发送给WEB Server之后,WEB Server就直接返回了,WEB Server就断开了C1的连接了,那么WEB Server会主动给C2发送信息吗?
WEB 服务器默认是被动接收请求的,如果你没打开浏览器,博客园可以给你发信息吗?即便你打开了浏览器,你获取到数据之后就断开了,你看到的是本地缓存的数据。 你和服务器之间就没有联系了。如果服务器想把数据发送给C2那的等C2连接过来,服务器一看有一条C2的数据然后发给C2.那么问题又来了?他知道C2什么时候连接过来吗?服务端不知道C2什么时候连接过来服务端又想能时时把数据发送给C2怎么做呢?《轮询》
轮询方式:
短轮询:
C2客户端有个循环,去Server端取数据。不断的循环去取(会对Server端造成压力)
C2客户端有个时间段的循环,每隔1分钟去取一次,但是不是时时的,这样也不好。
长轮询:
上面的方式也是不可取的那怎么做呢:有没有这么一种方法:当C2请求过来接收的时候,Server端没有C2的数据,Server端没有办法主动让C2等着那怎么办呢?把C2的请求挂起,当有数据的时候在把数据立刻返回,并且多久还是没有数据就把这个链接返回!
这样所有的链接就变成有意义的请求。我不给他断开他就不会发新的请求!
本质上还是轮询,但是他发请求的频率就非常低了!
但是有个问题:他本质上还是一个短链接(这里慢慢想下其实不难理解),如果消息频繁的话,他还是不断的重新建立链接。这样也会对服务器造成影响!每收一条消息都得往返两次。他其实也是不够高效的。
真正的WEBQQ就是用的这个原理来实现的!(因为WEB Socket只有部分浏览器支持(H5标准)IE不支持,在中国的这个环境下IE使用率还是较高的所以不能普及,所以这个方法还是OK得)
还有一个方法就是,真正的长连接,在浏览器上起一个Socket客户端然后连接到服务端,他俩建立一个Socket通道,这样就和Socket Server和Socket Client一样这样他们之间的数据传输就是,时时的了!这个就叫做WEB Socket !!!!!
Socket Server和Socket Client和WEB Socket的区别就是WEB Socket启动在浏览器上! 0 0 !
比如我们在支持H5的浏览器上比如Google的浏览器轻松起一个WEB Socket,但是这个不仅仅要客户端支持,Server端也得支持才可以!
sock = new WebSocket("ws://www.baidu.com")
WEB QQ 表结构
首先用户的好友在哪个表里?在用户表里那么他就的关联自己了并且是多对多的关系,你可以有多个朋友,你朋友也可以有多个朋友!
class UserProfile(models.Model): ''' 用户表 ''' #使用Django提供的用户表,直接继承就可以了.在原生的User表里扩展!(原生的User表里就有用户名和密码) #一定要使用OneToOne,如果是正常的ForeignKey的话就表示User中的记录可以对应UserProfile中的多条记录! #并且OneToOne的实现不是在SQL级别实现的而是在代码基本实现的! user = models.OneToOneField(User) #名字 name = models.CharField(max_length=32) #属组 groups = models.ManyToManyField("UserGroup") #朋友 friends = models.ManyToManyField('self',related_name='my_friends')
然后在建立一个APP然后APP名称为:web_chat 他调用WEB里的UserProfile用户信息,然后在web_chat的models里新创建一个表:QQGroup!(复习不同APP间的Model调用~)
#/usr/bin/env python #-*- coding:utf-8 -*- from __future__ import unicode_literals from django.db import models from web.models import UserProfile # Create your models here. class QQGroup(models.Model): ''' QQ组表 ''' #组名 name = models.CharField(max_length=64,unique=True) #注释 description = models.CharField(max_length=255,default="The Admin is so lazy,The Noting to show you ....") ''' 下面的members和admins在做跨APP关联的时候,关联的表不能使用双引号!并且在调用,Django的User表的时候也不能加双引号。 ''' #成员 members = models.ManyToManyField(UserProfile,blank=True) #管理员 admins = models.ManyToManyField(UserProfile,blank=True,related_name='group_admins') ''' 如果在一张表中,同样调用了另一张表同样的加related_name ''' #最大成员数量 max_member_nums = models.IntegerField(default=200) def __unicode__(self): return self.name
这里:members和admins在做跨APP关联的时候,关联的表不能使用双引号!并且在调用,Django的User表的时候也不能加双引号。
WEBQQ相关知识点总结
1、URL相关
在之前做不同APP的时候,我们都是输入完全的URL,我们可以定义一个别名来使用它很方便!
别名的好处:如果说那天想修改url里的这个url名称了,是不是所有前端都得修改!并且在有好几层的时候怎么改使用别名就会非常方便了!
projecet下的总URL
#!/usr/bin/env python #-*- coding:utf-8 -*- """Creazy_BBS URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/1.9/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') Class-based views 1. Add an import: from other_app.views import Home 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') Including another URLconf 1. Import the include() function: from django.conf.urls import url, include 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ from django.conf.urls import url from django.conf.urls import include from django.contrib import admin from web import views from web import urls as web_urls from web_chat import urls as chat_urls urlpatterns = [ url(r'^admin/', admin.site.urls), #include-app web url(r'^web/', include(web_urls)), #include-app web_chat url(r'^chat/', include(chat_urls)), #指定默认的URL, url(r'',views.index,name='index'), ]
web app中的URL指定相应的别名
from django.conf.urls import url import views urlpatterns = [ url(r'category/(\d+)/$',views.category,name='category'), url(r'article_detaill/(\d+)/$',views.article_detaill,name='article_detaill'), url(r'article/new/$',views.new_article,name='new_article'), url(r'account/logout$',views.acount_logout,name='logout'), url(r'account/login',views.acount_login,name='login'), ]
web_chat app中的别名
from django.conf.urls import url import views urlpatterns = [ url(r'^dashboard/$', views.dashboard,name='web_chat'), ]
在前端引用的时候需要注意:例如下面两个就需要使用别名来指定,格式也必须正确!
<li><a href="{% url 'new_article' %}">发帖</a></li> <li><a href="{% url 'logout' %}">用户注销</a></li>
2、使用Django自带的模块判断用户是否登录
#/usr/bin/env python #-*- coding:utf-8 -*- from django.shortcuts import render #导入Django自带的判断用户是否登录的模块 from django.contrib.auth.decorators import login_required # Create your views here. #应用装饰器 @login_required def dashboard(request): return render(request,'web_chat/dashboard.html')
然后在settings里配置,如果没有登录转向的URL
LOGIN_URL = '/web/account/login/'
3、事件链
//页面加载完成后 $(document).ready(function () { //delegate 事件链,把多个事件进行绑定 //给body下的textarea进行绑定,当回车键按下后执行的函数 $("body").delegate("textarea", "keydown",function(e){ if(e.which == 13) {//如果13这个按键(回车,可以通过console.log输出实际按下的那个键),执行下面的函数 //send msg button clicked var msg_text = $("textarea").val(); if ($.trim(msg_text).length > 0){ //如果去除空格后,大于0 //console.log(msg_text); //SendMsg(msg_text); //把数据进行发送 } //把数据发送到聊天框里 AddSentMsgIntoBox(msg_text); $("textarea").val(''); } });//end body });//页面也在完成,结束
这里需要注意,在$(document).ready中调用的函数不能写在$(document).ready中,$(document).ready你已加载就执行了,$(document).ready自己也是一个函数,你$(document).ready执行完之后就不存在了,就释放了,你在$(document).ready中定义的函数,外面就无法调用了。
4、聊天内容自动扩展并且可以感觉内容进行自动滑动
首先配置聊天的窗口样式:
.chat_contener { width: 100%; height: 490px; background-color: black; opacity: 0.6; overflow: auto; }
然后配置,当我们发送数据的时候自动的滚动
//定义发送到聊天框函数 function AddSentMsgIntoBox(msg_text){ //拼接聊天内容 /*气泡实现 <div class="clearfix"> <div class="arrow"></div> <div class="content_send"><div style="margin-top: 10px;margin-left: 5px;">Hello Shuaige</div></div> </div> */ var msg_ele = "<div class='clearfix' style='padding-top:10px'>" + "<div class='arrow'>" + "</div>" + "<div class='content_send'>" + "<div style='margin-top: 10px;margin-left: 5px;'>" + msg_text + "</div>" + "</div>"; $(".chat_contener").append(msg_ele); //animate 动画效果 $('.chat_contener').animate({ scrollTop: $('.chat_contener')[0].scrollHeight}, 500 );//动画效果结束 }//发送到聊天框函数结束
Ajax发送方式
正常情况下来说咱们在写一个Ajax请求的时候都是这么写的:
$.ajax({ url:'/save_hostinfo/', type:'POST', tradition: true, data:{data:JSON.stringify(change_info)}, success:function(arg){ //成功接收的返回值(返回条目) var callback_dict = $.parseJSON(arg);//这里把字符串转换为对象 //然后咱们就可以判断 if(callback_dict){//执行成功了 //设置5秒钟后隐藏 setTimeout("hide()",5000); var change_infos = '修改了'+callback_dict['change_count']+'条数据'; $('#handle_status').text(change_infos).removeClass('hide') }else{//如果为False执行失败了 alert(callback_dict.error) } } })
还有另一种方式:
//向后端发送数据 $.post("{% url 'send_msg' %}" ,{'data':JSON.stringify(msg_dic)},function(callback){ console.log(callback); });//向发送数据结束 //解释: // $.post 或者 $.get 是调用ajax方法 //("URL路径" ,{'data':JSON.stringify(msg_dic)},function(callback){}) // // 这个第一个参数为指定的ULR 第二个参数为发送的内容 第3个参数为回调函数和返回的值!!
AjaxPOST数据CSRF问题
在做Django的Form表单的时候学了,直接在提交表单哪里加上csrftoken就可以了,那Ajax怎么进行认证呢?可以使用下面的方法进行认证
//获取CSRF参数 function GetCsrfToken(){ return $("input[name='csrfmiddlewaretoken']").val() } //发送消息 function SendMsg(msg_text){ var contact_id = $('#chat_hander h2').attr("contact_id"); //获取发送给谁消息 var contact_type = $('#chat_hander h2').attr("contact_type");//获取聊天类型 var msg_dic = { 'contact_type':contact_type, 'to':contact_id, 'from':"{{ request.user.userprofile.id }}", 'from_name':"{{ request.user.userprofile.name }}", 'msg':msg_text }; //向后端发送数据 $.post("{% url 'send_msg' %}" ,{'data':JSON.stringify(msg_dic),'csrfmiddlewaretoken':GetCsrfToken()},function(callback){ console.log(callback); });//向发送数据结束 //解释: // $.post 或者 $.get 是调用ajax方法 //("URL路径" ,{'data':JSON.stringify(msg_dic)},function(callback){}) // // 这个第一个参数为指定的ULR 第二个参数为发送的内容 第3个参数为回调函数和返回的值!! }//发送消息结束
那有没有一劳永逸的方式呢:
function getCookie(name) { var cookieValue = null; if (document.cookie && document.cookie != '') { var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { var cookie = jQuery.trim(cookies[i]); // Does this cookie string begin with the name we want? if (cookie.substring(0, name.length + 1) == (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; } } } return cookieValue; } var csrftoken = getCookie('csrftoken');
function csrfSafeMethod(method) { // these HTTP methods do not require CSRF protection return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); } $.ajaxSetup({ beforeSend: function(xhr, settings) { if (!csrfSafeMethod(settings.type) && !this.crossDomain) { xhr.setRequestHeader("X-CSRFToken", csrftoken); } } });
还有一个插件,他实现了“一劳永逸的上半部分,下半部分还是得需要写:JavaScript Cookie library ” ,其实也不是很多自己写的就可以了。
WEBQQ消息存储方式
首先要知道如下几点:C1发给C2消息,消息被发送到服务端之后,当服务端请求过来之后C2接收到消息之后消息就服务端的数据就没有意义了。所以不能使用Mysql、这样的数据置于Redis和Memcache也是没有必要的,当然排除支持数据夸不同设备可以把数据持久化!
那咱们怎么做呢?想象一下数据被C2接收走之后,server端的数据就没有意义了,用消息队列方式是不是更好一点呢?
定义一个队列,队列不能写在接收函数哪里,写个全局的队列即可,并且不能创建一个队列,而是为每个用户创建一个队列。
import Queue GLOBAL_MQ = { } def new_msg(request): if request.method == 'POST': print request.POST.get('data') #获取用户发过来的数据 data = json.loads(request.POST.get('data')) send_to = data['to'] #判断队列里是否有这个用户名,如果没有新建一个队列 if send_to not in GLOBAL_MQ: GLOBAL_MQ[send_to] = Queue.Queue() data['timestamp'] = time.time() GLOBAL_MQ[send_to].put(data) return HttpResponse(GLOBAL_MQ[send_to].qsize()) else: #因为队列里目前存的是字符串所以我们需要先给他转换为字符串 request_user = str(request.user.userprofile.id) msg_lists = [] #判断是否在队列里 if request_user in GLOBAL_MQ: #判断有多少条消息 stored_msg_nums = GLOBAL_MQ[request_user].qsize() #把消息循环加入到列表中并发送 for i in range(stored_msg_nums): msg_lists.append(GLOBAL_MQ[request_user].get()) return HttpResponse(json.dumps(msg_lists))
使用Queue&JS实现长轮询
先看下使用下面的方法是否可行:
#因为队列里目前存的是字符串所以我们需要先给他转换为字符串 request_user = str(request.user.userprofile.id) msg_lists = [] #判断是否在队列里 if request_user in GLOBAL_MQ: #判断有多少条消息 stored_msg_nums = GLOBAL_MQ[request_user].qsize() #如果没有新消息 if stored_msg_nums == 0: print "\033[41;1m没有消息等待,15秒.....\033[0m" msg_lists.append(GLOBAL_MQ[request_user].get()) ''' 如果队列里面有没有消息,get就会阻塞,等待有新消息之后会继续往下走,这里如果阻塞到这里了,等有新消息过来之后,把消息加入到 msg_lists中后,for循环还是不执行的因为,这个stored_msg_mums是在上面生成的变量下面for调用这个变量的时候他还是为0 等返回之后再取得时候,现在stored_msg_nums不是0了,就执行执行for循环了,然后发送数据 ''' #把消息循环加入到列表中并发送 print "\033[43;1等待已超时......15秒.....\033[0m" for i in range(stored_msg_nums): msg_lists.append(GLOBAL_MQ[request_user].get(timeout=15)) else: #创建一个新队列给这个用户 GLOBAL_MQ[str(request.user.userprofile.id)] = Queue.Queue() return HttpResponse(json.dumps(msg_lists))
但是为什么不等待不超时呢?反倒重复的进行连接呢?我服务端不是已经给他阻塞了吗?
这个上面的问题就涉及到Client段的JS的:
//循环接收消息 var RefreshNewMsgs = setInterval(function(){ //接收消息 GetNewMsgs(); },3000);
你每一次的的请求,都是一个新的线程,当这个循环结束后自动释放但是,链接发到服务端就被阻塞了,过了一会setInterval又有一个新的连接向服务端,所以服务端每次阻塞的都是一个新的线程,就没有实现咱们想要的效果!
setInterval每一次都新起一个线程!!!
那怎么解决这个问题呢?自己调自己实现一个递归!
看代码:
//接收消息 function GetNewMsgs(){ $.get("{% url 'get_new_msg' %}",function(callback){ console.log("----->new msg:",callback); var msg_list = JSON.parse(callback); var current_open_session_id = $('#chat_hander h2').attr("contact_id");//获取当前打开的ID var current_open_session_type = $('#chat_hander h2').attr("contact_type");//获取当前打开的类型,是单独聊天还是群组聊天 $.each(msg_list, function (index,msg_item) { //接收到的消息的to,是我自己 from是谁发过来的,如果是当前打开的ID和from相同说明,我现在正在和他聊天直接显示即可 if(msg_item.from == current_open_session_id){ AddRecvMsgToChatBox(msg_item) }//判断挡墙打开ID接收 }) })}//接收消息结束
GetNewMsgs是不是一个AJAX啊!他请求完之后会执行一个回调函数啊! 这个回调函数执行的时候是不是代表这个请求结束了?在请求结束执行这个回调函数的时候我在执行以下GetNewMsgs()不就行了,又发起一个请求?
//接收消息 function GetNewMsgs(){ $.get("{% url 'get_new_msg' %}",function(callback){ console.log("----->new msg:",callback); var msg_list = JSON.parse(callback); var current_open_session_id = $('#chat_hander h2').attr("contact_id");//获取当前打开的ID var current_open_session_type = $('#chat_hander h2').attr("contact_type");//获取当前打开的类型,是单独聊天还是群组聊天 $.each(msg_list, function (index,msg_item) { //接收到的消息的to,是我自己 from是谁发过来的,如果是当前打开的ID和from相同说明,我现在正在和他聊天直接显示即可 if(msg_item.from == current_open_session_id){ AddRecvMsgToChatBox(msg_item) }//判断挡墙打开ID接收 });//结束循环 console.log('run.....agin.....'); GetNewMsgs(); })}//接收消息结束
然后把他加载到页面加载完后自动执行中:
//循环接收消息 GetNewMsgs();
Views函数也需要重新写下:(因为队列里如果没有数据,设置为timeout的话就会抛异常,所以我们的抓异常~~)
代码如下:
def new_msg(request): if request.method == 'POST': print request.POST.get('data') #获取用户发过来的数据 data = json.loads(request.POST.get('data')) send_to = data['to'] #判断队列里是否有这个用户名,如果没有新建一个队列 if send_to not in GLOBAL_MQ: GLOBAL_MQ[send_to] = Queue.Queue() data['timestamp'] = time.strftime("%Y-%m-%d %X", time.localtime()) GLOBAL_MQ[send_to].put(data) return HttpResponse(GLOBAL_MQ[send_to].qsize()) else: #因为队列里目前存的是字符串所以我们需要先给他转换为字符串 request_user = str(request.user.userprofile.id) msg_lists = [] #判断是否在队列里 if request_user in GLOBAL_MQ: #判断有多少条消息 stored_msg_nums = GLOBAL_MQ[request_user].qsize() try: #如果没有新消息 if stored_msg_nums == 0: print "\033[41;1m没有消息等待,15秒.....\033[0m" msg_lists.append(GLOBAL_MQ[request_user].get(timeout=15)) ''' 如果队列里面有没有消息,get就会阻塞,等待有新消息之后会继续往下走,这里如果阻塞到这里了,等有新消息过来之后,把消息加入到 msg_lists中后,for循环还是不执行的因为,这个stored_msg_mums是在上面生成的变量下面for调用这个变量的时候他还是为0 等返回之后再取得时候,现在stored_msg_nums不是0了,就执行执行for循环了,然后发送数据 ''' except Exception as e: print ('error:',e) print "\033[43;1等待已超时......15秒.....\033[0m" # 把消息循环加入到列表中并发送 for i in range(stored_msg_nums): msg_lists.append(GLOBAL_MQ[request_user].get()) else: #创建一个新队列给这个用户 GLOBAL_MQ[str(request.user.userprofile.id)] = Queue.Queue() return HttpResponse(json.dumps(msg_lists))
漂亮问题解决:
消息实时效果实现,NICE
这个在python中,如果这么递归,最多1000层,他的等前面的函数执行完后退出!看下面的结果这个CallMyself(n+1)递归下面的print是永远不执行的。
#!/usr/bin/env python #-*- coding:utf-8 -*- # Tim Luo LuoTianShuai def CallMyself(n): print('level:',n) CallMyself(n+1) print('\033[32;1m测试输出\033[0m') return 0 CallMyself(1)
但是在JS中它不是这样的,你会发现这个print还会执行,说面函数执行完了。
页面中的聊天框内容,切换聊天人后聊天信息的存储
有这么一种情况,现在我和ALEX聊天,我切换到和武Sir聊天了,但是窗口的内容还在怎么办?如下图:
怎么做呢?多层?如果200个人呢?
怎么做呢?
可以这样,我在和Alex聊天的时候,切换到武Sir之后,把和Alex老师聊天内容保存起来,当和武Sir结束聊天后,在返回来和Alex老师聊天的时候在把Alex老师内容展现,把和武Sir聊天内容存起来,其他亦如此!
//定义一个全局变量存储用户信息 GLOBAL_SESSION_CACHE = { 'single_contact':{}, 'group_contact':{}, }; //点击用户打开连天窗口 function OpenDialogBox(ele){ //获取与谁聊天 var contact_id = $(ele).attr("contact_id"); var contact_name = $(ele).attr("chat_to"); var contact_type = $(ele).attr("contact_type"); //先把当前聊天的内容存储起来 DumpSession(); //当前聊天内容存储结束 //修改聊天框与谁聊天 var chat_to_info = "<h2 style='color:whitesmoke;text-align:center;' contact_type='"+ contact_type +"' contact_id='"+ contact_id+ "'>" + contact_name + "</h2>"; $('#chat_hander').html(chat_to_info); $('.chat_contener').html(LoadSession(contact_id,contact_type)); //清除未读消息显示 var unread_msg_num_ele = $(ele).find('span')[0]; $(unread_msg_num_ele).text(0); $(unread_msg_num_ele).addClass('hide') }//打开聊天窗口结束 //存储未打开的聊天内容 function DumpSession2(contact_id,contact_type,content) { if(contact_id){ GLOBAL_SESSION_CACHE[contact_type][contact_id] = content; } } //加载新的聊天窗口,把要打开的聊天内容重新加载上 function LoadSession(current_contact_id,current_contact_type) { //通过hasOwnProperty判断key是否存在 if(GLOBAL_SESSION_CACHE[current_contact_type].hasOwnProperty(current_contact_id)){ var session_html = GLOBAL_SESSION_CACHE[current_contact_type][current_contact_id]; }else{ var session_html = ''; } //把内容返回 return session_html $('.chat_contener').html(session_html); }; //加载新窗口结束
更多参考:http://www.cnblogs.com/alex3714/articles/5311625.html