Java

1:1 양방향 통신(채팅 기본 기능 구현)

H_u 2024. 5. 31. 11:50
728x90
반응형
SMALL
💡 멀티 스레드 활용

양방향 통신을 지속적으로 수행하기 위해 서버와 클라이언트 모두에서 키보드 입력을 받아 상대방에게 데이터를 보내고 받을 수 있도록 스레드와 while문을 활용하여 코드를 작성해봅시다.

서버 측 코드

  • ServerSocket 을 생성하고 클라이언트의 연결을 기다립니다.
  • BufferedReader 를 사용하여 클라이언트로부터 메시지를 읽고, PrintWriter를 사용하여 클라이언트에게 메시지를 보냅니다.
  • 키보드 입력을 받기 위해 BufferedReader를 사용합니다.
  • 클라이언트로부터 데이터를 읽는 스레드와 키보드 입력을 클라이언트로 보내는 스레드를 각각 실행합니다.

 

package ch04;

 

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStreamReader;

import java.io.PrintWriter;

import java.net.ServerSocket;

import java.net.Socket;

 

public class MultiThreadServer {

 

// 메인 함수

public static void main(String[] args) {

 

System.out.println("=== 서버 실행 === ");

 

ServerSocket serverSocket = null;

Socket socket = null;

 

try {

serverSocket = new ServerSocket(5001);

socket = serverSocket.accept();

System.out.println("포트 번호 - 5001 할당 완료");

 

// 1. 클라이언트 데이터를 받을 입력 스트림 필요

// 2. 클라이언트에 데이터를 보낼 출력 스트림 필요

// 서버측 - 키보드 입력을 받기 위한 입력 스트림 필요

 

BufferedReader socketReader =

new BufferedReader(new InputStreamReader(socket.getInputStream()));

PrintWriter socketWriter =

new PrintWriter(socket.getOutputStream(), true);

BufferedReader keyboardReader =

new BufferedReader(new InputStreamReader(System.in));

 

 

// 멀티 스레딩 개념에 확장

// 클라이언트로 부터 데이터를 읽는 스레드

Thread readThread = new Thread(() -> {

try {

String clientMessage;

while( (clientMessage = socketReader.readLine()) != null ) {

System.out.println("서버측 콘솔 : " + clientMessage);

}

} catch (IOException e) {

e.printStackTrace();

}

});

 

// 클라이언트에게 데이터를 보내는 스레드 생성

Thread writeThread = new Thread(() -> {

try {

String serverMessage;

while( ( serverMessage = keyboardReader.readLine() ) != null) {

// 1. 먼저 키보드를 통해서 데이터를 읽고

// 2. 출력 스트림을 활용해서 데이터를 보내야 한다.

socketWriter.println(serverMessage);

}

 

} catch (Exception e2) {

 

}

});

 

// 스레드 동작 -> start() 호출

readThread.start();

writeThread.start();

 

// Thread join() 메서드는 하나의 스레드가 종료될때 까지 기다리도록 하는

// 기능을 제공 한다.

readThread.join();

writeThread.join();

 

System.out.println("--- 서버 프로그램 종료 --- ");

} catch (Exception e) {

e.printStackTrace();

} finally {

try {

socket.close();

serverSocket.close();

} catch (IOException e) {

e.printStackTrace();

}

}

} // end of main

 

} // end of class

 

Thread의 join() 메서드 - 역할과 기능

  1. 스레드 동기화: join() 메서드를 사용하여 여러 스레드가 순서대로 종료되도록 할 수 있습니다. 메인 스레드는 join() 메서드를 호출한 스레드가 작업을 마칠 때까지 기다립니다.
  2. 프로그램 흐름 제어: join() 메서드를 통해 스레드가 완료되기 전까지 메인 스레드가 종료되지 않도록 보장할 수 있습니다. 이는 프로그램이 모든 작업을 완료하기 전에 종료되는 것을 방지합니다.
  3. 정확한 종료 시점: join() 메서드를 사용하면 특정 스레드가 완료되기 전까지 다른 작업을 진행하지 않도록 제어할 수 있습니다. 이를 통해 정확한 종료 시점을 확인할 수 있습니다.

 

 

클라이언트 측 코드

  • Socket 을 생성하여 서버에 연결합니다.
  • BufferedReader를 사용하여 서버로부터 메시지를 읽고, PrintWriter를 사용하여 서버에게 메시지를 보냅니다.
  • 키보드 입력을 받기 위해 BufferedReader를 사용합니다.
  • 서버로부터 데이터를 읽는 스레드와 키보드 입력을 서버로 보내는 스레드를 각각 실행합니다.

