彗星チャットを書く

簡単なCometチャットを作成した経験を共有したいと思います。 私は時々このテクノロジーについて読み、今では自分で何かをしようとすることにしました。 結果は小さなチャットで、そのインターフェイスはmIRC ircクライアントインターフェイスに似たものを作成しようとしました。 私はこの種のことを初めて書いているので、プログラムと記事で考えられるエラーについてコメントし、問題を解決するためのより最適な方法を説明してください。 ここで作業チャットを見ることができます: http : //94.127.68.84 : 6884/



導入方法



コメットアプリケーションの顕著な特徴は、クライアントからのリクエストに応答することを急ぐことなく接続を維持するサーバーのポーリングが常に行われている状態にあることです。 このアプローチはロングポーリングと呼ばれ、 サーバーのプッシュを可能にします-サーバーでイベントが発生した瞬間にサーバーからクライアントにデータを送信します(新しいメンバーがチャットを入力し、メッセージが送信されました)。



したがって、チャットでは、少なくとも2つのクライアントサーバー接続を使用する必要があります。1つはデータの受信のみを担当し、常にサーバーをポーリングし、データ、タイムアウトまたはブレークを受信するとリセットされ、2つ目はサーバーへのデータ送信専用です。 データはJSON形式で送信され、ハッシュの配列になります-クライアントまたはサーバーが実行する必要のあるアクション(たとえば、メッセージの表示や承認要求の処理)。



実装方法



Nginx設定


チャットサーバーとクライアントの間は、nginx Webサーバーです。 もちろん、チャットクライアントはサーバー側と直接通信できますが、いくつかの理由からnginxを挿入することにしました。



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つしかありません。




All Articles