You are on page 1of 19

Dveloppons en Java

v 1.80

Copyri ght (C) 1999-2012 Jean-Mi chel DOUDOUX

25. L'appel de mthodes distantes : RMI

RMI (Remote Method Invocation) est une technologie dveloppe et fournie par Sun partir du JDK 1.1 pour permettre de mettre en oeuvre facilement des objets distribus. Ce chapitre contient plusieurs sections : La prsentation et l'architecture de RMI Les diffrentes tapes pour crer un objet distant et l'appeler avec RMI Le dveloppement cot serveur Le dveloppement cot client La gnration de la classe stub La mise en oeuvre des objets RMI open in browser PRO version Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

25.1. La prsentation et l'architecture de RMI


Le but de RMI est de permettre l'appel, l'excution et le renvoi du rsultat d'une mthode excute dans une machine virtuelle diffrente de celle de l'objet l'appelant. Cette machine virtuelle peut tre sur une machine diffrente pourvu qu'elle soit accessible par le rseau. La machine sur laquelle s'excute la mthode distante est appele serveur. L'appel cot client d'une telle mthode est un peu plus compliqu que l'appel d'une mthode d'un objet local mais il reste simple. Il consiste obtenir une rfrence sur l'objet distant puis simplement appeler la mthode partir de cette rfrence. La technologie RMI se charge de rendre transparente la localisation de l'objet distant, son appel et le renvoi du rsultat. En fait, elle utilise deux classes particulires, le stub et le skeleton, qui doivent tre gnres avec l'outil rmic fourni avec le JDK. Le stub est une classe qui se situe ct client et le skeleton est son homologue cot serveur. Ces deux classes se chargent d'assurer tous les mcanismes d'appel, de communication, d'excution, de renvoi et de rception du rsultat.

25.2. Les diffrentes tapes pour crer un objet distant et l'appeler avec RMI
Le dveloppement cot serveur se compose de : La dfinition d'une interface qui contient les mthodes qui peuvent tre appeles distance L'criture d'une classe qui implmente cette interface

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

L'criture d'une classe qui instanciera l'objet et l'enregistrera en lui affectant un nom dans le registre de nom RMI (RMI Registry) Le dveloppement ct client se compose de : L'obtention d'une rfrence sur l'objet distant partir de son nom L'appel la mthode partir de cette rfrence Enfin, il faut gnrer les classes stub et skeleton en excutant le programme rmic avec le fichier source de l'objet distant

25.3. Le dveloppement cot serveur


Ct serveur, l'objet distant est dcrit par une interface. Une instance de l'objet doit tre cr et enregistre dans le registre RMI.

25.3.1. La dfinition d'une interface qui contient les mthodes de l'objet distant
L'interface dfinir doit hriter de l'interface java.rmi.Remote. Cette interface ne contient aucune mthode mais indique simplement que l'interface peut tre appele distance. L'interface doit contenir toutes les mthodes qui seront susceptibles d'tre appeles distance. La communication entre le client et le serveur lors de l'invocation de la mthode distante peut chouer pour diverses raisons telles qu'un crash du serveur, une rupture de la liaison, etc ... Ainsi chaque mthode appele distance doit dclarer qu'elle est en mesure de lever l'exception java.rmi.RemoteException.

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

Exemple ( code Java 1.1 ) : 01. 02. 03. 04. 05. 06. 07. 08. 09. package com.jmdoudoux.test.rmi; import java.rmi.*; public interface Information extends Remote { public String getInformation() throws RemoteException; }

25.3.2. L'criture d'une classe qui implmente cette interface


Cette classe correspond l'objet distant. Elle doit donc implmenter l'interface dfinie et contenir le code ncessaire. Cette classe doit obligatoirement hriter de la classe UnicastRemoteObject qui contient les diffrents traitements lmentaires pour un objet distant dont l'appel par le stub du client est unique. Le stub ne peut obtenir qu'une seule rfrence sur un objet distant hritant de la classe UnicastRemoteObject. On peut supposer qu'une future version de RMI sera capable de faire du MultiCast, permettant RMI de choisir parmi plusieurs objets distants identiques la rfrence fournir au client. La hirarchie de la classe UnicastRemoteObject est : java.lang.Object java.rmi.Server.RemoteObject java.rmi.Server.RemoteServer java.rmi.Server.UnicastRemoteObject

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

