Welcome to Yserbius.Org! Join our Ultima Online Private Server and have some old school fun.

Chat Server/Client Code For discussion Part I

slohand

Yserbian
Staff member
#1
For a chat program you need a server and clients that register with that server. When one client sends a message, the other clients need to be notified; this implies server events and shared code. I will focus on the server and the elements of the server. Here is an overview of the topics I will demonstrate in this part:

An executable server application
Channel configuration using an App.config file (which describes the service and provides port information)
A custom delegate that is in a separate assembly to permit sharing between client and server
A remotable MarshalByRefObject
And, the OneWayAttribute
Defining the Executable Server Code
The server can be simple or complex, a console application or Windows service; you just need something running on the server that can service requests from clients. For this chat program, you will use a console application project. Console applications are easy to create and will let us stay focused on Remoting.

Because you will be configuring the remote server using the App.config file, the code for the remote server is very easy. Listing 1 contains the entire listing for the server, and listing 2 contains the App.config file with the configuration information. I will talk about the configuration file after listing 2.

Listing 1: All the code you need for a running server.

Code:
Option Strict On
Option Explicit On

Imports System
Imports System.Diagnostics
Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Http

Module Module1

    Sub Main()
        Console.WriteLine("Reading configuration information...")
        RemotingConfiguration.Configure("Server.exe.config")

        Console.WriteLine("press [enter] to quit")
        Console.ReadLine()
    End Sub

End Module
Listing 2: The App.config server configuration XML.

Code:
<?xml version="1.0" encoding="utf—8" ?>
<configuration>
  <system.runtime.remoting>
  <application>
    <channels>
      <channel ref="http" port="6007">
        <serverProviders>
          <provider ref="wsdl" />
          <formatter ref="soap" typeFilterLevel="Full" />
          <formatter ref="binary" typeFilterLevel="Full" />
        </serverProviders>
        <clientProviders>
          <formatter ref="binary" />
        </clientProviders>
      </channel>
    </channels>
    <service>
      <wellknown
        type="SharedCode.Chatter, SharedCode"
        objectUri="Chatter.soap"
        mode="Singleton"
       />
    </service>
  </application>
  </system.runtime.remoting>
</configuration>
You can programmatically configure your server or use the App.config file to configure the server. By using an external XML file you can change elements like the port number without recompiling and redistributing the server application.


Tip: You can copy and paste the above XML into the Toolbox—referred to as a code snippet&mdashp;and drag and drop the XML each time you need to configure a server. Generally, you will only need to update the service section and the port.

The remote configuration information is defined between the <system.runtime.remoting> </system.runtime.remoting> tags—the namespace for remoting information. Nested in the namespace is the channel configuration and service configuration. Let me divide the discussion between these two blocks of XML.

Configuring the Channel
I think of a channel like a pipeline for moving data between clients and servers. Channels are configured inside of the <channels></channels> tag. The outer tag supports configuring multiple channels. You only need one channel for the sample.

To configure the channel you need to supply arguments for the ref and port attributes. Ref describes the name of channel, which can be tcp or http. If you provide a Ref attribute then the type attribute is not needed. The port can be any port value, but you should avoid common ports like 20, 21 (FTP), 23 (Telnet), 25 (SMTP), 110, and 80. You can check online for a list of reserved port numbers, but ports above 1024 are probably safe, and you can use numbers as high as 65520.

The inner <serverProvider> and <clientProviders> tags are required in .NET 1.1. If you forget these elements then you will get an exception when you try to run your remote server. The formatter describes the level of serialization for each of the soap and binary formatters, and the provider tag is used to describe elements that participate in channeling the data between client and server.

The best resource for .NET Remoting I have found is Ingo Rammer's website www.ingorammer.com. For more information about providers and formatters pick up a copy of Ingo's book Advanced .NET Remoting from Apress.

Configuring the Service
The service tags indicate the class and assembly that represent your remote service. Using the App.config file, the service configuration information is described with the <wellknown> tag (see listing 2). The type is a string that contains the namespace and class—SharedCode.Chatter—delimited by a comma and the assembly name. When you see a string like this think Reflection and dynamic assembly loading.

The objectUri attribute—"Chatter.soap"—uses a .soap or .rem by convention and is a uniform resource indicator that uniquely identifiers our service by name. The URI will be part of the URL when you connect to your service from the client.

Finally, you specify the mode attribute. The mode can be SingleCall or Singleton. The mode attribute is a subtle but critical part of the chat server. If you used the SingleCall mode then every client would get a different instance of the remote server object, which means the call to connect your event handler would get one instance of the server and subsequent calls to send messages would go to additional instances of the server. For the example, data will be shared between clients, so you need to use the Singleton mode. As a result it is possible that data from one client can be shared by other clients, which is precisely what is wanted in a chat program—one user writes some text and other users can see it.


Writing the Shared Code
At this point your code will compile but not run. You need to define the Singleton remote service described in the App.config file, the Chatter.


There are a lot of ways to implement remotable objects. If serialization is used then you are basically mirroring what XML Web Services do. If you want a proxy reference to an object that resides on the server then you need a MarshalByRefObject. For the program you are creating, you want the latter. You want the clients to be able to get a handle to your remote service, store and address to your client's event handler, and invoke operations on that service. In general terms your remote service needs to:

