Bridging Python And PHP

11 Jan 2009 10:48

Imagine you have a PHP-based application (like Wikidot). Now, you want to extend it using Python. Through all ways to do it, I'll show you how to achieve this using XML-RPC protocol.

Background

XML-RPC is a client-server protocol for remote procedure call.

On server this works like getting a bunch of functions from your application and exporting it with HTTP.

On client this works like connecting to a XML-RPC server, finding out what function it delivers and constructing a so called server proxy — an object having a method for every function exported by an XML-RPC server.

Calling the methods of the server proxy connects to the server using HTTP, passes arguments and transport the result back to the client. So basically this works AS you have a remote located object locally available.

The data encoding between client and server is defined in XML-RPC specification and is a language based on XML (but you actually never touch it, the XML is converted to objects by libraries).

Overview

We want to run an XML-RPC server exposing a class in PHP and an XML-RPC client in Python to communicate with the XML-RPC server.

Traditionally we would need to have an HTTP server for the PHP XML-RPC server, because HTTP is used as the XML-RPC transport. But digging a bit into the specification, you'll discover, that none HTTP-specific parts of the protocol are used. It's just used as a line to transport the XML data.

So you may wonder if it's possible to use XML-RPC with transport other than HTTP. In short, yes. But you may need to hack around the XML-RPC libraries (because they usually suppose you'll want to use HTTP).

PHP XML-RPC server

First, you need some class, that you want to expose with PHP XML-RPC:

<?php
 
class MyClass {
    /**
     * @param string $input
     * @return string
         */
    public function repeat($input) {
        return $input;
    }
}

Notice I've set the parameter and return type in phpdoc.

Now let's expose this class with Zend Framework XML-RPC implementation.

You need to download Zend Framework first, let's say to /path/to/zf directory.

<?php
 
class MyClass {
    /**
     * @param string $input
     * @return string
         */
    public function repeat($input) {
        return $input;
    }
}
 
set_include_path(get_include_path() . PATH_SEPARATOR . 'zf/library');
require_once "Zend/XmlRpc/Server.php";
 
$server = new Zend_XmlRpc_Server();
$server->setClass('MyClass', 'myclass');
echo $server->handle();

Set_include_path line adds the /path/to/zf/library directory to PHP path, so you can import the Zend_XmlRpc_Server class (located in /path/to/zf/library/Zend/XmlRpc/Server.php file).

Then there is an instance of Zend_XmlRpc_Server created, then there is MyClass attached as the class for myclass XMLRPC namespace. This means the repeat method is to be called via the XML-RPC as myclass.repeat.

If you place the file on your server and have it under some URL, for example:

http://your-server.com/myclass.php

This URL is fully valid XML-RPC server endpoint for XML-RPC clients.

Python client

Having the XML-RPC server running we can connect to it from any XML-RPC enabled library in any programming language around.

In Python, to call the remote procedure myclass.repeat on the XML-RPC endpoint http://your-server.com/myclass.php, you would do the following:

from xmlrpclib import ServerProxy
 
server = ServerProxy('http://your-server.com/myclass.php')
print server.myclass.repeat('Hello RPC service')

Running this code:

# python xmlrpc-test.py

gives you:

Hello RPC service

Under the hood:

  • Python script makes a connection to http://your-server.com/myclass.php
    • your webserver runs the myclass.php script
      • the $server->handle() line processes the data received
        • chooses a class and a method to run (this would be MyClass and repeat)
        • passes the arguments (a string 'Hello RPC service') to the method
        • gets the return value
      • passes it back to the client wrapped in XML-RPC protocol
  • Python gets XML reply and converts it back to simple string ('Hello RPC service')
  • and prints it on the console

Omitting the HTTP protocol

Probably you have both Python and PHP scripts to be run on the same machine, so the HTTP part is quite useless and an additional point of failure.

As I already stated, the HTTP is only a transport and you can replace it (with some cost) with some other transport.

I came into an idea to use stdout/stdin as the transport, so Python would execute a PHP script (command line interface) and pass the XML-RPC request to the script's stdin. PHP would then have to get the XML-RPC request from stdin instead of from HTTP request.

This means two modifications in server and client code.

First the server:

<?php
 
class MyClass {
    /**
     * @param string $input
     * @return string
         */
    public function repeat($input) {
        return $input;
    }
}
 
set_include_path(get_include_path() . PATH_SEPARATOR . 'zf/library');
require_once "Zend/XmlRpc/Server.php";
require_once "Zend/XmlRpc/Request/Stdin.php";
 