package ch04;

 

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStreamReader;

import java.io.PrintWriter;

import java.net.Socket;

 

public class MultiThreadClient {

 

public static void main(String[] args) {

 

System.out.println("### 클라이언 실행 ### ");

 

try {

 

Socket socket = new Socket("localhost", 5001);

System.out.println("*** connected to the Server ***");

 

PrintWriter socketWriter = new PrintWriter(socket.getOutputStream(), true);

BufferedReader socketReader =

new BufferedReader(new InputStreamReader(socket.getInputStream()));

BufferedReader keyboardReader =

new BufferedReader(new InputStreamReader(System.in));

 

// 서버로 부터 데이터를 읽는 스레드

Thread readThread = new Thread(() -> {

// while <----

try {

String serverMessage;

while( (serverMessage = socketReader.readLine()) != null ) {

System.out.println("서버에서 온 MSG : " + serverMessage);

}

} catch (IOException e) {

e.printStackTrace();

}

});

 

// 서버에게 데이터를 보내는 스레드

Thread writeThread = new Thread(() -> {

try {

 

String clientMessage;

while( (clientMessage = keyboardReader.readLine()) != null ) {

// 1. 키보드에서 데이터를 응용프로그램 안으로 입력 받아서

// 2. 서버측 소켓과 연결 되어있는 출력 스트림 통해 데이터를 보낸다.

socketWriter.println(clientMessage);

}

} catch (Exception e2) {

e2.printStackTrace();

}

});

 

readThread.start();

writeThread.start();

 

readThread.join();

writeThread.join();

 

System.out.println(" 클라이언트 측 프로그램 종료 ");

 

} catch (Exception e) {

 

}

} // end of main

} // end of class

 

서버측 코드 리팩토링 1단계 - 함수로 분리해보기

package ch05;

 

import java.io.BufferedReader;

import java.io.InputStreamReader;

import java.io.PrintWriter;

import java.net.ServerSocket;

import java.net.Socket;

 

public class MultiThreadServer {

 

// 메인 함수

public static void main(String[] args) {

 

System.out.println("===== 서버 실행 ===== ");

 

// 서버측 소켓을 만들기 위한 준비물

// 서버 소켓 , 포트 번호

 

try (ServerSocket serverSocket = new ServerSocket(5000)) {

// 클라이언트 대기 ---> 연결 요청 -- 소켓 객체를 생성(클라이언트와 연결된 상태)

Socket socket = serverSocket.accept();

System.out.println("------ client connected ------ ");

 

// 클라이언트와 통신을 위한 스트림을 설정 (대상 소켓을 얻었다)

BufferedReader readerStream =

new BufferedReader(new InputStreamReader(socket.getInputStream()));

 

PrintWriter writerStream =

new PrintWriter(socket.getOutputStream(), true);

 

// 키보드 스트림 준비

BufferedReader keyboardReader =

new BufferedReader(new InputStreamReader(System.in));

 

// 스레드를 시작합니다.

startReadThread(readerStream);

startWriteThread(writerStream, keyboardReader);

 

System.out.println("main 스레드 작업 완료....");

 

} catch (Exception e) {

e.printStackTrace();

}

 

} // end of main

 

// 클라이언트로 부터 데이터를 읽는 스레드 분리

private static void startReadThread(BufferedReader bufferedReader) {

Thread readThread = new Thread(() -> {

try {

String clientMessage;

while ((clientMessage = bufferedReader.readLine()) != null) {

// 서버측 콘솔에 클라이언트가 보낸 문자 데이터 출력

System.out.println("클라이언트에서 온 MSG : " + clientMessage);

}

} catch (Exception e) {

e.printStackTrace();

}

});

readThread.start(); // 스레드 실행 -> run() 메서드 진행

waitForThreadToEnd(readThread); // <--- main 스레드 멈춰 !!! ( 다음 코드 동작 안함)

}

 

// 서버측에서 --> 클라이언트로 데이터를 보내는 기능

private static void startWriteThread(PrintWriter printWriter, BufferedReader keyboardReader) {

 

Thread writeThread = new Thread(() -> {

try {

String serverMessage;

while ((serverMessage = keyboardReader.readLine()) != null) {

printWriter.println(serverMessage);

printWriter.flush();

}

} catch (Exception e) {

e.printStackTrace();

}

});

 

writeThread.start();

waitForThreadToEnd(writeThread); // <--- main 스레드 멈춰 !!! ( 다음 코드 동작 안함)

}

 

// 워커 스레드가 종료될때까지 기다리는 메서드

private static void waitForThreadToEnd(Thread thread) {

try {

thread.join();

} catch (Exception e) {

e.printStackTrace();

}

}

 

} // end of class

 

