The "Start"
button puts the server program into a server state by starting
a thread with a loop in the run()
method. Clients requesting connections are detected via
a ServerSocket
and a new instance of DataWorker
is created to handle each new client. When a connection is made,
the program opens I/O streams to and from the client.
...
The run() method in class DataServer ...
/**
Create a ServerSocket and loop waiting for clients.
**/
public void run () {
// The server_socket is used
to make connections to
// DataClients at this port
number
try {
fServerSocket
= new ServerSocket (fDataServerPort);
}
catch (IOException e) {
println
("Error in server socket");
return;
}
println ("Waiting for users...");
// Loop here to grab clients
while (fKeepServing) {
try {
//
accept () blocks until a connection is made
Socket
socket = fServerSocket.accept ();
//
Do the setup this socket and then loop
//
back around to wait for the next DataClient.
DataWorker
worker = new DataWorker (this, socket);
worker.start
();
}
catch (IOException
ioe) {
println
("IOException: <" + ioe + ">");
break;
}
catch (Exception
e) {
println
("Exception: <" + e + ">");
break;
}
}
} // run
|
DataServer
uses a Vector
to keep a list of all the clients, and a limit on the number
of clients is set with the fMaxClients
variable. A DataWorker
calls back to the clientPermit()
method, shown below, to determine if it can join the list. If
so, then it invokes clientConnected(),
which adds the client to the list.
When the client disconnects, the DataWorker
invokes the server's clientDisconnected()
method to remove itself from the worker list. These methods
are synchronized to avoid any interference if two or more threads
are connecting/disconnecting at the same time.
... continue in class DataServer
...
/**
* When a DataWorker makes the
connection, it checks to see if
* there is room on the server
for it.
* We synchronize the method
to avoid any problems with multiple
* clients interfering with each
other.
**/
public synchronized boolean clientPermit
() {
if (fWorkerList.size () < fMaxClients__)
return
true;
else
return
false;
}
/**
* A DataWorker will set up the
connection with the client. If it
* decides that the conditions
are OK, then it will invoke this
* method so that the parent
server will add the worker to its
* list.
* We synchronize the method
to avoid any problems with multiple
* clients interfering with each
other.
**/
public synchronized void clientConnected
(DataWorker worker) {
fWorkerList.add (worker);
fClientCounter__++;
}
/**
* When a client disconnects,
the DataWorker object will
* call back to this method to
remove itself from the list
* of workers.
*
* We synchronize the method
to avoid any problems with multiple
* clients interfering with each
other.
**/
public synchronized void clientDisconnected
(String user,
DataWorker worker) {
println ("Client: "+user+" disconneced");
fWorkerList.remove (worker);
fClientCounter__--;
}
...
|
The DataServer
hands off the client socket to a DataWorker,
which then begins its job of communicating with the client and
providing the requested services.
DataWorker
Class
The job of the DataWorker
is to tend to the needs of the client. The worker waits for
a request for data from the client. When the request arrives,
the worker tells the client how many data values will be included
in the data set. It then generates a set integer values, each
according to a Gaussian distribution (different data channels
use different Gaussian widths and offsets) and sends the data
set to the client.
As shown in the following code snippet, the first
act by the run()
method is to invoke the serviceSetup()
method. This method sets up the streams for I/O with the client.
The PrintWriter
and BufferedReader
wrappers are used to send and receive text to and from the client.
A DataOutputStream
wrapper is used to send numerical values. The read/write methods
for these streams are put into some utility methods discussed
later.
If the maximum number of clients has been reached,
the worker sends a warning message to its client and breaks
off the connection. That worker thread itself then signs off
from the server and dies.
If there is space for the new client, the serviceSetup()
method performs a simple log-in procedure with the client that
consists of sending the string "Username:"
to the client and waiting for a string in return.
... The run() and serviceSetup() methods
in class DataWorker ...
/** Send
data to the client. **/
public void run () {
// If setup fails, end thread
processing.
if (!serviceSetup
()) return;
...
} // run
/**
* Set up the connection to the
client. This requires obtaining the
* IO streams, carrying out the
login prototcol, and then starting
* a DataWorker thread to tend
to the client.
*
* The bookkeeping code is a
bit messy because we check both reads
* and writes for errors in case
the connection breaks down.
*
* The reads catch their own
IOExceptions and return a null, while
* string writes use a PrintWriter
that doesn't throw IOException. So
* we use the checkError () method
and throw it ourselvefDataServer.
**/
public boolean serviceSetup () {
fDataServer.println ("Client
setup...");
// First get the in/out streams
from the socket to the client
try{
fNetInputStream =
fSocket.getInputStream ();
fNetOutputStream
= fSocket.getOutputStream ();
}
catch (IOException
e){
fDataServer.println
("Unable to get input/output streams");
return false;
}
// Create a PrintWriter class
for sending text to the client.
// The writeNetOutputLine method
will use this class.
try{
fPrintWriter
= new PrintWriter (
new OutputStreamWriter (fNetOutputStream, "8859_1"),
true );
}
catch (Exception e) {
fDataServer.println
("Fails to open PrintWriter to client!");
return
false;
}
// Check if the server has room
for this client.
// If not, then send a message
to this client to tell it
// the bad news.
if ( !fDataServer.clientPermit
() ) {
try{
String
msg= "Sorry, We've reached maximum of clients";
writeNetOutputLine
(msg);
fDataServer.println
(msg);
return
false;
}
catch (IOException
e){
fDataServer.println
("Connection fails during login");
return
false;
}
}
// Get a DataInputStream wrapper
so we can use its
// readLine () methods.
fNetInputReader =
new
BufferedReader (new InputStreamReader (fNetInputStream));
// Do a simple login protocol.
Send a request for the users name.
// Note a password check could
be added here.
try{
writeNetOutputLine
( "Username: ");
}
catch (IOException e){
fDataServer.println
("Connection fails during login");
return false;
}
// Read the user name.
fUser = readNetInputLine ();
if (fUser == null
) {
fDataServer.println
("Connection fails during login");
return
false;
}
// Send a message that the
login is OK.
try{
writeNetOutputLine
("Login successful");
fDataServer.println
("Login successful for " + fUser);
} catch (IOException
e){
fDataServer.println
("Connection fails during login for "
+ fUser);
return false;
}
fDataServer.println (fUser +
" connected! ");
fDataServer.println (fSocket.toString
());
// The login is successful so
now create a DataWorker to
// service this client. Pass
it an ID number
fDataServer.clientConnected
(this);
// Get a data output stream
for writing numerical data to the client
fDataOutputStream = new DataOutputStream
(fNetOutputStream);
return true;
} // serviceSetup
...
|
If the log-in procedure is successful, then a
call back to the server informs it that the connection for this
client is successful and it should be added to the client list.
The stream methods to send or receive strings
and data involve several lines of code since they can throw
exceptions. Also, to insure that data does not get stuck in
a buffer, a flush()
method is invoked. So rather than repeating all of this code
for each read or writer operation, the DataWorker
uses the following utility methods.
... Continue in DataWorker ...
/**
Utility method to read a whole text line.**/
String readNetInputLine () {
try {
return fNetInputReader.readLine
();
}
catch (IOException
e){
return null;
}
} // readNetInputLine
/**
* Output is wrapped with a PrintWriter,
which doesn't throw
* IOException. So we invoke
the checkError() method and then
* throw an exception if it detects
an error.
**/
void writeNetOutputLine (String string)
throws
IOException {
fPrintWriter.println (string);
if ( fPrintWriter.checkError
()) throw (new IOException ());
fPrintWriter.flush ();
if ( fPrintWriter.checkError
()) throw (new IOException ());
} // writeNetOutputLine
/** Utility to write integer values to the
output stream. **/
void writeNetOutputInt (int i)
throws IOException
{
fDataOutputStream.writeInt (i);
fDataOutputStream.flush ();
} // writeNetOutputInt
/** Utility to write float values to the
output stream.**/
void writeNetOutputFloat (float f)
throws IOException
{
fDataOutputStream.writeFloat
(f);
fDataOutputStream.flush ();
} // writeNetOutputFloat
...
|
Once the connection with the client has been established,
the run()
method (shown below) enters a loop where it waits for a request
from the client, responds to the request, and then waits for
the next request. For the simple situation here, any string
received from the client is assumed to be a request for data.
The worker first tells the client how many data
values are included the data set. It then generates an array
of integer values, where the generation for each array element
comes from a random Gaussian distribution with different widths
and offsets. This data array is then sent to the client just
as if it were a set of data readings from, say, an experimental
apparatus.
... the run() method in DataWorker
...
/** Send
data to the client. **/
public void run () {
// If setup fails, end thread
processing.
if (!serviceSetup
()) return;
fDataServer.println (
"Client connection and
login OK - Begin service...");
// Lower tpriority to give main
parent and
// other threads some processor
time.
setPriority (MIN_PRIORITY);
String client_last_msg = "";
// Begin the loop for communicating
with the client.
while (fKeepRunning){
// Read a request
from the DataClient
String client_msg
= readNetInputLine ();
if (client_msg ==
null ) break;
// Only print message
if it changes. Avoids printing same
// message for each
data set.
if (
!client_msg.equals (client_last_msg))
fDataServer.println
("Message from " + fUser + ": "
+ client_msg);
client_last_msg
= client_msg;
// Could interpret
the request and do something accordingly
// but here we will
just send a set of data values.
// Send the number
of data values.
try {
writeNetOutputInt
(DataServer.fNumDataVals__);
}
catch (IOException
e) {
break;
}
// Creat dummy data
values and send them to the DataClient.
for (int i=0; i<
DataServer.fNumDataVals__; i++){
// Select
that range of Gaussian widths for the data
// values
for each channel of the data set. Add an offset
// to
get most negative values above zero.
int
i_std_dev = i%6;
double
dat = 3.0*fStdDev[i_std_dev] +
fStdDev[i_std_dev] * fRan.nextGaussian ();
if (dat
< 0.0) dat = 0.0;
// Pass
only integer values;
int
idat = (int) dat;
try
{
writeNetOutputInt
(idat);
}
catch (IOException
e) {
break;
}
}
}
// Send message back to the
text area in the frame.
fDataServer.println (fUser +
" has disconnected.");
// Do any other tasks for ending
the worker.
signOff ();
} // run
|
The DataWorker
could be modified to obtain real data in various ways. For example,
a sensor might generate a file that the worker could read and
send to the client. In Chapter
23 we will discuss how Java programs can communicate with
devices via the serial ports. The worker might obtain data from
a device that way and then send it to the client.