Tutorial One
Contents:
Write client and server codes with uidparser.exe
Experiments with SocketPro server
Responsibilities of main thread with SocketPro server
SocketPro
is written with batching, asynchrony and parallel computations using raw
non-blocking socket, which is very different from common distribution framework
like Java RMI, DCOM, dotNet remoting and web service. Many features are
specific and can’t be found in other frameworks. Therefore, we have created a
series of tutorial samples to assist your development. As you study these
samples, it is strongly recommended that you need to understand every one of
features and code comments while experimenting them. All of tutorial samples
are written with C++, C# and VB.Net languages. Each of samples contains two
small applications, a client application and a server application for each of
these development languages.
Understand a uid (universal interface definition) file, and use SocketPro tool uidparser.exe to quickly create skeleton code for both client and server through the file.
This very first
sample is designed for quickly creating a service to process the following five
requests:
A. Echo an object data (VARIANT in
C++) between client and server applications.
B. Query the number of its requests
processed since a client is connected to a server.
C. Query the number of all of
processed requests (both fast and slow ones) from all of clients since a SocketPro
server is started.
D. Query the number of all of fast
requests processed from all of clients since a SocketPro server is started. The
number doesn’t include the number of slow requests processed.
E. Sleep for a specified time in
ms, which simulates a slow processing request.
A client window application is created to demonstrate how to build a socket
connection, how to send one or a set of requests in batch, and how to process
returned results. Besides, it shows how to switch between two computation models,
asynchrony and synchrony, and how to control window events.
A console server application is created to process the above five requests with
help of SocketProAdapter, an open source module for simplifying SocketPro
server development. The server application shows how to start a listening
socket for receiving requests to process, how to create a service and how to
process requests. All of these are very simple with SocketPro development at
server side. The most important objective is to explain thread management
within SocketPro, and shows you what variables are required to be synchronized
with window thread locking objects and what variables should not be
synchronized. Besides, the sample gives you clues how to give or deny a socket
connection at server side.
2. Write client and server codes with
uidparser.exe
SocketPro now comes with a tool to write skeleton client and server code from a given universal interface definition file having a file extension uid. It is very similar to other interface definition files like COM and CORBA. Take this sample, its uid file contains the below code.
[
ServiceID = 20 //service id
can't be less than 0
]
CTOne
{
int QueryCount(); //Returns the count of calls from one socket connection
/*Returns the count of all
requests from all of socket connections */
int
QueryGlobalCount();
/*Returns the count of fast
requests from all of socket connections */
int
QueryGlobalFastCount();
/* Sleep for a given time
nTime. It is a slow request. */
$void Sleep(in int nTime);
object Echo(in object objInput); //echo an arbituary data
}
In regards to correct use of uidparser.exe, please see detailed comments inside the file TOne.uid. There is no need to re-describe various simple rules in this tutorial again. However, you must be clear if a request takes a long time to process. Take this sample as an example, the request Sleep requires a long time to process, and all of other four requests will require very little time to be completed. You can put a char ‘$’ in front of a function to indicate the request is a slow one.
Once having such a file, you can use the tool to quickly write basic client and
server codes by executing the tool from MS DOS command. See the below Figure
for creating skeleton code for C#, C++ and VB.NET by use of uidparser.exe.

