하나의 TCP 접속에 전이중 통신 채널을 제공하는 컴퓨터 통신 프로토콜입니다.
HTTP나 HTTPS 위에서 동작하도록 설계되었으며 포트를 80번 443번 사용합니다.
이에 따라 HTTP를 통해 연결을 시작하되 TCP 소켓과 유사한 통신 stream을 허용합니다.
네트워크상 서버와 클라이언트 두 개의 프로그램이 특정 포트를 통해 양뱡향 통신이 가능하도록 만들어주는 소프트웨어 장치입니다.
intranet 바운더리(조직 내 네트워크)에서 작업하는 경우에는 해당 네트워크의 컴퓨터를 제어하고 TCP 연결에 적합한 포트를 열 수 있기 때문에 TCP 소켓을 통해 통신하는 것이 더 쉽습니다.
TCP Socket은 저수준, 4 계층(전송 계층)에 위치하여 동작, 바이트 스트림을 통한 데이터 전송
WebSocket은 HTTP 기반으로 7 계층(응용 계층)에서 동작, 구조화된 메세지 형식의 데이터 전송
AES128은 "Advanced Encryption Standard (고급 암호 표준)"의 약자로, 컴퓨터와 통신 시스템에서 데이터를 암호화하고 해독하는 데 사용되는 대표적인 암호화 알고리즘 중 하나입니다. AES는 대칭 키 암호화 알고리즘 중 하나로, 동일한 키를 암호화와 해독에 모두 사용합니다. AES128은 AES 알고리즘에서 사용하는 키의 길이가 128 비트인 버전을 나타냅니다.
@Slf4j
public class AES128 {
private final String ips;
private final Key keySpec;
public AES128(String key) {
try {
byte[] keyBytes = new byte[16];
byte[] b = key.getBytes(StandardCharsets.UTF_8);
System.arraycopy(b, 0, keyBytes, 0, keyBytes.length);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
this.ips = key.substring(0, 16);
this.keySpec = keySpec;
} catch (Exception e) {
log.error("AES128 객체 생성 오류", e);
throw new RuntimeException("AES128 객체 생성 오류", e);
}
}
public String encrypt(String str) {
Cipher cipher;
try {
cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, keySpec,
new IvParameterSpec(ips.getBytes()));
byte[] encrypted = cipher.doFinal(str.getBytes(StandardCharsets.UTF_8));
return new String(Base64.encodeBase64(encrypted));
} catch (Exception e) {
log.error("AES128 암호화 과정 오류", e);
throw new RuntimeException("AES128 암호화 과정 오류", e);
}
}
public String decrypt(String str) {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec,
new IvParameterSpec(ips.getBytes(StandardCharsets.UTF_8)));
byte[] byteStr = Base64.decodeBase64(str.getBytes());
return new String(cipher.doFinal(byteStr), StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("AES128 복호화 과정 오류", e);
throw new RuntimeException("AES128 복호화 과정 오류", e);
}
}
}
ips: 초기화 벡터(Initialization Vector, IV)의 문자열 표현을 저장하는 멤버 변수입니다.
keySpec: AES 알고리즘에 사용되는 키를 나타내는 Key 객체를 저장하는 멤버 변수입니다.
이 생성자는 AES128 객체를 초기화하고 AES 키를 설정합니다.
입력으로 주어진 key 문자열을 UTF-8 문자열로 인코딩하고, 첫 16바이트를 AES 키로 사용합니다.
초기화 벡터 ips는 key 문자열의 처음 16바이트로 설정됩니다.
주어진 문자열 str을 AES 알고리즘을 사용하여 암호화합니다.
AES/CBC/PKCS5Padding 모드를 사용하여 암호화된 데이터를 Base64로 인코딩한 문자열을 반환합니다.
주어진 암호화된 문자열 str을 AES 알고리즘을 사용하여 복호화합니다.
AES/CBC/PKCS5Padding 모드를 사용하여 Base64로 디코딩한 바이트 배열을 복호화하고, 원래 문자열로 디코딩하여 반환합니다.
@Slf4j
public class EchoServer {
public static void on() {
int port = 8081;
try (ServerSocket serverSocket = new ServerSocket(port)) {
log.info("[Echo Server]Echo Server is running on port = {}", port);
Socket clientSocket = serverSocket.accept();
while (true) {
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter writer = new PrintWriter(clientSocket.getOutputStream(), true);
String clientMessage = reader.readLine();
String decryptMessage = decryptMessage(clientMessage);
log.info("[Echo Server] Received from client message: {}", clientMessage);
log.info("[Echo Server] Decrypt message: {}", decryptMessage);
// Echo the message back
writer.println(decryptMessage);
}
// clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private static String decryptMessage(String clientMessage) {
String key = "keykeykeykeykeykey";
AES128 aes = new AES128(key);
String decryptMessage = aes.decrypt(clientMessage);
return decryptMessage;
}
}
해당 EchoServer를 static으로 선언하였고 메인 함수에서 실행시켜주었습니다.
Socket을 열어주고 Client에게서 AES128로 암호화된 데이터를 전달 받고 복호화하여 다시 전달해줍니다.
@Slf4j
public class EchoClient {
public static void on() {
String serverIP = "127.0.0.1";
int serverPort = 8081;
try (Socket socket = new Socket(serverIP, serverPort)) {
PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("[Echo Client] Enter a message to send to the server : ");
String message = scanner.nextLine();
String encryptMessage = encryptMessage(message);
writer.println(encryptMessage);
String serverResponse = reader.readLine();
System.out.println("[Echo Client] Received from server: " + serverResponse);
}
} catch (IOException e) {
log.error("[Echo Client]socket send message error", e);
}
}
private static String encryptMessage(String message) {
String key = "keykeykeykeykeykey";
AES128 aes = new AES128(key);
String encryptMessage = aes.encrypt(message);
return encryptMessage;
}
}
해당 EchoClient를 static으로 선언하였고 메인 함수에서 실행시켜주었습니다.
Server로 평문을 AES128로 암호화하여 데이터를 전송하고 복호화된 데이터를 다시 전달받습니다.
ServerSocket은 단지 클라이언트와 서버의 TCP 연결만 지원하는 특별한 소켓이다.
실제 클라이언트와 서버가 정보를 주고 받으려면 Socket 객체가 필요하다.
1. accept()를 호출하면 backlog queue에서 TCP 정보를 조회한다.
-TCP 연결 정보가 없다면, 연결 정보가 생성될 때까지 블로킹하여 대기한다.
2. 정보가 들어온다면 해당 정보를 기반으로 Socket 객체를 생성한다.
3. 사용한 TCP 연결 정보는 backlog queue에서 제거된다.
- 클라리언트가 연결을 시도하면 OS 계층에서 TCP 3way-handshake가 발생하고, TCP 연결이 완료된다.
- TPC 연결이 완료되면 서버는 OS backlog queue에 클라이언트와 서버의 TCP 연결 정보를 보관한다.
- 연결 정보엔 클라이언트 IP, Port 서버의 IP, Port 정보가 모두 들어있다.
Application을 실행할때 arguments로 server
, client
값을 받아 따로따로 실행해주었습니다.
client와 server의 port도 따로 변경하여 각각 독립적으로 실행시켜줍니다.
위와 같이 개발을 하다보면 Client와 Server가 1:1로 관계되어 데이터를 주고 받을 수 있습니다.
한개의 EchoServer에 여러개의 Client를 붙을 수 있게 하려면 Thread를 분리하여 처리해줘야 합니다.
변경된 코드
@Slf4j
public class EchoServer {
public static void on() {
int port = 8081;
try (ServerSocket serverSocket = new ServerSocket(port)) {
log.info("[Echo Server]Echo Server is running on port = {}", port);
while (true) {
Socket clientSocket = serverSocket.accept();
log.info("[Echo Server] Accepted connection from client: {}:{}", clientSocket.getInetAddress().getHostAddress(), clientSocket.getPort());
// 각 클라이언트와의 통신을 처리하기 위한 스레드 생성
Thread clientThread = new Thread(() -> handleClient(clientSocket));
clientThread.start();
}
// clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private static void handleClient(Socket clientSocket) {
try (
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter writer = new PrintWriter(clientSocket.getOutputStream(), true)
) {
while (true) {
String clientMessage = reader.readLine();
String decryptMessage = decryptMessage(clientMessage);
log.info("[Echo Server] Received from client {}: {}", clientSocket.getInetAddress().getHostAddress(), clientMessage);
log.info("[Echo Server] Decrypt message: {}", decryptMessage);
writer.println(decryptMessage);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static String decryptMessage(String clientMessage) {
String key = "keykeykeykeykeykey";
AES128 aes = new AES128(key);
return aes.decrypt(clientMessage);
}
}
데이터를 주고받을 수 있게 처리 해준 부분을 메소드분리를 해줍니다.
serverSocket.accept()
를 반복적으로 실행하여 새로운 Client의 연결을 받아줍니다.
클라이언트가 새로 연결이 된다면 Thread를 새로 생성해서 병렬적으로 분리한 handleClient()
를 실행시켜 줍니다.
한 개의 EchoServer
에 EchoClient:61134
와 EchoClient:61143
이 동시에 통신에 성공한것을 확인할 수 있습니다.
reference: