Home
  Software
    HowTo
      GTK+
        FIFO Chat

Idioma:
  English

En esta página
  GUI
  Código liente
  Código servidor
  Gracias

Temas
  GIOChannel
  FIFO chat
  Socket chat

GTK+ - Chat entre Python y C: GIOChannel y FIFO

Un buen ejercicio parece ser la fabricación de un programa 'chat' - por mas sencillo que sea - para conocer las intricacias de manejar los canales de comunicación. Este ejemplo muestra un ejemplo de como realizar una aplicación 'chat' entre dos ventanas. Para hacer el ejemplo mas (y para aprender mas), hice el cliente en Python, y el servidor en C (en realidad C++, pero sin el uso de clases).

El problema principal fue el requerimiento que la atención a la recepción de datos tenía que implementarse asincronicamente - es decir, sin un 'lazo de espera', ya que la aplicación final de este código es un programa de control.

Hay una versión disponible que utiliza sockets en lugar de FIFOs.

La interfaz de usuario (común para Python/C)

El diagrama a la izquerda muestra los elementos de gtk+ utilizados para construir la interfaz de usuario:
  • GtkTextView: Para mostrar la conversación
  • GtkEntry: Para prepara la transmisión. Lo tipeado se transmitirá al apretar 'Enter'
  • GtkLabel: Muestra mensajes del sistema
  • GtkButton: Botón de salida del programa

El rectángulo verde es el contenedor (GtkVBox) para toda la pantalla, mienteas que el GtkLabel y el GtkButton están ubicados en un GtkHBox (rectángulo azul).

Código Cliente en Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  client.py
#  
#  Copyright 2014 John Coppens <john@jcoppens.com>
#  
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#  
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#  
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#  
#  

import gtk
import pygtk
import sys
import gobject, glib
import os, os.path

FIFO_ROOT = "./fifo"

class Client():
    """ The Client class takes care of our communications by FIFO.
    """
    def __init__(self, fifo_root):
        """ Create the FIFO
        """
        self.fifo_root = fifo_root

        # First check if the client socket is left over from a previous session
        if os.path.exists(self.fifo_root + '.client'):
            os.remove(self.fifo_root + '.client')
        # Then create a new one
        os.mkfifo(self.fifo_root + '.client')

        # Then check if the server has created a fifo
        if os.path.exists(self.fifo_root + '.server'):
            self.out_fd = os.open(self.fifo_root + '.server',
                                  os.O_WRONLY | os.O_APPEND)
            self.out_f = os.fdopen(self.out_fd, "w")
            print "opening client for read"
            self.in_fd = os.open(self.fifo_root + '.client',
                                 os.O_RDONLY | os.O_NONBLOCK, 0o644)
            self.in_f = os.fdopen(self.in_fd)
            print "client open"
        else:
            print "Server is not running!"
            sys.exit(1)

        self.show_status_cb = None
        self.show_data_cb = None

        glib.io_add_watch(self.in_fd, glib.IO_IN, self.process_input)

    def process_input(self, port, cond):
        """ This function is called asynchronously when data is received.
        """
        msg = self.in_f.readline()                # Get the message from the FIFO
        self.show_data(msg)                        # And send it to MainWindow
        return True                                # We want to stay alive

    def send(self, msg):
        self.out_f.write(msg)
        self.out_f.flush()

    def show_status(self, msg):
        """ Show status changes from this class, either on the terminal,
            or else by calling the callback installed by set_status_show().
        """
        if self.show_status_cb == None:
            print "Status: ", msg
        else:
            self.show_status_cb(msg)

    def show_data(self, msg):
        """ Show received messages, either on the terminal,
            or else by calling the callback installed by set_data_show().
        """
        if self.show_data_cb == None:
            print "Data: ", msg
        else:
            self.show_data_cb(msg)

    def set_status_show(self, show_status_cb):
        self.show_status_cb = show_status_cb

    def set_data_show(self, show_data_cb):
        self.show_data_cb = show_data_cb


