본문 바로가기
Category/Project

JAVA TCP 소켓을 사용하여 HTTP통신이 가능한 WAS 직접 구현하기 -2

by developer__Y 2024. 9. 19.

 

WAS는 어떤기능을 가지고있어야 하는가?

 

이전에 알아보았던 Tomcat이 클라이언트의 요청을 받아 관련된 서블릿을 생성해주는 과정을 바탕으로 

직접 구현해볼 WAS가 어떠한 기능을 가지고 있어야할지 일종의 기능요구사항들을 작성해보자.

 

1. 클라이언트의 HTTP 메시지를 수신할수있어야한다.
     - 해당 HTTP 메시지를 읽고,쓰기쉽도록 적절한 형식으로 파싱할수 있어야한다.

2. 수신된 HTTP 메시지(Request)를 누구에게 전달할지 결정할수있도록 Connector가 필요하다.
    - Connector은 특정 PORT와 특정 프로토콜에 해당하는 커넥터들이 여러개 필요하지만, 여기에서는 HTTP 프로토콜만 다루도록 한다.

3. 전달받은 Request를 처리해줄 일종의 Servlet 기능을 제공해야한다.
     - 요청된 Request의 url을 기반으로 구분하여 처리한다.
     - Request를 처리할때에 멀티스레드 방식으로 1개의 요청당 1개의 스레드를 사용하여 다양한 요청에 대응할 수있어야한다.

4. 처리가 완료된 Response를 클라이언트에게 응답한다.
   - 마찬가지로 클라이언트에게 적절한 형식으로 파싱하여 응답한다.

 

 

위의 과정대로 Spring을 사용하지않고 실제 java 코드로 구현해보자!

 

개발환경 :  java maven project & jdk 1.8

 

 

 

 

1. Server와 Connector 만들기

 

 

가장먼저 구현해야할 Server와 Connector의 아키텍처는 다음과 같다.

 

 

 

main 메소드에서 Server 클래스를 실행하여 WAS 를 구동시킨다.

Server 클래스는 실행시 List에 담긴 Connector 객체들을 꺼내 별도의 Thread를 생성하여 커넥터를 활성화시킨다.

 

Server로부터 스레드로 실행된 각각의 커넥터들은 특정 PORT에 맞는 Socket을 생성하여 클라이언트의 요청을 대기하고있으며, 별도의 스레드풀을 가지고있어 사용자의 요청을 수신할경우 스레드풀에서 스레드를 받아와 해당 요청을 RequestHandler로 전달한다.

 

public class Server {
	
	 	private static final Logger logger = LoggerUtil.getLogger(Server.class);
		private List<Connector> connectorList;
		
		public Server(List<Connector> connectorList) {
			this.connectorList = connectorList;
		}
		

		public void start() {
			for(Connector connector : connectorList) {
				 // 각 Connector를 새로운 스레드에서 실행
		        Thread thread = new Thread(connector);
		        thread.start();
		        
		        logger.info("Connector started on port: {}", connector.getPort());
			}
			
		}
}

 

 

Server 클래스의 start() 메소드를 통해 List 에 담긴 Connector들을 Thread 생성하여 실행시켜준다.

별도의 Thread에서 실행되기위해 Connector는 Runnable을 구현하여 run메소드를 통해 보유한 PORT의 소켓을 생성시키고 대기한다.

 

public class Connector implements Runnable  {
	
	private static final Logger logger = LoggerUtil.getLogger(Connector.class); 
	private static final int DEFAULT_MAX_REQUEST = 100; 
	private int PORT;
	private final ServerSocket serverSocket;
	private final ExecutorService excutorService;
	
	
	public Connector(final int SERVER_PORT,final int MAX_REQUEST,final ExecutorService executorService) {
		this.PORT = SERVER_PORT;
		this.serverSocket = createSocket(SERVER_PORT,MAX_REQUEST);
		this.excutorService = executorService;
	}
	
	public Connector(final int SERVER_PORT,int max_request) {
		this(SERVER_PORT,max_request,Executors.newFixedThreadPool(max_request));
	}
	public Connector() {
		this(8080, DEFAULT_MAX_REQUEST, Executors.newCachedThreadPool());
	}
	
	@Override
	public void run() {
		logger.info("server start ! port : {}",PORT);
		while (true) {
			try {
				// 클라이언트 요청 수신
				Socket clientSocket = serverSocket.accept();
				logger.info("클라이언트 연결됨: {}", clientSocket.getInetAddress());
				
				//스레드풀에 작업을 처리할 핸들러 제출
				excutorService.submit(new RequestHandler(clientSocket));
			} catch (IOException e) {
				logger.error("클라이언트 요청 수신 중 오류 발생: {}", e.getMessage(), e);
			}
		}
	}
	
