Friday, November 19, 2010

Diameter Educational Series: Creating a "Hello World" Application with Mobicents Diameter

For some time now the Mobicents Diameter Team have been wondering how should we introduce diameter use cases to mobicents community. After some talk we decided to create series of blog posts with step by step instructions and examples.

In next posts, me and Alex will cover topics ranging from simple "Hello World Example" to prepaid billing service or some IMS components.

Some basics

As weird as it may seem in diameter domain, first example is always "hello world", so... Always simplistic, to show how to play in new environment. In this post I will describe basics of interaction in diameter world. I wont however describe protocol level details(like CER/CEA capabilities exchange).


Diameter Nodes.

Diameter node is a network entity which supports diameter protocol and certain application. Each node is identified by certain set of informations:
  • realm
  • URI


Diameter realm is simply string, for instance "mobicents.org".  It identifies nodes domain of responsibility.
URI identifies diameter nodes in realm. It has following form: aaa://[:port][;params]*

Nodes which has direct connection are called Diameter Peers. Diameter Peers have set of common applications. When connection is established between two diameter nodes, this set  is determined. If there are no common applications, connection is not created. This implies that peers understand all exchanged messages and AVPs. 

Messages and AVPs

All diameter data structures exchanged over wire have similar structure:
  • header
  • payload
Structure is proper word, since proper interpretation depends on metadata. Metadata is encoded in header.
Header contains following information:
  • ID
  • flags
  • length
  • (possibly additional information)

ID is a set of unique codes identifying structure. IDs include structure code and vendor ID(or application ID).
Flags are a set of bit switches. Flags carry additional information which may be helpful in proper interpretation of structure.
Payload is simply byte encoded content of structure, it may be simple data like integer number, or another set of structures. To see visualization please refer to this page. Since payload is byte[], its proper interpetation depends on type associated to ID of structure.



Diameter Session

Messages are exchanged within session. Session is a relation of exchanged messages to particular activity, for instance accounting. Each diameter application defines life cycle of session and types of messages that can be exchanged within session. This allows peers to communicate with integrity and confidentiality.
Sessions are identified by unique session id(AVP) present in each message exchanged. 


Diameter Application

Diameter Application is not a software application. It is definition of protocol based on Diameter Base Protocol(RFC3588). Protocol is correct term since Diameter Application defines new messages, session state machines and AVPs.
Step by step example
Now for purpose of this example lets define our own, private application. Our example application has to meet following requirements:

  • define new messages
  • define new AVPs
  • define very simple FSM for session to allow strict message exchange

Lets consider IDs of application as follows:
ApplicationID: 33333
VendorID : 66666

In this example we want our application to exchange text data within custom messages.
Lets consider new type of message with name: SomeApplication-Exchange with message code equal to686 and application ID set to examples application ID.

Request(SER - SomeApplication Exchange Request) is defined as follows:
SER ::=  < Diameter Header: 686, 33333, REQ, PXY > 
                                        < Session-Id >
                                        { Origin-Host }
                                        { Origin-Realm }
                                        { Destination-Realm }
                                       { Destination-Host }
                                       { Auth-Application-Id }
                                       { Exchange-Type }
                                       { Exchange-Data } 
                                       * [ Proxy-Info ] 
                                       * [ Route-Record ] 
                                       * [ AVP ]

Answer is defined as follows:
SEA ::= < Diameter Header: 686, 33333, PXY >

                                       < Session-Id > 
                                       { Result-Code } 
                                       { Origin-Host } 
                                       { Origin-Realm }
                                       { Auth-Application-Id } 
                                       { Exchange-Type }
                                       { Exchange-Data }
                                       * [ Proxy-Info ] 
                                       * [ AVP ]



Message definition contains two non standard AVPs which are crucial to example application.
Both AVPs are specific to this application and are simple, not grouped AVPs. They are defined as follows:

                                +---------------------+
                                |    AVP Flag rules   |
                                |----+-----+----+-----|----+
                |  | |SHLD| MUST|MAY |
