Rails Tutorial DHBW-Heidenheim

SOAP und REST

Webservices unterstützen die Zusammenarbeit verschiedener (Web-)Anwendungen. Ein Webservice wird üblicherweise durch eine URI identifiziert und über HTTP aufgerufen. Das Datenaustauschformat ist meist XML.

Webservices sind nicht zwangsläufig an das HTTP-Protokoll gebunden, denkbar wäre auch eine Implementierung via SMTP oder FTP. Zur Übergabe von Daten und Parametern existieren verschiedene Techniken.

Zunächst betrachten wir SOAP und implementieren anschliessend einen mit REST bereitgestellten Webservice in die Anwendung.

SOAP

Um einen Webservice mit SOAP in eine Rails Applikaiton einzubinden ist ein entsprechender Client erforderlich, etwa handsoap oder savon. Als Beispiel-Service verwenden wir einen Webservice der einer übergebenen IP-Adresse das Ursprungsland zuordnet - diesen .

Bevor der Webservice implementiert werden kann, muss der SOAP-Client (hier Savon) installiert werden.


# Gemfile.rb
source 'http://rubygems.org'

gem 'rails', '3.0.3'

gem 'savon'

...

und anschliessend das Kommando


bundle install

ausgeführt werden.

Die Details der Verwendung eines mit SOAP bereitgestellten Webservices, etwa der korrekte Aufruf, Parameter oder Rückgabewerte können mit WSDL definiert werden. Ist eine solche Beschreibung vorhanden, kann direkt ein Soap Client erzeugt werden:


rb(main): client = Savon::Client.new
=> #<Savon::Client:0xb66436e0 @http=#<HTTPI::Request:0xb6643690>, 
@wsdl=#<Savon::WSDL::Document:0xb66436a4 @document=nil, 
@request=#<HTTPI::Request:0xb6643690>>>

Ist die WSDL-Beschreibung nicht als Datei lokal vorhanden, sondern soll diese direkt vom Anbieter geladen werden (an sich ja durchaus sinnvoll, aber nicht der Weisheit letzter Schluss, was die Performance angeht) kann auch eine auf die WSDL-Datei verweisende URL übergeben werden:


rb(main): client.wsdl.document = "http://www.webservicex.net/geoipservice.asmx?WSDL"
=> "http://www.webservicex.net/geoipservice.asmx?WSDL"

Die Methode soap_actions liefert eine Liste der definierten Funktionen:


rb(main): client.wsdl.soap_actions
Retrieving WSDL from: http://www.webservicex.net/geoipservice.asmx?WSDL
=> [:get_geo_ip_context, :get_geo_ip]

Testen wir mal die Funktion get_ggeo_ip, etwa mit einer Adresse von google - 209.85.148.104 - und schauen, was passiert.


rb(main): response = client.request  :v1, :get_geo_ip do
irb(main): soap.body = { :IPAddress => "209.85.148.104" }
irb(main): end
SOAP request: http://www.webservicex.net/geoipservice.asmx
SOAPAction: "http://www.webservicex.net/GetGeoIP", 
Content-Type: text/xml;charset=UTF-8
<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:v1="http://www.webservicex.net/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
<env:Body>
<v1:GetGeoIP>
<v1:IPAddress>209.85.148.104</v1:IPAddress>
</v1:GetGeoIP>
</env:Body>
</env:Envelope>

SOAP response (status 200):
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Body>
<GetGeoIPResponse xmlns="http://www.webservicex.net/">
<GetGeoIPResult>
<ReturnCode>1</ReturnCode>
<IP>209.85.148.104</IP>
<ReturnCodeDetails>Success</ReturnCodeDetails>
<CountryName>United States</CountryName>
<CountryCode>USA</CountryCode>
</GetGeoIPResult></GetGeoIPResponse>
</soap:Body></soap:Envelope>=> 

