Pull to refresh

Простой чат с помощью Channel API на Google App Engine для Python

Reading time 10 min
Views 5.6K
Представляю вашему вниманию вольный перевод статьи под названием "A Simple Chat using the Channel API". Так же я решил немного добавить своего кода.

Сегодня мы представляем вам новую статью для Google App Engine посвященную Сhannel API, которое появилось в декабре 2010 года в релизе 1.4. С этого момента стала возможной отправка сообщений напрямую с сервера клиенту и обратно без использования polling.
Поэтому стало достаточно просто реализовать чат на Google App Engine. Процесс реализации описан под катом.

Вы можете посмотреть демо по адресу http://chat-channelapi.appspot.com/.
Код проекта можно скачать здесь(код из ориганальной статьи лежит здесь).

В нашем приложении, чтобы создать канал между пользователем и программой надо сделать следующие шаги, приведенные ниже на картинке.
image
Шаги:
1) Приложение создает id канала(channel id) и токен и отправляет их клиенту
2) Клиент использует токен, чтобы открыть сокет, который будет слушать канал
3) Клиент 2 отправляет сообщение для чата в приложение, вместе с его уникальным id канала
4) Приложение отправляет сообщение всем клиентам, которые слушают канал через сокет. Для этого используется id канала каждого клиента.

Прежде чем удут описаны все шаги, следует отметить, что мы упростили максимально сущности(entity) базы данных, которые будут принимать участие в программе. Мы создали две — модель User и Message.
class OnlineUser(db.Model):
    nick=db.StringProperty(default="")
    channel_id=db.StringProperty(default="")

class Message(db.Model):
    text=db.StringProperty(default="")
    user=db.ReferenceProperty(User)

В коде также используется механизм сессий, реализованный в библиотеке GAE utilities. Но не обращайте много внимания на сессии.

Шаг 1.

В этом шаге наше приложение для чата создает id канала и токен и отправляет их клиенту. Код этого шага достаточно прост. Только не забудьте импортировать Channel API:
from google.appengine.api import channel

После этого создайте обработчик(handler), который генерирует уникальный id каждому пользователю(мы воспользуемся функцией uuid4() из модуля uuid). Ниже приведенный обработчик как раз это делает и передает данные в шаблон клиенту:
class ChatHandler(webapp.RequestHandler):
    def get(self):
        self.redirect('/')
        
    def post(self):
        # сессия из библиотеки http://gaeutilities.appspot.com/
        self.session = Session()
        # получаем ник
        nick = self.request.get('nick')
        if not nick:
            self.redirect('/')
        # проверяем, не существует ли пользователя с таким ником
        user = OnlineUser.all().filter('nick =', nick).get()
        if user:
            self.session['error']='That nickname is taken'
            self.redirect('/')
            return
        else:
            self.session['error']=''
        # генерируем уникальный id канала для Channel API
        channel_id=str(uuid.uuid4())
        chat_token = channel.create_channel(channel_id)
        # сохраняем пользователя 
        user = OnlineUser(nick=nick,channel_id=channel_id)
        user.put()
        # получаем последние 100 сообщений
        messages=Message.all().order('date').fetch(1000)
        # генерируем шаблон и отправляем его в качестве ответа клиенту
        template_vars={'nick':nick,'messages':messages,'channel_id':channel_id,'chat_token':chat_token}
        temp = os.path.join(os.path.dirname(__file__),'templates/chat.html')
        outstr = template.render(temp, template_vars)
        self.response.out.write(outstr)

Чтобы не перегружать данную статью, я не привожу здесь код шаблона. Его вы можете посмотреть в github.

Шаг 2

Теперь клиент отвечает за извлечение токена и открытие сокета. Мы используем jQuery для уменьшения кода javascript. Ниже приведен наш код:
var chat_token = $('#channel_api_params').attr('chat_token');
var channel = new goog.appengine.Channel(chat_token);
var socket = channel.open();
socket.onopen = function(){
};
socket.onmessage = function(m){
  var data = $.parseJSON(m.data);
  $('#center').append(data['html']);
  $('#center').animate({scrollTop: $("#center").attr("scrollHeight")}, 500);
};
  socket.onerror =  function(err){
  alert("Error => "+err.description);
};
  socket.onclose =  function(){
  alert("channel closed");
};

Шаг 3

На этом шаге Клиент 2 отправляет сообщение в наш чат через интерфейс. Для этого надо только сделать текстовое поле и кнопку отправки сообщения. Сообщение будет отправляться javascript кодом с помощью простого слушателя(listener), которого мы реализуем с помощью jQuery. Вы можете использовать вместо этого любую javsctipt библиотеку или просто с помощью объекта XMLHttpRequest. Только учтите, что обязательно посылать уникальный id канала клиента, чтобы верно идентифицировать клиента в приложении.
$('#send').click(function(){
  var text = $('#text').val();
  var nick = $('#nick').attr('value');
  var channel_id = $('#channel_api_params').attr('channel_id');
  $.ajax({
    url: '/newMessage/',
    type: 'POST',
    data:{
      text:text,
      nick:nick,
      channel_id:channel_id,
    },
    success: function(data){
    },
    complete:function(){ 
        }      
  });
});

