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:
- client sends SAR with Exchange-Type set to INITIAL, server resonds with SAA
- session starts
- it can be followed by number of SAR with Exchange-Type set to INTERMEDIATE
- session continues
- client sends SAR with Exchange-Type set to TERMINATING
- 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:
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:
is a bit more complicated. It is used to inform application on result of request send operation. It is defined as follows:
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:
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:
To our example only two are relevant(Parameters are of no interest for now):
Network tag contains configuration information concerning network environment:
Below is example configuration file for stack which:
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:
Thirdly, we need code which will initialize stack, session factory and register us as dummy listener for network requests. It could look as follows:
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
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</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
- 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."); } }
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
@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.