Programming     Travel Logs     Life Is Good     Surfing Online     About Me
Pick an industry where you can play long term games with long term people.
-Naval Ravikant
2018-07-22 22:34:04

Copy this link when reproducing:
http://www.casperlee.com/en/y/blog/227

Since the goal is to create a common Socket server, which means it can be used in very different occasions, I need a good structure to support it.

Just like before, let's take it easy and enjoy a few beautiful photos first.

1. There are 2 buttons in the main form: btnStart and btnStop. As their names imply, their functions are very clear: "Start the server" and "Stop the server" respectively. Here are the handler functions for their onAction event:

import javax.swing.JOptionPane;
...
public class MainForm extends JFrame {
    private void performStartAction(ActionEvent e) {
    	
    	if (this.server != null && this.server.isAlive()) {
    		
    		return;
    	}
    	
    	this.server = new CServer();
    	this.server.start();
    	this.updateUI();
    }
    private void performStopAction(ActionEvent e) {
    	
    	if (!this.server.isAlive()) {
    		
    		return;
    	}
    	
    	try {
    		
			this.server.stopServer();
			
		} catch (Exception e2) {
			
			JOptionPane.showMessageDialog(null, e2.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
			e2.printStackTrace();
			return;
		}
    	
    	this.updateUI();
    }
}

2. The main form holds an instance of the CServer class, which is actually a subclass of the Thread class. The reason why it has to use another thread is that some functions of the Socket object may block the execution thread. I surely do not want the main thread to be blocked, so I have to create a new thread. Here is the code of the CServer class:

package com.casperlee.CSocketServer;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class CServer extends Thread {
	
	// Constants
	public static final int defaultPortNum = 9527;
	
	// Fields
	private int portNum;
	private ServerSocket ss;
	
	// Constructor
	public CServer(int aPort) {
		super();
		this.portNum = aPort;
	}
	public CServer() {
		super();
		this.portNum = defaultPortNum;
	}
	
	// Override functions
	@Override
	public void run() {
		
		try {
			ss = new ServerSocket(this.portNum);
		
			try {
				while (true) {
			
					if (this.isInterrupted()) {
				
						break;
					}
            	
					Socket s = ss.accept();
					CSocketThread sThread = new CSocketThread(s);
					sThread.start();
				}
				
			} finally {
                
				ss.close();
			}
            
		} catch (IOException e) {
			
			e.printStackTrace();
		}
	}
	
	public void stopServer() throws Exception {
		
		try {
			
			int clientAcount = CSocketThreadManager.getInstance().getThreadCount();
			if (clientAcount > 0) {
				
				throw new Exception("There are " + Integer.toString(clientAcount) +  " clients connected!");
			}
			
			ss.close();
	    	        this.interrupt();
	    	        try {
	    		
				this.join();
				
			} catch (InterruptedException e1) {
				
				e1.printStackTrace();
			}
			
		} catch (IOException e) {
			
			e.printStackTrace();
		}
	}
}

    Note:
    I. When creating a Socket server, it requires a port number. If the port number has been used by other applications, it will raise an error or exception.
    II. When "Socket s = ss.accept();" being executed, it will block the CServer thread, until a Socket client is connected to this Socket server. And then I created another thread named "CSocketThread" to do the job for this client, which means my application will create one thread for each connected client.
    III. When trying to stop the server (in the "stopServer" function), "this.interrupt()" is not enough. When no clients being connected to this server, "ss.accept()" will block the CServer thread, so the CServer thread will never be ended, then "this.join()" will never return. Luckily, "ss.close()" will close the server socket and interrupt the execution of the "accept()" function.

3. The CServer thread creates a new thread for each of the connected clients, and here is the code of the CSocketThread class:

package com.casperlee.CSocketServer;

import java.io.IOException;
import java.net.Socket;

import com.casperlee.CSocketServer.handlers.CSocketHandler;

public class CSocketThread extends Thread {
	
	// Fields
	private Socket socket;

	// Constructor
	public CSocketThread(Socket aSocket) {
		
		this.socket = aSocket;
	}
	
	@Override
	public void run() {
		
		CSocketThreadManager.getInstance().add(this);
		try {
			
			CSocketHandler handler = CSocketHandler.getHandler(this.socket);
			if (handler.performAuthentication()) {
				
				handler.interact();
			}
			
		} catch (IOException e) {
			
			e.printStackTrace();
			
		} finally {
			
		    CSocketThreadManager.getInstance().remove(this);
		}
	}
}

The code is very straightforward. The class holds an instance of the Socket class which is passed from the CServer class, and delegate all the jobs to the CSocketHandler class.

4. To support all kinds of occasions, CSocketHandler is defined as an abstract class. Which means I can define different sub-classes for different occasions. Here is the code of the CSocketHandler class:

package com.casperlee.CSocketServer.handlers;

import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;

public abstract class CSocketHandler {
	
	protected Socket socket;

	public static CSocketHandler getHandler(Socket aSocket) {
		
		DataInputStream input;
		int commandCode;
		try {
			input = new DataInputStream(aSocket.getInputStream());
			commandCode = input.readInt();
			
		} catch (IOException e) {
			
			e.printStackTrace();
			return null;
		}
		
		switch (commandCode) {
		case CommandConstants.GPS:
			
			return new GPSHandler(aSocket); 
			
		default:

            return null;
		}
	}
	
	public CSocketHandler(Socket s) {
		
		this.socket = s;
	}
	
	public abstract boolean performAuthentication();
	public abstract void interact() throws IOException;
}

    Note: In the "getHandler" function, it will return different sub-classes based on the Command Code which is retrieved from the Socket client.

5. For the first occasion which came into my mind, I create a sub-class named "GPSHandler". As its name implies, it will accept the GPS information from the client, and handle it in the server side. Here is the code of the GPSHandler class:

package com.casperlee.CSocketServer.handlers;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;

import com.casperlee.CSocketServer.dal.GPSDal;

public class GPSHandler extends CSocketHandler {

	// Constructor
	public GPSHandler(Socket s) {
		super(s);
	}

	// Overridden functions
	@Override
	public boolean performAuthentication() {
		
		return true;
	}

	@Override
	public void interact() throws IOException {
		
		DataOutputStream output = new DataOutputStream(this.socket.getOutputStream());
		output.writeInt(CommandConstants.Continue);
		DataInputStream input = new DataInputStream(this.socket.getInputStream());
		double longitude = input.readDouble();
		double latitude = input.readDouble();
		GPSDal.getInstance().addGPS(longitude, latitude);
	}
}

6. The main part of the code is listed above. Now let me list the code of other related classes:

    I. com.casperlee.CSocketServer.handlers.CommandConstants

package com.casperlee.CSocketServer.handlers;

public class CommandConstants {

	public static final int GPS = 100001;
	
	public static final int Continue = 1001;
	public static final int AuthenticationFailed = 1002;
}

    II. com.casperlee.CSocketServer.CSocketThreadManager

package com.casperlee.CSocketServer;

import java.util.Vector;

public class CSocketThreadManager {

	// Singleton
	private CSocketThreadManager() {
		
	}
	private static CSocketThreadManager instance;
	public static CSocketThreadManager getInstance() {
		
		if (instance == null) {
			
			instance = new CSocketThreadManager();
		}
		
		return instance;
	}
	
	// Fields
	private Vector<CSocketThread> vector = new Vector<CSocketThread>();
	
	// Public functions
	public void add(CSocketThread socket) {
		
		vector.add(socket);
	}
	public void remove(CSocketThread socket) {
		
		vector.remove(socket);
	}
	public int getThreadCount() {
		
		return vector.size();
	}
}

    III. com.casperlee.CSocketServer.dal.GPSDal

package com.casperlee.CSocketServer.dal;

public class GPSDal {

	// Singleton
	private GPSDal() {
	}
	private static GPSDal instance;
	public static GPSDal getInstance() {
		
		if (instance == null) {
			
			instance = new GPSDal();
		}
		
		return instance;
	}
	
	// Public functions
	public void addGPS(double aLongitude, double aLatitude) {
		
		// TODO
	}
}

7. Done!