소개
멀티스레드 환경에서 다수의 TCP 요청을 처리하고, 이를 백엔드 서버로 효율적으로 분배하는 TCP 프록시 서버를 구축하는 방법을 소개합니다. 이 프로젝트는 Java의 강력한 네트워크 라이브러리와 라운드 로빈 로드밸런싱 알고리즘을 활용해 간단하고 효율적인 로드밸런싱을 구현합니다.
1. 프로젝트 개요
목표
- TCP 요청을 처리하는 프록시 서버를 구축.
- 백엔드 서버 그룹으로 요청을 분배.
- 라운드 로빈 알고리즘을 통해 요청을 균등하게 분배.
- 멀티스레드를 활용해 여러 클라이언트 요청을 동시에 처리.
구성
- 클라이언트: TCP 요청을 보냄.
- 프록시 서버: TCP 요청을 수신하고 백엔드 서버로 전달.
- 백엔드 서버 그룹: 요청을 처리하고 결과를 반환.
2. 라운드 로빈 로드밸런싱
로드밸런싱이란?
로드밸런싱은 다수의 서버에 작업을 균등하게 분배하여 시스템 성능과 가용성을 최적화하는 기술입니다.
라운드 로빈 방식
라운드 로빈은 각 서버에 순차적으로 요청을 보내는 간단한 로드밸런싱 방식입니다.
- 첫 번째 요청은 서버 A로 전달.
- 두 번째 요청은 서버 B로 전달.
- 모든 서버가 요청을 한 번씩 처리한 후 다시 처음으로 돌아감.
3. 구현 코드
3.1 서버 소켓 설정
Java의 ServerSocket 클래스를 활용하여 클라이언트 요청을 수신합니다. 백엔드 서버 연결은 소켓을 사용합니다.
import java.io.*;
import java.net.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class TcpProxyServer {
private static final String[] BACKEND_SERVERS = {"192.168.10.1:9577", "192.168.10.2:9577"};
private static final AtomicInteger INDEX = new AtomicInteger(0); // 라운드 로빈 인덱스
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10); // 스레드 풀 생성
try (ServerSocket serverSocket = new ServerSocket(9577)) {
System.out.println("Proxy server listening on port 9577...");
while (true) {
Socket clientSocket = serverSocket.accept(); // 클라이언트 연결 대기
executor.submit(() -> handleClient(clientSocket)); // 요청 처리
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void handleClient(Socket clientSocket) {
try (Socket backendSocket = connectToBackend();
InputStream clientIn = clientSocket.getInputStream();
OutputStream clientOut = clientSocket.getOutputStream();
InputStream backendIn = backendSocket.getInputStream();
OutputStream backendOut = backendSocket.getOutputStream()) {
// 클라이언트 -> 백엔드 데이터 전달
Thread clientToBackend = new Thread(() -> forwardData(clientIn, backendOut));
clientToBackend.start();
// 백엔드 -> 클라이언트 데이터 전달
Thread backendToClient = new Thread(() -> forwardData(backendIn, clientOut));
backendToClient.start();
clientToBackend.join();
backendToClient.join();
} catch (Exception e) {
e.printStackTrace();
}
}
private static Socket connectToBackend() throws IOException {
// 라운드 로빈으로 백엔드 서버 선택
int targetIndex = INDEX.getAndUpdate(i -> (i + 1) % BACKEND_SERVERS.length);
String[] backend = BACKEND_SERVERS[targetIndex].split(":");
String host = backend[0];
int port = Integer.parseInt(backend[1]);
System.out.println("Forwarding to backend: " + host + ":" + port);
return new Socket(host, port);
}
private static void forwardData(InputStream in, OutputStream out) {
try {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
out.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
4. 코드 설명
- 프록시 서버 설정:
- ServerSocket은 9577 포트를 열어 클라이언트 요청을 수신.
- 요청이 들어올 때마다 스레드 풀에서 작업을 처리.
- 백엔드 연결:
- AtomicInteger를 활용해 라운드 로빈 방식으로 백엔드 서버를 선택.
- 선택된 서버로 TCP 연결을 생성.
- 데이터 전달:
- 양방향 데이터 전달: 클라이언트 -> 백엔드, 백엔드 -> 클라이언트 데이터를 각각 다른 스레드에서 처리.
- 스레드 풀:
- ExecutorService를 사용해 동시에 여러 클라이언트 요청을 처리.
- 스레드 풀 크기는 고정(10개)으로 설정하여 자원 사용 제한.
5. 실행 방법
- 백엔드 서버 설정:
- "192.168.10.1:9577" 및 "192.168.10.2:9577" 로 백엔드 서버를 실행합니다.
- 프록시 서버 실행:
- 위 코드를 컴파일 및 실행합니다:
- 위 코드를 컴파일 및 실행합니다:
- 클라이언트 요청 테스트:
- 클라이언트에서 127.0.0.1:9577로 요청을 보냅니다.
- 요청이 백엔드 서버로 순차적으로 전달되는지 확인합니다.
javac TcpProxyServer.java
java TcpProxyServer
4.Telnet을 사용해 프록시 서버에 요청을 보냅니다.
telnet 127.0.0.1 9577
결과:
- 첫 번째 요청: Backend 1 응답
- 두 번째 요청: Backend 2 응답
6. 로드밸런싱 결과 확인
위 테스트를 통해 각 요청이 라운드 로빈 방식으로 백엔드 서버에 분배되는 것을 확인할 수 있습니다:
- 백엔드 서버 1 (192.168.10.1:9577)과 백엔드 서버 2 (192.168.10.2:9577)의 터미널에서 클라이언트의 연결 로그가 번갈아 출력됩니다.
- 클라이언트 응답 메시지가 두 서버에서 번갈아 반환됩니다.
7. 라운드 로빈 로드밸런싱의 장점
- 균등 분배: 각 서버가 동일한 양의 요청을 처리.
- 구현 간단: 복잡한 로직 없이 배열과 인덱스를 활용.
- 스레드 안전: AtomicInteger를 사용해 스레드 간 충돌 방지.
'프로그래밍 언어 > JAVA' 카테고리의 다른 글
Java 직렬화와 NotSerializableException 문제 (0) | 2025.01.02 |
---|