Permit clients to begin listening for messages
Permit clients to send messages
And, then send that message to all listening clients


Collectively these elements are supported by a custom class that inherits from EventArgs. This custom EventArgs derivative is used to contain your message. You need a new delegate that accepts the custom event arguments, and you need the Chatter class. The Chatter class is the remotable object. You will put all of these elements in a new Class Library project because these definitions must be shared between client and server.


Defining the Custom Event Arguments
The custom event arguments include the sender and that sender's message. Because this object is sent between client and server it has to be remotable. To make the class remotable you can apply the SerializableAttribute to make the event arguments serializable, marshal—by—value objects, that is remotable. Listing 3 contains the implementation of the custom event arguments.

Listing 3: Our serializable event arguments class, ChatEventArgs.

Code:
<alizable()> _
Public Class ChatEventArgs
    Inherits System.EventArgs

    Private FSender As String
    Private FMessage As String

    Public Sub New()
        MyBase.New()
    End Sub

    Public Sub New(ByVal sender As String, _
      ByVal message As String)
        MyClass.New()
        FSender = sender
        FMessage = message
    End Sub

    Public ReadOnly Property Sender() As String
        Get
            Return FSender
        End Get
    End Property

    Public ReadOnly Property Message() As String
        Get
            Return FMessage
        End Get
    End Property
End Class

Next a delegate that accepts your new event arguments type needs to be defined.

Declaring the Delegate
.NET uses multicast delegates to manage events. A multicast delegate is really a list that can contain function pointers with a specific signature. What you are doing when you define a delegate is implicitly defining a class that can contain a list of function pointers that have the same number, order, and type of arguments, that is the addresses of event handlers. The delegate I defined for our new event arguments class is show here:

Public Delegate Sub MessageEventHandler(ByVal Sender As Object, _
ByVal e As ChatEventArgs)
Implementing the Chatter Class
The Chatter is remotable. The basic behavior is that clients assign their message event handler to an instance of the Chatter's public event property. Then, each client can call a Send method and the server notifies clients of a message via their event handlers. I implemented Send to store a copy of the message and then raise the MessageEvent to send the message to all of the registered clients.

There are some interesting features in the Chatter class. I will go over those elements after the code in listing 4.

Listing 4: Our remotable Chatter class which tracks messages send and recipients using a delegate.

Code:
Imports System.Collections
Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Messaging
Imports System.Runtime.Remoting.Lifetime

Public Class Chatter
    Inherits MarshalByRefObject

    Private history As Queue = New Queue

    Public Event MessageEvent As MessageEventHandler

    Public Overrides Function InitializeLifetimeService() As Object
        Return Nothing
    End Function

    Public Sub New()

    End Sub

    <OneWay()> _
    Public Sub Send(ByVal sender As String, _
    ByVal message As String)

        Console.WriteLine(New String("—"c, 80))
        Console.WriteLine("{0} said: {1}", sender, message)
        Console.WriteLine(New String("—"c, 80))

        history.Enqueue(String.Format("At {0} {1} said: {2}", _
          DateTime.Now, sender, message))
        DoMessageEvent(sender, message)
    End Sub


    Private Sub DoMessageEvent(ByVal sender As String, _
        ByVal message As String)

        RaiseEvent MessageEvent(Me, _
            New ChatEventArgs(sender, message))
    End Sub

    <OneWay()> _
    Public Sub ShowHistory()
        Dim O As Object
        For Each O In history
            DoMessageEvent("server—history", O.ToString())
        Next
    End Sub
End Class
If you want clients to have a reference to an object that lives on the server, as opposed to a deserialized copy of the object, then you need to inherit from MarshalByRefObject.

The Queue is not relevant to this discussion other than it demonstrates an interesting way to keep a running log of all the messages received.

Next, declare a public event. When a client attaches an event handler to this event the server can talk back to the client by raising the event. Remember events are multicast—which means there might be many handlers for one event in .NET—so you can have an unlimited number of clients receiving this event.

By overriding the InitializeLifetimeService method inherited from MarshalByRefObject you can extended the life of the remotable object. By returning Nothing from InitializeLifetimeService the remotable object hangs around indefinitely.

The last step is to provide behaviors to consumers. In this example, two are offered: Send and ShowHistory. Both methods are implemented the same way, using the OneWayAttribute to describe how this method behaves. The OneWayAttribute means that no data is returned and only input parameters are passed to the method. In practice the OneWayAttribute means you are defining a subroutine with only ByVal parameters.

When ShowHistory is called the queue containing all of the previous messages is dumped. When Send is called, the MessageEvent is simply raised. All that is left to do is create a single client application that listens for MessageEvents. That piece will be tackled in the next part of this series of articles.

Summary
.NET Remoting requires a lot of knowledge about a lot of little pieces of the .NET framework. In this first part of a three, you learned how to configure a remote server, define remotable objects, and define custom event arguments and delegates.

In the next part, you will implement a client for your server and complete the chat application. The client is the other half of a distributed solution, so it has some special configuration needs too. In addition, because the server has to connect to the client to invoke the client's event handler, the client has to be remotable too and consequently has some special requirements. More on this later.
 
Top