ClientServer Architecture

Client/Server Architecture

Virtually all asynchronous services in Symbian OS are provided by servers through Client/Server Architecture. Clients are simply programs that make use of a particular service provided by a server. A server is the program that services those requests in some way. Client/Server Architecture allows:

  • Extensibility, since plug-in modules can be added to service new types of object. Often this can take place at runtime, so that a device can, for instance, support a new type of filing system that has been developed after the File Server that makes use of it.

  • Efficiency, since multiple clients can be serviced by the same server.

  • Safety, since servers and their clients exist in separate processes (usually) and communicate via message passing. A misbehaving client cannot bring down its server. (Note also that servers are expected to panic misbehaving clients, through a handle to the client's thread.)

  • Asynchronicity, since servers use the Active Object framework to notify their clients when their work is done. The use of Active Objects with servers is important ”by suspending the client thread, rather than polling the status of a request, Symbian OS reduces the amount of processor cycles required to process that request. This helps to improve power management, which is very important in a mobile device.

Details of how to write a server are beyond the scope of this book ”this section simply focuses on how to use a server from its Client-side interface. It is not important to completely understand this when starting out.

One important server has already been introduced during the course of this chapter ”the File Server. This uses an RFs session and an RFile subsession ”these terms will be explained later in this section. Many other servers are used throughout the system: Comms Server, Window Server, Font and Bitmap Server and so on. All of them provide a (relatively) easy to use asynchronous API via an R -class server session.

Server Sessions

At the beginning of this chapter, when introducing the different class types, it was noted that R -classes do not have a common base class. While this is true, many R -classes are in fact server sessions which do have a common base class: RSessionBase .

RSessionBase provides part of the underlying API for communication between client and server. In the most part this is not used directly, since each derived server session class provides an API that abstracts it, hiding the detail and simplifying the interface. For example, the File Server derives from RSessionBase to create RFs as its session class. However, it may be informative to learn a little about what is happening "under the hood."

Server Sessions and Inter-Process Communication

The Symbian OS Kernel and Memory Management Unit implement a different memory-mapped address space for each process, preventing a rogue process from being able to overwrite the memory of another. The only process capable of "seeing" the entire physical memory is the Kernel process itself. All threads are clients of the Kernel server, and it is the Kernel that facilitates communication between processes and the threads they contain.

Here is part of the class definition for RSessionBase :

 class RSessionBase : public RHandleBase    { protected:    inline TInt CreateSession(const TDesC& aServer,       const TVersion& aVersion);    void SendReceive(TInt aFunction, TAny* aPtr,       TRequestStatus& aStatus) const;    TInt SendReceive(TInt aFunction, TAny* aPtr) const;    }; 

The CreateSession() function is used by the server session to connect to its server. The server is specified by name , and the Kernel can use this to establish the connection. As you can see, the function is protected, and methods like RFs::Connect() will wrap up this call and present an easy client-side API, so that the caller does not actually have to know the name of the server it is connecting to.

The connection may then be thought of as a "pipe" between the client and server, through which all communication is routed. The communication itself takes the form of messages, passed from the client via the SendReceive() methods. Each overload takes a function ID (as a TInt ) and a pointer to an array of four 32-bit values.

The function ID is used by the server to identify which service is being requested by the client. Depending on the requested service, any one of the four 32-bit values may be treated as:

  • An integer.

  • A pointer to a descriptor in the client's address space. This may be a package descriptor containing any "flat" data structure ”see the discussion on package descriptors earlier.

In the latter case the Kernel can, at the server's request, copy the contents of the descriptor into the server's address space. That data may be modified and copied back into the client's address space if required. Note that the server is never granted direct access to the client's address space ”the Kernel marshals the data back and forth, because it can see the entire extent of the memory as a unified, flat address space. Naturally, copying large amounts of data back and forth can take some time, so clever steps are taken to ensure that servers such as the Media Server and the Font and Bitmap Server do not have to move large quantities of data across the process boundary.

You will notice that the two overloads shown differ by return type and whether or not they take a TRequestStatus argument. This is because both synchronous and asynchronous requests are permitted. Naturally, synchronous requests return their completion code directly from the function, whereas asynchronous requests return it via the TRequestStatus of the Active Object used to encapsulate the request.

On the server side, the client's message is represented as an RMessage object:

 class RMessage    { public:    void Complete(TInt aReason) const;    void ReadL(const TAny* aPtr, TDes8& aDes) const;    void ReadL(const TAny* aPtr, TDes16& aDes) const;    void WriteL(const TAny* aPtr, const TDesC8& aDes) const;    void WriteL(const TAny* aPtr, const TDesC16& aDes) const;    TInt Function() const;    TInt Int0() const;    TInt Int1() const;    TInt Int2() const;    TInt Int3() const;    const TAny* Ptr0() const;    const TAny* Ptr1() const;    const TAny* Ptr2() const;    const TAny* Ptr3() const;    }; 

It is pretty clear from the section of the API shown above how the server can use the RMessage class to access the data that has been sent over from the client. Function() returns the value of the function ID that the client requested. ReadL() is used to read data into the server's address space ” WriteL() is used to write the data back to the client. IntX() and PtrX() provide different interpretations of the same four data items ”the array of four 32-bit values in the RServerSession 's SendReceive() call.

Once the request has been completed, Complete() is invoked with the appropriate error code (hopefully KErrNone ). In the case of a synchronous function being requested, this will become the return value of SendReceive() . If an asynchronous function was requested, then this will be the value of the TRequestStatus .

How does the server get the message in the first place? Well, here is some of the class declaration for CSharableSession , the base class of the server-side objects that represent the session with the client:

 class CSharableSession : public CBase    {    friend class CServer; public:    virtual void CreateL(const CServer& aServer);    virtual void ServiceL(const RMessage& aMessage) =0;    const CServer* iServer;    }; 

The server creates the session when a client initially connects. It calls CreateL() to complete construction, passing in a reference to itself to allow the session to access it.

When a request is issued by the client, the server invokes the ServiceL() function on the relevant session. It is this method that invokes the appropriate functionality as determined by the function ID of the message, acting on any of the data in the message. The functionality is usually built into the session itself.

For completeness, here is some of the declaration of CServer :

 class CServer : public CActive    { public:    IMPORT_C void StartL(const TDesC& aName);    IMPORT_C void DoCancel();    IMPORT_C void RunL();    }; 

Note that all servers derive from CActive ”they are all Active Objects. The StartL() method adds them to the Active Scheduler and starts them waiting for their first request. It is the implementation of RunL() that is actually responsible for invoking ServiceL() on the session, and this is executed when the Kernel signals the server's TRequestStatus that a request has come in from the client.

How does the server itself come into existence? This depends on the nature of the server. Many of the essential servers are started as the system boots up, and they are always available. Others, particularly servers that are added to the system at a later date, are started explicitly by their client APIs when a request is issued for the first time ”a server of this type will usually keep a count on the number of clients, and automatically unload itself when this number falls to zero.

Server Review

The basic process by which a server services an asynchronous request is given here. For simplicity's sake you can assume that the server in question has already been started.

1.
The client establishes a connection with the server via an RSession -derived object which presents the client-side API.

2.
The server creates a new session object associated with the client session.

3.
The client makes a request via the client-side API.

4.
The client API wraps up any data associated with the request into integers, descriptors or package descriptors, depending on the request.

5.
The client API invokes SendReceiveL() , with the appropriate function ID and argument pointers.

6.
The Kernel completes the server's iStatus member.

7.
The server's RunL() is invoked, passing the message received from the client via the Kernel to the session's ServiceL() method.

8.
The session's ServiceL() invokes the appropriate service handler based on the value of the function ID in the message.

9.
The service handler copies data from the client's address space, using ReadL() as appropriate.

10.
The service handler services the request.

11.
The service handler copies data to the client's address space, using WriteL() as appropriate.

12.
The service handler calls Complete() on the message with the appropriate error code.

13.
The Kernel increments the client thread's semaphore and sets the trequestStatus to the value specified in Complete() .

14.
The client's RunL() is called by the client thread's Active Scheduler.

15.
The client's RunL() performs any processing on the data returned via the client API.

So long as the client-side API is sufficiently well designed and implemented, the client need be concerned only with steps 1, 3 and 15.

Subsessions

The methodology described above works well, but it can be inefficient. Many of the resources needed in the Kernel to maintain the session have been left out, as they are nontrivial and their coverage is beyond the scope of this book. However, each time a session is opened, more Kernel resources are consumed. This is why it was suggested earlier that the number of File Server sessions ( RFs ) should be kept to a minimum.

It was also mentioned that RFile is a subsession . A subsession is a lightweight way for a client to communicate with its server without needing individual sessions to represent each client-side object. The client establishes a single session with the server, then creates subsessions for each object. Each subsession is associated with a session through which the actual Inter-Process Communication ( IPC ) takes place.

The subsession base class API looks like this:

 class RSubSessionBase    { protected:    TInt CreateSubSession(RSessionBase& aSession, TInt aFunction, const TAny* aPtr);    void CloseSubSession(TInt aFunction);    void SendReceive(TInt aFunction, const TAny* aPtr, TRequestStatus& aStatus) const;    TInt SendReceive(TInt aFunction, const TAny* aPtr) const; private:    RSessionBase iSession;    TInt iSubSessionHandle;    }; 

Notice how the call to CreateSubSession() requires an RSessionBase object, as outlined above (for example, opening an RFile needs an RFs ). Also note how a function ID must be specified. This is the function ID which tells the server to create a subsession. Closing a subsession also needs a (different) function ID.

SendReceive() is available just as for an RSessionBase -derived class and is available with both synchronous and asynchronous overloads.

Finally, notice that the subsession maintains a copy of the RSessionBase and also a subsession handle that it uses to identify itself to the server. This is how the server is able to keep track of which subsession has made a particular request.

As with RSessionBase -derived classes, RSubSessionBase -derived classes should provide an easy-to-use API that hides the underlying IPC architecture ”for example, the File Server derives from RSubSessionBase to create RFile as its subsession class.



Developing Series 60 Applications. A Guide for Symbian OS C++ Developers
Developing Series 60 Applications: A Guide for Symbian OS C++ Developers: A Guide for Symbian OS C++ Developers
ISBN: 0321227220
EAN: 2147483647
Year: 2003
Pages: 139

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