Attribute Name  Code Data Type  |MUST| MAY | NOT|  NOT|Encr|
--------------------------------|----+-----+----+-----|----|
Exchange-Type    888 Enumerated | M,V|  P  |    |    | N  |
Exchange-Data    999 UTF8String | M,V|  P  |    |    | N  |


Exchange-Type is simple enumerated AVP. Its code is 888 with Vendor ID equal to our examples vendor ID.
There are three possible values that this AVP may carry:
  • 0: INITIAL - indicates start of data session exchange
  • 1: INTERMEDIATE - indicates follow up exchange(consequent exchange)
  • 2: TERMINATING - indicates last exchange, after answer is sent, session is terminated

Exchange-Data is simple UTF8String AVP. Its code is 999 with Vendor ID equal to our examples vendor ID. It contains data that has to be exchanged.

Since AVPs and messages are defined we need to define session FSM, so we know how to code exchange of messages. Since it has to be simple, lets consider such scenario:
  1. client sends SAR with Exchange-Type set to INITIAL, server resonds with SAA
  2. session starts
  3. it can be followed by number of SAR with Exchange-Type set to INTERMEDIATE
  4. session continues
  5. client sends SAR with Exchange-Type set to TERMINATING
  6. session ends
Diagram below depicts messages exchange:


Network
Since application logic and message flow has been defined, we can start creating client and server code. However to do that we need to understand how stack interacts with network.
For this example two types of listeners used by stack are relevant to us:

  • NetworkReqListener
  • EventListener
NetworkReqListener is very simple one. It is invoked for requests incoming from network layer. It has single method "processRequest". It is defined as follows:
public interface NetworkReqListener {
        Answer processRequest(Request request);
}
Instances of this listener type are registered in stack. With registration, stack is given information which applications are of interest for particular listener. That is, listener method is invoked for messages which belong to specific application.
Single method defined in this interface has return type of "Answer". Implementation of this interface may return non null value, in such case its sent as answer to request, for which method is invoked. However if method invocation does not return value, nothing bad happens. In such case, user code has to call session object to send answer explicitly.


Registration of this listener type looks as follows:
Stack stack;
 ....
 ApplicationId authAppId = ApplicationId.createByAuthAppId(....);

 Network network = stack.unwrap(Network.class);
    network.addNetworkReqListener(new NetworkReqListener() {
     
     @Override
     public Answer processRequest(Request request) {
      //this wontbe called.
      return null;
     }
    }, authAppId);
 
EventListener is a bit more complicated. It is used to inform application on result of request send operation. It is defined as follows:


public interface EventListener { 
  
     void receivedSuccessMessage(R request, A answer);
     
     void timeoutExpired(R request);
     
 }
It is invoked in two cases:

  • there has been no answer to request: timeoutExpired(R request)
  • answer has been received for request send from this stack: receivedSuccessMessage(R request, A answer)

Instance of this listener type is passed to session object when new request is sent through it.
Sessions and messages exchange
Messages can be exchanged between peers by means of Session objects(RawSession or Session). In our example we will use Session object. Each object of this type represents single diameter session(identified by session-id AVP present in messages). Session object is very basic one. Session object may be in two states: valid and invalid. It is invalidated explicitly by user. Once invalidated, it is not eligible for further use.
Session objects can be obtained from SessionFactory in following way:

SessionFactory factory = stack.getSessionFactory();
 //create new session, with auto generated Session-Id
 Session session = factory.getNewSession(); 


Stack configuration
Stack is configured with instance of "Configuration" object. Current implementation provides instance of this interface which parses XML configuration file. For detailed description of XML tags used in this file please refer to stack guide. Below there is quick guide to essentials we need to make our example application work.
Definition of configuration file looks as follows:


<xsi:element name="Configuration">
        <xsi:annotation>
            <xsi:documentation>JDiameter configuration&lt/xsi:documentation>
        </xsi:annotation>
        <xsi:complexType>
            <xsi:sequence>
                <xsi:element ref="LocalPeer" minOccurs="1" maxOccurs="1"/>
                <xsi:element ref="Parameters" minOccurs="1" maxOccurs="1"/>
                <xsi:element ref="Network" minOccurs="1" maxOccurs="1"/>
                <xsi:element ref="Security" minOccurs="0" maxOccurs="1"/>
                <xsi:element ref="Extensions" minOccurs="0" maxOccurs="1"/>
            </xsi:sequence>
        </xsi:complexType>
    </xsi:element