Comme indiqu dans l'interface, toutes les mthodes distantes, mais aussi le constructeur de la classe, doivent indiquer qu'elles peuvent lever l'exception RemoteException. Ainsi, mme si le constructeur ne contient pas de code il doit tre redfini pour inhiber la gnration du constructeur par dfaut qui ne lve pas cette exception. Exemple ( code Java 1.1 ) : 01. 02. 03. 04. 05. 06. 07. 08. 09. 10. 11. 12. 13. 14. 15. 16. 17. 18. package com.jmdoudoux.test.rmi; import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; public class InformationImpl extends UnicastRemoteObject implements Information { private static final long serialVersionUID = 2674880711467464646L; protected InformationImpl() throws RemoteException { super(); } public String getInformation() throws RemoteException { System.out.println("Invocation de la mthode getInformation()"); return "bonjour"; }

25.3.3. L'criture d'une classe pour instancier l'objet et l'enregistrer dans le registre
Ces oprations peuvent tre effectues dans la mthode main d'une classe ddie ou dans la mthode main de la classe de l'objet distant. L'intrt d'une classe ddie et qu'elle permet de regrouper toutes ces oprations pour un ensemble d'objets distants.

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

La marche suivre contient trois tapes : la mise en place d'un security manager ddi qui est facultative l'instanciation d'un objet de la classe distante l'enregistrement de la classe dans le registre de noms RMI en lui donnant un nom

25.3.3.1. La mise en place d'un security manager


Cette opration n'est pas obligatoire mais elle est recommande en particulier si le serveur doit charger des classes rcupres sur des machines distantes. Sans security manager, il faut obligatoirement mettre la disposition du serveur toutes les classes dont il aura besoin (Elles doivent tre dans le CLASSPATH du serveur). Avec un security manager, le serveur peut charger dynamiquement certaines classes. Cependant, le chargement dynamique de ces classes peut poser des problmes de scurit car le serveur va excuter du code d'une autre machine. Cet aspect peut conduire ne pas utiliser de security manager. Exemple ( code Java 1.1 ) : 01. public static void main(String[] args) { 02. try { 03. if (System.getSecurityManager() == null) { 04. System.setSecurityManager(new RMISecurityManager()); 05. } 06. } catch (Exception e) { 07. e.printStrackTrace(); 08. } 09. }

Il est aussi possible d'activer un security manager en utilisant simplement l'option -Djava.security.manager de la JVM.

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

25.3.3.2. L'instanciation d'un objet de la classe distante


Cette opration est trs simple puisqu'elle consiste simplement en la cration d'un objet de la classe de l'objet distant Exemple ( code Java 1.1 ) : 01. public static void main(String[] args) { 02. try { 03. if (System.getSecurityManager() == null) { 04. System.setSecurityManager(new RMISecurityManager()); 05. } 06. 07. InformationImpl informationImpl = new InformationImpl(); 08. } catch (Exception e) { 09. e.printStrackTrace(); 10. } 11. }

25.3.3.3. L'enregistrement dans le registre de noms RMI


La dernire opration consiste enregistrer l'objet cr dans le registre de noms en lui affectant un nom. Ce nom est fourni au registre sous forme d'une URL constitu du prfix rmi://, du nom du seveur (hostname) et du nom associ l'objet prcd d'un slash. Le nom du serveur peut tre fourni en dur sous forme d'une constante chane de caractres ou peut tre dynamiquement obtenu en utilisant la classe InetAddress pour une utilisation en locale.

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

