Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions .idea/uiDesigner.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

119 changes: 119 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Java 소켓 기반 멀티 클라이언트 채팅 프로그램

## 📖 프로젝트 소개
이 프로젝트는 **Java 표준 라이브러리(`java.net`, `java.io`)만**을 사용하여 개발된 경량 텍스트 기반 채팅 애플리케이션입니다.
동아리나 수업 환경에서 **별도의 외부 라이브러리 설치 없이**, 같은 네트워크 내 여러 사용자가 동시에 대화할 수 있는 간단한 채팅 도구를 목표로 합니다.
서버-클라이언트 모델을 기반으로 하며, **스레드 풀**을 이용해 다수의 클라이언트를 효율적으로 처리합니다.

---

## 주요 기능
- **다중 클라이언트 접속**: `ExecutorService`(스레드 풀)를 활용하여 안정적인 동시 접속 처리
- **닉네임 시스템**
- 서버 접속 시 고유한 닉네임을 설정 (`NICK <이름>`)
- 중복 또는 공백이 포함된 닉네임 거절
- **실시간 채팅**: 메시지를 모든 클라이언트에게 브로드캐스트
- **입장/퇴장 알림**: 사용자 입장/퇴장 시 전체 알림
- **기본 명령어**
- `/who` : 현재 접속자 목록 출력
- `/quit` : 정상 종료 및 연결 해제
- **안정적인 종료 처리**: 서버가 `Ctrl+C`로 종료될 경우, 클라이언트에게 알림 후 안전 종료

---

## 사용 기술
- **언어**: Java (JDK 11 이상 권장)
- **핵심 라이브러리**
- `java.net.Socket`, `java.net.ServerSocket` : TCP/IP 기반 소켓 통신
- `java.io.*` : 데이터 입출력 스트림
- `java.util.concurrent.*` : 멀티스레딩 및 동시성 제어 (`ExecutorService`, `ConcurrentHashMap`)
- **인코딩**: UTF-8 (한글 입출력 지원)

---

## 빌드 및 실행 방법

### 1. 사전 준비
- JDK가 설치되어 있어야 합니다.

### 2. 컴파일
`src` 폴더에 `ChatServer.java`, `ChatClient.java` 파일이 있다고 가정합니다.
```bash
javac -d out src/ChatServer.java src/ChatClient.java
```

→ 컴파일된 .class 파일은 out 디렉토리에 생성됩니다.

### 3. 서버 실행
```bash
# 기본 포트(5000)
java -cp out ChatServer

# 특정 포트(예: 5001)
java -cp out ChatServer 5001
```

```bash
실행 시:

[서버] 채팅 서버를 포트 5001에서 시작합니다.
```

### 4. 클라이언트 실행
```bash
# 형식
java -cp out ChatClient <서버 IP> <포트> <닉네임>

# 예시 1
java -cp out ChatClient 127.0.0.1 5001 Yumi

# 예시 2
java -cp out ChatClient 127.0.0.1 5001 ShinSaegae
```

테스트 시나리오 (실행 예시)
```bash
1. 서버 실행
[서버] 채팅 서버를 포트 5001에서 시작합니다.

2. 클라이언트 1 (Yumi) 접속
채팅 서버에 연결되었습니다 (127.0.0.1:5001)
OK 채팅 서버에 오신 것을 환영합니다. 'NICK <닉네임>' 형식으로 닉네임을 설정해주세요.
OK 닉네임이 'Yumi'으로 설정되었습니다.
Yumi님이 채팅에 참여했습니다.


서버 로그:

[서버] /127.0.0.1:xxxxx 님이 'Yumi' 닉네임으로 접속했습니다.

3. 클라이언트 2 (Bob) 접속
OK 닉네임이 'Bob'으로 설정되었습니다.
Yumi님이 채팅에 참여했습니다.
Bob님이 채팅에 참여했습니다.


(Yumi 클라이언트 창에도 "Bob님이 채팅에 참여했습니다." 출력)

4. 대화 및 명령어

(Yumi) 안녕하세요!
→ 모든 클라이언트: [Yumi] 안녕하세요!

(Bob) /who
→ 현재 접속자: Yumi, Bob

5. 클라이언트 종료

(Yumi) /quit
→ 모든 클라이언트: Yumi님이 채팅을 떠났습니다.

6. 서버 강제 종료 (Ctrl+C)
[서버] 종료 절차를 시작합니다...
```
```bash
남은 클라이언트:

SYSTEM: 서버가 곧 종료됩니다.
[클라이언트] 서버와의 연결이 끊어졌습니다. (Disconnected)
```
78 changes: 78 additions & 0 deletions src/ChatClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;

public class ChatClient {
public static void main(String[] args) throws IOException{
if (args.length != 3) {
System.out.println("사용법: java -cp out ChatClient <서버_IP> <포트> <닉네임>");
return;
}

String host = args[0];
int port = Integer.parseInt(args[1]);
String nickname = args[2];

try (Socket socket = new Socket(host, port)) {
System.out.println("채팅 서버에 연결되었습니다 (" + host + ":" + port + ")");

PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true);

Thread readerThread = new Thread(new ServerMessageReader(socket));
readerThread.start();

out.println("NICK " + nickname);

BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
String userInput;
while ((userInput = consoleReader.readLine()) != null) {
out.println(userInput);
if ("/quit".equalsIgnoreCase(userInput.trim())) {
break;
}
}
readerThread.join();

} catch (UnknownHostException e) {
System.err.println("호스트 " + host + "를 찾을 수 없습니다.");
} catch (IOException e) {
System.err.println(host + "에 연결할 수 없습니다. 서버가 실행 중인지 확인해주세요.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("클라이언트가 중단되었습니다.");
} finally {
System.out.println("서버와의 연결이 종료되었습니다.");
}
}

private static class ServerMessageReader implements Runnable {
private final Socket socket;
private BufferedReader in;

public ServerMessageReader(Socket socket) {
this.socket = socket;
}

@Override
public void run() {
try {
in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
String serverMessage;
while ((serverMessage = in.readLine()) != null) {
System.out.println(serverMessage);
}
} catch (IOException e) {
System.out.println("서버와의 연결이 끊어졌습니다.");
} finally {
try {
if (!socket.isClosed()) {
socket.close();
}
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
}
}
Loading