
에코서버 : 클라이언트가 보낸 메시지를 그대로 다시 돌려주는 서버
우리가 흔히 "실행부"라고 부르는 main() 내부 코드는
사실상 "메인 스레드(Main Thread)", 즉 프로그램을 운영하는 핵심 스레드임
안 지키면 화면 멈춤, 버튼 안 눌림, 이벤트 무반응
즉, 모든 게 멈춘다.
while(true) { ... } // GUI 완전 멈춤
server.accept(); // 클라이언트 접속 기다림
input.read(); // 데이터 들어올 때까지 대기
Thread thread; // 메인스레드가 대기상태에 빠지지 않도록 스레드로 accept 구현
/*----------------위에는 멤버변수 밑에는 생성자 내부--------------------*/
bt.addActionListener(e -> {
thread = new Thread() {
@Override
public void run() {
startServer(); // 내부에 server.accept() 실행됨
}
};
thread.start(); // 새 스레드에서 runServer() 실행 시작
});
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 인터페이스run() 메서드 하나만 갖고 있는 함수형 인터페이스Thread 객체에 작업(Task)을 넘기기 위해 사용implements RunnableThread 객체 생성 시, 생성자 매개변수로 this(Runnable 구현 객체)를 전달thread.start()를 호출하여 run() 메서드를 실행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가 한다!

Server창에서 서버가동하면 "서버 객체 생성 및 접속 청취 중..."
Client창에서 접속 누르면 "접속자 발견!"
Client창의 TextField에 Text 입력 후 엔터 누르면
Sever+Client 둘 다 Text 출력됨
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() 해줘야 모아놓은 문자열 실제로 전송함