Using Socket Servers


A socket server is an application that can accept "socket" connections. Socket connections are persistent, which means that they let you remain connected to a server rather than making a connection just long enough to download information before disconnecting. Unlike a scripted page, a socket server is an application that's always running. It can accept simultaneous connections from multiple computers and exchange information among them. While you're connected to a socket server, you can send or receive information at any time. Using socket connections to continually transfer data to and from the server is how most chats and multiplayer games are created in Flash.

A key principle to understand about using socket connections with Flash is that you don't have to request information to get informationfor example, in a chat application, a message can be pushed into Flash at any time without Flash having to ask for it.

Introduction to the XMLSocket Class

This section provides a basic introduction to Flash's built-in XMLSocket class. This discussion is simply a guide to the use of this built-in class, so you can familiarize yourself with the general concepts needed for plugging your applications into nearly any socket server. The exercise that follows makes use of a special socket server that wraps most of the functionalities you're about to learn into a simple-to-use class. But more on this in a bit. Let's look at the inherent way Flash communicates with a socket server.

Before you can connect a Flash movie to a socket server, you must create a new XMLSocket instance, using the constructor for the XMLSocket class. You can then use the methods of the instance to connect to a server and exchange information. In this section, we'll show you how to create and use an XMLSocket instance while also using the XML class methods and properties introduced earlier in this lesson.

To create a new XMLSocket instance, you must use the constructor for XMLSocket. Here's an example:

  var server:XMLSocket = new XMLSocket();


This line of ActionScript creates a new XMLSocket instance named server. To connect the XMLSocket to a server, you simply employ the connect() method using the following syntax:

  server.connect(hostName,port)


The hostName parameter is the IP address on which the socket server residesusually a numeric sequence (for example, 65.134.12.2). IP addresses such as 127.0.0.1 or localhost are valid references to your own computer. If you type http://localhost into your web browser's address bar, it would try to connect to your computer as if it were a website. The port parameter refers to the port on which the server is listening. Flash can connect only to ports higher than 1024. For example:

  var server:XMLSocket = new XMLSocket();   server.connect("localhost", 9999)


You can close a connection with a socket by using the close() method:

  server.close();


To send information via the socket connection, simply use the send() method and pass in the instance you want to send. For example:

  server.send("<Text>Hi</Text>");


The XMLSocket class can respond to the following types of events:

  • onConnect This event fires when the connection is accepted or fails.

  • onXML This event fires when information arrives via the socket connection. This action lets you know that new information has arrived so that you can use it.

  • onClose This event fires when the connection with the socket is lost. This event will not fire as a result of purposely closing the connection from Flash using the XMLSocket.close() method.

As we did with the onLoad event in the XML class, we have to define these event handlers with the XMLSocket instance that we create. For example:

   function serverConnected (success:Boolean) {      trace(success);    }    server.onConnect = serverConnected;


Here the serverConnected() function is called when the onConnect event is fired. The success parameter in the function definition has a value of true if the connection was successful and false if the connection was unsuccessful.

The onXML event is used as follows:

  function xmlReceived (data:XML) {     trace(data);   }   server.onXML = xmlReceived;


The xmlReceived() function is called each time information arrives via the socket. The data parameter contains the XML document pushed into Flash.