class MainWindow(gtk.Window):
    def __init__(self):
        gtk.Window.__init__(self);
        self.connect("delete-event", self.destroy);
        self.set_size_request(400, 300)

        # Make the upper window, which the received text.
        txtview = gtk.TextView()
        self.txtbff = gtk.TextBuffer()
        self.txttagtable = self.txtbff.get_tag_table()
        txtview.set_buffer(self.txtbff)
        txtview.set_can_focus(False)

        texttag_red = gtk.TextTag("red")
        texttag_red.set_property("foreground", "red")
        self.txttagtable.add(texttag_red)

        texttag_black = gtk.TextTag("black")
        texttag_black.set_property("foreground", "black")
        self.txttagtable.add(texttag_black)

        # This is where the text to be transmitted is typed.
        # Pressing the 'Enter' key transmits the string.
        entry = gtk.Entry()
        entry.connect("activate", self.entry_activated);

        # And the button to exit the chat program
        button = gtk.Button("Quit")
        button.set_border_width(4)
        button.set_can_focus(False)
        button.connect("clicked", self.quit_clicked)

        # Left of the button is a label, which can be used to
        # show status messages.
        hbox = gtk.HBox()
        self.statlbl = gtk.Label("")
        hbox.pack_start(self.statlbl, expand = True, fill = True)
        hbox.pack_start(button, expand = False)

        # Pack everything into a VBox, then that into the window.
        vbox = gtk.VBox()
        vbox.set_spacing(4)
        vbox.pack_start(txtview)
        vbox.pack_start(entry, expand = False)
        vbox.pack_start(hbox, expand = False)
        self.add(vbox)

        entry.grab_focus()
        self.show_all()

    def destroy(self, window, data):
        """ Called when the MainWindow is closed - this stops the
            GTK+ main loop, and terminates the program
        """
        gtk.main_quit()

    def entry_activated(self, widget):
        msg = widget.get_text() + "\n"
        self.client.send(msg)
        self.show_data(msg, color = 'black')
        widget.set_text("")

    def show_status(self, msg):
        self.statlbl.set_text(msg)    

    def show_data(self, msg, color = 'red'):
        self.txtbff.insert_with_tags_by_name(self.txtbff.get_end_iter(), msg, color)    

    def set_client(self, client):
        """ The client is the communication system which wants to show
            things here, and get strings to transmit from the GtkEntry
        """
        self.client = client
        # We connect callbacks to get news from the client
        client.set_status_show(self.show_status)
        client.set_data_show(self.show_data)

    def quit_clicked(self, widget):
        gtk.main_quit()

    def run(self):
        gtk.main()


def main():
    main = MainWindow()               # Prepare the main window,
    client = Client(FIFO_ROOT)        # and the communications package
    main.set_client(client)           # Announce the client to the main window

    main.run()

    return 0

if __name__ == '__main__':
    main()
Código: gtk_fifo_chat_client.py

Código Servidor en C(++)

El servidor consta de tres partes: El programa principal: main.cc, y los headers y el código de la parte de comunicaciones: fifocom.h, y fifocom.cc. El paquete completo para instalar: fifo_test-0.1.tar.gz