>


To our example only two are relevant(Parameters are of no interest for now):

  • LocalPeer
  • Network
LocalPeer contains configuration options for local diameter node. It includes information like:

    • local peer URI( like aaa://our.localhost:34200 )
    • IP addresses
    • Realm name to which local peer belongs to
    • overload monitor and applications configuration

Network tag contains configuration information concerning network environment:

    • Realms to which local peer is connected
    • Remote peers to which local peer is connected

Below is example configuration file for stack which:

  • identifies itself as billing.server.company.org, bound to port 13868
  • bound to two NICs
  • belongs to "billing" domain.
  • supports one realm: "service"
  • is connected to single remote peer


<?xml version="1.0"?>
<Configuration xmlns="http://www.jdiameter.org/jdiameter-server">

        <LocalPeer>
                <URI value="aaa://billing.server.company.org:3868" />

                <IPAddresses>
                        <IPAddress value="212.30.0.12" />
                        <IPAddress value="212.30.1.12" />
                </IPAddresses>
                <Realm value="billing" />
                <VendorID value="0" />
                <ProductName value="jDiameter" />
                <FirmwareRevision value="1" />
                <OverloadMonitor>
                        <Entry index="1" lowThreshold="0.5" highThreshold="0.6">
                                <ApplicationID>
                                        <VendorId value="0" />
                                        <AuthApplId value="333333" />
                                        <AcctApplId value="0" />
                                </ApplicationID>
                        </Entry>
                </OverloadMonitor>
        </LocalPeer>

        <Parameters>
                ...
        </Parameters>

        <Network>
                <Peers>
                        <Peer name="aaa://212.31.0.12:3868" attempt_connect="true" rating="1" />
                </Peers>
                <Realms>
                        <Realm name="service" peers="212.31.0.12" local_action="LOCAL" dynamic="false" exp_time="1">
                                <ApplicationID>
                                        <VendorId value="0" />
                                        <AuthApplId value="333333" />
                                        <AcctApplId value="0" />
                                </ApplicationID>
                        </Realm>
                </Realms>
        </Network>
        <Extensions />
</Configuration>

Application Client
Since now we know some basics, we can start creating our client code. Step by step, we should be able to create simple source which will act according to our application FSM.
First we need definition of some static fields, with some values relevant to our application and configuration. Static definition may look as follows:
//configuration files
 private static final String configFile = "org/example/client/client-jdiameter-config.xml";
 private static final String dictionaryFile = "org/example/client/dictionary.xml";
 //our destination
 private static final String serverHost = "127.0.0.1";
 private static final String serverPort = "3868";
 private static final String serverURI = "aaa://" + serverHost + ":" + serverPort;
 //our realm
 private static final String realmName = "exchange.example.org";
 // definition of codes, IDs
 private static final int commandCode = 686;
 private static final long vendorID = 66666;
 private static final long applicationID = 33333;
 private ApplicationId authAppId = ApplicationId.createByAuthAppId(applicationID);
        //avp codes
 private static final int exchangeTypeCode = 888;
 private static final int exchangeDataCode = 999;
 // enum values for Exchange-Type AVP
 private static final int EXCHANGE_TYPE_INITIAL = 0;
 private static final int EXCHANGE_TYPE_INTERMEDIATE = 1;
 private static final int EXCHANGE_TYPE_TERMINATING = 2;
 //list of data we want to exchange.
 private static final String[] TO_SEND = new String[] { "I want to get 3 answers", "This is second message", "Bye bye" };
 





Secondly, we require definition of some objects, like stack, session etc. It could look as follows:

//Dictionary, for informational purposes.
 private AvpDictionary dictionary = AvpDictionary.INSTANCE;
 private Stack stack;
 private SessionFactory factory;
        // ////////////////////////////////////////
 // Objects which will be used in action //
 // ////////////////////////////////////////
 private Session session;  // session used as handle for communication
 private int toSendIndex = 0;  //index in TO_SEND table
 private boolean finished = false;  //boolean telling if we finished our interaction



Thirdly, we need code which will initialize stack, session factory and register us as dummy listener for network requests. It could look as follows:

private void initStack() {
  if (log.isInfoEnabled()) {
   log.info("Initializing Stack...");
  }
  InputStream is = null;
  try {
   //Parse dictionary, it is used for user friendly info.
   dictionary.parseDictionary(this.getClass().getClassLoader().getResourceAsStream(dictionaryFile));
   log.info("AVP Dictionary successfully parsed.");
   
   this.stack = new StackImpl();
   //Parse stack configuration
   is = this.getClass().getClassLoader().getResourceAsStream(configFile);
   Configuration config = new XMLConfiguration(is);
   factory = stack.init(config);
   if (log.isInfoEnabled()) {
    log.info("Stack Configuration successfully loaded.");
   }
   //Print info about applicatio
   Set<org.jdiameter.api.ApplicationId> appIds = stack.getMetaData().getLocalPeer().getCommonApplications();

   log.info("Diameter Stack  :: Supporting " + appIds.size() + " applications.");
   for (org.jdiameter.api.ApplicationId x : appIds) {
    log.info("Diameter Stack  :: Common :: " + x);
   }
   is.close();
   //Register network req listener, even though we wont receive requests
   //this has to be done to inform stack that we support application
   Network network = stack.unwrap(Network.class);
   network.addNetworkReqListener(new NetworkReqListener() {
    
    @Override
    public Answer processRequest(Request request) {
     //this wontbe called.
     return null;
    }
   }, this.authAppId); //passing our example app id.
   
  } catch (Exception e) {
   e.printStackTrace();
   if (this.stack != null) {
    this.stack.destroy();
   }

   if (is != null) {
    try {
     is.close();
    } catch (IOException e1) {
     // TODO Auto-generated catch block
     e1.printStackTrace();
    }
   }
   return;
  }

  MetaData metaData = stack.getMetaData();
  //ignore for now.
  if (metaData.getStackType() != StackType.TYPE_SERVER || metaData.getMinorVersion() <= 0) {
   stack.destroy();
   if (log.isEnabledFor(org.apache.log4j.Level.ERROR)) {
    log.error("Incorrect driver");
   }
   return;
  }

  try {
   if (log.isInfoEnabled()) {
    log.info("Starting stack");
   }
   stack.start();
   if (log.isInfoEnabled()) {
    log.info("Stack is running.");
   }
  } catch (Exception e) {
   e.printStackTrace();
   stack.destroy();
   return;
  }
  if (log.isInfoEnabled()) {
   log.info("Stack initialization successfully completed.");
  }
 }
Finally we need three more pieces of code to handle initialization of whole procedure, request send and answer handling.
Initialization part could be straightforward and could look as follows:


private void start() {
  try {
   //wait for connection to peer
   try {
    Thread.currentThread().sleep(5000);
   } catch (InterruptedException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
   }
   //do send
   this.session = this.factory.getNewSession("BadCustomSessionId;YesWeCanPassId;" + System.currentTimeMillis());
   sendNextRequest(EXCHANGE_TYPE_INITIAL);
  } catch (InternalException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  } catch (IllegalDiameterStateException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  } catch (RouteException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  } catch (OverloadException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }

 }



Part which actually creates request and sends it via session objects is also quite trivial. It could look as follows:

private void sendNextRequest(int enumType) throws InternalException, IllegalDiameterStateException, RouteException, OverloadException {
  Request r = this.session.createRequest(commandCode, this.authAppId, realmName, serverURI);
  // here we have all except our custom avps

  AvpSet requestAvps = r.getAvps();
  // code , value , vendor, mandatory,protected,isUnsigned32
  // (Enumerated)
  Avp exchangeType = requestAvps.addAvp(exchangeTypeCode, (long) enumType, vendorID, true, false, true); // value is set on creation
  // code , value , vendor, mandatory,protected, isOctetString
  Avp exchangeData = requestAvps.addAvp(exchangeDataCode, TO_SEND[toSendIndex++], vendorID, true, false, false); // value is set on creation
  // send, pass oursevles as EventListener object
  this.session.send(r, this);
  dumpMessage(r,true);
 }


In above code, when request is sent this is passed as argument(EventListener). So our client object, must also handle answers. Lets consider that we want to perform following:


  • Send request with some data encoded as string
  • Receive properly formatted answer, with the same data client send
  • In case of error answer, client should terminate operations
Code which does above could look as follows:
@Override
 public void receivedSuccessMessage(Request request, Answer answer) {
  dumpMessage(answer,false);
  if (answer.getCommandCode() != commandCode) {
   log.error("Received bad answer: " + answer.getCommandCode());
   return;
  }
  AvpSet answerAvpSet = answer.getAvps();

  Avp exchangeTypeAvp = answerAvpSet.getAvp(exchangeTypeCode, vendorID);
  Avp exchangeDataAvp = answerAvpSet.getAvp(exchangeDataCode, vendorID);
  Avp resultAvp = answer.getResultCode();

  
  try {
   //for bad formatted request.
   if (resultAvp.getUnsigned32() == 5005 || resultAvp.getUnsigned32() == 5004) {
    // missing || bad value of avp
    this.session.release();
    this.session = null;
    log.error("Something wrong happened at server side!");
    finished = true;
   }
   switch ((int) exchangeTypeAvp.getUnsigned32()) {
   case EXCHANGE_TYPE_INITIAL:
    // JIC check;
    String data = exchangeDataAvp.getUTF8String();
    if (data.equals(TO_SEND[toSendIndex - 1])) {
     // ok :) send next;
     sendNextRequest(EXCHANGE_TYPE_INTERMEDIATE);
    } else {
     log.error("Received wrong Exchange-Data: " + data);
    }
    break;
   case EXCHANGE_TYPE_INTERMEDIATE:
    // JIC check;
    data = exchangeDataAvp.getUTF8String();
    if (data.equals(TO_SEND[toSendIndex - 1])) {
     // ok :) send next;
     sendNextRequest(EXCHANGE_TYPE_TERMINATING);
    } else {
     log.error("Received wrong Exchange-Data: " + data);
    }
    break;
   case EXCHANGE_TYPE_TERMINATING:
    data = exchangeDataAvp.getUTF8String();
    if (data.equals(TO_SEND[toSendIndex - 1])) {
     // good, we reached end of FSM.
     finished = true;
     // release session and its resources.
     this.session.release();
     this.session = null;
    } else {
     log.error("Received wrong Exchange-Data: " + data);
    }
    break;
   default:
    log.error("Bad value of Exchange-Type avp: " + exchangeTypeAvp.getUnsigned32());
    break;
   }
  } catch (AvpDataException e) {
   // thrown when interpretation of byte[] fails
   e.printStackTrace();
  } catch (InternalException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  } catch (IllegalDiameterStateException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  } catch (RouteException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  } catch (OverloadException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }

 }

As you can see, client code is quite simple. It is not smarty-pants code, its sole purpose is to show how to interact with stack.
Server code is very similar. Whole source with configuration files and ready to run scripts can be found in our gcode svn.






5 comments:

  1. How to run this Application, Do i have to run using Application server. i am looking for Stand alone client, as Server would be in remote place, again not sure server implementaion . My job would be keep connection and send CER request and get ANS.If you have any Sample Would be helpfull.

    ReplyDelete
  2. hello thank you for this great tutorial simple and clear.
    Which part (mainly) should i modify in order to SBBs and seagull to simulate the same ?
    regards

    ReplyDelete
  3. Hi, Can i have an implementation for DCCA, event based charging.

    Thanks

    ReplyDelete
    Replies
    1. HI, i just come across this blog.. I have to build a seperate application , which can charge from customer on event base.. I am studing/reviewing different solution.

      Do you have any solution or idea? .. As i have to do this is Diameter Credit Control Request.

      Please respond. Here is my email
      ehtasham@expertflow.com

      Delete
  4. can you send the references for this code to my mail.That would be very helpful.

    ReplyDelete