2
2

Javaでフレームワーク(Httpサーバ)を作成してみる

Posted at

Javaでフレームワークをなるべくスクラッチで作成してみました

フレームワーク自体を作成すれば、自由度が少しでも広がると思い作成してみました。

こちらが今回作成したコードになります。
https://github.com/yuki-yanagawa/FrameWorkTest

目次

1. 概要
2. エントリポイント
3. レシーバの内容
4. キューについて
5. タスクマネージャの内容
6. リクエストヘッダの解析
7. 処理の実行
8. 最後に

概要

大きな流れとしまして、「RecevierがHttpリクエストを受け取る -> QueueにHttpリクエストをためる -> TaskManagerがQueueからHttpリクエストを受け取り、処理を実行する」形になります。

リクエストヘッダを解析し、以下定義ファイルを元に実行する処理を決定します。

メソッド[空白]URI[空白]処理するファイルのパス と記載するようにします。

def/router
GET  /         testframe.gui.controller.IndexHtmlController.main
GET  /main.js  testframe.gui.controller.JavaScriptFileController.main

こちらに記載していないURIのリクエストがきた場合は現状対応できておりません。
クライアントに404をリターンする形になります。

以下より、具体的な内容を記載します。

エントリポイント

Propertiesファイルを読み込み、TaskManagerのスレッド、HTTPRecvierのスレッドを走らせる形になります。(Thread.sleep(100)は暫定的に入れました。。。)