/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*-  */
/*
 * main.cc
 * Copyright (C) 2014 John Coppens <john@jcoppens.com>
 * 
 * socket_test is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * socket_test is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <gtk/gtk.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

#include "config.h"
#include "fifocom.h"


#ifdef ENABLE_NLS
#  include <libintl.h>
#endif

// For testing propose use the local (not installed) ui file 
// #define UI_FILE PACKAGE_DATA_DIR"/ui/socket_test.ui" 
#define UI_FILE "src/socket_test.ui"

#define FIFO_ROOT "fifo"

GtkWidget       *MainWindow;
GtkTextBuffer   *txtbuffer;

// Note: For some strange reason the 'extern "C"' is necessary to define
// handlers for the gtk_builder_connect_signals() call - and only if
// g++ is used!

extern "C" gboolean
quit_button_clicked_cb(GtkButton *btn)
{
  gtk_main_quit();
}

extern "C" void
on_main_window_delete_event(GtkWidget *w)
{
  gtk_main_quit();
}

extern "C" void
tx_entry_activate(GtkEntry *entry)
{
  write_chars(channel, (char *) gtk_entry_get_text(entry));
  write_chars(channel, (char *) "\n");
  gtk_entry_set_text(entry, "");
}


void
add_to_textbuffer(char *message)
{
  GtkTextIter iter;
  
  gtk_text_buffer_get_end_iter(txtbuffer, &iter);
  gtk_text_buffer_insert(txtbuffer, &iter, message, strlen(message));
}

int
main (int argc, char *argv[])
{
  GtkWidget *txtview;
  GError *error = NULL;
  int listenfd;
	
  gtk_init(&argc, &argv);

  GtkBuilder *builder = gtk_builder_new();

  gtk_builder_add_from_file(builder, UI_FILE, &error);
  gtk_builder_connect_signals(builder, NULL);
	
  // Get references to some of the widgets we use frequently
  MainWindow = GTK_WIDGET(gtk_builder_get_object(builder, "main_window"));
  txtview    = GTK_WIDGET(gtk_builder_get_object(builder, "textview1"));
  MainWindow = GTK_WIDGET(gtk_builder_get_object(builder, "tx_entry"));
  
  // 'Manually' create a textbuffer and connect it to the textview
  txtbuffer = gtk_text_buffer_new(NULL);
  gtk_text_view_set_buffer(GTK_TEXT_VIEW(txtview), txtbuffer);
  
  g_object_unref(G_OBJECT(builder));		// Don't need the builder anymore

  gtk_widget_show_all(MainWindow);		// Queue drawing all widgets
	
  start_comm(FIFO_ROOT, add_to_textbuffer);
	
  gtk_main();
	
  close_comm();

  return 0;
}

/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*-  */
/*
 * socketcom.h
 * Copyright (C) 2014 Unknown <root@on6jc>
 *
 * socket_test is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * socket_test is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#ifndef _SOCKETCOM_H_
#define _SOCKETCOM_H_

typedef void (*read_callback)(char *);

#include <gtk/gtk.h>

extern  GIOChannel *channel;

gboolean	read_watch(GIOChannel *source, GIOCondition cond, gpointer data);
gboolean	write_chars(GIOChannel *dest, char *message);
void		start_comm(const char *fifo_root, read_callback callback);
void		close_comm();

#endif // _SOCKETCOM_H_

/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*-  */
/*
 * socketcom.cc
 * Copyright (C) 2014 Unknown <root@on6jc>
 *
 * socket_test is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * socket_test is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <glib.h>

#include "fifocom.h"

GIOChannel *channel;
char *client_fn, *server_fn;

gboolean
read_watch(GIOChannel *source, GIOCondition cond, gpointer data)
{
  char bff[100];
  GError  *error = NULL;
  gsize   bytes_read;
  GIOStatus ret;
  
  printf("read_watch called\n");
  ret = g_io_channel_read_chars(source, bff, sizeof(bff), &bytes_read, &error);
  
  if (ret == G_IO_STATUS_ERROR)
    g_error("Error reading: %s\n", error->message);
  else {
    bff[bytes_read] = 0;
    ((read_callback) data)(bff);
    printf("Got: %s\n", bff);
  }
  return TRUE;
}

gboolean
write_chars(GIOChannel *dest, char *message)
{
  int out_fd;
    
  printf("Trying to write %s\n", message);
  
  out_fd = open(client_fn, O_WRONLY);
  write(out_fd, message, strlen(message));
  close(out_fd);
  
  return TRUE;
}

void
start_comm(const char *fifo_root, read_callback callback)
{ 
  int in_fd, ch_id;
  
  server_fn = g_strdup_printf("%s.server", fifo_root);
  client_fn = g_strdup_printf("%s.client", fifo_root);
  
  if (g_file_test(server_fn, G_FILE_TEST_EXISTS))
    unlink(server_fn);				// g_remove wasn't declared???
    
  // Then create a new one
  mkfifo(server_fn, 0666);
  
  // Open the file for reading
  in_fd = open(server_fn, O_RDONLY | O_NONBLOCK);
  
  channel = g_io_channel_unix_new(in_fd);
  
  ch_id = g_io_add_watch(channel, G_IO_IN, (GIOFunc) read_watch,
                (gpointer) callback);
  printf("IOchannel ID: %d\n", ch_id);
}


void
close_comm()
{
  GError *error = NULL;
  
  g_io_channel_shutdown(channel, TRUE, &error);
}

Gracias

Gracias para todos quienes comparten su código por la internet - algunas partecitas fueron utilizadas en estos ejemplos. Y a la gente del canal IRC de gtk+.

Y a aquellos en Pygments por la rutina de colorización de código.

Anjuta fue utilizado para escribir y comprobar al código C, y Geany para la edición del código Python.

1793


(c) John Coppens ON6JC/LW3HAZ correo