C'est ce nom qui sera utilis dans une URL par le client pour obtenir une rfrence sur l'objet distant. L'enregistrement se fait en utilisant la mthode rebind de la classe Naming. Elle attend en paramtre l'URL du nom de l'objet et l'objet lui mme. Exemple ( code Java 1.1 ) : 01. public static void main(String[] args) { 02. try { 03. if (System.getSecurityManager() == null) { 04. System.setSecurityManager(new RMISecurityManager()); 05. } 06. 07. InformationImpl informationImpl = new InformationImpl(); 08. 09. String url = "rmi://" + InetAddress.getLocalHost().getHostAddress() + "/TestRMI"; 10. System.out.println("Enregistrement de l'objet avec l'url : " + url); 11. Naming.rebind(url, informationImpl); 12. 13. System.out.println("Serveur lanc"); 14. } catch (RemoteException e) { 15. e.printStackTrace(); 16. } catch (MalformedURLException e) { 17. e.printStackTrace(); 18. } catch (UnknownHostException e) { 19. e.printStackTrace(); 20. } 21. }

25.3.3.4. Le lancement dynamique du registre de noms RMI


Sur le serveur, le registre de nom RMI doit s'excuter avant de pouvoir enregistrer un objet ou obtenir une rfrence.

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

Ce registre peut tre lanc en tant qu'application fournie dans le JDK (rmiregistry) comme indiqu dans un chapitre suivant ou tre lanc dynamiquement dans la classe qui enregistre l'objet. Ce lancement ne doit avoir lieu qu'une seule et unique fois. Il peut tre intressant d'utiliser le code ci-dessous si l'on cre une classe ddie l'enregistrement des objets distants. Le code pour excuter le registre est la mthode createRegistry() de la classe java.rmi.registry.LocateRegistry. Cette mthode attend en paramtre un numro de port. Exemple ( code Java 1.1 ) : 01. 02. 03. 04. 05. 06. 07. 08. 09. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. package com.jmdoudoux.test.rmi; import import import import import import import java.net.InetAddress; java.net.MalformedURLException; java.net.UnknownHostException; java.rmi.Naming; java.rmi.RMISecurityManager; java.rmi.RemoteException; java.rmi.registry.LocateRegistry;

public class LanceServeur { public static void main(String[] args) { try { LocateRegistry.createRegistry(1099); System.out.println("Mise en place du Security Manager ..."); if (System.getSecurityManager() == null) { System.setSecurityManager(new RMISecurityManager()); } InformationImpl informationImpl = new InformationImpl(); String url = "rmi://" + InetAddress.getLocalHost().getHostAddress() + "/TestRMI"; System.out.println("Enregistrement de l'objet avec l'url : " + url); Naming.rebind(url, informationImpl); System.out.println("Serveur lanc"); } catch (RemoteException e) { e.printStackTrace(); } catch (MalformedURLException e) {
Are you a developer? Try out the HTML to PDF API

open in browser PRO version

pdfcrowd.com

31. } catch (MalformedURLException e) { 32. e.printStackTrace(); 33. } catch (UnknownHostException e) { 34. e.printStackTrace(); 35. } 36. } 37. }

25.4. Le dveloppement cot client


L'appel d'une mthode distante peut se faire dans une application ou dans une applet.

25.4.1. La mise en place d'un security manager


Comme pour le cot serveur, cette opration est facultative. Le choix de la mise en place d'un scurity manager ct client suit des rgles identiques celui du ct serveur. Sans son utilisation, il est ncessaire de mettre dans le CLASSPATH du client toutes les classes ncessaires dont la classe stub. Exemple ( code Java 1.1 ) : 1. public static void main(String[] args) { 2. if (System.getSecurityManager() == null) { 3. System.setSecurityManager(new RMISecurityManager()); 4. } 5. }

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

