HTTPプロトコルの特徴
HTTPプロトコルは短時間接続と無状態性を特徴としています。
短時間接続: サーバーがリクエストに応答すると、その接続は閉じられます。これは大量の同時アクセスに対応するためで、長時間開いたままの接続は資源を浪費します。
無状態: サーバーはクライアントの状態を記憶しません。各リクエストには必要な情報が含まれます。
WebQQの通信実装
WebQQでは、ユーザー間のメッセージ送受信にはサーバーが必要です。通常、C1からC2へのメッセージはサーバーを通じて転送されます。
しかし、サーバーがC2に直接メッセージを送ることはできません。なぜなら、サーバーはリクエストを待つだけだからです。
そのため、ポーリングという手法が使われます。
ポーリング方式
- ショートポーリング: C2が定期的にサーバーにリクエストを送ります。
- ロングポーリング: C2がサーバーにリクエストを送ると、サーバーにデータがない場合でもレスポンスを返さずに待機し続けます。新しいデータが到着したらすぐに返します。
WebQQのデータベース設計
ユーザーの友達リストは多対多の関係を持ちます。
class UserProfile(models.Model):
user = models.OneToOneField(User)
name = models.CharField(max_length=32)
groups = models.ManyToManyField("UserGroup")
friends = models.ManyToManyField('self', related_name='my_friends')
さらに、チャットグループ用のモデルも作成します。
class QQGroup(models.Model):
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 = models.ManyToManyField(UserProfile, blank=True)
admins = models.ManyToManyField(UserProfile, blank=True, related_name='group_admins')
max_member_nums = models.IntegerField(default=200)
def __unicode__(self):
return self.name
URLの設定
プロジェクト全体のURL設定:
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^web/', include(web_urls)),
url(r'^chat/', include(chat_urls)),
url(r'', views.index, name='index'),
]
webアプリケーションのURL設定:
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アプリケーションのURL設定:
urlpatterns = [
url(r'^dashboard/$', views.dashboard, name='web_chat'),
]
Djangoの認証モジュールを使用してログイン状態を確認
@login_required
def dashboard(request):
return render(request, 'web_chat/dashboard.html')
settings.pyで未ログイン時のリダイレクト先を設定します。
LOGIN_URL = '/web/account/login/'
イベントチェーン
$(document).ready(function () {
$("body").delegate("textarea", "keydown", function (e) {
if (e.which == 13) {
var msg_text = $("textarea").val();
if ($.trim(msg_text).length > 0) {
AddSentMsgIntoBox(msg_text);
$("textarea").val('');
}
}
});
});
チャット内容の自動展開とスクロール
.chat_contener {
width: 100%;
height: 490px;
background-color: black;
opacity: 0.6;
overflow: auto;
}
function AddSentMsgIntoBox(msg_text) {
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);
$('.chat_contener').animate({
scrollTop: $('.chat_contener')[0].scrollHeight
}, 500);
}
AJAXリクエスト
$.ajax({
url: '/save_hostinfo/',
type: 'POST',
data: {data: JSON.stringify(change_info)},
success: function (arg) {
var callback_dict = $.parseJSON(arg);
if (callback_dict) {
setTimeout("hide()", 5000);
var change_infos = '修改了' + callback_dict['change_count'] + '条数据';
$('#handle_status').text(change_infos).removeClass('hide');
} else {
alert(callback_dict.error);
}
}
});
AJAX POSTデータの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);
});
}
WebQQのメッセージ保存方法
import Queue
GLOBAL_MQ = {}
def new_msg(request):
if request.method == 'POST':
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))
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))
ページ内のチャットボックスの内容切り替え
var 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) {
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;
}