一、HTTP协议特色html
首先这里要知道HTTP协议的特色:短连接、无状态!前端
在不考虑本地缓存的状况举例来讲:我们在链接博客园的时候,当tcp链接后,我会把我本身的http头发给博客园服务器,服务器端就会看到我请求的URL,server端就会根据URL分发到相应的视图处理(Django的views里)。最后给我返回这个页面,当返回以后链接就断开了。python
短链接:git
服务器为何要断开?不少状况下我还会打开页面,我请求一次链接断开了为何会这样?为何不创建长期的链接?这个是HTTP设计的考虑,在大并发的状况下,若是链接不断开,我不知道你何时点,你可能马上就点有可能10分钟1个小时或者其余时间点,那么你就会占着这个链接(是很浪费的,而且链接是有限的),因此当返回后server端就会断开这个链接。github
无状态:web
服务器不保存客户端的任何状态,每一次客户端链接服务器的时候都要把相关的信息发给客户端告诉客户端你是谁,服务端不会保存你是谁?ajax
那么问题来了,为何咱们在登陆京东以后登陆一次以后,服务器就不会让我们在登陆了,根据我们以前的博客的Session和Cookie。服务器端会在用户登陆的时候,在服务器端生成一个sessionID(有有效期)而且返回给客户。客户端会把这个seesionID存到Cookie里。这样登陆以后就不须要再输入密码!sql
二、WEBqq通讯实现django
首先看下面的图json
根据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")
首先用户的好友在哪一个表里?在用户表里那么他就的关联本身了而且是多对多的关系,你能够有多个朋友,你朋友也能够有多个朋友!
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表的时候也不能加双引号。
一、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>
二、使用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/'
三、事件链
//页面加载完成后 $(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中定义的函数,外面就没法调用了。
四、聊天内容自动扩展而且能够感受内容进行自动滑动
首先配置聊天的窗口样式:
.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({ 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个参数为回调函数和返回的值!!
在作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 ” ,其实也不是不少本身写的就能够了。
首先要知道以下几点: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))
先看下使用下面的方法是否可行:
#由于队列里目前存的是字符串因此咱们须要先给他转换为字符串 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