25.4.2. L'obtention d'une rfrence sur l'objet distant partir de son nom
Pour obtenir une rfrence sur l'objet distant partir de son nom, il faut utiliser la mthode statique lookup() de la classe Naming. Cette mthode attend en paramtre une URL indiquant le nom qui rfrence l'objet distant. Cette URL est compose de plusieurs lments : le prfix rmi://, le nom du serveur (hostname) et le nom de l'objet tel qu'il a t enregistr dans le registre prcd d'un slash. Il est prfrable de prvoir le nom du serveur sous forme de paramtres de l'application ou de l'applet pour plus de souplesse. La mthode lookup() va rechercher dans le registre du serveur l'objet et retourner un objet stub. L'objet retourn est de la classe Remote (cette classe est la classe mre de tous les objets distants). Si le nom fourni dans l'URL n'est pas rfrenc dans le registre, la mthode lve l'exception NotBoundException. Exemple ( code Java 1.1 ) : 01. public static void main(String[] args) { 02. 03. if (System.getSecurityManager() == null) { 04. System.setSecurityManager(new RMISecurityManager()); 05. } 06. 07. try { 08. Remote r = Naming.lookup("rmi://10.0.0.13/TestRMI"); 09. } catch (Exception e) { 10. e.printStrackTrace(); 11. } 12. 13. }

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

25.4.3. L'appel la mthode partir de la rfrence sur l'objet distant


L'objet retourn tant de type Remote, il faut raliser un cast vers l'interface qui dfinit les mthodes de l'objet distant. Pour plus de scurit, on vrifie que l'objet retourn est bien une instance de cette interface. Un fois le cast ralis, il suffit simplement d'appeler la mthode. Exemple ( code Java 1.1 ) : 01. package com.jmdoudoux.test.rmi; 02. 03. import java.net.MalformedURLException; 04. import java.rmi.Naming; 05. import java.rmi.NotBoundException; 06. import java.rmi.RMISecurityManager; 07. import java.rmi.Remote; 08. import java.rmi.RemoteException; 09. 10. public class LanceClient { 11. 12. public static void main(String[] args) { 13. System.out.println("Lancement du client"); 14. if (System.getSecurityManager() == null) { 15. System.setSecurityManager(new RMISecurityManager()); 16. } 17. try { 18. Remote r = Naming.lookup("rmi://10.0.0.13/TestRMI"); 19. System.out.println(r); 20. if (r instanceof Information) { 21. String s = ((Information) r).getInformation(); 22. System.out.println("chaine renvoyee = " + s); 23. } 24. } catch (MalformedURLException e) { 25. e.printStackTrace(); 26. } catch (RemoteException e) { 27. e.printStackTrace(); 28. } catch (NotBoundException e) { open in browser PRO version Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

28. } catch (NotBoundException e) { 29. e.printStackTrace(); 30. } 31. System.out.println("Fin du client"); 32. } 33. }

25.4.4. L'appel d'une mthode distante dans une applet


L'appel d'une mthode distante est le mme dans une application et dans une applet. Seule la mise en place d'un security manager ddi dans les applets est inutile car elles utilisent dj un scurity manager (AppletSecurityManager) qui autorise le chargement de classes distantes. Exemple ( code Java 1.1 ) : 01. 02. 03. 04. 05. 06. 07. 08. 09. 10. 11. 12. 13. 14. 15. 16. 17. 18. package com.jmdoudoux.test.rmi; import java.applet.*; import java.awt.*; import java.rmi.*; public class AppletTestRMI extends Applet { private String s; public void init() { try { Remote r = Naming.lookup("rmi://10.0.0.13/TestRMI"); if (r instanceof Information) { s = ((Information) r).getInformation(); }
Are you a developer? Try out the HTML to PDF API

open in browser PRO version

pdfcrowd.com

18. } 19. } catch (Exception e) { 20. e.printStrackTrace(); 21. } 22. } 23. 24. public void paint(Graphics g) { 25. super.paint(g); 26. g.drawString("chaine retourne = "+s,20,20); 27. } 28. }

25.5. La gnration de la classe stub


Pour gnrer la classe stub, il suffit d'utiliser l'outil rmic fourni avec le JDK en lui donnant en paramtre le nom pleinement qualifi de la classe. Attention la classe doit avoir t compile : rmic besoin du fichier .class.