Шаг 4

Чтобы получать сообщения от клиентов, нам надо реализовать новый обработчик, который так же будет отправлять сообщение всем клиентам.
class NewMessageHandler(webapp.RequestHandler):    
    def post(self):
        # получаем параметры       
        text = self.request.get('text')
        channel_id = self.request.get('channel_id')        
        q = db.GqlQuery("SELECT * FROM OnlineUser WHERE channel_id = :1", channel_id)
        nick = q.fetch(1)[0].nick    
        date = datetime.datetime.now()
        # сохраняем сообщение
        message=Message(user=nick,text=strip_tags(text), date = date, date_string = date.strftime("%H:%M:%S"))
        message.put()
        # генерируем шаблон сообщения
        messages=[message]
        template_vars={'messages':messages}
        temp = os.path.join(os.path.dirname(__file__),'templates/messages.html')
        outstr = template.render(temp, template_vars)
        channel_msg = json.dumps({'success':True,"html":outstr})
        # отправляем всем клиентам сообщение
        users = OnlineUser.all().fetch(100)        
        for user in users:                        
            channel.send_message(user.channel_id, channel_msg)

Дополлнительный шаг

На данном этапе заканчивается исходная статья, но мне захотелось внести несколько изменений в код, связанные со следующей задачей. В исходной статье имена пользователей блокируются после входа в чат и уже под этим ником нельзя войти. Уберем данное ограничение. Для этого надо удалять после завершения действия ключа данные пользователя из базы данных. Ключ перестает действовать либо пока не пройдет два часа, либо пока клиент не вызовет функцию close() у сокета. После чего вызывается обработчик, зарегистрированный по адресу /_ah/channel/disconnected/. Напишем такой обработчик.
class ChannelDisconnectHandler(webapp.RequestHandler):
    def post(self):
        channel_id = self.request.get('from')
        q = OnlineUser.all().filter('channel_id =', channel_id)
        users = q.fetch(1000)  
        db.delete(users)

В javascript код добавим обработку события, которое возникает при уходе пользователя с данной страницы:
$(window).unload(function (){
    socket.close();        
});

Осталось обработать следующую ситуацию. Если пользователь вошел в чат, но достаточно быстро закрыл окно, то не происходит открытие канала. Это приводит к ситуации, что запись о пользователе есть в базе, но она не удаляется из-за того, что не закрывается канал. Изменим нашу модель данных пользователя:
class OnlineUser(db.Model):
  nick=db.StringProperty(default="")
  channel_id=db.StringProperty(default="")
  creation_date=db.DateTimeProperty(auto_now_add=True)
  opened_socket=db.BooleanProperty(default=False)

Теперь у нас есть время создания записи(creation_date) и информация о том, пришло ли подтверждение со стороны клиента об открытии канала(opened_socket). При открытии канала со стороны клиента с помощью вызова channel.open() на стороне сервера вызывается обработчик, зарегистрированный по адресу /_ah/channel/connected/. Этот обработчик будет выставлять у пользователя подтверждение открытия канала:
class ChannelConnectHandler(webapp.RequestHandler):
    def post(self):
        channel_id = self.request.get('from')
        q = OnlineUser.all().filter('channel_id =', channel_id)
        user = q.fetch(1)[0]
        user.opened_socket = True
        user.put()

Данный код отправляет серверу id канала для идентификации. Обработчик представлен ниже:
class RegisterOpenSocketHandler(webapp.RequestHandler):    
    def post(self):
        channel_id = self.request.get('channel_id')    
        q = OnlineUser.all().filter('channel_id =', channel_id)
        user = q.fetch(1)[0]
        user.opened_socket = True
        user.put() 

Последним этапом будет запуск c помощью сron-а обработчика, который будет выделять все записи из модели пользователей OnlineUser, у которых yе будет подтверждения открытия канала и время создание от текущего будет больше 120 секунд:
class ClearDBHandler(webapp.RequestHandler):    
    def get(self):
        q = OnlineUser.all().filter('opened_socket ='False)
        users = q.fetch(1000)
        for user in users:
            if ((datetime.datetime.now() - user.creation_date).seconds > 120):
                db.delete(user)

В итоге

Нам удалось построить простое приложение для чата. Четырех шагов из исходной статьи достаточно, чтобы показать Channel API, добавление в код было сделано чтобы приложение выглядело, на мой взгляд, более законченным.

P.S.Реальная работа приложения показала, что надо фильтровать сообщения, исключая из них html тэги. Для этого импортируем функцию strip_tags из фреймворка django:
from django.utils.html import strip_tags

В обработчике новых сообщений(NewMessageHandler) заменим код создания нового сообщения на следующий:
message=Message(user=nick,text=strip_tags(text), date = date, date_string = date.strftime("%H:%M:%S"))
Tags:
Hubs:
+21
Comments 13
Comments Comments 13

Articles