#<Savon::SOAP::Response:0xb6594b68 @http=#<HTTPI::Response:0xb65947f8 @raw_body="
<?xml version=\"1.0\" encoding=\"utf-8\"?>
<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" 
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">
<soap:Body>
<GetGeoIPResponse xmlns=\"http://www.webservicex.net/\">
<GetGeoIPResult>
<ReturnCode>1</ReturnCode>
<IP>209.85.148.104</IP>
<ReturnCodeDetails>Success</ReturnCodeDetails>
<CountryName>United States</CountryName>
<CountryCode>USA</CountryCode>
</GetGeoIPResult></GetGeoIPResponse>
</soap:Body>
</soap:Envelope>", @body="
<?xml version=\"1.0\" encoding=\"utf-8\"?>
<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" 
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">
<soap:Body>
<GetGeoIPResponse xmlns=\"http://www.webservicex.net/\">
<GetGeoIPResult><ReturnCode>1</ReturnCode>
<IP>209.85.148.104</IP>
<ReturnCodeDetails>Success</ReturnCodeDetails>
<CountryName>United States</CountryName>
<CountryCode>USA</CountryCode>
</GetGeoIPResult></GetGeoIPResponse>
</soap:Body>
</soap:Envelope>", @code=200, @headers={"x-powered-by"=>"ASP.NET", "x-aspnet-version"=>"4.0.30319", 
"content-type"=>"text/xml; charset=utf-8", "date"=>"Mon, 18 Apr 2011 19:49:25 GMT", 
"server"=>"Microsoft-IIS/7.0", "content-length"=>"517", 
"cache-control"=>"private, max-age=0"}>, 
@soap_fault=Savon::SOAP::Fault, 
@http_error=Savon::HTTP::Error>

Dieses Ergebnis ist zwar sehr ausführlich, aber nur schwer direkt weiter zu verarbeiten. Sinnvollerweise konvertiert man die Antwort in eine Hash-Tabelle:


irb(main): response.to_hash
=> {:get_geo_ip_response=>{:xmlns=>"http://www.webservicex.net/", 
:get_geo_ip_result=>{:country_name=>
"United States", :country_code=>"USA", 
:return_code_details=>"Success", :ip=>"209.85.148.104", :return_code=>"1"}}}

... das sieht schon deutlich besser aus. So kann vergleichsweise einfach ein einzelnes Element ausgewertet werden, hier etwa das Land:


irb(main): response.to_hash[:get_geo_ip_response][:get_geo_ip_result][:country_name]
=> "United States"

das wars - viel Spass.

REST

REST (Representational State Transfer) ist neben SOAP eine weitere Technik, mit der Web Services aufgerufen werden können. Die Idee ist, ausschliesslich mit dem HTTP-Protokoll auf Resourcen einer Web-Anwendung zuzugreifen. Die Idee dabei ist, dass viele Web-Anwendungen im wesentlichen eben Resourcen bereitstellen und manipulieren. Was genau mit einer Resource passieren soll, wird durch die HTTP-Methode festgelegt, um welche Resource es geht durch die URL. Der Verwendung des REST-Standards in einer Rails-Applikation wird auch als RESTful Rails bezeichnet.

In den vorhergehenden Lessons wurden die Bestandteile eines mit dem Scaffold-Generator generierten Controllers erläutert - die CRUD-Methoden. Mit dem REST-Standard kann ohne dass änderungen an der Applikation vorgenommen werden, von Außerhalb auf die Ressourcen zugegriffen werden. Wie das möglich ist, wird anhand des GroupsController der Beispielanwendung demonstriert.

Der GroupsController wurde mit dem Scaffold-Generator erzeugt. Alternativ hätte auch der Ressource-Generator angewendet werden können. Der Unterschied ist, dass der Scaffold-Generator Views anlegt werden, der Ressource-Generator nicht. Wenn die Daten eines Controllers für die Verarbeitung von anderen Methoden, Anwendungen oder Systemen vorgesehen sind, ist die Generierung mit dem Ressource-Generator ausreichend.

Während der Generierung des Controllers wurde in der Datei /config/routes.rb folgender Eintrag erzeugt:


Anwendung::Application.routes.draw do

  resources :groups

...

Durch die Klassifizierung der Gruppen als Ressource sind einige Routing-Einträge vorhanden:

Die Index-Methode des GroupsControllers


GET /groups
link_to 'Alle Gruppen', groups_path

Eine spezielle Ressource anfordern


GET /groups/1
link_to 'Gruppe anzeigen', group_path(1)

Eine Ressource anlegen


GET /groups/new
link_to 'neue Gruppe', new_group_path

Eine Ressource bearbeiten


GET /groups/1/edit
link_to 'Gruppe bearbeiten', edit_group_path(1)

Eine Ressource erzeugen - Die Create-Methode eines Controllers ausführen