Exemple ( code Java 1.1 ) : 1. rmic com.jmdoudoux.test.rmi.InformationImpl

rmic va gnrer et compiler la classe stub sous le nom InformationImpl_Stub.class. Cette classe sera utilis par la partie cliente pour invoquer l'objet distant correspondant.

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

25.6. La mise en oeuvre des objets RMI


La mise en oeuvre et l'utilisation d'objets distants avec RMI ncessite plusieurs tapes : 1. Dmarrer le registre RMI sur le serveur soit en utilisant le programme rmiregistry livr avec le JDK soit en excutant une classe qui effectue le lancement. 2. Excuter la classe qui instancie l'objet distant et l'enregistre dans le serveur de noms RMI 3. Lancer l'application ou l'applet pour tester.

25.6.1. Le lancement du registre RMI


La commande rmiregistry est fournie avec le JDK. Il faut la lancer en tche de fond : Sous Unix : rmiregistry& Sous Windows : start rmiregistry Ce registre permet de faire correspondre un objet un nom et inversement. C'est lui qui est sollicit lors d'un appel aux mthodes Naming.bind() et Naming.lookup()

25.6.2. L'instanciation et l'enregistrement de l'objet distant


Il faut excuter la classe qui va instancier l'objet distant et l'enregistrer sous son nom dans le registre prcdemment lanc.

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

Pour ne pas avoir de problme, il faut s'assurer que toutes les classes utiles (la classe de l'objet distant, l'interface qui dfinit les mthodes) sont prsentes dans un rpertoire dfini dans le classpath. Si un gestionnaire de scurit est mis en place, il faut dfinir un fichier qui va contenir la politique de scurit qu'il doit mettre en oeuvre. Exemple ( code Java 1.1 ) : le fichier ma_policy_serveur 1. 2. 3. 4. 5. grant{ permission java.net.SocketPermission "localhost:1099", "connect, resolve"; permission java.net.SocketPermission "*:1024-", "connect, resolve"; permission java.net.SocketPermission "*:1024-", "accept, resolve"; };

Les permissions dfinies concernent les permissions de connexions par socket au serveur. Lors du lancement du serveur, l'option java.security.policy permet de prciser le fichier qui sera utilis par le gestionnaire de scurit. Exemple ( code Java 1.1 ) : le fichier ma_policy_serveur 1. 2. 3. 4. 5. C:\Users\Jean Michel\workspace\TestRmiServer>java -cp bin -Djava.security.policy =ma_policy_serveur com.jmdoudoux.test.rmi.LanceServeur Mise en place du Security Manager ... Enregistrement de l'objet avec l'url : rmi://10.0.0.13/TestRMI Serveur lanc

25.6.3. Le lancement de l'application cliente


L'archive de la partie cliente doit contenir le client, l'interface de l'objet distant et le stub qui a t gnr par rmic.

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

Exemple : 1. 2. 3. 4. 5. C:\temp>jar -tf TestRMIClient.jar META-INF/MANIFEST.MF com/jmdoudoux/test/rmi/Information.class com/jmdoudoux/test/rmi/InformationImpl_Stub.class com/jmdoudoux/test/rmi/LanceClient.class

Le client qui invoque l'objet distant est invoqu de manire classique. Exemple : 1. 2. 3. 4. 5. 6. C:\temp>java -jar TestRMIClient.jar Lancement du client InformationImpl_Stub[UnicastRef [liveRef: [endpoint:[10.0.0.13:62802](remote),ob jID:[7b7739e4:135b4a87a5e:-7fff, -3323459310870193038]]]] chaine renvoyee = bonjour Fin du client

Si le serveur n'est pas dmarr, une exception est leve Exemple : 01. C:\temp>java -jar TestRMIClient.jar 02. Lancement du client 03. java.rmi.ConnectException: Connection refused to host: 10.0.0.13; nested excepti 04. on is: 05. java.net.ConnectException: Connection timed out: connect 06. at sun.rmi.transport.tcp.TCPEndpoint.newSocket(Unknown Source) 07. at sun.rmi.transport.tcp.TCPChannel.createConnection(Unknown Source) 08. at sun.rmi.transport.tcp.TCPChannel.newConnection(Unknown Source) 09. at sun.rmi.server.UnicastRef.newCall(Unknown Source) open in browser PRO version Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

09. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29.

at sun.rmi.server.UnicastRef.newCall(Unknown Source) at sun.rmi.registry.RegistryImpl_Stub.lookup(Unknown Source) at java.rmi.Naming.lookup(Unknown Source) at com.jmdoudoux.test.rmi.LanceClient.main(LanceClient.java:17) Caused by: java.net.ConnectException: Connection timed out: connect at java.net.TwoStacksPlainSocketImpl.socketConnect(Native Method) at java.net.AbstractPlainSocketImpl.doConnect(Unknown Source) at java.net.AbstractPlainSocketImpl.connectToAddress(Unknown Source) at java.net.AbstractPlainSocketImpl.connect(Unknown Source) at java.net.PlainSocketImpl.connect(Unknown Source) at java.net.SocksSocketImpl.connect(Unknown Source) at java.net.Socket.connect(Unknown Source) at java.net.Socket.connect(Unknown Source) at java.net.Socket.<init>(Unknown Source) at java.net.Socket.<init>(Unknown Source) at sun.rmi.transport.proxy.RMIDirectSocketFactory.createSocket(Unknown S ource) at sun.rmi.transport.proxy.RMIMasterSocketFactory.createSocket(Unknown S ource) ... 7 more Fin du client

La partie client peut tre lance avec un gestionnaire et une politique de scurit associe. Exemple ( code Java 1.1 ) : 1. 2. 3. 4. 5. 6. C:\temp>java -jar -Djava.security.policy=ma_policy_client TestRMIClient.jar Lancement du client InformationImpl_Stub[UnicastRef [liveRef: [endpoint:[10.0.0.13:62802](remote),ob jID:[7b7739e4:135b4a87a5e:-7fff, -3323459310870193038]]]] chaine renvoyee = bonjour Fin du client

Si le gestionnaire est activ sans policy associ, alors la connexion au serveur est impossible. Exemple ( code Java 1.1 ) : open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

01. 02. 03. 04. 05. 06. 07. 08. 09. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23.

C:\temp>java -jar -Djava.security.manager TestRMIClient.jar Lancement du client Exception in thread "main" java.security.AccessControlException: access denied ( "java.net.SocketPermission" "10.0.0.13:1099" "connect,resolve") at java.security.AccessControlContext.checkPermission(Unknown Source) at java.security.AccessController.checkPermission(Unknown Source) at java.lang.SecurityManager.checkPermission(Unknown Source) at java.lang.SecurityManager.checkConnect(Unknown Source) at java.net.Socket.connect(Unknown Source) at java.net.Socket.connect(Unknown Source) at java.net.Socket.<init>(Unknown Source) at java.net.Socket.<init>(Unknown Source) at sun.rmi.transport.proxy.RMIDirectSocketFactory.createSocket(Unknown S ource) at sun.rmi.transport.proxy.RMIMasterSocketFactory.createSocket(Unknown S ource) at sun.rmi.transport.tcp.TCPEndpoint.newSocket(Unknown Source) at sun.rmi.transport.tcp.TCPChannel.createConnection(Unknown Source) at sun.rmi.transport.tcp.TCPChannel.newConnection(Unknown Source) at sun.rmi.server.UnicastRef.newCall(Unknown Source) at sun.rmi.registry.RegistryImpl_Stub.lookup(Unknown Source) at java.rmi.Naming.lookup(Unknown Source) at com.jmdoudoux.test.rmi.LanceClient.main(LanceClient.java:17)

Dveloppons en Java

v 1.80

Copyri ght (C) 1999-2012 Jean-Mi chel DOUDOUX

open in browser PRO version

Are you a developer? Try out the HTML to PDF API

pdfcrowd.com

You might also like