簡単なCometチャットを作成した経験を共有したいと思います。 私は時々このテクノロジーについて読み、今では自分で何かをしようとすることにしました。 結果は小さなチャットで、そのインターフェイスはmIRC ircクライアントインターフェイスに似たものを作成しようとしました。 私はこの種のことを初めて書いているので、プログラムと記事で考えられるエラーについてコメントし、問題を解決するためのより最適な方法を説明してください。 ここで作業チャットを見ることができます:
http :
//94.127.68.84 :
6884/導入方法
コメットアプリケーションの顕著な特徴は、クライアントからのリクエストに応答することを急ぐことなく接続を維持するサーバーのポーリングが常に行われている状態にあることです。 このアプローチは
ロングポーリングと呼ばれ、
サーバーのプッシュを可能にします-サーバーでイベントが発生した瞬間にサーバーからクライアントにデータを送信します(新しいメンバーがチャットを入力し、メッセージが送信されました)。
したがって、チャットでは、少なくとも2つのクライアントサーバー接続を使用する必要があります。1つはデータの受信のみを担当し、常にサーバーをポーリングし、データ、タイムアウトまたはブレークを受信するとリセットされ、2つ目はサーバーへのデータ送信専用です。 データはJSON形式で送信され、ハッシュの配列になります-クライアントまたはサーバーが実行する必要のあるアクション(たとえば、メッセージの表示や承認要求の処理)。
実装方法
Nginx設定
チャットサーバーとクライアントの間は、nginx Webサーバーです。 もちろん、チャットクライアントはサーバー側と直接通信できますが、いくつかの理由からnginxを挿入することにしました。
- nginxは、静的な戻り値を受け取り、接続を維持し、一般にhttpプロトコルを介したすべての通信を維持します
- 余分なポートを照らす必要はありません
- 低血液洪水防御
limit_req_zone $binary_remote_addr zone=one:2m rate=1r/s;
server {
listen 6884;
location / {
root /home/vk/CometChat/htdocs/;
}
# /chat, FastCGI
location /chat {
#
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_intercept_errors on;
fastcgi_connect_timeout 3;
# FastCGI- 40
fastcgi_read_timeout 40;
fastcgi_pass unix:/home/vk/chat.socket;
# , 1
limit_req zone=one burst=5 nodelay;
}
}
サーバー側
イベント指向のアーキテクチャは、そのようなものを作成するのに最適であり、使用することが決定されました。 サーバーパーツのソースコードには、コメントが付けられていますが、添付されています。
前のトピックでイベントループとAnyEventについて読むことができます。
#!/usr/bin/perl
use strict;
use warnings;
use utf8;
#
use AnyEvent;
use AnyEvent::FCGI;
use JSON;
use Digest::MD5 qw/md5_hex/;
use URI::Escape;
# -
use constant LOGOUT => [{action => 'logout'}], 'Set-Cookie' => 'session=; path=/; expires=Thu, 01-Jan-70 00:00:01 GMT';
#
use constant NOTHING => [];
# ,
use constant TIMEOUT => 100;
# ,
use constant MAX_MESSAGES_COUNT => 20;
# ,
my %users;
#
my @messages;
# - ,
my %actions = (
requestLogin => sub {
#
my $params = shift;
if (($params->{nick} && $params->{session} && $users{$params->{nick}} && $users{$params->{nick}}->{session} eq $params->{session})) {
#
# , long-polling
return [{action => 'loginError', message => ' '}];
} elsif (!defined $params->{nickname} || !length $params->{nickname}) {
#
return [{action => 'loginError', message => ' '}];
} elsif (exists $users{$params->{nickname}}) {
return [{action => 'loginError', message => ' '}];
} elsif (length $params->{nickname} < 2 || length $params->{nickname} > 20) {
return [{action => 'loginError', message => ' 2 20 '}];
} elsif ($params->{nickname} !~ /^[\w\d\-]+$/) {
return [{action => 'loginError', message => ' '}];
} else {
# , -
my $session = md5_hex($params->{request}->param('REMOTE_ADDR') . time . rand);
foreach my $nick (keys %users) {
# ...
push_actions(
$nick,
# ... ( ) ...
{action => 'join', nick => $params->{nickname}},
# ...
{action => 'setUserList', users => [sort {$a cmp $b} ($params->{nickname}, keys %users)]},
);
}
# %users
$users{$params->{nickname}} = {
session => $session,
# long-polling
polling_request => undef,
# - , long-polling
queue => [],
};
#
update_timeout($params->{nickname});
# , long-polling
return (
[
#
{action => 'loginOk'},
#
{action => 'setUserList', users => [sort {$a cmp $b} keys %users]},
# MAX_MESSAGES_COUNT
{action => 'setMessageList', messages => [@messages]},
# long-polling ,
{action => 'startPolling'},
],
#
'Set-Cookie' => 'nick=' . uri_escape_utf8($params->{nickname}) . '; path=/',
'Set-Cookie' => 'session=' . $session . '; path=/',
);
}
},
restoreSession => sub {
# ,
# ( )
my $params = shift;
#
return LOGOUT unless ($params->{nick} && $params->{session} && $users{$params->{nick}} && $users{$params->{nick}}->{session} eq $params->{session});
#
update_timeout($params->{nick});
return [
{action => 'setUserList', users => [sort {$a cmp $b} keys %users]},
{action => 'setMessageList', messages => [@messages]},
{action => 'startPolling'},
];
},
sendMessage => sub {
#
my $params = shift;
return LOGOUT unless ($params->{nick} && $params->{session} && $users{$params->{nick}} && $users{$params->{nick}}->{session} eq $params->{session});
#
if (defined $params->{text} && length $params->{text} > 0 && length $params->{text} <= 300) {
if ($params->{text} =~ /^\/quit\s*$/) {
# /quit
# long-polling
if ($users{$params->{nick}}->{polling_request} && $users{$params->{nick}}->{polling_request}->is_active) {
#
respond($users{$params->{nick}}->{polling_request}, LOGOUT);
}
#
delete $users{$params->{nick}};
#
foreach my $nick (keys %users) {
push_actions(
$nick,
{action => 'leave', nick => $params->{nick}},
{action => 'setUserList', users => [sort {$a cmp $b} keys %users]},
);
}
return LOGOUT;
} elsif ($params->{text} =~ /^\/me\s+(.+)$/) {
# /me
my $action = {
action => 'me',
nick => $params->{nick},
text => $1,
};
#
store_message($action);
#
foreach my $nick (keys %users) {
push_actions($nick, $action);
}
} else {
# , /me
my $action = {
action => 'message',
nick => $params->{nick},
text => $params->{text},
};
store_message($action);
foreach my $nick (keys %users) {
push_actions($nick, $action);
}
}
}
#
return NOTHING;
},
poll => sub {
# long-polling
my $params = shift;
return LOGOUT unless ($params->{nick} && $params->{session} && $users{$params->{nick}} && $users{$params->{nick}}->{session} eq $params->{session});
# long-polling ...
if ($users{$params->{nick}}->{polling_request} && $users{$params->{nick}}->{polling_request}->is_active) {
# ... ,
respond($users{$params->{nick}}->{polling_request}, [
{action => 'logout'},
{action => 'loginError', message => ' '},
]);
}
#
$users{$params->{nick}}->{polling_request} = $params->{request};
#
push_actions($params->{nick}) if scalar @{$users{$params->{nick}}->{queue}};
#
update_timeout($params->{nick});
# !
return undef;
},
#
default => sub {return LOGOUT}
);
sub process_request {
# http-
my ($request) = @_;
# - CGI.pm
my %params;
foreach (
split(/;\s*/, $request->param('HTTP_COOKIE') || ''),
split('&', $request->param('QUERY_STRING') || ''),
) {
next unless $_;
my ($key, $value) = split '=';
if (defined $key && defined $value) {
$value = uri_unescape($value);
$value =~ tr/+/ /;
utf8::decode($value) unless utf8::is_utf8($value);
$params{$key} = $value;
}
}
$params{request} = $request;
# , default,
my ($response, @headers) = $actions{$params{action} && $actions{$params{action}} ? $params{action} : 'default'}->(\%params);
# ,
respond($request, $response, @headers) if $response;
}
sub respond {
# , JSON
my ($request, $response, @headers) = @_;
my $output = "Content-Type: text/plain; charset=utf-8\n";
while (scalar @headers) {
$output .= shift(@headers) . ': ' . shift(@headers) . "\n";
}
$output .= "\n" . to_json($response);
utf8::encode($output) if utf8::is_utf8($output);
$request->print_stdout($output);
$request->finish;
}
sub push_actions {
#
# long-polling ,
my ($nick, @actions) = @_;
push @{$users{$nick}->{queue}}, @actions;
if ($users{$nick}->{polling_request} && $users{$nick}->{polling_request}->is_active) {
respond($users{$nick}->{polling_request}, $users{$nick}->{queue});
$users{$nick}->{queue} = [];
}
}
sub store_message {
#
my ($action) = @_;
push @messages, $action;
shift @messages if scalar @messages > MAX_MESSAGES_COUNT;
}
sub update_timeout {
my ($nick) = @_;
# . TIMEOUT ...
$users{$nick}->{timeout} = AnyEvent->timer(
after => TIMEOUT,
interval => 0,
cb => sub {
# ...
delete $users{$nick};
foreach my $user (keys %users) {
push_actions(
$user,
{action => 'leave', nick => $nick},
{action => 'setUserList', users => [sort {$a cmp $b} keys %users]},
);
}
},
);
}
# - FastCGI-
umask(0);
my $fcgi = new AnyEvent::FCGI(on_request => \&process_request, unix => '/home/vk/chat.socket');
AnyEvent->loop;
コードの強調表示が行われていないことをおizeびします。Habrは投稿に<font>タグの束を追加したくありません。コメントのみが強調表示されます。
クライアント部
クライアント部分はサーバー部分に非常に似ています-サーバーからリクエストが送信されるアクションハンドラーと同じセットがあります(メッセージの追加、参加者のリストの確立)。 すべてのサーバー要求は、jQuery $ .ajax関数を使用して送信されます。 記事のすべてのコードをアップロードするわけではありません。
こちらで確認でき
ます 。
どうした
その結果、シンプルだが非常に使いやすいチャットができました。 欠点は2つしかありません。
- クライアントへのメッセージの配信に関する通知の不足-40秒のタイムアウトとサーバーの切断により、メッセージを失う可能性は非常に少ないですが、そうではありません。
- 発信要求の同期の欠如。 2番目のメッセージは、最初のメッセージよりも早くサーバーに到着することがわかります。