1. Socket에 관한 초초초초 간단 설명
TCP 통신을 위해 자바에선 Socket 클래스를 제공한다. TCP는 연결 지향형 전송 프로토콜이며, 신뢰성 있는 연결을 보장한다. 여기서 신뢰성 있는 연결이란, 송신측의 데이터가 수신측에게 도달하는 것을 보장하며, 그렇게 보낸 데이터에 손실이나 순서 뒤바뀜이 없음을 의미한다.
Socket에도 두 가지가 있는데 데이터의 송수신, 즉 양방향 통신에 이용되는 Socket, 서버에서 클라이언트의 요청을 받기 위한 ServerSocket 클래스가 존재한다.
클라이언트 측에서 소켓을 이용해 요청을 날리려면 직접 Socket 객체에 서버의 포트번호와 IP를 지정해 stream 형식의 데이터를 넣어 보내줘야한다. 서버 측에서 특정 포트 번호를 생성자로 넣어 ServerSocket 인스턴스를 만들면 JVM은 해당 포트로 들어오는 요청을 대기하게 된다. ServerSocket의 accept 메소드가 해당 역할을 수행하며, 만약 클라이언트의 요청이 들어오게 된다면 stream 데이터를 담은 Socket 객체를 리턴한다.
2. 코딩 START
대부분의 프로그래밍 설명이 그렇듯, 실제 코드가 없으면 이해가 잘 가지 않는다. 사용자 요청의 URI를 확인하고 이에 맞는 응답을 내려주는 초간단 서버를 열어보도록 하자.
1) 포트번호로 서버 열기
일단 서버 측에서, ServerSocket 클래스를 이용해 포트번호 9000번에서 TCP 서버를 여는 코드를 작성해보자.
public class MiniServer {
public static void main(String[] args) {
startServer();
}
public static void startServer() {
try (ServerSocket server = new ServerSocket(9000)) {
while (true) {
Socket request = server.accept();
RequestHandler requestHandler = new RequestHandler(request);
requestHandler.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
- server = new ServerSoket(9000) : 생성자로 포트번호를 넣으면 JVM은 자동으로 9000번 포트로 들어오는 TCP 요청을 대기하게 된다.
- Socket request = server.accept() : 서버가 요청을 받으면 요청으로 들어온 소켓 객체를 리턴하는 method이다. request 변수에 해당 소켓 객체를 담았다.
2) handling request
RequestHandler는 생성자의 인자로 받은 request 소켓 객체를 가지고 response를 내려주는 핸들러 객체이다. Thread 클래스를 상속받아 멀티 스레드로 구현하였다. 위의 start() 메소드가 시작되는 것을 확인하자.
public class RequestHandler extends Thread {
private final Socket request;
public RequestHandler(Socket request) {
this.request = request;
}
@Override
public void run() {
ResponseManager.writeResponse(request,
RequestManager.readRequest(request));
}
}
- ResponseManager.writeResponse() : 요청 소켓과 소켓의 데이터를 받아 응답을 작성하는 실제 메소드이다.
- RequestManager.readRequest() : 요청 소켓을 파싱하는 메소드이다. 구현은 바로 밑에서 확인하자.
3) parsing request (to DTO)
public class RequestManager {
private final static int BUFFER_SIZE = 2048;
public static RequestDto readRequest(Socket request) {
try {
InputStream inputStream = new BufferedInputStream(request.getInputStream());
byte [] received = new byte[BUFFER_SIZE];
inputStream.read(received);
String data = new String(received).trim();
String [] header = (data.split("\n")[0]).split(" ");
return RequestDto.builder()
.requestMethod(header[0])
.uri(header[1])
.httpVersion(header[2])
.build();
} catch (IOException e) {
e.printStackTrace();
return RequestDto.builder().build();
}
}
}
- request.getInputStream()을 이용해 받은 input stream에 byte 배열을 넣어 byte 단위의 요청 데이터를 받았다.
- 이를 data 변수에 String 형식으로 넣어놓고, 헤더의 첫째줄만 뽑아오기 위해 data.split("\n")을 수행한다.
- 요청 헤더의 첫번째 줄에는 "{HTTP_METHOD} {URI} {HTTP_VERSION}" 형식으로 요청 헤더가 적혀있는데, 이를 RequestDto의 빌더 인자값으로 넣어 requestDto 객체를 만들어준다.
4) RequestDto
사용자의 요청 정보가 담긴 RequestDto 클래스이다. 별 건 없다.
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RequestDto {
@Builder.Default
private String requestMethod = "GET";
@Builder.Default
private String uri = "/";
@Builder.Default
private String httpVersion = "HTTP/1.1";
}
생성자 짜기 귀찮아서 롬복씀
5) 요청 소켓에 응답 작성하기
RequestHandler에 있던 ResponseManager.writeResponse() 메소드를 구현할 차례다. 인자로 받은 요청 소켓에 URI에 맞는 응답 body를 내려주는 역할을 한다.
public class ResponseManager {
// 사용자에게 전달할 응답을 만들기 위해 사용
public static void writeResponse(Socket socket, RequestDto request) {
try (PrintStream response = new PrintStream(socket.getOutputStream())) {
response.println("HTTP/1.1 200 OK");
response.println("Content-type: text/html");
response.println();
if (request.getUri().equals("/today")) {
response.println(new Date());
} else {
response.println("It is working!");
}
response.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 요청과 마찬가지로 Socket에서 데이터를 다루기 위해 stream을 이용한다. PrintStream을 이용하였다.
- 응답 역시 요청과 마찬가지로 header/body는 "\n", 즉 줄바꿈으로 구분된다. 헤더의 첫줄에 "{HTTP_VERSION" {RESPONSE_STATUS}"를 추가하고, 다음 줄에 content type을 추가했다.
- 사용자 요청 URI가 "today"일 경우 오늘의 날짜를, 그 외일 경우 "It is working!"의 html text를 응답 body로 구성했다. response.flush()를 통해 스트림에 저장된 내용을 강제 작성했다.
일련의 과정이 끝나면 RequestHandler의 run() 메소드가 완료되는 것이므로 해당 스레드는 terminated 상태가 되어 사라진다. 하지만 main의 while문이 계속 돌아가고 있으므로, 메인 스레드를 종료하기 전까지 서버는 계속 server.accept()를 통해 9000번 포트로 들어오는 사용자의 요청을 계속 기다리고 있을 것이다.
3. 기능 확인!
이제 기능이 잘 돌아가는지 확인할 차례다. MiniServer을 컴파일하고 실행시키자. 그 뒤 브라우저의 주소창에 http://localhost:9000를 입력하자!
[사용자 요청 URI가 "today"일 경우 오늘의 날짜를, 그 외일 경우 "It is working!"의 html text를 응답 body로 구성했다] 라고 앞서 말했다. "It is working!"글자가 잘 나온 것을 확인할 수 있다.
그럼 이제 http://localhost:9000/today를 입력하고 엔터를 쳐보면,
new Date()를 통해 만들어진 날짜 객체의 결과값이 잘 나오는 것을 확인할 수 있다!
Reference)
자바의 신 - 이상민 지음