728x90
입장 : /join|{name} : 처음 채팅 서버에 접속할 때 사용자의 이름을 입력해야 한다.
메시지 /message|{내용} : 모든 사용자에게 메시지를 전달한다.
이름 변경 /change|{name} : 사용자의 이름을 변경한다.
전체 사용자 /users : 채팅 서버에 접속한 전체 사용자 목록을 출력한다.
종료 /exit : 채팅 서버의 접속을 종료한다.
블록킹 되어 있는 것들은 분리를 해야한다.
면 서버에서 모든 세션을 관리해야 한다. 그렇게 해야 모든 세션에 메시지를 전달할 수 있다. 세션을 관리하는 세션 매니저를 사용한다.
Client 구현
public class ReadHandler implements Runnable {
private final Client client;
public boolean closed = false;
private final DataInputStream input;
public ReadHandler(Client client, DataInputStream input) {
this.client = client;
this.input = input;
}
@Override
public void run() {
try{
while(true){
String received = input.readUTF();
System.out.println(received);
}
} catch (IOException e) {
log(e);
} finally {
client.close();
}
}
public synchronized void close(){
if(closed) return;
closed = true;
log("readHandler 종료");
}
}
public class WriteHandler implements Runnable{
private static final String DELIMITER = "|";
private final Client client;
private final DataOutputStream output;
private boolean closed = false;
public WriteHandler(Client client, DataOutputStream output) {
this.client = client;
this.output = output;
}
@Override
public void run() {
Scanner sc = new Scanner(System.in);
try {
String username = inputUsername(sc);
output.writeUTF("/join" + DELIMITER + username);
while (true) {
String toSend = sc.nextLine();
if (toSend.isEmpty()) {
continue;
}
//채팅방 나가기
if (toSend.equals("/exit")) {
output.writeUTF(toSend);
break;
}
// "/"로 시작하면 명령어, 나머지는 일반 메시지
if (toSend.startsWith("/")) {
output.writeUTF(toSend);
} else {
//정상적인 메세지로 보내는 것
output.writeUTF("/message" + DELIMITER + toSend);
}
}
}catch (NoSuchElementException | IOException e) {
log(e);
}finally {
client.close();
}
}
private static String inputUsername(Scanner sc) {
System.out.println("이름을 입력하세요.");
String username;
do {
username = sc.nextLine();
} while (username.isEmpty());
return username;
}
public synchronized void close() {
if (closed) {
return;
}
try {
System.in.close(); // Scanner 입력 중지 (사용자의 입력을 닫음)
}catch (IOException e) {
log(e);
}
closed = true;
log("writeHandler 종료");
}
}
public class Client {
private final String host;
private final int port;
private Socket socket;
private DataInputStream input;
private DataOutputStream output;
private ReadHandler readHandler;
private WriteHandler writeHandler;
private boolean closed = false;
public Client(String host, int port) {
this.host = host;
this.port = port;
}
public void start() throws IOException {
log("클라이언트 시작");
socket = new Socket(host, port);
input = new DataInputStream(socket.getInputStream());
output = new DataOutputStream(socket.getOutputStream());
readHandler = new ReadHandler(input, this);
writeHandler = new WriteHandler(output, this);
Thread readThread = new Thread(readHandler, "readHandler");
Thread writeThread = new Thread(writeHandler, "writeHandler");
readThread.start();
writeThread.start();
}
public synchronized void close() {
if (closed) {
return;
}
writeHandler.close();
readHandler.close();
closeAll(socket, input, output);
closed = true;
log("연결 종료: " + socket);
}
}
Server 구현
public class Server {
private final int port;
private final CommandManager commandManager;
private final SessionManager sessionManager;
private ServerSocket serverSocket;
public Server(int port, CommandManager commandManager, SessionManager sessionManager) {
this.port = port;
this.commandManager = commandManager;
this.sessionManager = sessionManager;
}
public void start() throws IOException {
log("서버 시작: " + commandManager.getClass());
serverSocket = new ServerSocket(port);
log("서버 소켓 시작 - 리스닝 포트: " + port);
addShutdownHook();
running();
}
private void addShutdownHook() {
ShutdownHook target = new ShutdownHook(serverSocket, sessionManager);
Runtime.getRuntime().addShutdownHook(new Thread(target, "shutdown"));
}
static class ShutdownHook implements Runnable {
private final ServerSocket serverSocket;
private final SessionManager sessionManager;
public ShutdownHook(ServerSocket serverSocket, SessionManager sessionManager) {
this.serverSocket = serverSocket;
this.sessionManager = sessionManager;
}
//정상종료 될 때 호출됨
@Override
public void run() {
log("shutdownHook 실행");
try {
sessionManager.closeAll();
serverSocket.close();
Thread.sleep(1000); // 자원 정리 대기
} catch (Exception e) {
e.printStackTrace();
System.out.println("e = " + e);
}
}
}
private void running() {
try {
while (true) {
Socket socket = serverSocket.accept(); // 블로킹
log("소켓 연결: " + socket);
Session session = new Session(socket, commandManager, sessionManager);
Thread thread = new Thread(session);
thread.start();
}
} catch (IOException e) {
log("서버 소캣 종료: " + e);
}
}
}
public class Session implements Runnable{
private final Socket socket;
private final DataInputStream input;
private final DataOutputStream output;
private final CommandManager commandManager;
private final SessionManager sessionManager;
private boolean closed = false;
private String username;
public Session(Socket socket, CommandManager commandManager, SessionManager sessionManager) throws IOException {
this.socket = socket;
this.input = new DataInputStream(socket.getInputStream());
this.output = new DataOutputStream(socket.getOutputStream());
this.commandManager = commandManager;
this.sessionManager = sessionManager;
this.sessionManager.add(this);
}
@Override
public void run() {
try{
while(true){
// 클라이언트로부터 문자 받기
String received = input.readUTF();
log("client -> server: " + received);
commandManager.execute(received, this);
}
}catch(IOException e){
log(e);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
sessionManager.remove(this);
sessionManager.sendAll(username + "님이 퇴장했습니다.");
close();
}
}
public void send(String message) throws IOException {
log("server -> client: " + message);
output.writeUTF(message);
}
public synchronized void close() {
if (closed) {
return;
}
closeAll(socket, input, output);
closed = true;
log("연결 종료: " + socket);
}
//username getter setter
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
public class SessionManager {
private final List<Session> sessions = new ArrayList<>();
public synchronized void add(Session session) {
sessions.add(session);
}
public synchronized void remove(Session session) {
sessions.remove(session);
}
public synchronized void closeAll() {
for (Session session : sessions) {
session.close();
}
sessions.clear();
}
public synchronized void sendAll(String message) {
for (Session session : sessions) {
try {
session.send(message);
}
catch (IOException e) {
log(e);
}
}
}
//username 전체 조회
public synchronized List<String> getAllUsername() {
List<String> usernames = new ArrayList<>();
for (Session session : sessions) {
if (session.getUsername() != null) {
usernames.add(session.getUsername());
}
}
return usernames;
}
}
public class UsersCommand implements Command {
private final SessionManager sessionManager;
public UsersCommand(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
@Override
public void execute(String[] args, Session session) throws IOException {
List<String> usernames = sessionManager.getAllUsername();
StringBuilder sb = new StringBuilder();
sb.append("전체 접속자 : ").append(usernames.size()).append("\n");
for (String username : usernames) {
sb.append(" - ").append(username).append("\n");
}
session.send(sb.toString());
}
}
public class MessageCommand implements Command {
private final SessionManager sessionManager;
public MessageCommand(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
@Override
public void execute(String[] args, Session session) {
String message = args[1];
sessionManager.sendAll("[" + session.getUsername() + "] " + message);
}
}
public class JoinCommand implements Command {
private final SessionManager sessionManager;
public JoinCommand(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
@Override
public void execute(String[] args, Session session) throws IOException {
String username = args[1];
session.setUsername(username);
sessionManager.sendAll(username+ "님이 입장하였습니다.");
}
}
public class ExitCommand implements Command {
@Override
public void execute(String[] args, Session session) throws IOException {
throw new IOException("exit");
}
}
public class DefaultCommand implements Command {
@Override
public void execute(String[] args, Session session) throws IOException {
session.send("처리할 수 없는 명령어 입니다: " + Arrays.toString(args));
}
}
public interface Command {
void execute(String[] args, Session session) throws IOException;
}
public class ChangeCommand implements Command {
private final SessionManager sessionManager;
public ChangeCommand(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
@Override
public void execute(String[] args, Session session) {
String changeName = args[1];
sessionManager.sendAll(session.getUsername() + "님이 " + changeName + "로 이름을 변경했습니다.");
session.setUsername(changeName);
}
}
728x90