에코 서버 프로그래밍

heeezni·2025년 6월 11일

Java GUI 프로젝트

목록 보기
13/20
post-thumbnail

에코서버 : 클라이언트가 보낸 메시지를 그대로 다시 돌려주는 서버

메인 스레드와 프로그램 운영의 원칙

우리가 흔히 "실행부"라고 부르는 main() 내부 코드는
사실상 "메인 스레드(Main Thread)", 즉 프로그램을 운영하는 핵심 스레드임

❌ 메인 스레드에서 절대 하면 안 되는 두 가지

안 지키면 화면 멈춤, 버튼 안 눌림, 이벤트 무반응
즉, 모든 게 멈춘다.

1. 무한 루프

while(true) { ... }  // GUI 완전 멈춤

2. 블로킹 대기 상태

server.accept(); // 클라이언트 접속 기다림
input.read(); // 데이터 들어올 때까지 대기

결론: 블로킹 작업은 별도의 스레드에서 실행한다!

람다로 Thread 구현

Thread thread; // 메인스레드가 대기상태에 빠지지 않도록 스레드로 accept 구현
/*----------------위에는 멤버변수 밑에는 생성자 내부--------------------*/
bt.addActionListener(e -> {
	thread = new Thread() {
		@Override
		public void run() {
			startServer(); // 내부에 server.accept() 실행됨
		}
	};
	thread.start(); // 새 스레드에서 runServer() 실행 시작
});

Runnable로 Thread 구현

public class EchoServer extends JFrame implements Runnable {
	Thread thread;

	// 가동버튼에 리스너 연결
	bt.addActionListener(e -> {
		thread = new Thread(EchoServer.this); // 1️⃣ Runnable 구현 객체 전달
		thread.start();                       // 2️⃣ 별도 스레드에서 run() 실행
	});

	@Override
	public void run() {
		startServer(); // 3️⃣ 블로킹 작업 포함된 메서드 호출
	}

	public void startServer() {
		server = new ServerSocket(Integer.parseInt(t_port.getText())); // 블로킹1
		Socket socket = server.accept(); // 블로킹2: 클라이언트 접속 대기
	}
}

Runnable 사용하는 방법

Runnable 인터페이스

  • run() 메서드 하나만 갖고 있는 함수형 인터페이스
  • Thread 객체에 작업(Task)을 넘기기 위해 사용

✅ 사용 순서

  1. 해당 클래스에 implements Runnable
  2. Thread 객체 생성 시, 생성자 매개변수로 this(Runnable 구현 객체)를 전달
  3. thread.start()를 호출하여 run() 메서드를 실행
    → 이 안에서 accept()처럼 블로킹이 걸리는 작업을 처리
public class EchoServer implements Runnable {

	Thread thread;

	@Override
    public void run() {
        // 블로킹 대기: 클라이언트 접속 기다림
        startServer(); // 내부에 server.accept() 실행됨
    }

    public static void main(String[] args) {
        // 1️⃣ Runnable 구현 객체
        new EchoServer(); 
        // 2️⃣ Thread에 Runnable 객체 넘김
        Thread thread = new Thread(Echoserver.this); 
        // 3️⃣ run() 실행 (새로운 스레드)
        thread.start(); 				  
    }
}

💬 Runnable은 Thread에 실행할 작업만 넘기고,
진짜 스레드를 돌리는 건 Thread가 한다!


Echo Programming 예제

Server창에서 서버가동하면 "서버 객체 생성 및 접속 청취 중..."
Client창에서 접속 누르면 "접속자 발견!"

Client창의 TextField에 Text 입력 후 엔터 누르면
Sever+Client 둘 다 Text 출력됨

TCP

Socket 클래스 → TCP 전용 소켓

Transmission Control Protocol
데이터를 정확하고 순서대로, 빠짐없이 전송하는 통신 규칙 (프로토콜)

특징설명
연결 지향적연결을 먼저 맺고(3-way handshake) 통신 시작
신뢰성 보장데이터가 빠지거나 깨지면 다시 전송 요청함
순서 보장보낸 순서 그대로 도착하게 정렬해줌
오류 검출 및 복구전송 중 오류가 있으면 감지하고 재전송