	public ServerSocket createSocket(int port,int maxRequest) {
		
		try {
			int validPort = valid_Port(port);
			int validRequest = valid_MaxRequest(maxRequest);
			return new ServerSocket(validPort,validRequest);
		} catch (IOException e) {
			
			e.printStackTrace();
			throw new UncheckedIOException(e);
		}
	}

	
	private int valid_Port(int port) {
		
		if(port < 1 || port > 65535 ) {
			return PORT;
		}
		
		return port;
	}
	
	private int valid_MaxRequest(int maxRequest) {
		if(maxRequest < 1) {
			return DEFAULT_MAX_REQUEST;
		}
		
		return maxRequest;
	}
	public int getPort() {
		return PORT;
	}
		

	
}

 

 

Connector 클래스의 특징은 커넥터마다 특정한 PORT와 ServerSocket, 그리고 별도의 ExecuteService를 가지고있다는 것이다. 이를 통해 각 커넥터들은 자기가 가진 특정 PORT에 대하여 소켓을 생성한뒤,

클라이언트의 요청이 들어오면 해당 요청을 ExecuteService를 통해 ThreadPool에 Submit하여 요청을 처리하는 형태이다.

 

 

 

커넥터로부터 받은 요청을 핸들링하는 RequestHandler

 

클라이언트의 요청이 특정 포트에서 수신중인 ServerSocket을 타고 들어오면, Connector은 

해당 요청을 스레드풀에 Submit 할때, RequestHandler 객체를 생성하여 해당 생성자로 요청받은 클라이언트 Socket을 담아 제출한다.

이는 멀티스레딩방식의 WAS를 구현하기위한것으로 클라이언트의 요청들을 다중 스레드 환경에서 각각 처리하기위한 것이다.

 

RequestHandler가 각각의 요청을 담은 ClientSocket을 받아, 해당 메시지를 읽은뒤에 다음 작업과정으로 전달해줄수있는 일종의 Handler 역할을 하도록 구현하였다.

 

public class RequestHandler implements Runnable {
	private static final Logger logger = LoggerUtil.getLogger(RequestHandler.class); 
	private final Socket clientSocket;
	 
	 
	  public RequestHandler(Socket socket) {
	        this.clientSocket = socket;
	    }
	  
	  public void run() {
		
		 BufferedReader in;
		try {
			in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
			String message = in.readLine(); // 클라이언트로부터 메시지 읽기
			
			logger.info("Client로 부터 받은 message : {}",message);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
         
		
	}

}

 

 

실제 테스트

 

 

public class App {
    public static void main(String[] args){
    	
    	List<Connector> list = new ArrayList<Connector>();
    	Connector connector = new Connector();
    	list.add(connector);
    	
             Server server = new Server(list);
             server.start();
       
    }
}

 

 

실제 클라이언트의 요청을 커넥터가 잘 수신하여 RequestHandler로 전달하는시 테스트해보자.

 

main 메소드에서 생성된 커넥터들은 Tomcat의 경우 server.xml에 정의되어있어 이를 읽어와 커넥터를 생성하지만,

일단 기본생성자로 커넥터를 하나 생성한뒤에 서버를 구동시켜주었다.

 

파라미터가 없는 기본생성자로 Connector을 생성할 경우 기본포트 8080을 설정하도록 셋팅해주었으므로,

server.start()을 통해  localhost:8080 에 클라이언트의 요청을 기다리는 소켓이 생성되었을 것이다.

 

클라이언트 요청을 테스트하는 방법은 POSTMAN이나 Junit HTTP등 여러가지 방법이있겠지만

단순히 크롬 브라우저를 켜고 주소창에 입력한뒤 엔터만 치면 클라이언트에서 요청을 보내준다!

 

 

위와같이 브라우저에서 입력을 하면 localhost:8080 포트에 경로는 /abc/1 로 GET HTTP요청을 보내준다.

 

 

로그

 

로그를 확인해보면, 8080 포트에 커넥터가 활성화 되었으며, 브라우저로부터 받은 MESSAGE를 RequestHandler가 그대로 문자열로 변환시켜 출력해주었다!

 

이제, 클라이언트의 메시지를 HTTP 형식에 맞는 HTTPRequest , HTTPResponse 객체에 담아 파싱하고,

해당 요청내용을 처리해줄 서블릿을 구현하면 된다!