Download Internet Programming with Java Course

Survey
yes no Was this document useful for you?
   Thank you for your participation!

* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project

Document related concepts
no text concepts found
Transcript
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;
}
}