Survey
* Your assessment is very important for improving the work of artificial intelligence, which forms the content of this project
* Your assessment is very important for improving the work of artificial intelligence, which forms the content of this project
Internet Programming with Java Course
1.10 Пример: Разработка на Forward сървър
Multithreaded Forward Server with Load Balancing and Failover Features
Multithreaded Forward Server Implementation – Nakov Forward Server
/**
* Nakov TCP Socket Forward Server - freeware
* Version 1.0 - March, 2002
* (c) 2001 by Svetlin Nakov - http://www.nakov.com
*
* Short decription: Nakov Forward Server is designed to forward (redirect) TCP
* connections from a client to a server choosen from a servers list. When a client
* is connected to Nakov Forward Server, a new connection is opened to some of the
* specified destination servers and all the traffic from destination server to
* Nakov Forward Server is redirected to the client and also all the traffic from
* the client to Nakov Forward Server is redirected to destination server. That
* way Nakov Forward Server makes transparent redirection of TCP connections.
* The data transfer schema is the following:
*
*
CLIENT <--> NAKOV_FORWARD_SERVER <--> DESTINATION_SERVER
*
* Clients and Destination Servers communicate only with Nakov Forward Server.
*
* Nakov Forward Server supports failt tolerance. When some of the servers in the
* list fail to respond to TCP connect request (dead server), Nakov Forward Server
* tries the next server in the list until it finds alive server. All dead servers
* are checked if they are alive through some time interval and when some server
* becomes available, it is added to alive list. When no server is alive, no
* connection will be established.
*
* Nakov Forward Server supports also load balancing features. If load balancing
* is enabled, when a client connection is accepted, Nakov Forward Server will
* redirect the client to the least loaded server from the servers list. We consider
* the server which hast minimal alive connections established by Nakov Forward
* Server is least loaded.
*
* What we gain when we use Nakov Proxy Server?
*
- Destination server does not know the real IP of the client. It thinks
* that the IP of Nakov Forward Server is its client. Thus we can use a server
* licensed for one IP address on several machines simultaneously.
*
- Nakov Forward Server can run on a port number that is allowed by the
* firewall and forward to a port number that is not allowed by firewall. Thus,
* started on a server in a local network, it can give access to some disabled
* by the firewall services.
*
- Nakov Forward Server can give access to multiple clients to some service
* that is allowed only for some fixed IP address when started on the machine
* with this IP.
*
- Fault Tolerance (failover) of Nakov Forward Server helps to avoid faults
* when some of the servers die. Of course there is special hardware for this, but
* it has high price. Instead you can use Nakov Forward Server (that is free).
* If you setup several Nakov Forward Servers configured to use the same set of
* destination servers and if you configure your routers to use redirect traffic
* to both servers, you will obtain a stable fault tolerance system. In such a
* system you have guarantee that crash of any of the servers (including some of
* the Nakov Forward Servers) will not stop the service that these servers provide.
* Of course the destination servers should run in a cluster and replicate their
* sessions.
*
- Load balancing helps to avoid overloading of the servers by distributing
* the clients between them. Of course this should be done by special hardware
* called "load balancer", but if we don't have such hardware, we can still use
* this technology. When we use load balancing, all the servers in the list should
* be running in a cluster and there should be no matter what of the servers the
* client is connected to. The servers should communicate each other and replicate
* their session data.
*
* NakovForwardServer.properties configuration file contains all the settings of
* Nakov Forward Server. The only mandatory field is "Servers"
* Destination servers should be in following format:
*
Servers = server1:port1, server2:port2, server3:port3, ...
* For example:
*
Servers = 192.168.0.22:80, rakiya:80, 192.168.0.23:80, www.nakov.com:80
* Nakov Forward Server listening port should be in format:
*
ListeningPort = some_port (in range 1-65535)
* Using load balancing algorithm is specified by following line:
*
LoadBalancing = Yes/No
* Check alive interval through which all dead threads should be re-checked if
* they are alive is specified by following line:
*
CheckAliveInterval = time_interval (in milliseconds)
*/
import
import
import
import
import
import
import
import
java.util.ArrayList;
java.net.ServerSocket;
java.net.Socket;
java.net.InetAddress;
java.io.IOException;
java.io.FileInputStream;
java.util.Properties;
java.util.StringTokenizer;
public class NakovForwardServer
{
private static final boolean ENABLE_LOGGING = true;
public static final String SETTINGS_FILE_NAME = "NakovForwardServer.properties";
private
private
private
private
ServerDescription[] mServersList = null;
int mListeningTcpPort = 2001;
boolean mUseLoadBalancingAlgorithm = true;
long mCheckAliveIntervalMs = 5*1000;
/**
* ServerDescription descripts a server (server hostname/IP, server port,
* is the server alive at last check, how many clients are connected to it, etc.)
*/
class ServerDescription
{
public String host;
public int port;
public int clientsConectedCount = 0;
public boolean isAlive = true;
public ServerDescription(String host, int port)
{
this.host = host;
this.port = port;
}
}
/**
* @return an array of ServerDescription - all destination servers.
*/
public ServerDescription[] getServersList()
{
return mServersList;
}
/**
* @return the time interval (in milliseconds) through which all dead servers
* should be re-checked if they are alive (a server is alive if accepts
* client connections on the specified port, otherwise is dead).
*/
public long getCheckAliveIntervalMs()
{
return mCheckAliveIntervalMs;
}
/**
* @return true if load balancing algorithm is enabled.
*/
public boolean isLoadBalancingEnabled()
{
return mUseLoadBalancingAlgorithm;
}
/**
* Reads the Nakov Forward Server configuration file "NakovForwardServer.properties"
* and load user preferences. This method is called once during the server startup.
*/
public void readSettings()
throws Exception
{
// Read properties file in a Property object
Properties props = new Properties();
props.load(new FileInputStream(SETTINGS_FILE_NAME));
// Read and parse the server list
String serversProperty = props.getProperty("Servers");
if (serversProperty == null )
throw new Exception("The server list can not be empty.");
try {
ArrayList servers = new ArrayList();
StringTokenizer stServers = new StringTokenizer(serversProperty,",");
while (stServers.hasMoreTokens()) {
String serverAndPort = stServers.nextToken().trim();
StringTokenizer stServerPort = new StringTokenizer(serverAndPort,": ");
String host = stServerPort.nextToken();
int port = Integer.parseInt(stServerPort.nextToken());
servers.add(new ServerDescription(host,port));
}
mServersList =
(ServerDescription[]) servers.toArray(new ServerDescription[] {});
} catch (Exception e) {
throw new Exception("Invalid server list format : " + serversProperty);
}
if (mServersList.length == 0)
throw new Exception("The server list can not be empty.");
// Read server's listening port number
try {
mListeningTcpPort = Integer.parseInt(props.getProperty("ListeningPort"));
} catch (Exception e) {
log("Server listening port not specified. Using default port : " +
mListeningTcpPort);
}
// Read load balancing property
try {
String loadBalancing = props.getProperty("LoadBalancing").toLowerCase();
mUseLoadBalancingAlgorithm = (loadBalancing.equals("yes") ||
loadBalancing.equals("true") || loadBalancing.equals("1") ||
loadBalancing.equals("enable") || loadBalancing.equals("enabled"));
} catch (Exception e) {
log("LoadBalancing property is not specified. Using default value : " +
mUseLoadBalancingAlgorithm);
}
// Read the check alive interval
try {
mCheckAliveIntervalMs =
Integer.parseInt(props.getProperty("CheckAliveInterval"));
} catch (Exception e) {
log("Check alive interval is not specified. Using default value : " +
mCheckAliveIntervalMs + " ms.");
}
}
/**
* Starts a thread that re-checks all dead threads if they are alive
* through mCheckAliveIntervalMs millisoconds
*/
private void startCheckAliveThread()
{
CheckAliveThread checkAliveThread = new CheckAliveThread(this);
checkAliveThread.setDaemon(true);
checkAliveThread.start();
}
/**
* Starts the forward server - binds on a given port and starts serving
*/
public void startForwardServer()
throws Exception
{
// Bind server on given TCP port
ServerSocket serverSocket;
try {
serverSocket = new ServerSocket(mListeningTcpPort);
} catch (IOException ioe) {
throw new IOException("Unable to bind to port " + mListeningTcpPort);
}
log("Nakov Forward Server started on TCP port " + mListeningTcpPort + ".");
log("All TCP connections to " + InetAddress.getLocalHost().getHostAddress() +
":" + mListeningTcpPort + " will be forwarded to the following servers:");
for (int i=0; i<mServersList.length; i++) {
log(" " + mServersList[i].host + ":" + mServersList[i].port);
}
log("Load balancing algorithm is " +
(mUseLoadBalancingAlgorithm ? "ENABLED." : "DISABLED."));
// Accept client connections and process them until stopped
while(true) {
try {
Socket clientSocket = serverSocket.accept();
String clientHostPort = clientSocket.getInetAddress().getHostAddress() +
":" + clientSocket.getPort();
log("Accepted client from " + clientHostPort);
ForwardServerClientThread forwardThread =
new ForwardServerClientThread(this, clientSocket);
forwardThread.start();
} catch (Exception e) {
throw new Exception("Unexpected error.\n" + e.toString());
}
}
}
/**
* Prints given log message on the standart output if logging is enabled,
* otherwise ignores it
*/
public void log(String aMessage)
{
if (ENABLE_LOGGING)
System.out.println(aMessage);
}
/**
* Program entry point. Reads settings, starts check-alive thread and
* the forward server
*/
public static void main(String[] aArgs)
{
NakovForwardServer srv = new NakovForwardServer();
try {
srv.readSettings();
srv.startCheckAliveThread();
srv.startForwardServer();
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* ForwardServerClientThread handles the clients of Nakov Forward Server. It
* finds suitable server from the server pool, connects to it and starts
* the TCP forwarding between given client and its assigned server. After
* the forwarding is failed and the two threads are stopped, closes the sockets.
*/
import
import
import
import
import
java.net.Socket;
java.net.SocketException;
java.io.IOException;
java.io.InputStream;
java.io.OutputStream;
public class ForwardServerClientThread extends Thread
{
private NakovForwardServer mNakovForwardServer = null;
private NakovForwardServer.ServerDescription mServer = null;
private
private
private
private
private
Socket mClientSocket = null;
Socket mServerSocket = null;
boolean mBothConnectionsAreAlive = false;
String mClientHostPort;
String mServerHostPort;
/**
* Creates a client thread for handling clients of NakovForwardServer.
* A client socket should be connected and passed to this constructor.
* A server socket is created later by run() method.
*/
public ForwardServerClientThread(NakovForwardServer aNakovForwardServer,
Socket aClientSocket)
{
mNakovForwardServer = aNakovForwardServer;
mClientSocket = aClientSocket;
}
/**
* Obtains a destination server socket to some of the servers in the list.
* Starts two threads for forwarding : "client in <--> dest server out" and
* "dest server in <--> client out", waits until one of these threads stop
* due to read/write failure or connection closure. Closes opened connections.
*/
public void run()
{
try {
mClientHostPort = mClientSocket.getInetAddress().getHostAddress() +
":" + mClientSocket.getPort();
// Create a new socket connection to one of the servers from the list
mServerSocket = createServerSocket();
if (mServerSocket == null) { // If all the servers are down
System.out.println("Can not establish connection for client " +
mClientHostPort + ". All the servers are down.");
try { mClientSocket.close(); } catch (IOException e) {}
return;
}
// Obtain input and output streams of server and client
InputStream clientIn = mClientSocket.getInputStream();
OutputStream clientOut = mClientSocket.getOutputStream();
InputStream serverIn = mServerSocket.getInputStream();
OutputStream serverOut = mServerSocket.getOutputStream();
mServerHostPort = mServer.host + ":" + mServer.port;
mNakovForwardServer.log("TCP Forwarding " + mClientHostPort +
" <--> " + mServerHostPort + " started.");
// Start forwarding of socket data between server and client
ForwardThread clientForward = new ForwardThread(this, clientIn, serverOut);
ForwardThread serverForward = new ForwardThread(this, serverIn, clientOut);
mBothConnectionsAreAlive = true;
clientForward.start();
serverForward.start();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
/**
* connectionBroken() method is called by forwarding child threads to notify
* this thread (their parent thread) that one of the connections (server or client)
* is broken (a read/write failure occured). This method disconnects both server
* and client sockets causing both threads to stop forwarding.
*/
public synchronized void connectionBroken()
{
if (mBothConnectionsAreAlive) {
// One of the connections is broken. Close the other
// and stop forwarding
// Closing these socket connections will close their
// and that way will stop the threads that read from
try { mServerSocket.close(); } catch (IOException e)
try { mClientSocket.close(); } catch (IOException e)
connection
input/output streams
these streams
{}
{}
mBothConnectionsAreAlive = false;
mServer.clientsConectedCount--;
mNakovForwardServer.log("TCP Forwarding " + mClientHostPort +
" <--> " + mServerHostPort + " stopped.");
}
}
/**
* @return a new socket connected to some of the servers in the destination
* servers list. Sequentially a connection to the least loaded server from
* the list is tried to be established. If connecting to some alive server
* fail, this server it marked as dead and next alive server is tried. If all
* the servers are dead, null is returned. Thus if at least one server is alive,
* a connection will be established (of course after some delay) and the system
* will not fail (it is fault tolerant). Dead servers can be marked as alive if
* revived, but this is done later by check alive thread.
*/
private Socket createServerSocket()
throws IOException
{
while (true) {
mServer = getServerWithMinimalLoad();
if (mServer == null) // All the servers are down
return null;
try {
Socket socket = new Socket(mServer.host, mServer.port);
mServer.clientsConectedCount++;
return socket;
} catch (IOException ioe) {
mServer.isAlive = false;
}
}
}
/**
* @return the least loaded alive server from the server list if load balancing
* is enabled or first alive server from the list if load balancing algorithm is
* disabled or null if all the servers in the list are dead.
*/
private NakovForwardServer.ServerDescription getServerWithMinimalLoad()
{
NakovForwardServer.ServerDescription minLoadServer = null;
NakovForwardServer.ServerDescription[] servers =
mNakovForwardServer.getServersList();
for (int i=0; i<servers.length; i++) {
if (servers[i].isAlive) {
if ((minLoadServer==null) ||
(servers[i].clientsConectedCount < minLoadServer.clientsConectedCount))
minLoadServer = servers[i];
// If load balancing is disabled, return first alive server
if (!mNakovForwardServer.isLoadBalancingEnabled())
break;
}
}
return minLoadServer;
}
}
/**
* ForwardThread handles the TCP forwarding between a socket input stream (source)
* and a socket output stream (destination). It reads the input stream and forwards
* everything to the output stream. If some of the streams fails, the forwarding
* is stopped and the parent thread is notified to close all its connections.
*/
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class ForwardThread extends Thread
{
private static final int READ_BUFFER_SIZE = 8192;
InputStream mInputStream = null;
OutputStream mOutputStream = null;
ForwardServerClientThread mParent = null;
/**
* Creates a new traffic forward thread specifying its input stream,
* output stream and parent thread
*/
public ForwardThread(ForwardServerClientThread aParent, InputStream aInputStream,
OutputStream aOutputStream)
{
mInputStream = aInputStream;
mOutputStream = aOutputStream;
mParent = aParent;
}
/**
* Runs the thread. Until it is possible, reads the input stream and puts read
* data in the output stream. If reading can not be done (due to exception or
* when the stream is at his end) or writing is failed, exits the thread.
*/
public void run()
{
byte[] buffer = new byte[READ_BUFFER_SIZE];
try {
while (true) {
int bytesRead = mInputStream.read(buffer);
if (bytesRead == -1)
break; // End of stream is reached --> exit the thread
mOutputStream.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
// Read/write failed --> connection is broken --> exit the thread
}
// Notify parent thread that the connection is broken and forwarding should stop
mParent.connectionBroken();
}
}
/**
* CheckAliveThread checks all dead servers in the server list and updates the
* list when some dead server becomes alive. Checking is done on a beforehand
* specified time itrervals. A server is considered alive if it accepts client
* connections on the specified port.
*/
import java.io.IOException;
import java.net.Socket;
public class CheckAliveThread extends Thread
{
private NakovForwardServer mNakovForwardServer = null;
/**
* Creates a check alive thread. NakovForwardServer object is needed
* for obtaining the servers list.
*/
public CheckAliveThread(NakovForwardServer aNakovForwardServer)
{
mNakovForwardServer = aNakovForwardServer;
}
/**
* Until stopped checks all dead servers if they are alive and waits
* specified time interval
*/
public void run()
{
while (!interrupted()) {
try {
Thread.sleep(mNakovForwardServer.getCheckAliveIntervalMs());
} catch (InterruptedException ie) {
ie.printStackTrace();
}
checkAllDeadServers();
}
}
/**
* Checks all dead servers if they are alive and updates their state if needed.
*/
private void checkAllDeadServers()
{
NakovForwardServer.ServerDescription[] servers =
mNakovForwardServer.getServersList();
for (int i=0; i<servers.length; i++) {
if (!servers[i].isAlive)
if (alive(servers[i].host, servers[i].port)) {
servers[i].isAlive = true;
}
}
}
/**
* Checks if given server is alive (if accepts client connections on specified port)
*/
private boolean alive(String host, int port)
{
boolean result = false;
try {
Socket s = new Socket(host, port);
result = true;
s.close();
} catch (IOException ioe) {
ioe.printStackTrace();
// Ignore unsuccessfull connect attempts
}
return result;
}
}