h2{ background-color: #00ccff; }
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 "