Post on 25-May-2015
description
by!Joseluis Laso!
!@jl_laso
jlaso@joseluislaso.es
Sockets al límite
AgradecimientosAl espacio GeeksHubs.com!Al grupo de Symfony de
Valencia
https://twitter.com/symfony_vlc
www.tradukoj.com
Las traducciones en SF2 son gestionadas mediante unos archivos en la carpeta Resources/translations de
cada uno de los distintos bundles, el nombre del archivo tiene esta estructura:
!{catalog}.{language}.{format}!
!Los formatos disponibles actualmente son:!
!!!
Normalmente catalog toma alguno de estos valores:!!!
!aunque puede ser otro cualquiera.
yml xml php
messages validators security
Traducciones en Symfony2
www.tradukoj.com
Los diferentes archivos al final lo que hacen es relacionar una clave con un texto:!
!!!!!!
Primer inconveniente:hay que mantener un sistema en el que se
repiten las claves en varios archivos.
yml!!
header:! menu:! label: "Menu"
xml!!
<trans-‐unit id="1">! <source>! header.menu.label! </source>! <target>Menu</target>!</trans-‐unit>
php!!
// messages.es.php return array(! 'header.menu.label' => 'Menu',!);
Traducciones en Symfony2
www.tradukoj.com
Las claves se separan en catálogos!(o espacios de nombres) por claridad.!
!En realidad SF2 no trabaja directamente!
con esos archivos.!!
Lo hace con la versión "compilada" que crea en app/cache/{env}/translations/catalogue.{language}.php
!Esta versión se "compila" en la!
regeneración de la cache.!!!
Veamos cómo es uno de esos archivos...
Traducciones en Symfony2
www.tradukoj.com
app/cache/dev/translations/catalog.es.php
<?php !use Symfony\Component\Translation\MessageCatalogue;!!$catalogue = new MessageCatalogue('es', array (! 'validators' => ! array (! 'This value should be false.' => 'Este valor debería ser falso.',! 'This value should be true.' => 'Este valor debería ser verdadero.',!//..! 'messages' => ! array (! 'header.menu.label' => 'Menú',!//..
www.tradukoj.com
Traducciones en Symfony2Ahora ya podemos usar esas claves en nuestro
código.!!En un twig:!{{ "header.menu.label"|trans }}!!En un controlador:!$this-‐>container! -‐>get("translator")! -‐>trans("header.menu.label");
www.tradukoj.com
El proyecto: motivación
Una vez aclarado cómo Symfony2 gestiona las traducciones, os voy!
a contar por qué quiero…!
!cambiar este comportamiento.
www.tradukoj.com
La manipulación de los archivos fuentes de las traducciones!
(xml, yml o php) requiere ciertos conocimientos técnicos que no siempre el traductor, revisor o
colaborador posee.
El proyecto: motivación
www.tradukoj.com
Por tanto hay que ir con mil ojos cuando se les pasa algún archivo
de éstos, porque a la vuelta es fácil que SF2 se queje porque haya tabulaciones, no coincidan
las claves, etc, etc, etc..
El proyecto: motivación
www.tradukoj.com
Incluso la edición por parte de técnicos puede producir los mismos conflictos
que el resto del código fuente.!!
Es fácil ponerse de acuerdo para editar, pero hay que acordarse de
hacerlo. Os aseguro que revisar un yml con conflictos es muy divertido!
;<)
El proyecto: motivación
www.tradukoj.com
Vale, entonces: ¿Qué propones?
www.tradukoj.com
Propongo …
Un servidor centralizado para gestionar las traducciones de los desarrollos en
Symfony2.!!
Un sistema con ciertas ventajas: !edición colaborativa, !permisos y roles, !sin necesidad de ningún conocimiento técnico para mantenerlo.
www.tradukoj.com
¿Suena bien?
www.tradukoj.com
El proyectoEmpezó siendo translations.com.es!
Al intuir su posible difusión decidí cambiar a un punto .com!
!Al no quedar libre ninguno en los
principales idiomas me decidí por un idioma menos conocido
(no es éste un proyecto de idiomas ;) ),!!
En esperanto tradukoj(leido TRADUCOI) significa traducciones.
www.tradukoj.com
Vale, pero…
Con un gestor de traducciones centralizado …
¿Qué pasa con los
archivos de traducciones de nuestros proyectos?
www.tradukoj.com
Ahí es donde entra en juego el bundle
que conecta tu proyecto con el
servidor centralizado.
www.tradukoj.com
A partir de la instalación del bundle, los archivos de
traducciones fuentestienen que ser ignorados.
Veamos cómo…
www.tradukoj.com
El bundle instala dentro de su código una clase que
intercepta las recreaciones de los archivos de
traducciones en cache.
www.tradukoj.com
Jlaso/Translations/ApiBundle/Translations/Loader/PdoLoader.php
class PdoLoader implements LoaderInterface, ResourceInterface { // Esta es la que se invoca para regenerar las traducciones public function load($resource, $locale, $domain = Translation::DEFAULT_DOMAIN) ! // Esta genera la sentencia que recupera las keys de la tabla local protected function getTranslationsStatement() ! // public function getTranslations($locale, $criteria, $hierarchicalArray = true) ! // public function registerResources(Translator $translator) ! // protected function getResourcesStatement() ! // public function isFresh($timestamp) ! // protected function getFreshnessStatement($timestamp) public function getResource() public function getConnection() }
www.tradukoj.com
Jlaso/Translations/ApiBundle/Translations/Loader/PdoLoader.php
class PdoLoader implements LoaderInterface, ResourceInterface { //.. ! public function load($resource, $locale, $domain = Translation::DEFAULT_DOMAIN) { if ($resource !== $this) { return new MessageCatalogue($locale); } $stmt = $this-‐>getTranslationsStatement(); $stmt-‐>bindValue(':locale', $locale, \PDO::PARAM_STR); $stmt-‐>bindValue(':domain', $domain, \PDO::PARAM_STR); ! $catalogue = new MessageCatalogue($locale); while ($row = $stmt-‐>fetch()) { $catalogue-‐>set($row['key'], $row['message'], $domain); } ! return $catalogue; } ! //.. } Se han condensado y/o
eliminado algunas partes por claridad
www.tradukoj.com
Hemos añadido una capa intermedia
MiProyectoEnSF2
TAB
TAB: TranslationsApiBundle
yml
www.tradukoj.com
/** * @ORM\Table(name="jlaso_translations") * @UniqueEntity(fields="domain,locale,key") */ class Translation { private $id; private $domain; private $locale; private $key; private $message; protected $bundle; protected $file; private $createdAt; private $updatedAt; // getters and setters .. }
CREATE TABLE `jlaso_translations` (! `id` int(11) NOT NULL AUTO_INCREMENT,! `domain` varchar(50) COLLATE utf8_unicode_ci NOT NULL,! `locale` varchar(10) COLLATE utf8_unicode_ci NOT NULL,! `key` varchar(255) COLLATE utf8_unicode_ci NOT NULL,! `message` longtext COLLATE utf8_unicode_ci,! `bundle` varchar(100) COLLATE utf8_unicode_ci NOT NULL,! `file` varchar(255) COLLATE utf8_unicode_ci NOT NULL,! `created_at` datetime NOT NULL,! `updated_at` datetime NOT NULL,! PRIMARY KEY (`id`)!) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
www.tradukoj.com
Y toda esta introducción es para hablaros de ese conector,
y de cómo he optimizado la ejecución del comando más
pesado:
la sincronización.
www.tradukoj.com
Conexión local/remoto
MiProyectoEnSF2
TAB
TAB: TranslationsApiBundle
www.tradukoj.com
jlaso/translations-apibundle
• Veamos como ha ido evolucionando la conexión. !
• La primera versión en 45 minutos no había terminado la sincronización. !
• Actualmente en 30 segundos se produce todo el proceso. !
• Los datos de prueba son siempre los mismos.
www.tradukoj.com
Evoluciónde la conexión
• Ruta convencional.
• ~ Api-REST. • Una petición
por cada key.
www.tradukoj.com
Empezando• Un controlador para
cada acción. !
• Una petición por key.
Ventajas:Arquitectura REST conocida. !
Inconvenientes:A mayor número de claves, más
peticiones. Cada una de ellas tiene que negociar de nuevo con el
servidor. !Resultado:
Deplorable, 45 minutos
www.tradukoj.com
Evolución de la conexión
• Ruta convencional.
• ~ Api-REST. • Una petición
por cada key.
• Una petición por cada catálogo e idioma.
www.tradukoj.com
Mejorando
• Se concentran todos los datos de un catálogo en una petición.
Ventajas:Mejora el rendimiento. !
Inconvenientes:problemas con el tamaño de los datos enviados, en ocasiones se
pierden datos. !Resultado:
mejorable, 25 minutos
www.tradukoj.com
Evolución de la conexión• Ruta
convencional. • ~ Api-REST. • Una petición
por cada key.
• Una petición por cada catálogo e idioma.
• Socket • Una petición
por cada catálogo e idioma.
www.tradukoj.com
La evolución• Petición de socket libre. !
• Se evoluciona el modelo API-REST anterior tal cual.
Ventajas:Mejora el rendimiento de manera
brutal. !Inconvenientes:
sigue habiendo problemas con la pérdida de datos. !
Resultado:muy bueno, menos de 5 minutos.
www.tradukoj.com
Evolución de la conexión• Ruta
convencional. • ~ Api-REST. • Una petición
por cada key.
• Una petición por cada catálogo e idioma.
• Socket • Una petición
por cada catálogo e idioma.
• Fraccionando en bloques y comprimido.
www.tradukoj.com
La revolución• Comunicación mediante
bloques de tamaño fijo y comprimiendo los datos enviados por el canal. !
• Reconocimiento de cada paquete recibido.
Inconvenientes:No se controla la pérdida de
paquetes aunque van numerados. !Resultado:
perfecto, medio minuto.
www.tradukoj.com
Comando sync desde la consola
www.tradukoj.com
SyncCommand
www.tradukoj.com
SyncCommand (sigue)
www.tradukoj.com
SyncCommand (sigue)
www.tradukoj.com
TAB pide al servidor un socket
MiProyectoEnSF2
TAB
TAB: TranslationsApiBundle
/create-socket
www.tradukoj.comClientSocketService
www.tradukoj.com
En el bundle TABpublic function createSocket() { $url = $this-‐>base_url . 'create-‐socket/' . $this-‐>project_id; $postFields = json_encode(array( 'key' => $this-‐>api_key, 'secret' => $this-‐>api_secret, )); $hdl = curl_init($url); curl_setopt($hdl, CURLOPT_RETURNTRANSFER, true); curl_setopt($hdl, CURLOPT_HTTPHEADER, array('Accept: json')); curl_setopt($hdl, CURLOPT_POST, true); curl_setopt($hdl, CURLOPT_POSTFIELDS, $postFields); curl_setopt($hdl, CURLINFO_CONTENT_TYPE, 'application_json'); $body = curl_exec($hdl); $info = curl_getInfo($hdl); curl_close($hdl); $result = json_decode($body, true); if(!count($result)){ var_dump($info); die; } return $result; }
Se han condensado y/o eliminado algunas partes
por claridad
www.tradukoj.com
Servidor devuelve los datos de conexión (puerto)
MiProyectoEnSF2
TAB
TAB: TranslationsApiBundle
/create-socket
{"port":"10000"}
www.tradukoj.com
En el servidor este controlador atiende la ruta !de petición de creación de un socket
@Route("/create-‐socket/{projectId}") public function createSocketAction(…) { $host = php_uname('n'); $found = false; for ($port = self::MIN_PORT; $port < self::MAX_PORT; $port++) { $connection = @fsockopen($host, $port, $errno, $errtxt, 500); if (is_resource($connection)){ fclose($connection); }else{ $found = true; break; } } if($found){ $srcDir = dirname($this-‐>get('kernel')-‐>getRootDir()); $cmd = "php $srcDir /app/console ". self::COMMAND . " $host $port >/dev/null 2>/dev/null &"; exec($cmd); } ! return $this-‐>resultOk(array('port' => $port)); }
Se han condensado y/o eliminado algunas partes
por claridad
www.tradukoj.com
Ahora la comunicación es por el socket creado y no por http
MiProyectoEnSF2
TAB
TAB: TranslationsApiBundle
/create-socket
{“port”:”10000”}
send => read
www.tradukoj.com
Formato de los mensajes
block-len : block-num : num-blocks : info
• block-len: indica la longitud del último campo (info)
• block-num: es el número de bloque que se está enviando/recibiendo
• num-blocks: la cantidad total de bloques que se quieren enviar y se van a recibir
• info: el bloque que se está enviando/recibiendo en formato comprimido (lzf) !
Ejemplo de mensaje: 000010:001:001:0123456789
www.tradukoj.com
Campo info en los mensajes
Una vez se ha recompuesto todo el campo info a base de juntar todos los bloques, se descomprime (lzf_decompress) e inmediatamente se decodifica con json_decode !Veamos una petición del índice de catálogos de un proyecto: !{ "auth.key":"key1234", "auth.secret":"secret1234", "command":"catalog-‐index", "project_id":1 } !Está claro que este campo no necesita ni comprimirse ni enviarse en bloques, pero cuando empiezas a trabajar con las keys y sus traducciones, os aseguro que la cosa se complica por momentos, en términos de longitud.
www.tradukoj.com
Flujo de datosCliente Servidor
Envío de un bloque
ACK
www.tradukoj.com
En el bundle TAB enviamos los mensajes
protected function sendMessage($msg, $compress = true) { $msg = lzf_compress($msg); $len = strlen($msg); $blocks = ceil($len / self::BLOCK_SIZE); for($i=0; $i<$blocks; $i++){ // get Block to send $block = substr($msg, $i * self::BLOCK_SIZE, ($i == $blocks-‐1) ? $len -‐ ($i-‐1) * self::BLOCK_SIZE : self::BLOCK_SIZE); $prefix = sprintf("%06d:%03d:%03d:", strlen($block), $i+1, $blocks); $aux = $prefix . $block; if(false === socket_write($this-‐>socket, $aux, strlen($aux))){ die('error'); }; // Wait for ACK do{ $read = socket_read($this-‐>socket, 10, PHP_NORMAL_READ); }while(strpos($read, self::ACK) !== 0); } ! return true; } Se han condensado y/o
eliminado algunas partes por claridad
www.tradukoj.com
En el bundle TABprotected function readSocket() { $buffer = ''; $overload = strlen('000000:000:000:'); do{ $buf = socket_read($this-‐>socket, $overload + self::BLOCK_SIZE, PHP_BINARY_READ); if($buf === false){ echo socket_strerror(socket_last_error($this-‐>socket)); return -‐2; } list($size, $block, $blocks) = explode(":", $buf); $aux = substr($buf, $overload); if($size == strlen($aux)){ $this-‐>send(self::ACK); }else{ $this-‐>send(self::NO_ACK); die('error in size'); } $buffer .= $aux; }while($block < $blocks); return lzf_decompress($buffer); }
Se han condensado y/o eliminado algunas partes
por claridad
www.tradukoj.com
El servidor utiliza el canal de la misma manera
MiProyectoEnSF2
TAB
TAB: TranslationsApiBundle
/create-socket
{“port”:”10000”}
send => read
read => send
www.tradukoj.com
En el servidor discriminamos por el comando solicitado
do{ $buf = $this-‐>readSocket(); $read = json_decode($buf, true); $command = isset($read['command']) ? $read['command'] : ''; // .. switch($command){ case self::CMD_CATALOG_INDEX: … case self::CMD_TRANSDOC_INDEX: … case self::CMD_TRANSDOC_SYNC: … case self::CMD_TRANSDOC_GET: … case self::CMD_UPLOAD_KEYS: … case self::CMD_DOWNLOAD_KEYS: … case self::CMD_SHUTDOWN: $this-‐>resultOk(); sleep(1); socket_close($this-‐>msgsock); exit; default: $this-‐>exception(sprintf('command \'%s\' unknow', $command)); break; } } while (true);
Se han condensado y/o eliminado algunas partes
por claridad
www.tradukoj.com
www.tradukoj.com
¿Qué queda por hacer?• Dejar el socket siempre abierto.
!• Control completo de todas las excepciones.
!• Control de envío de paquetes en orden o
repetir si fallo. !
• Tratar archivos xml y php. !
• Subir claves nuevas en bloque. !
• En el editor: mejorar la experiencia de usuario, chat, mailing, edición colaborativa …
www.tradukoj.com
¿Qué más queda por hacer?
• Gestión de usuarios (invitar/añadir). !
• Permitir traducciones abiertas (al estilo de translate.whatsapp.com) en las que colaboran o validan un público más abierto. !
• Poder reservar subdominios para lo anterior o apuntar subdominios externos (estos servicios probablemente serán de pago)
www.tradukoj.com
Agradecimientos
A mi empresa: por hacer de "conejillo de indias" con las traducciones de
!!!
A mis compañeros por prestarse a este
experimento y soportar los inconvenientes iniciales de la
implantación, y sobre todo por aportar las críticas que me han ayudado a
mejorarlo.
www.tradukoj.com
¿Te gusta?
¡Pruébalo!
tradukoj.com !
Es gratis.
El proceso es sencillo.
Es reversible.
www.tradukoj.com
¿Quieres colaborar?Se agradece todo tipo de ayuda: !• desarrollando • testeando • traduciendo • criticando • twitteando • proponiendo • donando • …
www.tradukoj.com
¿ Preguntas ?
GraciasJoseluis Laso!
!@jl_laso
jlaso@joseluislaso.es!http://www.slideshare.net/JoseluisLaso/sockets-al-limite!
http://www.github.com/jlaso/translations-apibundle