📜 ⬆️ ⬇️

Writing Comet-chat

I want to share my experience creating a simple Comet chat. Periodically read about this technology, and now decided to try to do something myself. It turned out a small chat, the interface of which I tried to make similar to the irc-client interface mIRC. Since I am writing a similar thing for the first time, please comment on possible errors in the program and article and describe more optimal ways to solve problems. You can see the working chat here: http://94.127.68.84:6884/

How I introduced it


A distinctive feature of comet-applications is to be in a state of constant polling of the server, which does not hasten to respond to requests from the client and keeps the connection. This approach is called long-polling and makes server push possible - sending data from the server to the client at the exact moment when an event occurred on the server (a new participant entered the chat, a message was sent).

Thus, in the chat you will have to use at least 2 client connections with the server, one of which is responsible only for receiving data, constantly polls the server, and is reset if it receives data, timeout or a break, and the second only for transferring data to the server. The data will be transmitted in JSON format and will be an array of hashes - the actions to be performed by the client or server (for example, display the received message or process the authorization request).

How I implemented it


Nginx configuration

Between the chat server and the client is the nginx web server. The chat client can, of course, communicate directly with the server part, but I decided to insert nginx for several reasons:

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;
}
}

')
Server part

Event-oriented architecture is perfect for creating such things, and it was decided to use it. The source code of the server part, well-seasoned with comments, is attached. About event loops and AnyEvent can be read in my previous topic .
#!/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;

I apologize for the lack of code highlighting - Habr does not want to add a bunch of <font> tags to the post, only comments are highlighted.

Client part

The client part is quite similar to the server part - there is the same set of action handlers for which requests come from the server (add a message, set a list of participants). All requests to the server are sent using the jQuery $ .ajax function. I will not post all the code in the article, you can see it here .

What happened


It was a simple, but quite usable chat. I see only 2 drawbacks in it:

Source: https://habr.com/ru/post/76144/


All Articles