Figure 1. Create SocketPro client and server
skeleton codes from a given uid file with uidparser.exe.
For this sample, uidparser creates
three files with proper file extensions for each of three development
languages. The first file named as TOne_i.cs for C# contains all of constants
definitions as shown in the following.
public class TOneConst
{
//defines for service CTOne
public const int sidCTOne =
((int)USOCKETLib.tagOtherDefine.odUserServiceIDMin
+ 20);
public const short
idQueryCountCTOne = ((short)USOCKETLib.tagOtherDefine.odUserRequestIDMin
+ 0);
public const short
idQueryGlobalCountCTOne = (idQueryCountCTOne + 1);
public const short
idQueryGlobalFastCountCTOne = (idQueryGlobalCountCTOne + 1);
public const short
idSleepCTOne = (idQueryGlobalFastCountCTOne + 1);
public const short
idEchoCTOne = (idSleepCTOne + 1);
}
This
file will be referenced by both client and server applications. The constant
sidCTOne is a service id for this sample service. All of others are ids for
five requests. SocketPro supports multiple services within one listening
socket. Each of services must be identified by one unique integer number, named
as service id. A service id should be between 0x10000000
(USOCKETLib.tagOtherDefine.odUserServiceIDMin) and
0xFFFFFFFF. UDAParts reserves service ids from
0x00000000 through 0x10000000 - 1. This
sample has a service id (USOCKETLib.tagOtherDefine.odUserServiceIDMin
+ 20).
For each of requests with the scope of one specific service, it requires one
unique short integer number, named as request id. The request id must be
between 0x2000 (USOCKETLib.tagOtherDefine.odUserRequestIDMin)
and 0xFFFF. Similarly, UDAParts reserves
request id from 0x0000 through 0x2000. For example, this sample service has five
request ids defined (0x2001 ~ 0x2005). Each of ids represents one of the above
requests.
3. Server side development
Reference
SocketProAdapter for easy development -- At the very beginning, you
need to reference SocketProAdapter dll if your development environment is one
of dotNet languages (C# and VB.NET), or add the files sprowrap.cpp and
sprowrap.h into your C++ project for a server application. Also, use namespaces
SocketProAdapter and SocketProAdapter.ServerSide.
Derive CTOnePeer from CClientPeer -- Next, derive a class from the abstract class CClientPeer inside the namespace SocketProAdapter.ServerSide as shown in the file TOneImpl.cs. You must implement two pure virtual functions, OnFastRequestArrive and OnSlowRequestArrive. Each of its instances represents a socket connection, corresponding to a client. As function names indicate, you should process all of fast requests inside the first function that is called inside a main thread, and slow requests inside the second function that is called within a worker thread. The main thread is running with listening socket. At this point, you simply ignore it, and we’ll discuss more about the main thread later in this section. As shown in the sample code, only the request Sleep is processed inside the second request, and all of other four requests are processed inside the first function. See the below code.
public class CTOnePeer :
CClientPeer
{
protected override void
OnSwitchFrom(int nServiceID)
{
//initialize
the object here
}
protected override void
OnReleaseResource(bool bClosing, int nInfo)
{
if(bClosing)
{
//closing the socket with error code = nInfo
}
else
{
//switch to a new service with the service id = nInfo
}
//release
all of your resources here as early as possible
}
protected void QueryCount(out int QueryCountRtn)
{
QueryCountRtn = 0;
// TODO:
Add your code here
}
protected void QueryGlobalCount(out
int QueryGlobalCountRtn)
{
QueryGlobalCountRtn = 0;
// TODO:
Add your code here
}
protected void QueryGlobalFastCount(out
int QueryGlobalFastCountRtn)
{
QueryGlobalFastCountRtn = 0;
// TODO:
Add your code here
}
protected void Sleep(int nTime)
{
// TODO:
Add your code here
}
protected void Echo(object
objInput, out object
EchoRtn)
{
EchoRtn = null;
// TODO:
Add your code here
}
protected override void
OnFastRequestArrive(short sRequestID, int nLen)
{
switch(sRequestID)
{
case
TOneConst.idQueryCountCTOne:
{
int QueryCountRtn;
QueryCount(out QueryCountRtn);
m_UQueue.SetSize(0); //initialize memory chunk size to 0
PushNullException();
m_UQueue.Push(QueryCountRtn);
SendReturnData(sRequestID,
m_UQueue);
}
break;
case
TOneConst.idQueryGlobalCountCTOne:
{
int QueryGlobalCountRtn;
QueryGlobalCount(out QueryGlobalCountRtn);
m_UQueue.SetSize(0); //initialize memory chunk size to 0
PushNullException();
m_UQueue.Push(QueryGlobalCountRtn);
SendReturnData(sRequestID,
m_UQueue);
}
break;
case
TOneConst.idQueryGlobalFastCountCTOne:
{
int QueryGlobalFastCountRtn;
QueryGlobalFastCount(out QueryGlobalFastCountRtn);
m_UQueue.SetSize(0); //initialize memory chunk size to 0
PushNullException();
m_UQueue.Push(QueryGlobalFastCountRtn);
SendReturnData(sRequestID,
m_UQueue);
}
break;
case
TOneConst.idEchoCTOne:
{
object objInput = null;
object EchoRtn;
m_UQueue.Pop(ref objInput);
Echo(objInput, out EchoRtn);
m_UQueue.SetSize(0); //initialize memory chunk size to 0
PushNullException();
m_UQueue.Push(EchoRtn);
SendReturnData(sRequestID,
m_UQueue);
}
break;
default:
break;
}
}
protected override int
OnSlowRequestArrive(short sRequestID, int nLen)
{
switch(sRequestID)
{
case
TOneConst.idSleepCTOne:
{
int nTime = 0;
m_UQueue.Pop(ref nTime);
Sleep(nTime);
m_UQueue.SetSize(0); //initialize memory chunk size to 0
PushNullException();
SendReturnData(sRequestID,
m_UQueue);
}
break;
default:
break;
}
return
0;
}
protected void PushNullException()
{
if(TransferServerException)
{
int nRtn = 0;
m_UQueue.Push(nRtn); //required by SocketProAdapter client side!!!
}
}
}
SocketPro is dependent on a memory queue object to easily pack and unpack various data before exchanging data between a client and a server. Take the request Echo as an example, we first load an object from m_UQueue by calling m_UQueue.Pop. Afterwards, call the method Echo. Once the method returns, we initialize the memory queue by setting its size to zero. At the last, save a returned object into the memory queue m_UQueue. At the very end, we send its return data to a client. For all of requests, it is the same.
Derive the class CTOneSvs and associate CTOnePeer with CTOneSvs – Afterwards, you derive a class CTOneSvs from CBaseService as shown in this sample. You must overwrite the pure virtual function GetPeerSocket to associate an instance of CSOnePeer simply by creating an instance of CTOnePeer. SocketPro uses an instance of CTOneSvs to manage a set of CTOnePeer instances from different clients. It is very simple to do so as shown in the below code.
//SocketPro
will call this function to create a ClientPeer on the fly if needed
protected override CClientPeer
GetPeerSocket(int hSocket)
{
//Associate a peer object
into this service
//Note that you must not
delete the object, but SocketProAdapter will take care it for you
return new CTOnePeer();
}
Register a service and deal with slow requests – SocketPro requires registering all of services supported. Also, SocketPro server requires all of services having a unique identification number like sidCTOne. In order to simplify SocketPro server side development, SocketPro automatically creates and manages threads for you at run time. Here is the sample code to register a service and a slow request.
private void AddService()
{
bool bSuc;
//No COM -- taNone; STA COM
-- taApartment; and Free COM -- taFree
bSuc = m_CTOne.AddMe(TOneConst.sidCTOne, 0,
tagThreadApartment.taNone);
//If bSuc is false, very
possibly you have two services with the same service id!
bSuc =
m_CTOne.AddSlowRequest(TOneConst.idSleepCTOne);
}
Inside the function, it calls
CBaseService::AddMe with a service id, events required and a COM thread
apartment model. For this sample, they are set to sidCTOne, 0, and taNone,
respectively. The taNone indicates a worker thread will not be initialized into
any COM thread apartment because this sample doesn’t create any COM object. The
second parameter indicates what events a client will be interested in. No
matter what data are set, you always get common events OnSwitchTo, OnClose,
OnIsPermitted, OnFastRequestArrive, OnSlowRequestArrive and OnCleanPool.
SocketPro can expose many events to your code. The above events are common
ones, and all of others are rarely used. Therefore, if you use these rarely
used events, just set these events and overwrite functions of either
CClientPeer or CBaseService. The second call inside the function is to tell
SocketPro all of slow requests for a specific service. The request Sleep is the
slow one for this sample. When SocketPro server receives a request, it will
analysis the request and route it into the function OnFastRequestArrive for a
fast request and OnSlowRequestArrive for a slow request. When calling the
function CBaseService::AddSlowRequest, it trains SocketPro server. SocketPro
server uses the information to route a request to different threads and also
create a thread automatically on the fly if necessary. SocketPro simplifies
your code development at server side a lot. It manages all of threads for you
automatically. Usually, you will never create a thread from your code even
though you are indeed able to create your own threads. SocketPro manages three
pools of threads, taNone, taApartment and taFree. The last two pools are for threads
entering COM single-threaded and free-threaded apartments, respectively. The
threads inside the pools taNone and taFree are sharable among different
clients. However, a thread inside the pool taApartment is not sharable or
reusable, and always dedicated to one socket connection. After the function
CClientPeer::OnReleaseResource is called, the associated taApartment thread is
killed. Threads in the pools taNone and taFree are always returned into their
pools for reuse after completing a request. However, these threads will be
killed if they are idle for a period of time and have nothing to work. You can
control the time by setting CSocketProServer::MaxThreadIdleTimeBeforeSuicide.
Start listening socket -- At this point, we almost complete the server application. Now let’s start a listening socket. Look at the sample code and see the code inside function CMySocketProServer::Run.
public void Run()
{
do
{
//try
amIntegrated and amMixed
AuthenticationMethod =
tagAuthenticationMethod.amOwn;
//add
service(s) into SocketPro server
AddService();
//start
listening socket
if(!StartSocketProServer(20901))
{
//Can't start SocketPro server!
//Check if the port 20901 is available, please.
//Error code = CClientPeer.LastSocketError.
break;
}
//SocketPro
successfully started at the port 20901!
AskForEvents((int)tagEvent.eOnAccept + (int)tagEvent.eOnClose
+ (int)tagEvent.eOnIsPermitted); //three common global events
//start
message pump. SocketPro requires it and runs at 100% non-blocking fashion
//if a
message already exist, don't call the following function
StartMessagePump();
//stop
listening socket, and shutdown all of socket connections
StopSocketProServer();
}while(false);
}
At the very beginning, set the CMySocketProServer property
AuthenticationMethod to amOwn. Then, add one or more service into SocketPro.
Afterwards, start a SocketPro with a given port number and set global events by
calling the function AskForEvents for subscribing a few events like OnAccept,
OnClose and OnIsPermitted. At last, start a window message pump by calling the
function CSocketProServer::StartMessagePump. At the very end, call the function
CSocketProServer::StopSocketProServer to stop listening socket when we don’t
need it any more. In addition to these basic functions, you can call many
others to tune SocketPro server. We’ll discus other features in other tutorial
samples.
Permissions to client – We need to control socket connections from different clients for the sake of security. You can deny or give permission to a client from client credentials (user id and password). To achieve such a goal, you can implement it within the below function. If the function returns true, you give permission to a client. Otherwise, we deny the socket connection and SocketPro will automatically close it for you. We’ll discuss this topic further in Tutorial two.
protected override bool OnIsPermitted(int
hSocket, int nSvsID)
{
//give permission to all
return true;
}
At very end, run a SocketPro server with following code.
//MTA
preferred at server side usually
[System.MTAThread]
static void
{
CMySocketProServer
MySocketProServer = new CMySocketProServer();
MySocketProServer.Run();
}
By this time, you can run the sample SocketPro server although it does not do any thing useful. It is multiple-threaded to supports thousands or more clients.
You may think that it requires a lot time to write the above code. However, it is very simple with help of uidparser.exe, and all of these skeleton codes are automatically generated from the file TOne.uid in less than one second.
Complete server side code -- Now, open attached sample source code file TOneImpl.cs. We complete server side coding inside functions with TODO comments as shown in the below.
protected void QueryCount(out int
QueryCountRtn)
{
QueryCountRtn
= (int)m_uCount;
}
protected void QueryGlobalCount(out int
QueryGlobalCountRtn)
{
m_CS.Lock();
QueryGlobalCountRtn
= (int)m_uGlobalCount;
m_CS.Unlock();
}
protected void
QueryGlobalFastCount(out int QueryGlobalFastCountRtn)
{
QueryGlobalFastCountRtn
= (int)m_uGlobalFastCount;
}
protected void Sleep(int nTime)
{
if (TransferServerException && nTime <
200)
throw new
CSocketProServerException(12345, "Sleeping time
is too short!");
System.Threading.Thread.Sleep(nTime);
}
protected void Echo(object objInput, out object EchoRtn)
{
EchoRtn
= objInput;
}
Understand one statement for synchronizing
sharable data --
This sample service has three state data, m_GlobalCount, m_uGlobalFastCount and
m_uCount. The first two variables are static (global), and record the number of
all of processed requests (both fast and slow ones) and the number of fast
requests from all of clients since a SocketPro server is started, respectively.
The m_uCount records the number of the number of its requests processed since a
client is connected to a server. It is not a static or global variable. Look at
the sample code, and you will find that the static member m_uGlobalFastCount is
always accessed from the main thread, and never from another thread, even
through it tracks all of fast requests from different clients. It requires no
data synchronization at all. Therefore, we should not synchronize it with a
thread lock object. The variable m_uCount is indeed accessed from different
threads, but it also requires no synchronization at all because it accessed
from one client only. A client can send multiple requests in batch, but all of
requests always processed one by one like a queue within one socket connection.
In reality, there is no way for the variable m_uCount to be accessed by two
threads at the same time. However, the variable m_GlobalCount is accessed from
different threads and different clients. It could be accessed from multiple
threads at a time. Therefore, it will require you a careful consideration. As
shown in the sample code, the variable m_GlobalCount must be properly
synchronized (locked). Please keep the following statement in mind, which will
help you figure out if a variable should be synchronized. Pay close attention
to the words bolded.
A variable should be synchronized only
when it is accessed from different
threads and also accessed from different
socket connections.
SocketPro server development is extremely simple. As long as you understand the
above statement, you will not meet other problems at all.
Within this sample, the derived class CTOnePeer also overwrites the base class
functions OnSwithFrom and OnReleaseResource. At the very beginning after a
socket connection is established, a client is connected with a startup service
with a service id (sidStartup) equal to 256, which only supports one built-in
request, SwitchTo. After a client sends the request for a service with a
destination service id, the virtual function OnSwithFrom is called. When a
client sends the request SwitchTo for another service, or the client socket
connection is going to be shut down, the virtual function OnReleaseResource is
called. Usually, you should release all of unwanted resources back to operation
system as soon as possible for the sake of better performance.
Look at the class CTOneSvs constructor, you will find the protected member
m_bUsePool is set to true. As the name implies, it is related to object pool
for reusing an object in the pool without creating a new one for the better
performance. When the function OnReleaseResource is called, an instance of
CTOnePeer is not destroyed immediately but put into an object pool of its
service if its service m_bUsePool is set to true. Later, when a new instance of
CTOnePeer is required, it is just reused from the pool instead of creating a
new one. This considerably benefits server performance and scalability for
supporting a large number of clients if your CClientPeer derived objects
contains large collections of objects, COM objects and large memory chunks but
your clients often call the request SwitchTo or disconnect/reconnect socket
connections. Typically, you may need to set these objects onto an initial state
inside the function either OnReleaseResource or OnSwithFrom. A pooled object
could be destroyed after a specific time. See the
SocketProServer::CleanPoolInterval.
4. Experiments
with the sample SocketPro server
Let’s do two simple experiments
against the same server by using telnet, a tool for detecting a socket server running
a port. Please make sure that MS loopback adapter is installed.
Inside the directory socketpro4\tutorial\......\Release, double click the
application SOne.exe. A console application will be started and a listening
socket should be successfully started at the port 20001. Now go to
Start->Run->cmd and click the button OK, and cmd.exe should be started.
After typing telnet localhost 20901,
immediately SOne.exe will output a few lines of words after a connection is
established. If you don’t type any thing, the application cmd.exe will say connection to host lost in about 60
seconds. By default, SocketPro will automatically turn off the connection in 60
seconds with error code 0x8F100003 (uecClientRejected) after a socket
connection is built, if a client does not make the call SwitchTo. You can
change the setting by changing CSocketProServer::SwitchTime. Re-connect to
SOne.exe, type any eight chars and SOne will close the socket connection with
error code 0x8F000000 (uecUnexpected). SocketPro monitors all of data from a
client and will abort a socket connection if data is strange or unexpected.
5. Responsibilities
of main thread with SocketPro server
Main thread inside a SocketPro
server is the thread running with listening socket. The main thread has a lot
of responsibilities. Major ones are listed in the following.
a. Monitoring various network
events and communication events between worker threads and the main thread.
b. Manage various pools such as
thread pools, global and private memory pools, pools of CClientPeer-derived
objects, and pool holding incoming requests to be processed.
c. Route various slow requests
onto different worker threads.
d. Process all of fast requests
from all of clients.
e. Authenticate a client.
f. Decompress incoming data if it
is originally compressed (zipped).
g. Decrypt incoming data if it is
originally encrypted.
h. Create or kill threads on the
fly if necessary.
i. Encrypt or compress returned
data of all of fast requests.
j. Manage plug-in libraries
written from C/C++.
You are not required to understand how the main thread to manage these with
details. However, you should make sure that no slow requests should be
processed in the main thread. Worker threads are used to process all of slow
requests, and to encrypt or compress their returned results.
SocketPro has a standard COM library (usocket.dll) that must be used for all of
client applications development and must be registered first. No matter what a
development language you use, you should know how to reference the library, how
to instantiate a USocket COM object, and how to advise for various events from
the COM object USocket. Assuming you have all of these basic skills, we are
going to study coding SocketPro client applications. Note that you can use any
one language that supports MS COM technology although these tutorial samples
cover C++, C# and VB.net only.
Step 1: Create an instance USocket
and advise for various events. The most important events are OnSocketConnected,
OnSocketClosed and OnRequestProcessed.
Step 2: Call ISocketBase::Connect
for establishing a socket connection. After calling IUSocket::Connect, the
event OnSocketConnected will be always called with an error code. If no error
happens, you can call the method IUSocket::SwitchTo with a specified service id
and enable various window controls.
Step 3: Send one or more requests in
batch. No matter what a request is called and its return result is available,
the event OnRequestProcessed is called. Inside the function, you need to
retrieve its return data in bytes usually with an instance of CUQueue. Finally,
use CUQueue.Pop to pop out each of data.