The onClose event handler can be defined and used as follows:

  function socketClosed () {     //notify the user   }   server.onClose = socketClosed;


You would typically use this type of event to let the user know that a connection has been lost.

ElectroServer 3

To utilize the functionality of any socket server, you can't just upload a script into the CGI-bin of your website or place it in a normal web-accessible directory. Usually written in Java, C, C++, or Visual Basic, socket servers generally require root-level access to the web server. This usually means that you must be running your own dedicated server to install and use a socket server. Fortunately, this isn't as scary as it sounds. As a matter of fact, you can set up a socket server on your own personal computer so that you can develop with it, which is a recommended and common practice when developing applications that use a socket server.

For the next exercise, we'll show you how to get a socket server up and running on your local machine so that you can go on to build a simple chat application that connects to the socket server. To test it, you'll need to use Windows 98, Windows 2000, Windows XP, Windows ME, or newer.

The accompanying CD-ROM contains the installer for a Java-based socket server called ElectroServer 3. To get the latest version of ElectroServer or to find more examples, please visit www.electro-server.com.

Note

ElectroServer 3 is supported by any operating system that supports the JRE. This includes Macintosh OS-X and higher, Linux, UNIX, Windows, and so on. For non-Windows installation instructions for ElectroServer 3 see www.electro-server.com.


The next exercise guides you through the steps to get ElectroServer 3 up and running on your Windows computer.

1.

To install and start ElectroServer 3 on Windows, open the Lesson14/Assets directory. Double-click the file called InstallElectroServer.exe to install ElectroServer 3, and follow the series of prompts to completion. You don't need to change any of the default options during the installation process.

You have just installed ElectroServer 3, the socket server that we'll use in the next exercise to build a Flash chat. If you left the default options selected while installing ElectroServer 3, it also installed several example files onto your hard drive.

2.

To start ElectroServer 3, click Start > All Programs (or Program Files) > Electrotank > ElectroServer 3 > Start ElectroServer.

ElectroServer 3 should start without any problem.

By default, ElectroServer 3 will connect to the 127.0.0.1 IP address, which is the IP address by which your computer refers to itself. Also, the default port on which ElectroServer 3 will exchange data is 9875. Both the IP and the port are configurable, but you won't need to change the settings for the chat exercise.

ElectroServer Class

In the next exercise, you'll build a chat program that communicates with ElectroServer 3. When being developed, a socket server must be programmed to look for a certain protocol. XML is a protocol, but even deeper than that, the socket server must look for XML in a certain structurea protocol within a protocol. For example, if you want to send an XML-formatted login request from Flash to ElectroServer 3, it must use this format:

   <XmlDoc>      <Action>Login</Action>      <Parameters>        <Name>myName</Name>        <Password>myPassword</Password>      </Parameters>    </XmlDoc>


ElectroServer 3 reads the Action node, and then knows what else to look for. When it sees that the Action is Login, it knows to expect a Name node and a Password node. You must use a specific XML protocol for every socket server. XML itself is a standard, but the structure of the XML is specific to the socket server being used.

Does this sound daunting? You can send or receive 100 or so different XML packets in ElectroServer 3 to accomplish tasks such as sending a message, creating a room, and so on. There is good news, though: The ElectroServer class is included with this lesson. The ElectroServer class internally handles all the XML formats that need to be sent or received. You can talk to ElectroServer 3 easily through the ElectroServer class, without having to write a single line of XML!

Note

Within the directory of this project's lesson files is a file named ElectroServer.as. This file defines the ElectroServer class and its capabilities. During exporting or compiling of this movie, Flash uses the contents of this file to enable the exported movie to utilize the functionality of the ElectroServer class. It's important that this file (and its supporting files, named Wddx.as and WddxRecordset.as) exist in the same directory as the completed project file; otherwise, an error will occur when you export the movie.


To send a chat message to the server, this is all you need to do:

   ElectroServer.sendMessage("public", "Hello world!");


This line of ActionScript executes the sendMessage() method of the ElectroServer class. The first parameter, "public", tells the ElectroServer class to send a public message to the entire room. The second parameter is the message to send.

To send a private message to a user named Derek, you would use this line of ActionScript:

   ElectroServer.sendMessage("private", "Hello Derek!", "Derek");


Note

Documentation for every method, property and event of the ElectroServer class can be found in a file named Class_Documentation.html in the directory Program Files\Electrotank\ElectroServer 3\Examples\Flash MX 2004 on your hard drive. To find the most up-to-date ElectroServer class and documentation, visit www.electro-server.com.


It's time to build a simple chat application using ElectroServer 3. A few more basic concepts as well as specific methods and events of the ElectroServer class will be discussed as we go along.

1.

Open Chat1.fla in the Lesson14/Start folder.

The file contains four layers: the Labels layer, which contains the labels for the movie; the Actions layer, where we'll keep the ActionScript; the Assets layer, containing the text fields and buttons; and the Background layer, which contains the interface graphics.

We'll begin by scripting the code to get a user connected to ElectroServer 3, logged in, and joined to a room. A room is nothing more than a collection of users. When a user sends a chat message, it's automatically sent to everyone in the room. ElectroServer 3, like most socket servers, supports multiple rooms. Many rooms can exist at once, each with users. A user can switch from one room to another, as you'll see later in this exercise.

After we've scripted our project to the point where a user can log in and join a room, we'll add the ActionScript needed to display the user list and room list and allow the user to chat. All of this can be done in about 80 lines of code!

2.

With the Actions panel open, select Frame 1 of the Actions layer and add the following script:

  var es:ElectroServer = ElectroServer.getInstance();


The ElectroServer class is a static class (also known as a singleton), which means that only one instance of it can exist within your movie. To create this instance of the ElectroServer class, simply call the getInstance() method directly on the class, and it will return a reference to itself. The line of code in this step creates a variable named es, which is our reference to the instance of the ElectroServer class.

For the rest of this exercise, the ElectroServer class will be accessed by using the es reference created in this step.

Note

We can create an instance of the ElectroServer class only because of the ElectroServer.as file that exists in the same directory as this project file. This .as file is loaded during the process of exporting the project file SWF, enabling all the functionality of the ElectroServer class that we'll script in the following steps.

3.

Using the following code, set the IP and port to which the chat should connect, and load the policy file:

  var ip:String = "127.0.0.1";   var port:Number = 9875;   System.security.loadPolicyFile("xmlsocket://"+ip+":"+port);   es.setIP(ip);   es.setPort(port);


When you installed ElectroServer 3, it created default settings that it would use for its operation. Unless these settings are changed, when you start ElectroServer 3 it will bind to your local IP address (127.0.0.1) and listen on port 9875.

The first two lines of ActionScript create variables to store the IP and port. The third line of ActionScript loads the policy file from ElectroServer. This is a very important line of code if you are planning on having your SWF file and ElectroServer on different domains or IPs. By including that line of ActionScript, the chat file will be given permission to exchange information with ElectroServer.

The final two lines of ActionScript tell the ElectroServer instance which IP and port to use. The ElectroServer class instance will not attempt to connect to ElectroServer 3 until you invoke the connect() method. We'll do that later in the exercise.

4.

With the same frame selected, add the following code to capture the connection response from ElectroServer 3:

  es.onConnection = function(success:Boolean, error:String) {     if (success) {       gotoAndStop("Login");     } else {       msg_txt.text = error;     }   };


In a moment, we'll create the script that connects our application to ElectroServer 3. When the connection happens, an onConnection event occurs, which is what this script handles. Two parameters are passed to the function when the onConnection event is fired: success and error.

The first parameter, success, is a Boolean value. If true, the connection was a success, and the user is taken to the Login frame label to log in. If false, the connection failed. If the connection failed, the second parameter, error, is passed to the function. This parameter contains a string that explains what went wrong. When a failed connection occurs, the else part of the statement is executed. This part of the statement displays the error message in the text field named msg_txt. This text field exists at the Connecting frame label on the Timeline. To understand how this works, it's important to realize that one of the last scripts we will place on this frame (in Step 9) will move the Timeline of our application to the Connecting frame label, where it will wait for a response from the server. If an error occurs, the part of our script that reads as follows will display the error message in the msg_txt text field on the Connecting frame label because our application is paused at that label:

   msg_txt.text = error


What can cause the connection to fail and what kind of error messages are generated? If the connection failed because the ElectroServer class could not find ElectroServer 3 (possibly due to a wrongly specified IP or port, firewall issues, or the fact that the server was not running), error will contain a string that reads, "Could not establish a server connection." Otherwise, an error will be generated dynamically from the server. The server could deny a connection because it has reached its connection limit, which is 20 simultaneous users in the free license version.

Before proceeding further, take a look at the essential steps necessary for chatting using ElectroServer 3. The user must do the following successfully:

1.

Connect to ElectroServer 3.

2.

Log in to ElectroServer 3, which assigns you a username.

3.

Join a room.

In Step 4, we scripted what happens when the connection occurs. In the steps that follow, we'll script our application to take care of the login process as well as the process of joining a chat room.

5.

Add the following script to capture the login response:

  es.loggedIn = function(success:Boolean, error:String) {     if (success) {       joinRoom();     } else {       msg_txt.text = error;     }   };


On the frame labeled Login, which will be covered later in this exercise, the user is allowed to enter a username and a room name, and click a button to send a login request to the server. The server will then send back a response either allowing or denying the login request. The loggedIn event is triggered when the server responds to a login request.

Similar to the onConnection event, the loggedIn event has two parameters: success and error. If success is true, the login attempt was successful and the joinRoom() function is called. If success is false, the login attempt was not successful and an error string is placed in the msg_txt field. A user might receive an error if attempting to log into the server with a username that's already being used.

6.

Add the following function to handle joining a user to a room:

  function joinRoom() {     var roomOb:Object = new Object();     roomOb.roomName = roomToJoin;     es.createRoom(roomOb);   }


Before discussing this function, it's important to realize that on the frame labeled Login there are two TextInput component instances: username_ti and room_ti. The user will use these text input boxes to enter a username and the name of the chat room that he or she wants to join. A script we will be adding to that frame will take the room name that the user enters and convert it to a variable named roomToJoin, which is used by the function in this step (third line down). Now let's look at how this function works.

As shown in Step 5, when the login response from the server is a success, the joinRoom() function is called, and the room that was specified by the user in the login screen is created. Here's how.

There are two methods of the ElectroServer class that are appropriate to mention here: createRoom() and joinRoom(). The joinRoom() method tells ElectroServer 3 that you want to join a specified room. Here's an example:

   es.joinRoom("Sports");


If a room called Sports exists, you will join it. If it doesn't exist, an error will be returned by the server. This error is captured in the roomJoined event, which we'll script in the next step.

With the createRoom() method, you can create a room that doesn't yet exist. If you attempt to create a room that already exists, internally the ElectroServer class will capture the error and attempt to join you to that room instead.

Because the createRoom() function more easily facilitates joining a room, we use that in our function.

In the joinRoom() functionnot to be confused with the joinRoom() method of ElectroServer 3an object named roomOb is created and given a property named roomName. The value of roomName is the string that the user enters into the room_ti field on the Login frame. This object is then passed into the createRoom() method, which either creates the room (based on the properties of the passed-in object), if the user is the first person in that room, or joins the user to that room if it already exists.

It might seem like overkill to create an object just to store a single variable: the name of the room. This would be true if the name of the room were the only configurable property of a new room; however, many default properties of a room can be overridden if requested. For example, a room can be password-protected, hidden from the room list, set to allow a maximum number of people, and much more. For simplicity, our room needs only to be given a name, so the roomOb object has a single property (roomName) attached to it.

Note

To learn about advanced properties, see the ElectroServer class documentation on the CD-ROM.

7.

To capture the ElectroServer 3 response to attempting to create/join a room, add the following code:

  es.roomJoined = function(Results:Object) {     if (Results.success) {       gotoAndStop("Chat");     } else {       msg_txt.text = Results.error;     }   };


In Step 6, you added the joinRoom() function, which requests that a room of a certain name be created. If that room doesn't exist, it's created, you're automatically joined to it, and the roomJoined event is fired. If the room already exists, internally the ElectroServer 3 class captures that error and attempts to join you to it, and the roomJoined event is fired. Here we've scripted what should occur when this event is fired.

An object is passed into the roomJoined event handler when it's triggered. This object contains two properties: success and error. If success is true, the user has successfully been joined to the room and is taken to the Chat label, which contains the elements that facilitate chatting. If there was an error joining the room, the error is shown in the msg_txt field (which exists on the Connecting label).

8.

Add the following line of script at the end of the current script:

  _global.style.setStyle("themeColor", 0xE5EEF4);


This line of script colors all our component instances a light shade of blue to match the overall color of our design.

9.

For the final action on this frame, add this line of script:

  gotoAndStop("Connecting");


This step moves the Timeline of our chat application to the Connecting frame label. In the next step, we'll add the code that asks the ElectroServer class to connect to ElectroServer 3.

10.

On the frame labeled Connecting in the Actions layer, add the following two lines of script:

  msg_txt.text = "Connecting..."   es.connect();


The first line of ActionScript populates the msg_txt text field with some text informing the user that the application is attempting to establish a connection. The next line calls the connect() method of the ElectroServer class. The connect() method takes the IP address and port (set in Step 3) and uses them to try to find ElectroServer 3 to establish a connection. The result is captured in the onConnection event (created in Step 4).

11.

Move to the frame labeled Login. In the Actions layer, add the following variable declaration and button event handler:

  var roomToJoin:String;   login_btn.onRelease = function() {     var username:String = username_ti.text;     roomToJoin = room_ti.text;     if (username.length > 2 && username.length < 15) {       es.login(username);       gotoAndStop("Waiting");     }   };


This frame includes two TextInput instances named username_ti and room_ti, and a button with an instance name of login_btn. When login_btn is clicked, the onRelease event handler shown here is called. It populates the variable roomToJoin with the contents the user entered into the room_ti instance (remember that the value of roomToJoin is used in the script added in Step 6). It also checks to make sure that the username entered is a reasonable sizemore than 2 characters, but fewer than 15 characters. If the username has an acceptable length, the login() method of the ElectroServer class is called, passing in the username, and the application moves to the Waiting frame label.

Internally, the ElectroServer class takes the username, formats an appropriate XML document, sends it to ElectroServer 3, and waits to hear a response. When a response is received, the loggedIn event (scripted in Step 5) is fired.

12.

Add the following line of script to the Waiting frame in the Actions layer:

  msg_txt.text = "Waiting..." ;


When the user clicks the login button on the Login frame, he or she is taken to the Waiting frame to wait for a response from the server, which will trigger the loggedIn event we scripted in Step 5. As shown in that script, if the loggedIn event captures an error, the msg_txt field will display that error; otherwise, the joinRoom() function is called to join the user to a room and, as a result, to take the user to the Chat frame label (as described in Steps 6 and 7). The elements enabling the user to chat are at this frame.

13.

Move to the Chat frame.

This is the frame from which all users who have successfully connected to ElectroServer 3, logged in, and joined a room will chat. Notice that the TextArea component on the screen has an instance name of chatBox_ta, which will be used to display the chat messages. To the right of this instance are two List components with instance names of roomListBox_lb and userListBox_lb. As you can probably guess, the roomListBox_lb instance will be used to show the list of rooms that exist on the server, and userListBox_lb will display the list of users in your current room.

Below the chatBox_ta instance is a TextInput instance in which the user can type a chat message. It has an instance name of msgBox_ti. The button with the instance name send_btn is used to send a chat message.

14.

Select the frame on the Chat label in the Actions layer and open the Actions panel. Enter this button's onRelease event handler:

  send_btn.onRelease = function() {     var msg:String = msgBox_ti.text;     if (msg.length > 0) {       es.sendMessage("public", msg);       msgBox_ti.text = "";     }   };


This script is executed when the Send button is clicked. A variable called msg is created to store the contents of the msgBox_ti instance. If the length of the message to send is greater than 0, the sendMessage() method of the ElectroServer class is executed. The first parameter of this method, "public", informs the ElectroServer class that the message is intended for everyone in the room. The second parameter contains the message to send. In addition to sending a message, the content of the msgBox_ti instance is erased so the user can type another message.

You have just created the script needed to send a chat message to everyone in the room.

15.

Add the following event handler to capture the chat messages coming in from ElectroServer 3:

  es.messageReceived = function(type:String, message:String, from:String) {     chatBox_ta.text += from + ": " + message + newline;     chatBox_ta.vPosition = chatBox_ta.maxVPosition;   };


This script assigns an event handler to the messageReceived event, which is triggered whenever an incoming message is received from the server. When this event is fired, it is passed three parameter values. The first parameter, type, can be a value of either "public" or "private". In this exercise, all messages are public messages, so we don't need to worry about using that first parameter. However, in a full-featured chat front-end, you would want to know whether an arriving message was public or private. If it's a private message, you might want Flash to play a sound or color-code the text to give an indication to the person chatting that he or she just received a private message.

The second parameter, message, contains the chat message that has arrived. The final parameter, from, contains the username of the person who sent the message.

The first line of script inside the function adds a line of text to the chatBox_ta TextArea component. It starts with the name of the user who sent the message, adds a colon and the contents of the message, and finally adds a newline so the next message received will be on its own line.

Next, the script sets the scroll position of the text in the chatBox_ta component. In a chat application, incoming messages are typically appended to the bottom of the text field, and the field is automatically scrolled to the bottom. This line of script sets the scroll position of the chatBox_ta instance to be the maximum scroll position possible.

16.

To display the list of users in the room, the userListUpdated event must be captured and used. Add the following script at the end of the current script:

  function showUsers() {     var userlist:Array = es.getUserList();     userListBox_lb.setDataProvider(userlist);   }   es.userListUpdated = showUsers;   showUsers();


First we create a function called showUsers(). This function grabs the most recent user list from the ElectroServer class using the getUserList() method and stores it as an array called userlist. As a result, the userlist array will contain one object for each user in the room. Each of these objects has a property named label that contains the username of the user that the object represents. If there are seven users in the room, the getUserList() method will return an array containing seven items; the array is stored in the array named userlist.

The second line in the function takes the userlist array instance and passes it into the setDataProvider() method of the userListBox_lb List component instance. The result is that the userListBox_lb is populated with a list of all of the usernames in the room.

On the first line of script below the function definition, we assign the showUsers() function to the userListUpdated event. This event is triggered by the server automatically whenever the list of users in the room changes. Thus, whenever someone enters or leaves the chat room, the showUsers() function is called, and the user list shown in the userListBox_lb instance is updated.

The last line of script calls the showUsers() function manually because when we first arrive at the Chat frame we want to display the current list of users. If we didn't call the showUsers() function manually, the current user list wouldn't display in the userListBox_lb instance until the next person came into the room or left the room, causing a userListUpdated event to fire.

17.

To show the list of rooms, add the following code:

  function showRooms() {     var roomlist:Array = es.getRoomList();     roomListBox_lb.setDataProvider(roomlist);   }   es.roomListUpdated = showRooms;   showRooms();


This script is almost identical to the script that captures and displays the user list. The showRooms() function grabs the room list (an array of objects, each representing one room) from the ElectroServer class and passes it into the setDataProvider() method of the roomListBox_lb List component instance.

The showRooms() function is then assigned as the event handler for the roomListUpdated event. Every time something about a room list changes, such as the number of people in a room or the addition/removal of a room, this event is fired.

Finally, the showRooms() function is manually called so that the current list of rooms can be displayed. After that, it will be called only when the roomListUpdated event fires.

18.

To allow a person to click the name of a room to join that room, add the following script:

  var roomClickListenerObject:Object = new Object();   roomClickListenerObject.change = function(eventObject:Object) {     var room:String = eventObject.target.value;     es.joinRoom(room);   };   roomListBox_lb.addEventListener("change", roomClickListenerObject);


When users arrive at the Chat frame, they will see the list of rooms in the roomListBox_lb instance. As the list of rooms changes, the box will update. The script that you just added gives the user the ability to click any room in the list and be joined to that room automatically.

We first create an object and then create a function called change on the object. Finally, we assign this object as an event listener to the roomListBox_lb component instance. The change event is fired whenever a user changes the selected room in the roomListBox_lb instance. Usually, this is accomplished by the user's clicking a room in the list, although it can also be accomplished using the keyboard.

When the change event is fired, an Event object is passed into the event handler. We extract the name of the item that was selected and give it a variable name of room. The value of that variable is then passed into the joinRoom() method to join the user to that room.

When users join a new room, they receive an updated user list, which is handled by code we have added to this frame. So, by simply clicking the name of a room, the user will be joined to that room, and the list of users will change to display the list of users who are in that room.

19.

Start ElectroServer 3.

The default IP and port for ElectroServer 3 are 127.0.0.1 and 9875. When you start ElectroServer 3, it will attempt to bind itself to your local IP and listen on the default port.

It's now time to test your chat application.

20.

Choose File > Export > Movie to export this project to a SWF file in this lesson's directory on your hard drive. Navigate to that directory and double-click the exported SWF file. Log in and send some chat messages.

You should see your chat messages appear in the chatBox_ta TextArea component. Try opening more than one copy of this SWF file and log in with different usernames. You should be able to see all the users in the userListBox_lb List component.

Try to log in two users with the same username to see whether you receive an error. Log users into separate rooms and try to join a room by clicking the room name.

21.

Close the test movie and save your work as Chat2.fla.

You accomplished a lot in this exercise. You created a basic chat application using the ElectroServer class, a little bit of ActionScript, and some components.

If you're interested in creating a more advanced chat room, look through the ElectroServer class documentation on the CD-ROM or download some source files from www.electrotank.com/ElectroServer/.

You can modify this chat to do any of the following:

  • Enable password-protected rooms

  • Allow private messaging

  • Let a user create a new room from the Chat frame label

  • Build in support for emoticons




Macromedia Flash 8 ActionScript Training from the Source
Macromedia Flash 8 ActionScript: Training from the Source
ISBN: 0321336194
EAN: 2147483647
Year: 2007
Pages: 221

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net