waitForThreadToEnd(writeThread); ← 제거 대상 or 리팩토링 대상

 

 

서버측 코드 리팩토링 2단계 - 상속 활용

package ch05;

 

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStreamReader;

import java.io.PrintWriter;

import java.net.ServerSocket;

import java.net.Socket;

 

// 상속에 활용

public abstract class AbstractServer {

 

private ServerSocket serverSocket;

private Socket socket;

private BufferedReader readerStream;

private PrintWriter writerStream;

private BufferedReader keyboardReader;

 

// set 메서드

// 메서드 의존 주입(멤버 변수에 참조 변수 할당)

protected void setServerSocket(ServerSocket serverSocket) {

this.serverSocket = serverSocket;

}

// 메서드 의존 주입(멤버 변수에 참조 변수 할당)

protected void setSocket(Socket socket) {

this.socket = socket;

}

 

// get 메서드

protected ServerSocket getServerSocket() {

return this.serverSocket;

}

 

// 실행에 흐름이 필요하다.(순서가 중요)

public final void run() {

// 1. 서버 셋팅 - 포트 번호 할당

try {

setupServer();

connection();

setupStream();

startService(); // 내부적으로 while 동작

} catch (IOException e) {

e.printStackTrace();

} finally {

System.out.println("cleanup() 호출 확인");

cleanup();

}

}

 

// 1. 포트 번호 할당(구현 클래스에서 직접 설계)

protected abstract void setupServer() throws IOException;

 

// 2. 클라이언트 연결 대기 실행 (구현 클래스)

protected abstract void connection() throws IOException;

 

// 3. 스트림 초기화 (연결된 소켓에서 스트림을 뽑아야 함) - 여기서 함(private)

private void setupStream() throws IOException {

readerStream = new BufferedReader(new InputStreamReader(socket.getInputStream()));

writerStream = new PrintWriter(socket.getOutputStream(), true);

keyboardReader = new BufferedReader(new InputStreamReader(System.in));

}

// 4. 서비스 시작

private void startService() {

Thread readThread = createReadThread();

Thread writeThred = createWriteThread();

 

readThread.start();

writeThred.start();

 

try {

readThread.join();

writeThred.join();

// main 스레드 잠깐 기다려

} catch (InterruptedException e) {

e.printStackTrace();

}

}

 

// 캡슐화

private Thread createReadThread() {

return new Thread(() -> {

try {

String msg;

// scnnner.nextLine(); <--- 무한 대기 (사용자가 콘솔에 값 입력 까지 대기)

// 코드 ....

while( (msg = readerStream.readLine()) != null ) {

// 서버측 콘솔에 출력

System.out.println("client 측 msg : " + msg);

}

} catch (Exception e) {

e.printStackTrace();

}

 

});

}

 

private Thread createWriteThread() {

return new Thread(() -> {

try {

String msg;

// 서버측 키보드에서 데이터를 한줄라인으로 읽음

while( (msg = keyboardReader.readLine()) != null ) {

// 클라이언트와 연결된 소켓에다가 데이터를 보냄

writerStream.println(msg);

writerStream.flush();

}

} catch (Exception e) {

e.printStackTrace();

}

});

}

 

// 캡슐화 - 소켓 자원 종료

private void cleanup() {

try {

if(socket != null) {

socket.close();

}

 

if(serverSocket != null) {

serverSocket.close();

}

 

} catch (Exception e) {

e.printStackTrace();

}

}

}

 

구현 클래스 - AbstractServer 상속 활용

package ch05;

 

import java.io.IOException;

import java.net.ServerSocket;

 

public class MyThreadServer extends AbstractServer {

 

@Override

protected void setupServer() throws IOException {

// 추상 클래스 --> 부모 -- 자식 (부모 기능에 확장 또는 사용)

// 서버측 소켓 통신 -- 준비물 : 서버 소켓

super.setServerSocket(new ServerSocket(5000));

System.out.println(">>> Server started on port 5000 <<<");

}

 

@Override

protected void connection() throws IOException {

// 서버 소켓.accept() 호출이다. !!!

super.setSocket(super.getServerSocket().accept());

}

 

public static void main(String[] args) {

MyThreadServer myThreadServer = new MyThreadServer();

myThreadServer.run();

}

 

}

 

복잡한 애플리케이션에서는 추상 클래스와 구현 클래스를 분리하는 것이 유용할 수 있지만, 간단한 경우에는 단일 클래스 설계가 더 적합할 수 있습니다. 상황에 따라 적절한 설계를 선택하는 것이 중요합니다.

 

 