EntryPoint.java
public class EntryPoint {
	public static void main(String[] args) {
		try {
			//Properties File read
			PropFileReader.refresh();
			Thread.sleep(100);
			
			//Task Manager Start
			TaskManagerEntry.taskManagerStart();
			Thread.sleep(100);
			
			//HttpReciver Start
			HttpReciver reciver = new HttpReciver();
			reciver.httpReciverStart();
			
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

レシーバの内容

ServerSocketを利用し、クライアントからの要求を待ちます。リクエストを受け取ったら、HandleThreadQueueManagerのQueueにリクエストを追加します。
HandleThreadQueueManagerはシングルトンパターンで定義し、HttpReciverでQueueにため、TaskManagerでQueueの中身を取得する処理になります。

HttpReciver.java
public class HttpReciver {
    ........
	public void httpReciverStart() throws IOException {
		Thread.currentThread().setName("HttpReciver");
		while(serverExeuteFlg) {
			try {
				Socket socket = svrSock_.accept();
				HandleThreadQueueManager.getInstacnce().addHandleThread(new HttpHandleThread(socket));
			} catch(InterruptedException e) {
				Logger.warn(Thread.currentThread().getName(), "HandleThread Add Queue Failed...");
			} catch(IOException e) {
				Logger.warn(Thread.currentThread().getName(), "Server Socket Accept Failed...");
			} catch(Error e) {
				Logger.error(e.getMessage());
				serverExeuteFlg = false;
			}
		}
		closeServerSocket();
	}
}

キューについて

RecvierがaddHandleThreadでリクエストをQueueをためる。Queueのサイズが設定より大きくなる場合は待つ。
TaskManagerがgetHandleThreadでQueueからリクエストを受け取り、リクエストを解析する。Queueが空の場合は待つ。

Producer-Consumerパターンを意識して作成しました。

HandleThreadQueueManager.java
public class HandleThreadQueueManager {
	.....
	public synchronized void addHandleThread(HandleThread e) throws InterruptedException {
		while (threadCountMax_ < queue_.size()) {
			wait();
		}
		queue_.add(e);
		notifyAll();
	}

	public synchronized HandleThread getHandleThread() throws InterruptedException {
		while(queue_.size() <= 0) {
			wait();
		}
		HandleThread handleThread = queue_.poll();
		notifyAll();
		return handleThread;
	}
}

タスクマネージャの内容

内部クラスのReciverHandleThreadMainがQueueからリクエストを取得する形になります。
TaskManagerがリクエストを解析すると記載しましたが、詳細はQueueに貯めていたHttpHandleThreadを実行する形になります。

ReciverHandleThreadTaskManager.java
public class ReciverHandleThreadTaskManager implements TaskManager {
	@Override
	public void execute() {
		ReciverHandleThreadMain reciverHandleThreadMain = new ReciverHandleThreadMain();
		reciverHandleThreadMain.start();
	}

	class ReciverHandleThreadMain extends Thread {
		@Override
		public void run() {
			Thread.currentThread().setName("ReciverHandleThreadMain");
			while(true) {
				HandleThread handleThread = trytakeFromHandleThreadQueue();
				if(handleThread != null) {
					handleThread.start();
				}
			}
		}
		
		private  HandleThread trytakeFromHandleThreadQueue() {
			Logger.info(Thread.currentThread().getName(),"try take start......");
			HandleThread handleThread = null;
			try {
				handleThread = HandleThreadQueueManager.getInstacnce().getHandleThread();
			} catch(InterruptedException e) {
				Logger.error(Thread.currentThread().getName(), "Take HandleThread From Queue");
			}
			Logger.info(Thread.currentThread().getName(),"try take end......");
			return handleThread;
		}
	}
}

リクエストヘッダの解析

簡単な内容と致しましては、リクエストヘッダは「\r\n」を基にパラメータの項目毎に分解できるので分解し、先頭のリクエストラインは「空白」を基にメソッド、URIに分解できるので分解しております。
取得した「メソッド」、「URI」を基に「def/router」から実行する処理を選択します。

HttpHandleUtil.java
public class HttpHandleUtil {
	private static int recvBufSize = 2048;
	private static String serverName;
	
	public static int getRecvBufize() {
		return recvBufSize;
	}

	public static String httpRequestAnalizeAndCreateResponse(String httpRequest) throws HttpCreateResponseException {
		String retStr = "";
		String[] httpRequestLine = httpRequest.split("\r\n");
		String httpRequestHeader = httpRequestLine[0];
		String[] httpRequestHeaderLine = httpRequestHeader.split(" ");
		if(httpRequestHeaderLine.length < 2) {
			throw new HttpCreateResponseException("CreateResponsError");
		}
		String requestMethod = httpRequestHeaderLine[0].trim();
		String requestData = httpRequestHeaderLine[1].trim();
		
		String excludeHeaderHttpRequest = httpRequest.substring(httpRequestHeader.length());
		Map<String,String> requestParameterMap = new HashMap<>();
		requestParameterMapConverter(httpRequestLine,requestParameterMap);
		
		Map<String,Object> routesrFileMapAllList = RouterFileReader.getInstance().getRouterFileMap();
		Map<String,String> routerFileMap = (Map<String,String>)routesrFileMapAllList.get(requestMethod);
		String actionClassPath = routerFileMap.get(requestData);
		if(actionClassPath == null || actionClassPath.trim().equals("")) {
		Logger.warn(Thread.currentThread().getName(), "this web api is not define : " + requestData);
			return HttpResponseHeader.createResponseHeaderBad();
		}
		if(ControllerCache.getInstance().existData(actionClassPath)) {
			Logger.info(Thread.currentThread().getName(), actionClassPath + " is From Cashe");
			HttpResFrame httpResFrame = ControllerCache.getInstance().getData(actionClassPath);
			return httpResFrame.createResponse();
		}
		if(requestMethod.toUpperCase().equals("POST")) {
			//Web API
			int dataLength = 0;
			dataLength = requestParameterMap.get("CONTENT-LENGTH") != null ? Integer.parseInt(requestParameterMap.get("CONTENT-LENGTH")) : 0;
			if(dataLength == 0) {
				try { 
					HttpResFrame httpResFrame = actionCalssCallHandler(actionClassPath);
					return httpResFrame.createResponse();
				} catch(Exception e) {
					e.printStackTrace();
					return HttpResponseHeader.createResponseHeaderInternalServerError(e.getMessage());
				}
			} else {
				String requestPrameterData = excludeHeaderHttpRequest.trim().substring(excludeHeaderHttpRequest.trim().length() - dataLength);
				Map<String,Object> reqMap = null;
				try {
					requestPrameterData = URLDecoder.decode(requestPrameterData, "UTF-8");
				} catch (UnsupportedEncodingException e) {
					requestPrameterData = "";
				}
				if(requestPrameterData.trim().equals("")) {
					reqMap = new HashMap<>();
				} else {
					reqMap = createReqParamMap(requestPrameterData);
				}
				try { 
					HttpResFrame httpResFrame = actionCalssCallHandler(actionClassPath, reqMap);
					return httpResFrame.createResponse();
				} catch(Exception e) {
					e.printStackTrace();
					return HttpResponseHeader.createResponseHeaderInternalServerError(e.getMessage());
				}
			}
			
		} else if(requestMethod.toUpperCase().equals("GET")) {
			try {
				HttpResFrame httpResFrame = actionCalssCallHandler(actionClassPath);
				if(!ControllerCache.getInstance().existData(actionClassPath)) {
					Logger.info(Thread.currentThread().getName(), actionClassPath + " join Cashe");
					ControllerCache.getInstance().addCacheData(actionClassPath, httpResFrame);
				}
				return httpResFrame.createResponse();
			} catch(Exception e) {
				e.printStackTrace();
				return HttpResponseHeader.createResponseHeaderInternalServerError(e.getMessage());
			}
		}
		return retStr;
	}
}

処理の実行

取得した「メソッド」、「URI」を基に「def/router」から実行する処理を実行します。
「GET」メソッドでURIが「/」の場合、testframe.gui.controller.IndexHtmlController.mainの処理を実行します。
callFunctionの第一引数のpathが「testframe.gui.controller.IndexHtmlController.main」、
第二引数以降の内容はHttpリクエストにボディが存在した場合、mapにして渡すように現在しております。

def/router
GET  /         testframe.gui.controller.IndexHtmlController.main
GET  /main.js  testframe.gui.controller.JavaScriptFileController.main
HttpHandleUtil.java
public class RouterContainer {
	public static <T> T callFunction(String path, Object... arglist) throws Exception {
		String[] pathList = path.split("\\.");
		String method = pathList[pathList.length - 1];
		path = path.substring(0,path.length() - method.length() - 1);
		Class<?> actionClass = (Class<?>)Class.forName(path);
		Object action = actionClass.getConstructor().newInstance();
		Method[] methods = actionClass.getDeclaredMethods();
		for(Method m : methods) {
			if(m.getName().equals(method)) {
				return (T)m.invoke(action, arglist);
			}
		}
		return null;
	}
}

処理が呼び出される。
この処理はhtmlファイルをリターンするイメージです。

IndexHtmlController.java
public class IndexHtmlController extends ControllerFrame {
	public HttpResFrame main() {
		return html("frontparts/html/index.html");
	}
}
ControllerFrame.java
public abstract class ControllerFrame {
	public HttpResFrame html(String path) {
		try(FileInputStream fi = new FileInputStream(path)){
			File fileData = new File(path);
			long filesize = Files.size(fileData.toPath());
			byte[] readfile = new byte[(int)filesize];
			fi.read(readfile);
			String header = HttpResponseHeader.createResponseHeaderSuccess(ContentType.HTML,(int)filesize);
			header = header + "\r\n";
			String body = new String(readfile,"UTF-8");
			return new HttpResFrame(header, body, ContentType.HTML);
		} catch(IOException e) {
			String header = HttpResponseHeader.createResponseHeaderInternalServerError();
			return new HttpResFrame(header, "", ContentType.HTML);
		}
	} 
}

最後に

記事を最後まで読んで頂き有難うございました。
ご指摘等御座いましたら、頂けると幸いです。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2