$server = new Zend_XmlRpc_Server();
$server->setClass('MyClass', 'myclass');
echo $server->handle(new Zend_XmlRpc_Request_Stdin());

The change is passing an instance of Zend_XmlRpc_Request_Stdin to $server->handle(). This is all needed. Guys from Zend Framework already predicted such a use.

Then, the client part.

Xmlrpclib allows passing a custom transport in case you want to implement some proxies or other thing. We'll make a transport, that instead of making a HTTP connection, runs a PHP script, passes the request to its stdin and gets the response from stdout:

from xmlrpclib import Transport, Server
from subprocess import Popen, PIPE
 
class LocalFileTransport(Transport):
    class Connection:
        def setCmd(self, cmd):
            self.cmd = Popen(['php', cmd], stdin=PIPE, stdout=PIPE)
 
        def send(self, content):
            self.cmd.stdin.write(content)
            self.cmd.stdin.close()
 
        def getreply(self):
            return 200, '', []
 
        def getfile(self):
            return self.cmd.stdout
 
    def make_connection(self, host):
        return self.Connection()
 
    def send_request(self, connection, handler, request_body):
        connection.setCmd(handler)
 
    def send_content(self, connection, request_body):
        connection.send(request_body)
 
    def send_host(self, connection, host):
        pass
 
    def send_user_agent(self, connection):
        pass
 
server = Server('http://host.com/path/to/the/php/script/myclass.php', transport = LocalFileTransport())
print server.myclass.repeat('Hello XML-RPC with no HTTP service')

Notes:

  • host.com in the URL is completely ignored, use whatever value you want
  • /path/to/the/php/script/myclass.php in URL is passed as the PHP script to run

What to do next?

Having this simple skeleton, you can now extend the MyClass, actually give it more proper name first! You can also attach more classes to the XML-RPC server using different namespaces:

