서버 측 코드
- 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 할당 완료");
// 클라이언트로 데이터를 받을 입력 스트림 필요
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 clientMesaage;
while ((clientMesaage = socketReader.readLine()) != null) {
System.out.println("서버측 콘솔 : " + clientMesaage);
}
} 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) {
e2.printStackTrace();
}
});
// 스레드 동작 -> start() 호출
readThread.start();
writeThread.start();
// join() 메서드는 하나의 스레드가 종료될 때 까지 기다리도록 하는 기능
readThread.join();
writeThread.join();
System.out.println("--- 서버 프로그램 종료 ---");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
socket.close();
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Thread의 join() 메서드
- 스레드 동기화: join() 메서드를 사용하여 여러 스레드가 순서대로 종료되도록 할 수 있습니다. 메인 스레드는 join() 메서드를 호출한 스레드가 작업을 마칠 때까지 기다립니다.
- 프로그램 흐름 제어: join() 메서드를 통해 스레드가 완료되기 전까지 메인 스레드가 종료되지 않도록 보장할 수 있습니다. 이는 프로그램이 모든 작업을 완료하기 전에 종료되는 것을 방지합니다.
- 정확한 종료 시점: 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(() -> {
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) {
e.printStackTrace();
}
}
}
서버 측 코드 리팩토링 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 keyboardStream = new BufferedReader(new InputStreamReader(System.in));
// 스레드를 시작합니다.
startReadThread(readerStream);
startWriteThread(writerStream, keyboardStream);
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() 메서드 진행
// 메인 스레드 대기 처리 -> join() -> 반복될 것 같은 기능
waitForThreadToEnd(readThread);
}
// 서버측에서 클라이언트로 데이터를 보내는 기능
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);
}
// 워커 스레드가 종료될 때까지 기다리는 메서드
private static void waitForThreadToEnd(Thread thread) {
try {
thread.join();
} catch (Exception e) {
e.printStackTrace();
}
}
}
서버 측 코드 리팩토링 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 serverSocket;
}
// 실행의 흐름이 필요하다(순서가 중요)
public final void run() {
try {
setupServer();
connection();
setupStream();
startService(); // 내부적으로 while 동작
} catch (IOException e) {
e.printStackTrace();
} finally {
cleanup();
}
}
// 1. 포트 번호 할당(구현 클래스에서 직접 설계)
protected abstract void setupServer() throws IOException;
// 2. 클라이언트 연결 대기 실행 (구현 클래스)
protected abstract void connection() throws IOException;
// 3. 스트림 초기화 (연결된 소켓에서 스트림을 뽑아야 함) - 여기서 함
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 writeThread = createWriteThread();
readThread.start();
writeThread.start();
try {
readThread.join();
writeThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 캡슐화
private Thread createReadThread() {
return new Thread(() -> {
try {
String msg;
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();
}
}
}
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();
}
}
구현 클래스
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();
}
}
클라이언트 측 코드 리팩토링 - 상속 활용
package ch05;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public abstract class AbstractClient {
private Socket socket;
private BufferedReader readerStream;
private PrintWriter writerStream;
private BufferedReader keyboardReader;
public final void run() {
try {
setupClient();
setupStream();
startService();
} catch (IOException e) {
e.printStackTrace();
} finally {
cleanup();
}
}
// set 메서드
protected void setSocket(Socket socket) {
this.socket = socket;
}
// 1. 소켓IP주소, 포트 번호
protected abstract void setupClient() throws IOException;
// 2. 스트림 초기화
private void setupStream() throws IOException {
writerStream = new PrintWriter(socket.getOutputStream(), true);
readerStream = new BufferedReader(new InputStreamReader(socket.getInputStream()));
keyboardReader = new BufferedReader(new InputStreamReader(System.in));
}
// 3. 서비스 시작
private void startService() {
Thread readThread = createReadThread();
Thread writeThread = createWriteThread();
readThread.start();
writeThread.start();
try {
readThread.join();
writeThread.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// 캡슐화
private Thread createReadThread() {
return new Thread(() -> {
try {
String msg;
while ((msg = readerStream.readLine()) != null) {
// 클라이언트 측 콘솔에 출력
System.out.println("server 측 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();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
구현 클래스
package ch05;
import java.io.IOException;
import java.net.Socket;
public class MyClient extends AbstractClient {
@Override
protected void setupClient() throws IOException {
super.setSocket(new Socket("localhost", 5000));
System.out.println("----- connected on port 5000 -----");
}
public static void main(String[] args) {
MyClient myClient = new MyClient();
myClient.run();
}
}
※ 복잡한 애플리케이션에서는 추상 클래스와 구현 클래스를 분리하는 것이 유용할 수 있지만, 간단한 경우에는 단일 클래스 설계가 더 적합할 수 있습니다. 상황에 따라 적절한 설계를 선택하는 것이 중요합니다.
'Java > Java' 카테고리의 다른 글
네트워크 프로토콜 (0) | 2024.05.24 |
---|---|
1:N 소켓 양방향 통신 (0) | 2024.05.24 |
1:1 단방향 통신 (0) | 2024.05.22 |
소켓 ( Socket ) (0) | 2024.05.22 |
고수준 스트림 ( Data/Object Stream ) (0) | 2024.05.22 |