POST /groups
form_for(@groups) do 
#Felder für die Gruppe
end

Eine Ressource ändern - Die Update-Methode des Controllers ausführen


Put /groups
form_for(@groups) do 
#Felder für die Gruppe
end

Eine Ressource löschen


DELETE /groups/1
link_to 'Gruppe löschen', group_path(1), :method => :delete

Das Anbieten eines Webservices wird durch den Einsatz vom Ressource- oder Scaffold-Generator vorbereitet:


class GroupsController < ApplicationController

  # GET /groups
  # GET /groups.xml
  def index
    @groups = Group.all

    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @groups }
    end
  end
...

Der respond_to Block erbt von der Superclasse ApplicationController. Das Rückgabeformat der Index-Methode kann damit bestimmt werden. Im Standard kann html und xml angefordert werden. Die entsprechenden Formate können allerdings erweitert werden, etwa pdf oder csv.

In Html - http://localhost:3000/groups

In XML - http://localhst:3000/groups.xml

Implementierung eines Webservices in die Beispielanwendung

Manchmal ist es ganz interesant zu wissen, wie gross die Entfernung zu dem Menschen hinter dem Profil ist. Die Beispielanwendung wird um die Methoden get_distance erweitert, die genau diese Entfernung berechnen soll. Das selber zu realisieren wäre allerdings extrem aufwendig, hier greifen wir auf die Dienste anderer zurück - wir nutzen einen Webservice.

Wie arbeitet get_distance?

Zunächst werden die geografischen Koordinaton (Länge und Breite) einer Person aus der Postleitzahl (steht im Profil) ermittelt. Dazu verwenden wir einen service von yahoo.

Bleibt noch die Berechnung der Entfernung zwischen zwei Punkten mit bekannten Koordinaten. Da die Erde bekanntlich eine Kugel ist, ist dies nicht ganz so einfach. Die Haversine-Formel liefert genau diese Entfernung. Natürlich kann so nur der "Luftlinienabstand" berechnet werden, um etwa die Fahrtstrecke zu erhalten, müsste ein Routenplaner verwendet werden. Den gibt's allerdings (unserer Kenntnis nach) nicht umsonst :-(

Wie im Screencast gezeigt, wird die Antwort des Webservices im JSON-Format ankommen. JSON steht für Java Script Object Notation und ist ein weit verbreitetes Format für den Datenaustausch.


# Gemfile.rb
source 'http://rubygems.org'

gem 'rails', '3.0.3'

gem 'json'

...

und noch installieren:


bundle install

Ermittlung der Koordinaten im UserModel:


def self.get_location(plz)
   url = "http://where.yahooapis.com/geocode?country=Germany&postal=" + plz +"&flags=CJ"
   res = Net::HTTP.get_response(URI.parse(url))
   parsed = JSON.parse(res.body)
   return parsed['ResultSet']['Results'][0]
end

Wie im Screencast gezeigt, wird der Webservice über eine URL aufgerufen. Es wird das Land, die Postleitzahl sowie das Format der Antwort - JSON - übergeben. Die Methode get_location gibt das Element zurück, das Länge und Breite enthält.

Die Entfernung der beiden Koordinaten werden mit der Haversine-Formel berechnet. Es gibt bereits fertige Ruby-Implementierunten, etwa diese . Der Quellcode wird in die Datei /app/models/haversine.rb kopiert.

Erweiterung der Profil-Methode im UsersController:


requrire 'haversine.rb'
class UsersController < ApplicationController

def profile
   @user = User.find(params[:id])       
   @distances = Hash.new # Hash für Ergebnis erzeugen
   @loc1 = User.get_location(current_user.plz)
   @loc2 = User.get_location(@user.plz)
   haversine_distance(Float(@loc1["latitude"]), Float(@loc1["longitude"]), 
Float(@loc2["latitude"]), Float(@loc2["longitude"]))
end   

Das Ergebnis in der Profil-View ausgeben - /app/views/users/profile.html.erb


<h1><%= @user.nickname %>'s Profil</h1>

Der Abstand zwischen Euch beträgt:<%= @distances['km'].to_i %>km

Die errechnete Entfernung steht im Hash @distances. Für Deutschland wird die Entfernung in Kilometer angegeben (@distances['km']), andere Möglichkeiten sind etwa Meilen (@distances['mi']), Feet(@distances['ft']) oder Meters(@distances['m']).