$server->setClass('SomeClass', 'some);
$server->setClass('MyClass', 'my');
$server->setClass('YourClass', 'your');

Only public methods are exposed to the XML-RPC clients, so you can hide some logic inside of private or protected methods and only expose what you need from given classes.

This solution is a quick way to actually use some of your well-working PHP code in your fancy-new and elegant Python application. This can help if you want to make a filesystem with Python-FUSE, but want to data be taken from PHP application.

Did it help you?

I hope this helps someone. Feel free to comment.

Comments: 1

Wikidot is BIG

10 Jan 2009 12:48

As you may know I'm implementing a new search engine for Wikidot.

This seemed quite easy at first having nice Lucene implementation in PHP — included in Zend Framework and indeed during tests it was fast, simple and powerful. But this was tested on about 100,000 documents (document is a Wikidot page or forum thread) and we have about 2,500,000 documents in Wikidot now. And this is where the problem begins.

After indexing roughly 1,800,000 documents there were some problems with memory consumed by the indexing process (500 MB merory limit was not enough in SOME cases).

Even earlier I realized that the search times weren't good enough. This is why I implemented the searching part in Java, which is the native platform for the Lucene indexer. This sped things up.

Do you think indexing a document in just a second is fast? I though this is a good result. Indexing a document takes about 0.2 s when having small amount of documents in the index already. But when you have 400,000 documents in index, adding another document to the index takes about 0.4 s. And having even this "good" indexing time (below a second), indexing the whole Wikidot would take at least a few days.

This leads me to a conclusion, that Wikidot is really BIG.

A similar situation also applied to the user uploaded files. There was a problem of a limit of filesystem reached, which was about 32,000 directories max in a single directory. Having all user-uploaded files in a directory structure of one-directory-per-wiki, this resulted in a problem when having more than 32,000 wikis.

Replicating this structure to another machine (also known as live-backup of user-uploaded files) was also quite a challenge, because we've reached a limit of directory watches in the kernel-level filesystem-monitoring system (inotify).

It all shows, that things that seem easy are not necessarily easy because of the high scale of the Wikidot, which touches some limits on nearly every piece of software we use. But this is also a great chance to really test those projects and how they react to such a high load.

Comments: 3

Na Wydziale

07 Jan 2009 19:52

Jakich przedmiotów powinno się uczyć na informatyce:

Powinny być

  • projektowanie i tworzenie serwisów internetowych
  • język Python (1 semestr?)
  • programowanie urządzeń mobilnych
  • systemy autoryzacji, szyfrowania i bezpiecznej transmisji danych
  • nowoczesne bazy danych (czyli nierelacyjne)

Dodatkowe pomysły:

  • TRZEBA zrewidować programowanie równoległe i rozproszone. Bazowanie na MPI jest nieludzkie. Należy przedstawić język Erlang oraz biblioteki do równoległego przetwarzania w Javie (tematyka seminarium Bały)
  • zwrócenie uwagi na styl programowania — główny problem z kodem, który produkują studenci
    • zadanie dla inżynierii oprogramowania — w praktyce jest ona równie ważna (a może i ważniejsza) już w trakcie realizowania projektu, co przed
    • zadanie dla każdego przedmiotu "programowanie *" — zwrócić uwagę na elegancję kodu, ale również pokazywać jak elegancko programować
  • tworzenie przydatnych rzeczy na zajęciach
  • rozwijanie projektów open-source, posiadanie specjalistów nie tylko w systemach komercyjnych typu Oracle, ale również (przede wszystkim!) w dziedzinach otwartego oprogramowania — uczmy się na cudzych błędach i ogarnijmy cały ten darmowy kod, gdyż można z niego sporo wykorzystać (ale i poprawić)
  • szukanie optymalnych rozwiązań — jeśli można coś zrobić w języku X, ale w języku Y jest to rozwiązanie, które ma jedną linię, to jaki jest sens zadawanie studentowi napisanie tego w języku X
  • lepsze motywowanie do działania — np. przez robienie rzeczy, które nie są trywialne, realizują pomysły studentów, lub rzeczy przydatnych/potrzebnych

Nie powinny być obowiązkowe

  • analiza matematyczna 2, równania różniczkowe
  • podstawy przetwarzania sygnałów
  • rachunek prawdopodobieństwa i statystyka matematyczna — wstęp do statystycznej analizy danych wystarcza, aby ogarnąć temat statystyki (laboratoria są OK, choć mogłyby pokazać również inne środowiska niż SPSS)

Potrzebne matematyczne przedmioty

Wydaje się, że przydatne są następujące przedmioty matematyczne:

  • algebra — głównie macierze
  • analiza matematyczna — pochodne, całki, być może transformata Fouriera
  • matematyka dyskretna — bardzo przydatne, można nawet pójść dalej w kierunku kryptografii
  • logika matematyczna i teoria mnogości — dość sensowne, można iść troszeczkę dalej (ale niezbyt głęboko): logika trójwartościowa (występuje w systemach bazodanowych)

Comments: 3

Kupię: dysk z serwerem Samby

06 Jan 2009 19:47

Są takie urządzenia — obudowy, do których wkłada się dyski twarde, podłącza przewodem do sieci Ethernetowej a one udostępniają zasoby dysku twardego w trybie zapis/odczyt poprzez protokół Samba.

Zamierzam kupić sobie taki nowoczesny dysk przenośny. Jedyne sensowne urządzenie, które znalazłem, to Welland ME-747AN-S.

strona producenta http://www.welland.com.tw/html/network/747an.html
do kupienia http://www.acd.pl/product_info.php?products_id=1918

Jest to serwer SAMBA/FTP z możliwością konfigurowania przez WWW z dodatkową możliwością zaprogramowania urządzenia do ściągania plików przez sieć torrent.

Do tego należy kupić dysk SATA. Myślę, że będzie to Seagate 320 GB Barracuda 7200.11 (16MB, Serial ATA II).

Całość podepnę sobie do routera, żeby nie hałasowało mi w pokoju i mam ładny zestaw.

Myślę sobie, że będę trzymać na tym dysku pliki zaszyfrowane przez encfs (odszyfrowane przez lokalny komputer). Może pokuszę się o specjalnego użytkownika (read-only), który posłuży serwerowi w Niemczech do robienia backupów. Przekierowanie portu Samby na zewnątrz na routerze + DynDNS i system jest spięty. Po stronie softwaru można zastosować klasycznie rsync z odpowiednimi opcjami. Po synchronizacji danych warto zapuścić na serwerze svn commit, tak, by dane, które zrzucono trafiły do repozytorium. Ryzyko wykradzenia danych minimalne, bo jeśli ktoś już dane wykradnie, to zaszyfrowane.

Jednocześnie jedyne co potrzeba, żeby dane odszyfrować, to hasło, więc można to zrobić nawet na tym serwerze w Niemczech w razie bardzo nagłej potrzeby.

A jak już się wyprowadzę, to zewnętrzny dysk się na pewno przyda tak czy inaczej :).

Comments: 0

page 14 of 15« previous12...12131415next »
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License