Home
  Software
    HowTo
      GTK+
        Socket Chat

Idioma:
  English

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

Temas
  GIOChannel
  Gio.IOChannel
  FIFO chat
  Socket chat

GTK+ - Chat entre Python y C: GIOChannel y Internet Sockets

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 FIFOs en lugar de sockets.

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).

Client code in 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 socket as skt
import sys
import gobject, glib

HOST = "localhost"
PORT = 5000

class Client():
    """ The Client class takes care of our communications by socket.
    """
    def __init__(self, host, port):
        """ Create the socket, but do NOT connect yet. That way we
            can still change some parameters of the 
        """
        self.host = host
        self.port = port
        self.socket = skt.socket(skt.AF_INET, skt.SOCK_STREAM)
                                                   # Construir el zocalo
        self.show_status_cb = None
        self.show_data_cb = None
        self.data = '\n'

    def connect(self):
        """ Now really try to connect to the server.
            I really need to add an exception handler in case the server
            is not on-line.
        """
        self.socket.connect((HOST, PORT))
        self.socket.setblocking(False)
        self.fd = self.socket.fileno()             # Obtener el file descriptor
        self.show_status("Connected to " + self.host)
        glib.io_add_watch(self.fd, glib.IO_IN, self.process_input)
                                                   # Instalar la funcion en caso
                                                   # de datos recibidos

    def process_input(self, skt, cond):
        """ This function is called asynchronously when data is received.
        """
        msg = self.socket.recv(100)                # Recibir el mensage de la red
        self.show_data(msg)                        # Enviar los datos a MainWindow
        return True                                # Queremos quedar activos

    def send(self, msg):
        self.socket.send(msg)

    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()
        txtview.set_buffer(self.txtbff)
        txtview.set_can_focus(False)

        # 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):
        self.client.send(widget.get_text() + "\n")
        widget.set_text("")

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

    def show_data(self, msg):
        self.txtbff.insert(self.txtbff.get_end_iter(), msg)    

    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(HOST, PORT)        # and the communications package
    main.set_client(client)            # Announce the client to the main window
    client.connect()                   # and try to do the real connection

    main.run()

    return 0

if __name__ == '__main__':
    main()
Código: gtk_socket_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 <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 <gtk/gtk.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

#include "config.h"
#include "socketcom.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 TCP_HOST "127.0.0.1"
#define TCP_PORT 5000 

GtkWidget *MainWindow,
	  *entry;
GtkTextBuffer *txtbuffer;


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)
{
  network_write_chars(channel, (char *) gtk_entry_get_text(entry));
  network_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);
	
  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"));
  
  txtbuffer = gtk_text_buffer_new(NULL);
  gtk_text_view_set_buffer(GTK_TEXT_VIEW(txtview), txtbuffer);
  
  g_object_unref(G_OBJECT(builder));

  gtk_widget_show_all(MainWindow);
	
  start_socket(TCP_HOST, TCP_PORT, add_to_textbuffer);
	
  gtk_main();
	
  close_socket(listenfd);

  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 (*netread_callback)(char *);

#include <gtk/gtk.h>

extern  GIOChannel *channel;

gboolean	network_read(GIOChannel *source, GIOCondition cond,
     			gpointer data);
gboolean	network_write_chars(GIOChannel *dest, char *message);
gboolean	new_connection(GSocketService *service,
			GSocketConnection *connection,
			GObject *source_object,
			gpointer user_data);
void		start_socket(const char *host, int port,
                netread_callback callback);
void		close_socket(int listenfd);

#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 "socketcom.h"

GIOChannel *channel;

gboolean
network_read(GIOChannel *source,
             GIOCondition cond,
             gpointer data)
{
  char bff[100];
  GError  *error = NULL;
  gsize   bytes_read;
  GIOStatus ret;
  
  printf("Channel mon installed\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;
    ((netread_callback) data)(bff);
    printf("Got: %s\n", bff);
  }
  return TRUE;
}

gboolean
network_write_chars(GIOChannel *dest, char *message)
{
  GError  *error = NULL;
  gsize   bytes_written;
  GIOStatus ret;
  
  printf("Trying to write %s\n", message);
  ret = g_io_channel_write_chars(dest, message, strlen(message), 
		&bytes_written, &error);
  
  if (ret == G_IO_STATUS_ERROR)
    g_error("Error writing: %s\n", error->message);

  g_io_channel_flush(dest, &error);	// Important! Else message stays
					// in the buffer
  
  return TRUE;
}

gboolean
new_connection(GSocketService *service,
               GSocketConnection *connection,
               GObject *source_object,
               gpointer user_data)
{
  g_object_ref(connection); 
 
  GSocketAddress *sockaddr;
  GInetAddress   *addr;
  guint16         port;
  
  sockaddr = g_socket_connection_get_remote_address(connection, NULL);
  addr     = g_inet_socket_address_get_address(G_INET_SOCKET_ADDRESS(sockaddr));
  port     = g_inet_socket_address_get_port(G_INET_SOCKET_ADDRESS(sockaddr));
  
  printf("New Connection from %s:%d\n", g_inet_address_to_string(addr), port);

  GSocket *socket = g_socket_connection_get_socket(connection);

  gint fd = g_socket_get_fd(socket);
  channel = g_io_channel_unix_new(fd);
  g_io_add_watch(channel, G_IO_IN, (GIOFunc) network_read, user_data);
  
  return TRUE;
}

void
start_socket(const char *host, int port, netread_callback callback)
{
  GSocketService *service;
  GInetAddress   *address;
  GSocketAddress *socket_address;
  
  service = g_socket_service_new();
  address = g_inet_address_new_from_string(host);
  socket_address = g_inet_socket_address_new(address, port);
  
  g_socket_listener_add_address(
		        G_SOCKET_LISTENER(service), 
		        socket_address, 
		        G_SOCKET_TYPE_STREAM,
		        G_SOCKET_PROTOCOL_TCP, 
		        NULL, NULL, NULL);
  g_object_unref(socket_address);
  g_object_unref(address);

  g_signal_connect(service, "incoming", G_CALLBACK(new_connection),
                (gpointer) callback);

  g_socket_service_start(service);
}


void
close_socket(int listenfd)
{
  close(listenfd);
}

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.

10247
(c) John Coppens ON6JC/LW3HAZ correo