Sockets form the underpinnings of almost all networking protocols. JDBC, RMI, CORBA, EJB, and the non-Java RPC (Remote Procedure Call) and NFS (Network File System) are all implemented by connecting various types of sockets together. Socket connections can be implemented in most any language, not just Java: C, C++, Perl, and Python are also popular, and many others are possible. A client or server written in any one of these languages can communicate with its opposite written in any of the other languages. Therefore, it’s worth taking a quick look at how the ServerSocket
behaves, even if you wind up utilizing the higher-level services such as RMI, JDBC, CORBA, or EJB.
The discussion looks first at the ServerSocket
itself, then at writing data over a socket in various ways. Finally, I show a complete implementation of a usable network server written in Java: the chat server from the client in the previous chapter.
Most production work in server-side Java uses the Java Enterprise Edition (Java EE), recently donated to the Eclipse Software Foundation and renamed to Jakarta, but still widely referred to by the previous name (and occasionally by its very old name, “J2EE”, which was retired in 2005). Java EE provides scalability and support for building well-structured, multi-tiered distributed applications. EE provides the “servlet” framework; a servlet is a strategy object that can be installed into any standard Java EE web server. EE also provides two web “view” technologies: the original JSP (JavaServer Pages) and the newer, component-based JSF (JavaServer Faces). Finally, EE provides a number of other network-based services, including EJB3 remote access and Java Messaging Service (JMS). These are unfortunately outside the scope of this book, and are covered in several other books such as Arun Gupta’s Java EE 7 Essentials: Enterprise Developer Handbook. This chapter is only for those who need or want to build their own server from the ground up.
You need to write a socket-based server.
Create a ServerSocket
for the given port number.
The ServerSocket
represents the “other end” of a connection, the server that waits patiently for clients to come along and connect to it. You construct a ServerSocket
with just the port number.1 Because it doesn’t need to connect to another host, it doesn’t need a particular host’s address as the client socket constructor does.
Assuming the ServerSocket
constructor doesn’t throw an exception, you’re in business. Your next step is to await client activity, which you do by calling accept()
. This call blocks until a client connects to your server; at that point, the accept()
returns to you a Socket
object (not a ServerSocket
) that is connected in both directions to the Socket
object on the client (or its equivalent, if written in another language). Example 13-1 shows the code for a socket-based server.
public
class
Listen
{
/** The TCP port for the service. */
public
static
final
short
PORT
=
9999
;
public
static
void
main
(
String
[]
argv
)
throws
IOException
{
ServerSocket
sock
;
Socket
clientSock
;
try
{
sock
=
new
ServerSocket
(
PORT
);
while
((
clientSock
=
sock
.
accept
())
!=
null
)
{
// Process it, usually on a separate thread
// to avoid blocking the accept() call.
process
(
clientSock
);
}
}
catch
(
IOException
e
)
{
System
.
err
.
println
(
e
);
}
}
/** This would do something with one client. */
static
void
process
(
Socket
s
)
throws
IOException
{
System
.
out
.
println
(
"Accept from client "
+
s
.
getInetAddress
());
// The conversation would be here.
s
.
close
();
}
}
You would normally use the same socket for both reading and writing, as shown in the next few recipes.
You may want to listen only on a particular network interface. Though
we tend to think of network addresses as computer addresses, the
two are not the same. A network address is actually the address of
a particular network card, or network interface connection, on a
given computing device. A desktop computer, laptop, tablet,
or mobile phone might have only a single interface, hence a single
network address. But a large server machine might have two or more
interfaces, usually when it is connected to several networks. A
network router is a box, either special purpose (e.g., a Cisco
router), or general purpose (e.g., a Unix host), that has interfaces
on multiple networks and has both the capability and the
administrative permission to forward packets from one network to
another. A program running on such a server machine might want to
provide services only to its inside network or its outside network.
One way to accomplish this is by specifying the network interface
to be listened on. Suppose you want to provide a different view of
web pages for your intranet than you provide to outside customers.
For security reasons, you probably wouldn’t run both these services
on the same machine. But if you wanted to, you could do this by
providing the network interface addresses as arguments to the
ServerSocket
constructor.
However, to use this form of the constructor, you don’t have the
option of using a string for the network address’s name, as you did
with the client socket; you must convert it to an InetAddress
object. You also have to provide a backlog argument, which is the
number of connections that can queue up to be accepted before clients
are told that your server is too busy. The complete setup is shown
in Example 13-2.
public
class
ListenInside
{
/** The TCP port for the service. */
public
static
final
short
PORT
=
9999
;
/** The name of the network interface. */
public
static
final
String
INSIDE_HOST
=
"acmewidgets-inside"
;
/** The number of clients allowed to queue */
public
static
final
int
BACKLOG
=
10
;
public
static
void
main
(
String
[]
argv
)
throws
IOException
{
ServerSocket
sock
;
Socket
clientSock
;
try
{
sock
=
new
ServerSocket
(
PORT
,
BACKLOG
,
InetAddress
.
getByName
(
INSIDE_HOST
));
while
((
clientSock
=
sock
.
accept
())
!=
null
)
{
// Process it.
process
(
clientSock
);
}
}
catch
(
IOException
e
)
{
System
.
err
.
println
(
e
);
}
}
/** Hold server's conversation with one client. */
static
void
process
(
Socket
s
)
throws
IOException
{
System
.
out
.
println
(
"Connected from "
+
INSIDE_HOST
+
": "
+
s
.
getInetAddress
(
));
// The conversation would be here.
s
.
close
();
}
}
The InetAddress.getByName()
looks up the given hostname in a system-dependent way, referring to a configuration file in the /etc or windows directory, or to some kind of resolver such as the Domain Name System. Consult a good book on networking and system administration if you need to modify this data.
You wish to find out about the computer’s networking arrangements.
Use the NetworkInterface
class.
Every computer on a network has one or more “network interfaces.” On typical desktop machines, a network interface represents a network card or network port or some software network interface, such as the loopback interface. Each interface has an operating system–defined name. On most versions of Unix, these devices have a two- or three-character device driver name plus a digit (starting from 0); for example, eth0
or en0
for the first Ethernet on systems that hide the details of the card manufacturer, or de0
and de1
for the first and second Digital Equipment2 DC21x4x-based Ethernet card, xl0
for a 3Com EtherLink XL, and so on. The loopback interface is almost invariably lo0
on all Unix-like platforms.
So what? Most of the time this is of no consequence to you. If you have only one network connection, like a cable link to your ISP, you really don’t care. Where this matters is on a server, where you might need to find the address for a given network, for example. The NetworkInterface
class lets you find out. It has static methods for listing the interfaces and other methods for finding the addresses associated with a given interface. The program in Example 13-3 shows some examples of using this class. Running it prints the names of all the local interfaces. If you happen to be on a computer named laptop, it prints the machine’s network address; if not, you probably want to change it to accept the local computer’s name from the command line; this is left as an exercise for the reader.
public
class
NetworkInterfaceDemo
{
public
static
void
main
(
String
[]
a
)
throws
IOException
{
Enumeration
<
NetworkInterface
>
list
=
NetworkInterface
.
getNetworkInterfaces
();
while
(
list
.
hasMoreElements
())
{
// Get one NetworkInterface
NetworkInterface
iface
=
list
.
nextElement
();
// Print its name
System
.
out
.
println
(
iface
.
getDisplayName
());
Enumeration
<
InetAddress
>
addrs
=
iface
.
getInetAddresses
();
// And its address(es)
while
(
addrs
.
hasMoreElements
())
{
InetAddress
addr
=
addrs
.
nextElement
();
System
.
out
.
println
(
addr
);
}
}
// Try to get the Interface for a given local (this machine's) address
InetAddress
destAddr
=
InetAddress
.
getByName
(
"laptop"
);
try
{
NetworkInterface
dest
=
NetworkInterface
.
getByInetAddress
(
destAddr
);
System
.
out
.
println
(
"Address for "
+
destAddr
+
" is "
+
dest
);
}
catch
(
SocketException
ex
)
{
System
.
err
.
println
(
"Couldn't get address for "
+
destAddr
);
}
}
}
You need to write a string or binary data to the client.
The socket gives you an InputStream
and an OutputStream
. Use them.
The client socket examples in the previous chapter called the getInputStream()
and getOutputStream()
methods. These examples do the same. The main difference is that these ones get the socket from a ServerSocket
’s accept()
method. Another distinction is, by definition, that normally the server creates or modifies the data and sends it to the client. Example 13-4 is a simple Echo
server, which the Echo
client of Recipe 12.5 can connect to. This server handles one complete connection with a client, then goes back and does the accept()
to wait for the next client.
public
class
EchoServer
{
/** Our server-side rendezvous socket */
protected
ServerSocket
sock
;
/** The port number to use by default */
public
final
static
int
ECHOPORT
=
7
;
/** Flag to control debugging */
protected
boolean
debug
=
true
;
/** main: construct and run */
public
static
void
main
(
String
[]
args
)
{
int
p
=
ECHOPORT
;
if
(
args
.
length
==
1
)
{
try
{
p
=
Integer
.
parseInt
(
args
[
0
]);
}
catch
(
NumberFormatException
e
)
{
System
.
err
.
println
(
"Usage: EchoServer [port#]"
);
System
.
exit
(
1
);
}
}
new
EchoServer
(
p
).
handle
();
}
/** Construct an EchoServer on the given port number */
public
EchoServer
(
int
port
)
{
try
{
sock
=
new
ServerSocket
(
port
);
}
catch
(
IOException
e
)
{
System
.
err
.
println
(
"I/O error in setup"
);
System
.
err
.
println
(
e
);
System
.
exit
(
1
);
}
}
/** This handles the connections */
protected
void
handle
()
{
Socket
ios
=
null
;
while
(
true
)
{
try
{
System
.
out
.
println
(
"Waiting for client..."
);
ios
=
sock
.
accept
();
System
.
err
.
println
(
"Accepted from "
+
ios
.
getInetAddress
().
getHostName
());
try
(
BufferedReader
is
=
new
BufferedReader
(
new
InputStreamReader
(
ios
.
getInputStream
(),
"8859_1"
));
PrintWriter
os
=
new
PrintWriter
(
new
OutputStreamWriter
(
ios
.
getOutputStream
(),
"8859_1"
),
true
);)
{
String
echoLine
;
while
((
echoLine
=
is
.
readLine
())
!=
null
)
{
System
.
err
.
println
(
"Read "
+
echoLine
);
os
.
(
echoLine
+
" "
);
os
.
flush
();
System
.
err
.
println
(
"Wrote "
+
echoLine
);
}
System
.
err
.
println
(
"All done!"
);
}
}
catch
(
IOException
e
)
{
System
.
err
.
println
(
e
);
}
}
/* NOTREACHED */
}
}
To send a string across an arbitrary network connection, some authorities recommend sending both the carriage return and the newline character; many protocol specifications require that you do so. This explains the
in the code. If the other end is a DOS program or a Telnet-like program, it may be expecting both characters. On the other hand, if you are writing both ends, you can simply use println()
—followed always by an explicit flush()
before you read—-to prevent the deadlock of having both ends trying to read with one end’s data still in the PrintWriter
’s buffer!
If you need to process binary data, use the data streams from java.io
instead of the
readers/writers. I need a server for the DaytimeBinary
program of
Recipe 12.6. In operation, it should look like the following:
C:javasrc etwork>java network.DaytimeBinary Remote time is 3161316799 BASE_DIFF is 2208988800 Time diff == 952284799 Time on localhost is Sun Mar 08 19:33:19 GMT 2014 C:javasrc etwork>time/t Current time is 7:33:23.84p C:javasrc etwork>date/t Current date is Sun 03-08-2014 C:javasrc etwork>
Well, it happens that I have such a program in my arsenal, so I present it in Example 13-5. Note that it directly uses certain public constants defined in the client class. Normally these are defined in the server class and used by the client, but I wanted to present the client code first.
public
class
DaytimeServer
{
/** Our server-side rendezvous socket */
ServerSocket
sock
;
/** The port number to use by default */
public
final
static
int
PORT
=
37
;
/** main: construct and run */
public
static
void
main
(
String
[]
argv
)
{
new
DaytimeServer
(
PORT
).
runService
();
}
/** Construct a DaytimeServer on the given port number */
public
DaytimeServer
(
int
port
)
{
try
{
sock
=
new
ServerSocket
(
port
);
}
catch
(
IOException
e
)
{
System
.
err
.
println
(
"I/O error in setup "
+
e
);
System
.
exit
(
1
);
}
}
/** This handles the connections */
protected
void
runService
()
{
Socket
ios
=
null
;
DataOutputStream
os
=
null
;
while
(
true
)
{
try
{
System
.
out
.
println
(
"Waiting for connection on port "
+
PORT
);
ios
=
sock
.
accept
();
System
.
err
.
println
(
"Accepted from "
+
ios
.
getInetAddress
().
getHostName
());
os
=
new
DataOutputStream
(
ios
.
getOutputStream
());
long
time
=
System
.
currentTimeMillis
();
time
/=
1000
;
// Daytime Protocol is in seconds
// Convert to Java time base.
time
+=
RDateClient
.
BASE_DIFF
;
// Write it, truncating cast to int since it is using
// the Internet Daytime protocol which uses 4 bytes.
// This will fail in the year 2038, along with all
// 32-bit timekeeping systems based from 1970.
// Remember, you read about the Y2038 crisis here first!
os
.
writeInt
((
int
)
time
);
os
.
close
();
}
catch
(
IOException
e
)
{
System
.
err
.
println
(
e
);
}
}
}
}
You need to return an object across a network connection.
Create the object you need, and write it using an ObjectOutputStream
created on top of the socket’s output stream.
The program in Example 12-7 in the previous chapter reads a Date
object over an ObjectInputStream
. Example 13-6, the DaytimeObjectServer
(the other end of that process), is a program that constructs a Date
object each time it’s connected to and returns it to the client.
public
class
DaytimeObjectServer
{
/** The TCP port for the object time service. */
public
static
final
short
TIME_PORT
=
1951
;
public
static
void
main
(
String
[]
argv
)
{
ServerSocket
sock
;
Socket
clientSock
;
try
{
sock
=
new
ServerSocket
(
TIME_PORT
);
while
((
clientSock
=
sock
.
accept
())
!=
null
)
{
System
.
out
.
println
(
"Accept from "
+
clientSock
.
getInetAddress
());
ObjectOutputStream
os
=
new
ObjectOutputStream
(
clientSock
.
getOutputStream
());
// Construct and write the Object
os
.
writeObject
(
LocalDateTime
.
now
());
os
.
close
();
}
}
catch
(
IOException
e
)
{
System
.
err
.
println
(
e
);
}
}
}
Your server needs to handle multiple clients.
Use a thread for each.
In the C world, several mechanisms allow a server to handle multiple clients. One is to use a special “system call” select()
or poll()
, which notifies the server when any of a set of file/socket descriptors is ready to read, ready to write, or has an error. By including its rendezvous socket (equivalent to our ServerSocket
) in this list, the C-based server can read from any of a number of clients in any order. Java does not provide this call, because it is not readily implementable on some Java platforms. Instead, Java uses the general-purpose Thread
mechanism, as described in Chapter 16 (threads are now commonplace in many programming languages, though not always under that name). Each time the code accepts a new connection from the ServerSocket
, it immediately constructs and starts a new thread object to process that client.3
The Java code to implement accepting on a socket is pretty simple, apart from having to catch IOException
s:
/** Run the main loop of the Server. */ void runServer( ) { while (true) { try { Socket clntSock = sock.accept( ); new Handler(clntSock).start( ); } catch(IOException e) { System.err.println(e); } } }
To use a thread, you must either subclass Thread
or implement Runnable
. The Handler
class must be a subclass of Thread
for this code to work as written; if Handler
instead implemented the Runnable
interface, the code would pass an instance of the Runnable
into the constructor for Thread
, as in:
Thread t = new Thread(new Handler(clntSock)); t.start( );
But as written, Handler
is constructed using the normal socket returned by accept()
, and normally calls the socket’s getInputStream()
and getOutputStream()
methods and holds its conversation in the usual way. I’ll present a full implementation, a threaded echo client. First, a session showing it in use:
$ java network.EchoServerThreaded EchoServerThreaded ready for connections. Socket starting: Socket[addr=localhost/127.0.0.1,port=2117,localport=7] Socket starting: Socket[addr=darian/192.168.1.50,port=13386,localport=7] Socket starting: Socket[addr=darian/192.168.1.50,port=22162,localport=7] Socket ENDED: Socket[addr=darian/192.168.1.50,port=22162,localport=7] Socket ENDED: Socket[addr=darian/192.168.1.50,port=13386,localport=7] Socket ENDED: Socket[addr=localhost/127.0.0.1,port=2117,localport=7]
Here, I connected to the server once with my EchoClient
program and, while still
connected, called it up again (and again) with an operating system–provided Telnet client.
The server communicated with all the clients concurrently, sending the answers from the
first client back to the first client, and the data from the second client back to the
second client. In short, it works. I ended the sessions with the end-of-file character in
the program and used the normal disconnect mechanism from the Telnet client.
Example 13-7 is the code for the server.
public
class
EchoServerThreaded
{
public
static
final
int
ECHOPORT
=
7
;
public
static
void
main
(
String
[]
av
)
{
new
EchoServerThreaded
().
runServer
();
}
public
void
runServer
()
{
ServerSocket
sock
;
Socket
clientSocket
;
try
{
sock
=
new
ServerSocket
(
ECHOPORT
);
System
.
out
.
println
(
"EchoServerThreaded ready for connections."
);
/* Wait for a connection */
while
(
true
)
{
clientSocket
=
sock
.
accept
();
/* Create a thread to do the communication, and start it */
new
Handler
(
clientSocket
).
start
();
}
}
catch
(
IOException
e
)
{
/* Crash the server if IO fails. Something bad has happened */
System
.
err
.
println
(
"Could not accept "
+
e
);
System
.
exit
(
1
);
}
}
/** A Thread subclass to handle one client conversation. */
class
Handler
extends
Thread
{
Socket
sock
;
Handler
(
Socket
s
)
{
sock
=
s
;
}
public
void
run
()
{
System
.
out
.
println
(
"Socket starting: "
+
sock
);
try
(
BufferedReader
is
=
new
BufferedReader
(
new
InputStreamReader
(
sock
.
getInputStream
()));
PrintStream
os
=
new
PrintStream
(
sock
.
getOutputStream
(),
true
);)
{
String
line
;
while
((
line
=
is
.
readLine
())
!=
null
)
{
os
.
(
line
+
" "
);
os
.
flush
();
}
sock
.
close
();
}
catch
(
IOException
e
)
{
System
.
out
.
println
(
"IO Error on socket "
+
e
);
return
;
}
System
.
out
.
println
(
"Socket ENDED: "
+
sock
);
}
}
}
A lot of short transactions can degrade performance, because each client causes the creation
of a new threaded object. If you know or can reliably predict the degree of concurrency
that is needed, an alternative paradigm involves the precreation of a fixed number of
threads. But then how do you control their access to the ServerSocket
? A look at the
ServerSocket
class documentation reveals that the accept()
method is not
synchronized, meaning that any number of threads can call the method concurrently. This
could cause bad things to happen. So I use the synchronized
keyword around this call to
ensure that only one client runs in it at a time, because it updates global data. When no
clients are connected, you will have one (randomly selected) thread running in the
ServerSocket
object’s accept()
method, waiting for a connection, plus n-1 threads
waiting for the first thread to return from the method. As soon as the first thread
manages to accept a connection, it goes off and holds its conversation, releasing its lock
in the process so that another randomly chosen thread is allowed into the accept()
method. Each thread’s run()
method has an infinite loop beginning with an accept()
and then holding the conversation. The result is that client connections can get started
more quickly, at a cost of slightly greater server startup time. Doing it this way also
avoids the overhead of constructing a new Handler
or Thread
object each time a request
comes along. This general approach is similar to what the popular Apache web server does,
although it normally creates a number or pool of identical processes (instead of threads)
to handle client connections. Accordingly, I have modified the EchoServerThreaded
class
shown in Example 13-7 to work this way, as you can see in
Example 13-8.
public
class
EchoServerThreaded2
{
public
static
final
int
ECHOPORT
=
7
;
public
static
final
int
NUM_THREADS
=
4
;
/** Main method, to start the servers. */
public
static
void
main
(
String
[]
av
)
{
new
EchoServerThreaded2
(
ECHOPORT
,
NUM_THREADS
);
}
/** Constructor */
public
EchoServerThreaded2
(
int
port
,
int
numThreads
)
{
ServerSocket
servSock
;
try
{
servSock
=
new
ServerSocket
(
port
);
}
catch
(
IOException
e
)
{
/* Crash the server if IO fails. Something bad has happened */
throw
new
RuntimeException
(
"Could not create ServerSocket "
,
e
);
}
// Create a series of threads and start them.
for
(
int
i
=
0
;
i
<
numThreads
;
i
++)
{
new
Handler
(
servSock
,
i
).
start
();
}
}
/** A Thread subclass to handle one client conversation. */
class
Handler
extends
Thread
{
ServerSocket
servSock
;
int
threadNumber
;
/** Construct a Handler. */
Handler
(
ServerSocket
s
,
int
i
)
{
servSock
=
s
;
threadNumber
=
i
;
setName
(
"Thread "
+
threadNumber
);
}
public
void
run
()
{
/*
* Wait for a connection. Synchronized on the ServerSocket while
* calling its accept() method.
*/
while
(
true
)
{
try
{
System
.
out
.
println
(
getName
()
+
" waiting"
);
Socket
clientSocket
;
// Wait here for the next connection.
synchronized
(
servSock
)
{
clientSocket
=
servSock
.
accept
();
}
System
.
out
.
println
(
getName
()
+
" starting, IP="
+
clientSocket
.
getInetAddress
());
try
(
BufferedReader
is
=
new
BufferedReader
(
new
InputStreamReader
(
clientSocket
.
getInputStream
()));
PrintStream
os
=
new
PrintStream
(
clientSocket
.
getOutputStream
(),
true
);)
{
String
line
;
while
((
line
=
is
.
readLine
())
!=
null
)
{
os
.
(
line
+
" "
);
os
.
flush
();
}
System
.
out
.
println
(
getName
()
+
" ENDED "
);
clientSocket
.
close
();
}
}
catch
(
IOException
ex
)
{
System
.
out
.
println
(
getName
()
+
": IO Error on socket "
+
ex
);
return
;
}
}
}
}
}
It is quite possible to implement a server of this sort with NIO, the “new” (back in J2SE 1.4) IO package. However, the code to do so outweighs anything in this chapter, and it is fraught with “issues.” There are several good tutorials on the Internet for the person who truly needs the performance gain of using NIO to manage server connections.
You want to serve up a protocol such as HTTP.
Create a ServerSocket
and write some code that “speaks” the particular protocol.
Or, better, use a Java-powered web server such as Apache Tomcat or a Java Enterprise Edition (Java EE)
server such as JBoss WildFly.
You can implement your own HTTP protocol server for very simple applications, which we’ll do here. For any serious development, you want to use the Java Enterprise Edition; see the note at the beginning of this chapter.
This example just constructs a ServerSocket
and listens on it. When connections come in,
they are replied to using the HTTP protocol. So it is somewhat more involved than the
simple Echo
server presented in Recipe 13.3. However, it’s not a
complete web server; the filename in the request is ignored, and a standard message is
always returned. This is thus a very simple web server; it follows only the bare minimum
of the HTTP protocol needed to send its response back.
For a real web server written in Java, get Tomcat from the
Apache Tomcat website or any of the Jakarta/JavaEE Application Servers.
The code shown in Example 13-9, however, is enough to understand how to build
a simple server that responds to requests using a protocol.
public
class
WebServer0
{
public
static
final
int
HTTP
=
80
;
public
static
final
String
CRLF
=
" "
;
ServerSocket
s
;
/** A link to the source of this program, used in error message */
static
final
String
VIEW_SOURCE_URL
=
"https://github.com/IanDarwin/javasrc/tree/master/main/src/main/java/network"
;
/**
* Main method, just creates a server and call its runServer().
*/
public
static
void
main
(
String
[]
args
)
throws
Exception
{
System
.
out
.
println
(
"DarwinSys JavaWeb Server 0.0 starting..."
);
WebServer0
w
=
new
WebServer0
();
int
port
=
HTTP
;
if
(
args
.
length
==
1
)
{
port
=
Integer
.
parseInt
(
args
[
0
]);
}
w
.
runServer
(
port
);
// never returns!!
}
/** Get the actual ServerSocket; deferred until after Constructor
* so subclass can mess with ServerSocketFactory (e.g., to do SSL).
* @param port The port number to listen on
*/
protected
ServerSocket
getServerSocket
(
int
port
)
throws
Exception
{
return
new
ServerSocket
(
port
);
}
/** RunServer accepts connections and passes each one to handler. */
public
void
runServer
(
int
port
)
throws
Exception
{
s
=
getServerSocket
(
port
);
while
(
true
)
{
try
{
Socket
us
=
s
.
accept
();
Handler
(
us
);
}
catch
(
IOException
e
)
{
System
.
err
.
println
(
e
);
return
;
}
}
}
/** Handler() handles one conversation with a Web client.
* This is the only part of the program that "knows" HTTP.
*/
public
void
Handler
(
Socket
s
)
{
BufferedReader
is
;
// inputStream, from Viewer
PrintWriter
os
;
// outputStream, to Viewer
String
request
;
// what Viewer sends us.
try
{
String
from
=
s
.
getInetAddress
().
toString
();
System
.
out
.
println
(
"Accepted connection from "
+
from
);
is
=
new
BufferedReader
(
new
InputStreamReader
(
s
.
getInputStream
()));
request
=
is
.
readLine
();
System
.
out
.
println
(
"Request: "
+
request
);
os
=
new
PrintWriter
(
s
.
getOutputStream
(),
true
);
os
.
(
"HTTP/1.0 200 Here is your data"
+
CRLF
);
os
.
(
"Content-type: text/html"
+
CRLF
);
os
.
(
"Server-name: DarwinSys NULL Java WebServer 0"
+
CRLF
);
String
reply1
=
"<html><head>"
+
"<title>Wrong System Reached</title></head> "
+
"<h1>Welcome, "
;
String
reply2
=
", but...</h1> "
+
"<p>You have reached a desktop machine "
+
"that does not run a real Web service. "
+
"<p>Please pick another system!</p> "
+
"<p>Or view <a href=""
+
VIEW_SOURCE_URL
+
"">"
+
"the WebServer0 source on github</a>.</p> "
+
"<hr/><em>Java-based WebServer0</em><hr/> "
+
"</html> "
;
os
.
(
"Content-length: "
+
(
reply1
.
length
()
+
from
.
length
()
+
reply2
.
length
())
+
CRLF
);
os
.
(
CRLF
);
os
.
(
reply1
+
from
+
reply2
+
CRLF
);
os
.
flush
();
s
.
close
();
}
catch
(
IOException
e
)
{
System
.
out
.
println
(
"IOException "
+
e
);
}
return
;
}
}
You want to protect your network traffic from prying eyes or malicious modification, while the data is in transit.
Use the Java Secure Socket Extension, JSSE, to encrypt your traffic.
JSSE provides services at a number of levels, but the simplest way to use it is simply to get your ServerSocket
from an SSLServerSocketFactory
instead of using the ServerSocket
constructor directly. SSL is the Secure Sockets Layer; a revised version is known as TLS. It is specifically for use on the Web. To secure other protocols, you’d have to use a different form of the SocketFactory
.
The SSLServerSocketFactory
returns a ServerSocket
that is set up to do SSL encryption. Example 13-10 uses this technique to override the getServerSocket()
method in Recipe 13.6. If you’re thinking this is too easy, you’re wrong!
/**
* JSSEWebServer - subclass trivial WebServer0 to make it use SSL.
* N.B. You MUST have set up a server certificate (see the
* accompanying book text), or you will get the dreaded
* javax.net.ssl.SSLHandshakeException: no cipher suites in common
* (because without it JSSE can't use any of its built-in ciphers!).
*/
public
class
JSSEWebServer0
extends
WebServer0
{
public
static
final
int
HTTPS
=
8443
;
public
static
void
main
(
String
[]
args
)
throws
Exception
{
if
(
System
.
getProperty
(
"javax.net.ssl.keyStore"
)
==
null
)
{
System
.
err
.
println
(
"You must pass in a keystore via -D; see the documentation!"
);
System
.
exit
(
1
);
}
System
.
out
.
println
(
"DarwinSys JSSE Server 0.0 starting..."
);
JSSEWebServer0
w
=
new
JSSEWebServer0
();
w
.
runServer
(
HTTPS
);
// never returns!!
}
/** Get an HTTPS ServerSocket using JSSE.
* @see WebServer0#getServerSocket(int)
* @throws ClassNotFoundException if the SecurityProvider cannot be instantiated.
*/
protected
ServerSocket
getServerSocket
(
int
port
)
throws
Exception
{
SSLServerSocketFactory
ssf
=
(
SSLServerSocketFactory
)
SSLServerSocketFactory
.
getDefault
();
return
ssf
.
createServerSocket
(
port
);
}
}
That is, indeed, all the Java code one needs to write. You do have to set up an SSL Certificate. For demonstration purposes, this can be a self-signed certificate; the steps in https://darwinsys.com/java/selfsigncert.html (Steps 1–4) will suffice. You have to tell the JSSE layer where to find your keystore:
java -Djavax.net.ssl.keyStore=/home/ian/.keystore -Djavax.net.ssl. keyStorePassword=secrit JSSEWebServer0
The typical client browser raises its eyebrows at a self-signed certificate (see Figure 13-1), but, if the user OKs it, will accept the certificate.
Figure 13-2 shows the output of the simple WebServer0
being displayed
over the HTTPS protocol (notice the padlock in the lower-right corner).
JSSE can do much more than encrypt web server traffic; this is, however, sometimes seen as its most exciting application. For more information on JSSE, see the Sun website or Java Security by Scott Oaks (O’Reilly).
You want to implement a RESTful server, using the provided Java EE/Jakarta EE APIs.
Use JAX-RS annotations on a class that provides a service; install it in an enterprise application server.
This operation consists of both coding and configuration.
The coding steps consist of creating a class that extends the JAX-RS Application class, and adding annotations to a class that provides a service.
Here is a minimal Application class:
import javax.ws.rs.ApplicationPath; import javax.ws.rs.core.Application; @ApplicationPath("") public class RestApplication extends Application { // Empty }
Here is a “hello world” type service class with the annotations needed to make it a service class and to have three sample methods.
@Path
(
""
)
@ApplicationScoped
public
class
RestService
{
public
RestService
()
{
System
.
out
.
println
(
"RestService.init()"
);
}
@GET
@Path
(
"/timestamp"
)
@Produces
(
MediaType
.
TEXT_PLAIN
)
public
String
getDate
()
{
return
LocalDateTime
.
now
().
toString
();
}
/** A Hello message method
*/
@GET
@Path
(
"/greeting/{userName}"
)
@Produces
(
"text/html"
)
public
String
doGreeting
(
@PathParam
(
"userName"
)
String
userName
)
{
System
.
out
.
println
(
"RestService.greeting()"
);
if
(
userName
==
null
||
userName
.
trim
().
length
()
<=
3
)
{
return
"Missing or too-short username"
;
}
return
String
.
format
(
"<h1>Welcome %s</h1><p>%s, We are glad to see you back!"
,
userName
,
userName
);
}
/** Used to download all items */
@GET
@Path
(
"/names"
)
@Produces
(
MediaType
.
APPLICATION_JSON
)
public
List
<
String
>
findTasksForUser
()
{
return
List
.
of
(
"Robin"
,
"Jedunkat"
,
"Lyn"
,
"Glen"
);
}
}
Now the class must be deployed. If we have created a proper Maven project structure (see Recipe 1.7)
and have provided an application-server-specific Maven plug-in, and our development server is running,
we can use some variation on mvn deploy
.
In the present case I have set this up, in the rest subdirectory, for deployment to WildFly,
a server from the JBoss open source community funded by RedHat Inc.
I need only say mvn wildfly:deploy
to have the application compiled, package,
and deployed to my server.
For deploying REST services as a micro-service based on Eclipse MicroProfile, you may wish to investigate the Quarkus Framework.
Once the service is deployed, you can explore it interactively with a browser or, for simple GET requests, a Telnet client:
$ telnet localhost 8080 # output cleaned up Escape character is '^]'. GET /rest/timestamp HTTP/1.0 Connection: keep-alive HTTP/1.1 200 OK Content-Type: text/plain;charset=UTF-8 2019-10-16T19:54:31.42 GET /rest/greeting/Ian%20Darwin HTTP/1.0 HTTP/1.1 200 OK Content-Type: text/html;charset=UTF-8 <h1>Welcome Ian Darwin</h1><p>Ian Darwin, We are glad to see you back! get /rest/names HTTP/1.0 Accept: Application/JSON HTTP/1.1 200 OK Content-Type: application/json ["Robin","Jedunkat","Lyn","Glen"] ^] (CTRL/C) $
An issue with REST is that there is not an official standard for documenting the API or protocol offered by a server (there are several competing specifications). So people writing clients must either rely on plain documentation offered by the server’s developers, or use trial and error to discover the protocol. Our example here is simple enough that we don’t have this problem, but imagine a class with 20 or 30 methods in it.
The Spring Framework offers an API that is very similar to the JAX-RS API used here; if you are already using Spring, it may be simpler to use their annotations.
Your class is running inside a server container, and its debugging output is hard to obtain.
Use a network-based logger like the Java Logging API (JUL
), Apache Logging Services Project’s log4j
, or the simple one shown here.
Getting the debug output from a desktop client is fairly easy on most operating systems. But if the program you want to debug is running in a “container” like a servlet engine or an EJB server, it can be difficult to obtain debugging output, particularly if the container is running on a remote computer. It would be convenient if you could have your program send messages back to a program on your desktop machine for immediate display. Needless to say, it’s not that hard to do this with Java’s socket mechanism.
Many logging APIs can handle this:
Java has had for years a standard logging API JUL
(discussed in Recipe 13.12) that talks to various logging mechanisms including Unix syslog
.
The Apache Logging Services Project produces log4j
, which is used in many open source projects that require logging (see Recipe 13.11).
The Apache Jakart Commons Logging (JCL). Not discussed here; similar to the others.
SLF4J (Simple Logging Facade For Java, see Recipe 13.10) is the newest and, as the name implies, a facade that can use the others.
And, before these became widely used, I wrote a small, simple API to handle this type of logging function.
My netlog
is not discussed here because it is preferable to use one of the standard logging mechanisms;
its code is in the logging subdirectory of the javasrc repo if you want to exhume it.
The JDK logging API, log4j
, and SFL4J
are more fully fleshed out and can write to such destinations as a file, an OutputStream
or Writer
, or a remote log4j
, Unix syslog
, or Windows Event Log server.
The program being debugged is the “client” from the logging API’s point of view—even though it may be running in a server-side container such as a web server or application server—because the “network client” is the program that initiates the connection. The program that runs on your desktop machine is the “server” program for sockets because it waits for a connection to come along.
If you want to run any network-based logger reachable from any public network, you need to be more aware of security issues. One common form of attack is a simple denial-of-service (DoS), during which the attacker makes a lot of connections to your server in order to slow it down. If you are writing the log to disk, for example, the attacker could fill up your disk by sending lots of garbage. In common use, your log listener would be behind a firewall and not reachable from outside, but if this is not the case, beware of the DoS attack.
You want to use a logging API that lets you use any of the other logging APIs, for example, so your code can be used in other projects without requiring them to switch logging APIs.
Use SLF4J
: get a Logger
from the LoggerFactory
, and use its various methods for logging.
Using SLF4j
requires only one JAR file to compile, slf4j-api-1.x.y.jar (where x and y will change
over time). To actually get logging output, you need to add one of several implementation JARs to your runtime
classpath, the simplest of which is slf4j-simple-1.x.y.jar (where x and y should match between
the two files).
Once you’ve added those JAR files to your build script or on your classpath, you can
get a Logger
by calling LoggerFactory.getLogger()
, passing either the string name of
a class or package, or just the current Class
reference. Then call the logger’s logging methods.
A simple example is in Example 13-12.
public
class
Slf4jDemo
{
final
static
Logger
theLogger
=
LoggerFactory
.
getLogger
(
Slf4jDemo
.
class
);
public
static
void
main
(
String
[]
args
)
{
Object
o
=
new
Object
();
theLogger
.
info
(
"I created this object: "
+
o
);
}
}
There are various methods used to log information at different levels of severity, which are shown in Table 13-1.
Name | Meaning |
---|---|
trace |
Verbose debugging (disabled by default) |
debug |
Verbose debugging |
info |
low-level informational message |
warn |
Possible error |
error |
Serious error |
One of the advantages of SLF4j over most of the other logging APIs is the avoidance of the “dead string” anti-pattern. In the use of many other logger APIs you may find code like:
logger
.
log
(
"The value is "
+
object
+
"; this is not good"
);
This can lead to a performance problem, in that the object’s toString()
is implicitly called,
and two string concatenations performed, before we even know if the logger is going to use them!
If this is in code that is called repeatedly, a lot of overhead can be wasted.
This led the other logging packages to offer “code guards,” based on logger methods that can find out very quickly if a logger is enabled, leading to code like the following:
if
(
logger
.
isEnabled
())
{
logger
.
log
(
"The value is "
+
object
+
"; this is not good"
);
}
This solves the performance problem, but clutters the code! SLF4J’s solution is to
use a mechanism similar to (but not quite compatible with) Java’s MessageFormat
mechanism,
as shown in Example 13-13.
public
class
Slf4jDemo2
{
final
static
Logger
theLogger
=
LoggerFactory
.
getLogger
(
Slf4jDemo2
.
class
);
public
static
void
main
(
String
[]
args
)
{
try
{
Person
p
=
new
Person
();
// populate person's fields here...
theLogger
.
info
(
"I created an object {}"
,
p
);
if
(
p
!=
null
)
{
// bogus, just to show logging
throw
new
IllegalArgumentException
(
"Just testing"
);
}
}
catch
(
Exception
ex
)
{
theLogger
.
error
(
"Caught Exception: "
+
ex
,
ex
);
}
}
}
Although this doesn’t demonstrate network logging, it is easy to accomplish this in conjunction with
a logging implementation like Log4J
or JUL
(Java Util Logging, a standard part of the JDK)
which allow you to provide configurable logging.
Log4J
is described in the next recipe.
The SLF4J website contains a Getting Started manual that discusses the various classpath options. There are also some Maven artifacts for the various options.
You wish to write log file messages using log4j
.
Get a Logger
and use its log()
method or the convenience methods. Control logging by changing a properties file. Use the org.apache.logging.log4j.net
package to make it network based.
This recipe describes Version 2 of the log4j API. Between Version 1 and
Version 2, there are changes to the package names, file names, and the
method used to obtain a logger. If you see code using, for example,
Logger.getLogger("class name")
, that code is written to the older API,
which is no longer maintained
(the Log4J web site refers to Log4j 1.2, and versions up to 2.12, as “legacy”;
we are using 2.13 in this recipe).
A good degree of compatibility is offered for code written to the 1.x API;
see https://logging.apache.org/log4j/2.x/manual/compatibility.html.
Logging using log4j
is simple, convenient, and flexible. You need to get a Logger
object from the static method LogManager.getLogger()
,
The Logger
has public void methods (debug()
, info()
, warn()
, error()
,
and fatal()
), each of which takes one Object
to be logged (and an optional Throwable
). As with
System.out.println()
, if you pass in anything that is not a String
, its toString()
method is called. A generic logging method is also included:
public void log(Level level, Object message);
The Level
class is defined in the log4j2
API. The standard levels are, in
order, DEBUG
< INFO
< WARN
< ERROR
< FATAL
. That is, debug messages are considered
the least important, and fatal the most important. Each Logger
has a level associated with
it; messages whose level is less than the Logger
’s level are silently discarded.
A simple application can log messages using these few statements:
public
class
Log4JDemo
{
private
static
Logger
myLogger
=
LogManager
.
getLogger
();
public
static
void
main
(
String
[]
args
)
{
Object
o
=
new
Object
();
myLogger
.
info
(
"I created an object: "
+
o
);
}
}
If you compile and run this program with no log4j2.properties file, it does not produce any logging output (see the log4j2demos script in the source folder).
We need to create a configuration file, whose default name is log4j2.properties.
You can also provide the logfile name via System Properties: -Dlog4j.configurationFile=URL
.
Log4j configuration is very flexible, and therefore very complex. Even their own documentation admits that “Trying to configure Log4j without understanding [the logging architecture] will lead to frustration.” See this Apache website for full details on the logging configuration file location and format.
Every Logger
has a Level
to specify what level of messages to write, and an Appender
, which is the code that writes the messages out. A ConsoleAppender
writes to System.out
, of course; other loggers write to files, operating system–level loggers, and so on. A simple configuration file looks something like this:
# Log4J2 properties file for the logger demo programs. # tag::generic[] # Ensure file gets copied for Java Cookbook # WARNING - log4j2.properties must be on your CLASSPATH, # not necessarily in your source directory. # The configuration file for Version 2 is different from V1! rootLogger.level = info rootLogger.appenderRef.stdout.ref = STDOUT appender.console.type = Console appender.console.name = STDOUT appender.console.layout.type = PatternLayout appender.console.layout.pattern = %m%n appender.console.filter.threshold.type = ThresholdFilter appender.console.filter.threshold.level = debug
This file gives the root logger a level of DEBUG, which causes it to write all messages.
The config file also sets up an appender of APPENDER1, which is configured on the next few lines. Note that I didn’t have to refer to the com.darwinsys Logger
.
Because every Logger
inherits from the root logger, a simple application needs to configure only the root logger. The properties file can also be an XML document or you can write your own configuration parser (almost nobody does this).
If the logging configuration file is not found, the default root logger defaults
the root logger to Level.ERROR
, so you will not see any output below the ERROR
level.
With the configuration file in place, the demonstration works better. Running this program (with the appropriate classpath as done in the scripts) produces this output:
$ java Log4j2Demo I created an object: java.lang.Object@477b4cdf $
A common use of logging is to log a caught Exception
, as shown in Example 13-14.
public
class
Log4JDemo2
{
private
static
Logger
myLogger
=
LogManager
.
getLogger
();
public
static
void
main
(
String
[]
args
)
{
try
{
Object
o
=
new
Object
();
myLogger
.
info
(
"I created an object: "
+
o
);
if
(
o
!=
null
)
{
// bogus, just to show logging
throw
new
IllegalArgumentException
(
"Just testing"
);
}
}
catch
(
Exception
ex
)
{
myLogger
.
error
(
"Caught Exception: "
+
ex
,
ex
);
}
}
}
When run, Log4JDemo2
produces the expected output:
$ java Log4JDemo2 I created an object: java.lang.Object@477b4cdf Caught Exception: java.lang.IllegalArgumentException: Just testing java.lang.IllegalArgumentException: Just testing at logging.Log4JDemo2.main(Log4JDemo2.java:17) [classes/:?] $
Much of the flexibility of log4j2
stems from its use of external configuration files; you can enable or disable logging without recompiling the application. A properties file that eliminates most logging might have this entry:
rootLogger.level = fatal
Only fatal error messages print; all levels less than that are ignored.
To log from a client to a server on a remote machine, the SocketAppender
can be used.
There is also an SmtpAppender
to send urgent notices via email.
See https://logging.apache.org/log4j/2.x/manual/appenders.html for details
on all the supported Appenders.
Here is log4j2-network.properties, the socket-based networking version of the configuration file:
# Log4J2 properties file for the NETWORKED logger demo programs. # tag::generic[] # Ensure file gets copied for Java Cookbook # WARNING - log4j2.properties must be on your CLASSPATH, # not necessarily in your source directory. # The configuration file for Version 2 is different from V1! rootLogger.level = info rootLogger.appenderRef.stdout.ref = STDOUT appender.console.type = Socket appender.console.name = STDOUT appender.console.host = localhost appender.console.port = 6666 appender.console.layout.type = PatternLayout appender.console.layout.pattern = %m%n appender.console.filter.threshold.type = ThresholdFilter appender.console.filter.threshold.level = debug
This file gets passed to the demo programs via a Java System Property in the netdemos
script:
build=../../../../target/classes log4j2_jar= ${HOME}/.m2/repository/org/apache/logging/log4j/log4j-api/2.13.0/log4j-api-2.13.0.jar: ${HOME}/.m2/repository/org/apache/logging/log4j/log4j-core/2.13.0/log4j-core-2.13.0.jar echo "==> Log4JDemo" java -Dlog4j.configurationFile=log4j2-network.properties -classpath ".:${build}:${log4j2_jar}" logging.Log4JDemo echo "==> Log4JDemo2" java -Dlog4j.configurationFile=log4j2-network.properties -classpath ".:${build}:${log4j2_jar}" logging.Log4JDemo2
When run with the log4j2-network.properties file, you have to arrange for a listener on the other end.
On Unix systems the nc
(or netcat
) program will work fine:
$ nc -kl 6666 I created an object: java.lang.Object@37ceb1df I created an object: java.lang.Object@37ceb1df Caught Exception: java.lang.IllegalArgumentException: Just testing java.lang.IllegalArgumentException: Just testing at logging.Log4JDemo2.main(Log4JDemo2.java:17) [classes/:?] ^C $
Netcat
option -l
says to listen on the numbered port; -k
tells it to _k_eep listening,
that is, to re-open the connection when the client closes it, as happens when each
demo program exits.
There is a performance issue with some logging calls. Consider some expensive operation, like a
toString()
or two along with several string concatenations passed to a Log.info()
call
in an often-used piece of code.
If this is placed into production with a higher logging level, all the work will be done
but the resultant string will never be used.
In older APIs we used to use “code guards”, methods like “isLoggerEnabled(Level)”,
to determine whether to bother creating the string. Nowadays, the preferred method is to create
the string inside a Lambda expression (see Chapter 9).
All the log methods have an overload that accepts a Supplier
argument:
public
class
Log4JLambda
{
private
static
Logger
myLogger
=
LogManager
.
getLogger
();
public
static
void
main
(
String
[]
args
)
{
Person
customer
=
getPerson
();
myLogger
.
info
(
()
->
String
.
format
(
"Value %d from Customer %s"
,
customer
.
value
,
customer
)
);
}
This way the string operations will only be performed if needed: if the logger is operating
at the INFO
level it will call the Supplier
and if not, it won’t do the expensive operation.
When run as part of the log4j2demos
script, this prints:
Value 42 from Customer Customer[Robin]
For more information on log4j
, visit its main website.
log4j2
is free software, distributed under the Apache Software Foundation license.
You wish to write logging messages using the Java logging mechanism.
Get a Logger
, and use it to log your messages and/or exceptions.
The Java Logging API (package java.util.logging
) is similar to, and was obviously inspired by, the log4j
package. You acquire a Logger
object by calling the static Logger.getLogger()
with a descriptive String
. You then use instance methods to write to the log; these methods include:
public
void
log
(
java
.
util
.
logging
.
LogRecord
);
public
void
log
(
java
.
util
.
logging
.
Level
,
String
);
// and a variety of overloaded log( ) methods
public
void
logp
(
java
.
util
.
logging
.
Level
,
String
,
String
,
String
);
public
void
logrb
(
java
.
util
.
logging
.
Level
,
String
,
String
,
String
,
String
);
// Convenience routines for tracing program flow
public
void
entering
(
String
,
String
);
public
void
entering
(
String
,
String
,
Object
);
public
void
entering
(
String
,
String
,
Object
[]);
public
void
exiting
(
String
,
String
);
public
void
exiting
(
String
,
String
,
Object
);
public
void
throwing
(
String
,
String
,
Throwable
);
// Convenience routines for log( ) with a given level
public
void
severe
(
String
);
public
void
warning
(
String
);
public
void
info
(
String
);
public
void
config
(
String
);
public
void
fine
(
String
);
public
void
finer
(
String
);
public
void
finest
(
String
);
As with log4j
, every Logger
object has a given logging level, and messages below that level are silently discarded:
public
void
setLevel
(
java
.
util
.
logging
.
Level
);
public
java
.
util
.
logging
.
Level
getLevel
(
);
public
boolean
isLoggable
(
java
.
util
.
logging
.
Level
);
As with log4j
, objects handle the writing of the log. Each logger has a Handler
:
public
synchronized
void
addHandler
(
java
.
util
.
logging
.
Handler
);
public
synchronized
void
removeHandler
(
java
.
util
.
logging
.
Handler
);
public
synchronized
java
.
util
.
logging
.
Handler
[]
getHandlers
(
);
and each Handler
has a Formatter
, which formats a LogRecord
for display. By providing your own Formatter
, you have more control over how the information being passed into the log gets formatted.
Unlike log4j
, the Java SE logging mechanism has a default configuration, so Example 13-16 is a minimal logging example program.
Running it prints the following:
$ juldemos Jan 31, 2020 1:03:27 PM logging.JulLogDemo main INFO: I created an object: java.lang.Object@5ca881b5 $
As with log4j
, one common use is in logging caught exceptions; the code for this is in Example 13-17.
As with log4j
, java.util.logging
accepts a Lambda expression (and has done since Java 8):
/** Demonstrate how Java 8 Lambdas avoid extraneous object creation
* @author Ian Darwin
*/
public
class
JulLambdaDemo
{
public
static
void
main
(
String
[]
args
)
{
Logger
myLogger
=
Logger
.
getLogger
(
"com.darwinsys.jullambda"
);
Object
o
=
new
Helper
();
// If you change the log call from finest to info,
// you see both the systrace from the toString,
// and the logging output. As it is here,
// you don't see either, so the toString() is not called!
myLogger
.
finest
(()
->
"I created this object: "
+
o
);
}
static
class
Helper
{
public
String
toString
()
{
System
.
out
.
println
(
"JulLambdaDemo.Helper.toString()"
);
return
"failure!"
;
}
}
}
A good general reference on this chapter’s topic is Java Network Programming (O’Reilly).
The server side of any network mechanism is extremely sensitive to security issues. It is easy for one misconfigured or poorly written server program to compromise the security of an entire network! Of the many books on network security, two stand out: Firewalls and Internet Security by William R. Cheswick, Steven M. Bellovin, and Aviel D. Rubin (Addison-Wesley) and a series of books with Hacking Exposed in the title, the first in the series by Stuart McClure, Joel Scambray, and George Kurtz (McGraw-Hill).
This completes my discussion of server-side Java using sockets. A chat server could be implemented using several technologies, such as RMI (Remote Methods Invocation), an HTTP web service, JMS (Java Message Service), and a Java Enterprise API that handles store-and-forward message processing. This is beyond the scope of this book, but there’s an example of an RMI chat server in the chat folder of the source distribution, and a JMS chat server in Java Message Service by Mark Richards, Richard Monson-Haefel, and David Chappell.
1 You may not be able to pick just any port number for your own service, of course. Certain well-known port numbers are reserved for specific services and listed in your services file, such as 22 for Secure Shell, 25 for SMTP, and hundreds more. Also, on server-based operating systems, ports below 1024 are considered “privileged” ports and require root or administrator privilege to create. This was an early security mechanism; today, with zillions of single-user desktops connected to the Internet, it provides little real security, but the restriction remains.
2 Digital Equipment was absorbed by Compaq, which was then absorbed by HP, but the name remains de
because the engineers who name such things don’t care for corporate mergers anyway.
3 There are some limits to how many threads you can have, which affect only very large, enterprise-scale servers. You can’t expect to have thousands of threads running in the standard Java runtime. For large, high-performance servers, you may wish to resort to native code (see Recipe 18.6) using select()
or poll()
.
3.135.216.75