Java 정리

채팅 프로그램

으엉어엉 2024. 12. 25. 16:53
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

'Java 정리' 카테고리의 다른 글

리플렉션  (1) 2024.12.27
Logger  (0) 2024.11.22
코테 기술 정리  (0) 2024.11.13