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 clickedvar 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 protectionreturn (/^(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");//獲取當前打開的IDvar 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");//獲取當前打開的IDvar 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 LuoTianShuaidef 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