[Java / 실습] ServerSocket, Socket으로 간단한 서버 만들어보기

clean·2024년 1월 12일
0

*이 글은 책 <자바의 신2> 기말 실습 문제 부분을 보고 실습하여 작성하였습니다.

단일 클래스로 간단한 서버 만들기

java.net 패키지에 선언되어 있는 Socket, ServerSocket 클래스를 활용하여 간단한 웹 서버를 만들어보자. 우선은 다음 코드를 살펴보자.

package server;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;

public class SimpleWebServerInitial {
    public static void main(String[] args) {
        SimpleWebServerInitial server = new SimpleWebServerInitial();
        int port = 9000;
        server.start(port);
    }

    private final int BUFFER_SIZE = 2048;
    public void start(int port) {
        ServerSocket server = null;

        try {
            server = new ServerSocket(port);

            while(true) {
                Socket socket = server.accept();

                // Request Read.
                InputStream request = new BufferedInputStream(socket.getInputStream());
                byte[] receivedBytes = new byte[BUFFER_SIZE];
                request.read(receivedBytes);
                String requestData = new String(receivedBytes).trim();
                System.out.println("RequestData = " + requestData);
                System.out.println("------");

                // Make Response Data and Response
                PrintStream response = new PrintStream(socket.getOutputStream());
                response.println("HTTP/1.1 200 OK");
                response.println("Content-type: text/html");
                response.println();
                response.print("It is working.");
                response.flush();
                response.close();
            }

        } catch(Exception e) {
            e.printStackTrace();
        } finally {
            if(server != null) {
                try {
                    server.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

위의 코드는 'http://localhost:9000/' 에 GET 리퀘스트를 보냈을 때, 화면에 'It is working'이라는 응답을 띄우는 코드이다.

ServerSocket이 accept() 명령어를 통해 클라이언트의 요청을 기다리다가, 클라이언트가 요청을 보내면 ServerSocket 안에 Socket이 만들어진다. 그 소켓의 getInpuStream(), getOutputStream() 메소드를 통해 클라이언트의 요청을 읽어오고, 응답을 보낼 수 있다.

getInputStream()으로 리퀘스트를 읽어오면 가장 첫 줄(헤더의 첫 줄)에는 "RequestMethod URI HTTP_version"이 적혀있다. 예를 들어 "GET / HTTP/1.1"로 적혀있다면 Request Method는 GET 방식, URI는 "/", HTTP 버전은 1.1인 것이다.

인터넷 브라우저를 통해 GET 리퀘스트를 보낸 결과는 아래와 같다.

하지만 이렇게 하나의 클래스로 작성한다면 아래와 같은 단점이 존재한다.
1. 한번에 하나의 브라우처 요청만 처리할 수 있다.
2. 요청과 응답에 대한 처리가 분리되어 있지 않아서 추가적인 보완이 쉽지 않다.
3. '/'가 아닌 '/today'와 같은 다른 uri에 보낸 요청은 처리되지 않는다.

클래스로 역할을 분리하여 서버 구현

위의 단점들을 개선하기 위해서, 아래의 클래스들을 추가 구현하여 기능을 확장해보고자 한다.

1. RequestHandler: 여러 요청을 동시에 처리할 수 있는 스레드 클래스. 내부에서 RequestManager와 ResponseManager 객체를 생성하여 요청을 처리하고 응답을 보낸다.
2. RequestDTO: 요청 정보를 담을 수 있는 DTO 클래스.
3. RequestManager: 요청 정보를 소켓을 통해 읽어서 RequestDTO에 저장하는 클래스.
4. ResponseManager: 응답 정보를 클라이언트의 소켓에 써서 응답을 보내는 클래스.
5. SimpleWebServer: 메인 메소드가 있는 클래스. ServerSocket으로 while(true) 안에서 클라이언트의 요청이 올 때까지 기다리다가, 요청이 들어오면 RequestHandler 객체를 만들어 스레드릴 실행(start() 호출)한다.

이런 클래스들을 구현함으로써 다음 기능을 추가해보려고 한다.
1. 동시에 들어오는 여러 요청을 처리(스레드로 구현)
2. 요청을 보낸 URI가 '/today'일 때, Date 객체를 이용해 오늘 날짜와 시간 정보는 제공. (ResponseManager에서 구현)

RequestDTO 클래스 구현

RequestDTO는 클라이언트로부터 온 리퀘스트를 잠시 저장해두는 객체이다.
헤더의 정보인 RequestMethod, URI, HTTP Version 정보를 필드에 저장해둔다.
리퀘스트를 처리할 때 URI 정보가 필요하기 때문에, uri을 얻을 수 있는 getter도 만들어준다.

package server.dto;

public class RequestDTO {
    private String requestMethod = "GET";
    private String uri = "/";
    private String httpVersion = "HTTP/1.1";


    public void setRequestMethod(String requestMethod) {
        this.requestMethod = requestMethod;
    }

    public String getUri() {
        return uri;
    }

    public void setUri(String uri) {
        this.uri = uri;
    }

    public String getHttpVersion() {
        return httpVersion;
    }

    public void setHttpVersion(String httpVersion) {
        this.httpVersion = httpVersion;
    }
}

RequestHandler 구현

RequestHandler는 'extends Thread'로 Thread 클래스를 상속받고, run() 메소드를 오버라이딩 해준다.
run() 내부에서 RequestManager와 ResponseManager 객체를 만들어서 요청, 응답을 처리해야하는데, 그럴려면 클라이언트의 소켓이 필요하다.

따라서 클라이언트의 소켓을 저장하는 Socket 필드를 하나 만들고, 이를 매개변수로 받는 생성자를 만들어준다.

package server.handler;

import server.dto.RequestDTO;
import server.manager.RequestManager;
import server.manager.ResponseManager;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import server.dto.RequestDTO;

public class RequestHandler extends Thread {
    Socket socket; // 클라이언트의 웹 소켓

    // Socket 객체를 매개변수로 받는 생성자.
    public RequestHandler(Socket socket) {
        this.socket = socket;
    }

    public void run() {
        RequestManager requestManager = new RequestManager(socket); // 소켓 객체의 내용을 읽기 위한 클래스
        RequestDTO request =  requestManager.readRequest();

        ResponseManager responseManager = new ResponseManager(request, socket); // 클라이언트 소켓 객체에 응답 내용을 쓰는 클래스
        responseManager.writeResponse();
    }
}

RequestManager

RequestManager에는 socket에서 리퀘스트를 읽어와서 RequestDTO 객체에 저장하여 리턴하는 readRequest 메소드가 구현되어있다. 이 메소드를 통해서 만들어진 RequestDTO 객체를 받아서 ResponseManager 메소드가 리퀘스트 내용을 보고 적절하게 클라이언트에게 응답을 준다.

Request에서 지금 필요한 정보인 Request Method, URI, HTTP version은 리퀘스트의 가장 윗 줄에 있다.
따라서 읽어온 요(requestData)청을 "\n"으로 split하여 첫째줄을 얻어내고, 그걸 " "로 split하여 Request Method, URI, HTTP version 각각을 얻을 수 있다.

setter를 통해 그 값을 DTO 객체에 저장해주었다.

package server.manager;

import server.dto.RequestDTO;

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

// 사용자의 요청을 읽어 분석
public class RequestManager {

    private Socket socket;
    private final Integer BUFFER_SIZE = 2048;

    public RequestManager(Socket socket) {
        this.socket = socket;
    }

    public RequestDTO readRequest() {
        RequestDTO result = new RequestDTO();
        try {
            InputStream request = new BufferedInputStream(socket.getInputStream());

            byte[] receivedBytes = new byte[BUFFER_SIZE];
            request.read(receivedBytes); // 리퀘스트 읽기
            String requestData = new String(receivedBytes).trim();

            String[] parsedRequest = requestData.split("\n");
            String[] requestHeader = parsedRequest[0].split(" ");

            result.setRequestMethod(requestHeader[0]); // requestMethod. GET, POST 등등
            result.setUri(requestHeader[1]);
            result.setHttpVersion(requestHeader[2]);

        } catch (IOException ioe) {
            ioe.printStackTrace();
        }


        return result;

    }
}

ResponseManager

RequestDTO를 보고 리퀘스트의 URI를 확인하고, socket을 이용해 응답을 보내야하게 때문에 내부 필드로 RequestDTO와 Socket 변수를 선언해주었다. 그리고 이 두 필드는 생성자를 통해 초기화된다.

writeResponse가 응답을 Socket의 outputStream에 쓰는 메소드이다.
uri가 '/today'이면 오늘 날짜와 현재 시간을 띄워주는 기능을 구현하기로 했으므로, getUri를 통해 uri를 확인하여 '/'면 "Working"이라는 문자열을, 'today'이면 오늘 날짜, 현재 시간을, 그 외의 uri는 "Invalid path."를 화면에 띄우도록 switch문으로 구현했다.
(if문을 써도 되지만, java 7부터 String도 switch문 사용이 가능하다는 얘기를 들어서 한번 써보고 싶어서 써봤다.)

그리고 PrintStream은 꼭 닫아주어야한다.

package server.manager;

import server.dto.RequestDTO;

import javax.print.attribute.standard.RequestingUserName;
import java.io.IOException;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Date;

// 이 클래스의 용도는 사용자에게 전달할 응답의 내용을 만들기 위해서 사용된다.
public class ResponseManager {
    private RequestDTO request; // 리퀘스트 정보가 담긴 RequestDTO 객체
    private Socket socket; // 클라이언트의 소켓

    public ResponseManager(RequestDTO request, Socket socket) {
        this.request = request;
        this.socket = socket;
    }

    public void writeResponse() {
        try {
            PrintStream response = new PrintStream(socket.getOutputStream());

            String uri = request.getUri();

            response.println("HTTP/1.1 200 OK");
            response.println("Content-type: text/html");
            response.println();
            switch(uri) {
                case "/today":
                    response.println("Today is " + new Date());
                    break;
                case "/":
                    response.println("Working!!");
                    break;
                default:
                    response.println("Invalid path.");
            }


            response.flush();
            response.close(); // 꼭 닫아주어야한다. 안 닫아주면 스레드가 안끝나는 것 같다..
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

SimpleWebServer

메인메소드가 있는 클래스이다. 이 클래스의 메인메소드를 실행함으로써 서버를 돌릴 수 있다.

ServerSocket 객체를 포트번호 9000로 생성해주고, while(true)안에서 accept() 메소드로 클라이언트의 요청을 기다인다.
요청이 들어오면 RequestHandler 객체를 만들고 start() 명령어를 통해 스레드를 실행시킨다.

package server;

import server.handler.RequestHandler;

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

public class SimpleWebServer {
    public static void main(String[] args) {
        SimpleWebServer web = new SimpleWebServer();
        web.run();
    }

    public void run() {
        ServerSocket server;
        Socket client;

        try {
            server = new ServerSocket(9000);

            while(true) {
                client = server.accept();

                RequestHandler request = new RequestHandler(client);
                request.start();

            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

결과

profile
블로그 이전하려고 합니다! 👉 https://onfonf.tistory.com 🍀

0개의 댓글