도전 과제 - 클라이언트 측 코드 리팩토링

 

1단계, 2단계로 진행해 보기

 

풀이 - 클라이언트 측 코드 리팩토링 1 단계

package ch05;

 

import java.io.BufferedReader;

import java.io.InputStreamReader;

import java.io.PrintWriter;

import java.net.Socket;

 

// 1단계 - 함수로 분리 해서 리팩토링 진행

public class MultiThreadClient {

 

// 메인 함수

public static void main(String[] args) {

 

System.out.println("### 클라이언트 실행 ### ");

try(Socket socket = new Socket("localhost", 5000)) {

System.out.println("connected to the server !!");

 

// 서버와 통신을 위한 스트림 초기화

BufferedReader bufferedReader =

new BufferedReader(new InputStreamReader(socket.getInputStream()));

PrintWriter printWriter = new PrintWriter(socket.getOutputStream());

BufferedReader keyboardReader =

new BufferedReader(new InputStreamReader(System.in));

 

startReadThread(bufferedReader);

startWriteThread(printWriter, keyboardReader);

// 메인 스레드 기다려 어디에 있지??? 가독성이 떨어짐

// startWriteThread() <---- 내부에 있음

 

} catch (Exception e) {

e.printStackTrace();

}

 

} // end of main

 

// 1. 클라이언트로부터 데이터를 읽는 스레드 시작 메서드 생성

private static void startReadThread(BufferedReader reader) {

Thread readThread = new Thread(() -> {

try {

String msg;

while( (msg = reader.readLine()) != null ) {

System.out.println("client에서 온 msg : " + msg);

}

 

} catch (Exception e) {

e.printStackTrace();

}

});

readThread.start();

}

 

//2. 키보드에서 입력을 받아 클라언트 측으로 데이터를 전송하는 스레드

private static void startWriteThread(

PrintWriter writer, BufferedReader keyboardReader) {

 

Thread writeThread = new Thread(() -> {

try {

String msg;

while( (msg = keyboardReader.readLine()) != null ) {

// 전송

writer.println(msg);

writer.flush();

}

} catch (Exception e) {

e.printStackTrace();

}

});

writeThread.start();

 

try {

// 메인 스레드야 기다려!!

writeThread.join();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

 

} // end of class

 

풀이 - 클라이언트 측 코드 리팩토링 2단계 (상속 활용)

public abstract class AbstractClient {

 

private Socket socket;

private PrintWriter writerStream;

private BufferedReader readerStream;

private BufferedReader keyboardReader;

 

public final void run() {

try {

connectToServer();

setupStreams();

startCommunication();

} catch (IOException | InterruptedException e) {

e.printStackTrace();

} finally {

cleanup();

}

}

 

protected abstract void connectToServer() throws IOException;

 

private void setupStreams() throws IOException {

writerStream = new PrintWriter(socket.getOutputStream(), true);

readerStream = new BufferedReader(new InputStreamReader(socket.getInputStream()));

keyboardReader = new BufferedReader(new InputStreamReader(System.in));

}

 

private void startCommunication() throws InterruptedException {

Thread readThread = createReadThread();

Thread writeThread = createWriteThread();

 

readThread.start();

writeThread.start();

 

readThread.join();

writeThread.join();

}

 

private Thread createReadThread() {

return new Thread(() -> {

try {

String serverMessage;

while ((serverMessage = readerStream.readLine()) != null) {

System.out.println("서버에서 온 msg: " + serverMessage);

}

} catch (IOException e) {

e.printStackTrace();

}

});

}

 

private Thread createWriteThread() {

return new Thread(() -> {

try {

String clientMessage;

while ((clientMessage = keyboardReader.readLine()) != null) {

writerStream.println(clientMessage);

}

} catch (IOException e) {

e.printStackTrace();

}

});

}

 

protected void setSocket(Socket socket) {

this.socket = socket;

}

 

private void cleanup() {

try {

if (socket != null) {

socket.close();

}

} catch (IOException e) {

e.printStackTrace();

}

}

}

 

 

import java.io.IOException;

import java.net.Socket;

 

public class MultiThreadedClient extends AbstractClient {

 

@Override

protected void connectToServer() throws IOException {

setSocket(new Socket("localhost", 5000));

System.out.println("*** Connected to the server ***");

}

 

// 메인 함수

public static void main(String[] args) {

System.out.println("#### 클라이언트 실행 ####");

MultiThreadedClient client = new MultiThreadedClient();

client.run();

}

}

728x90
반응형
SMALL