서버

public class EchoServer extends JFrame implements Runnable {
    ...
    public EchoServer() {
        ...
        bt.addActionListener(e -> {
            // ✅ 서버 가동 버튼 누르면 별도 스레드에서 서버 실행
            thread = new Thread(EchoServer.this);
            thread.start();
        });
        ...
    }
    
	// ⭐ Runnable의 run 메서드 → 서버 가동하기
    @Override
    public void run() {
        startServer(); // ✅ 블로킹 작업은 별도 스레드에서
    }

	// 서버 가동 + 통신 루프
    public void startServer() {
        try {
            server = new ServerSocket(Integer.parseInt(t_port.getText()));
            area.append("서버 객체 생성 및 접속 청취 중...\n");

            Socket socket = server.accept(); // ✅ 클라이언트 접속 대기
            area.append(socket.getInetAddress().getHostAddress() + " 님 접속!\n");

            // ✅ 얻은 socket(종이컵)으로부터 스트림(실) 뽑기 
            buffr = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            buffw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));

            while (true) {
                String msg = buffr.readLine();  // 클라이언트로부터 메시지 수신
                area.append(msg + "\n");        // 서버 GUI에 출력
                buffw.write(msg + "\n");        // 클라이언트에게 다시 전송
                buffw.flush(); // ⭐ 버퍼 비우기! (버퍼 처리된 출력스트림 계열은 필수)
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

클라이언트

public class EchoClient extends JFrame implements Runnable {
    ...
	public EchoClient() {
        ...
		bt.addActionListener(e -> {
			thread = new Thread(EchoClient.this); // ✅ Runnable 구현 객체를 스레드에 전달
			thread.start();                       // ✅ run() 실행 → 서버 접속 처리
		});

		t_input.addKeyListener(new KeyAdapter() {
			@Override
			public void keyReleased(KeyEvent e) {
				if (e.getKeyCode() == KeyEvent.VK_ENTER) { // 엔터 키 입력 시
					send();   // 서버에 메시지 전송
					receive(); // 서버로부터 메시지 수신
				}
			}
		});
        ...
	}

	// 서버 접속 및 스트림 구성
	public void connect() {
		try {
			socket = new Socket((String) (cb_ip.getSelectedItem()), Integer.parseInt(t_port.getText()));
			area.append("접속 성공! \n");

			// 스트림 생성
			buffr = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			buffw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));

		} catch (NumberFormatException e) {
			e.printStackTrace();
		} catch (UnknownHostException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	// 서버로부터 메시지를 수신
	public void receive() {
		String msg = null;
		try {
			msg = buffr.readLine();         // 한 줄 수신
			area.append(msg + "\n");        // 메시지 출력
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	// 서버로 메시지를 전송
	public void send() {
		String msg = t_input.getText();     // 입력값 가져오기
		try {
			buffw.write(msg + "\n");        // \n으로 끝을 명확히 전달
			buffw.flush();                  // 버퍼 비우기 (전송)
		} catch (IOException e) {
			e.printStackTrace();
		}
		t_input.setText("");                // 입력창 초기화
	}

	// ⭐ Runnable의 run 메서드 → 서버 접속 처리
	@Override
	public void run() {
		connect(); // 서버 연결 + 스트림 구성
	}

	public static void main(String[] args) {
		new EchoClient(); // 프로그램 실행
	}
}

알아둬야 할 코드 정리

🔴 포트 입력을 받아 서버 소켓 생성하기

String port = t_port.getText();
ServerSocket server = new ServerSocket(Integer.parseInt(port));

➡ 문자열로 입력받은 포트 번호를 int로 변환하여, 서버 소켓을 생성한다.

🔴 buffw.write(msg + "\n") :
버퍼기반의 스트림은 데이터를 줄 단위로 처리하므로 문자열의 끝(\n)을 알려주지 않으면 무한대기에 빠짐

🔴 buffw.flush() :
버퍼처리된 출력 스트림은 flush() 해줘야 모아놓은 문자열 실제로 전송함

profile
아이들의 가능성을 믿었던 마음 그대로, 이제는 나의 가능성을 믿고 나아가는 중입니다.🌱

0개의 댓글