ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Java | Socket & Thread 채팅 프로그램 만들기
    JAVA/JAVA 2020. 2. 12. 17:10

    Socket & Thread 채팅 프로그램 만들기

    지금까지 학습했던 거의 모든 내용이 나오는 예제
    클라이언트끼리 실시간으로 채팅할 수 있는 프로그램 작성


    ① 클라이언트측

    • 사용자가 직접 사용하는 UI 디자인
    • 서버에 접속할 수 있도록 클라이언트 소켓 생성
    • 채팅은 실시간으로 이루어지기 때문에 각 클라이언트마다 스레드를 가져야 함
    • 채팅 메세지는 입출력 스트림으로 서버와 송수신할 수 있도록 함
    package j200212;
    
    // Socket 클래스
    import java.net.*;
    
    // 입출력 클래스
    import java.io.*;
    
    // 그래픽 관련 클래스
    import java.awt.*; // GUI
    import javax.swing.*; // JFrame, JTextField, JTextArea, JScrollPane
    
    // Event 처리
    import java.awt.event.*; // ActionListener
    
    public class ChatGUIClient extends JFrame implements ActionListener, Runnable {
    
        // ======== GUI =========
        JTextField tf; // 전송할 텍스트 입력창
        JTextArea ta; // 전송받은 텍스트 출력
    
        JScrollPane js; // 스크롤바 생성
    
        // ======== Socket =======
        Socket s; // 서버와의 통신을 위함
    
        // ======== Stream =======
        BufferedReader br; // 클라이언트에서의 문자열 입력 스트림
        PrintWriter pw; // 문자열 출력 스트림
    
        // 서버로 전송할 문자열과 서버에서 받아올 문자열 변수
        String str, str1;
    
        // ======== 생성자 ========
        public ChatGUIClient() {
            // 창, 부착할 컴포넌트 생성 및 연결
            tf = new JTextField();
            ta = new JTextArea();
    
            // 텍스트 출력창에 스크롤 바 연결
            js = new JScrollPane(ta);
    
            // BorderLayout 배치관리자, JTextArea를 정중앙에 부착
            add(js, "Center");
    
            // 텍스트 필드를 하단에 부착
            add(tf, BorderLayout.SOUTH);
    
            // 텍스트 필드에서 이벤트(enter)를 입력받고 해당 객체에서 이벤트 처리
            tf.addActionListener(this);
    
            // 창 크기 지정
            setBounds(200, 200, 500, 350);
    
            // 창이 보이도록 설정
            setVisible(true);
    
            // 텍스트 필드에 커서 입력
            tf.requestFocus();
    
            // X버튼 클릭시 정상 종료되도록 설정
            setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    
            // 서버와 연결, 연결되지 않을 수도 있기 때문에 예외 처리 필수
            try {
                // 클라이언트 측 소켓 정보 초기화
                // Socket(host, port), host: 접속 서버 IP 주소, port: 서버 포트 번호
                s = new Socket("192.168.0.145", 5432);
                System.out.println("s>>>" + s);
    
                // ========== Server와 Stream 연결 ===========
                br = new BufferedReader(new InputStreamReader(s.getInputStream()));
    
                // PrintWriter 스트림의 autoFlush 기능 활성화
                pw = new PrintWriter(s.getOutputStream(), true);
    
            } catch (Exception e) {
                System.out.println("접속 오류>>>" + e);
            }
    
            // Thread 객체 생성, Runnable 인터페이스를 구현하기 때문에 this 작성
            Thread ct = new Thread(this);
    
            // 클라이언트 스레드 실행 → run() 호출
            ct.start();
        }
    
        // Runnable 인터페이스 run() 메소드 오버라이딩
        public void run() {
            // 더 이상 입력을 받을 수 없을 때까지 JTextArea(채팅창)에 출력
            try {
                while ((str1 = br.readLine()) != null) {
                    ta.append(str1 + "\n"); // 상대방이 보낸 문자를 채팅창에 세로로 출력
                }
            } catch (Exception e) {
                e.printStackTrace();
                ;
            }
        }
    
        // ActionListener 메소드 오버라이딩, 입력란에서 enter입력시 실행할 코드
        public void actionPerformed(ActionEvent e) {
            // 내가 쓴 메세지를 str 변수에 저장
            str = tf.getText();
    
            // 변수에 저장 후 텍스트필드 초기화
            tf.setText("");
    
            // 내가 쓴 메세지 출력 -> 상대방은 br.readLine()으로 읽어들임
            pw.println(str);
            pw.flush();
        }
    
        public static void main(String[] args) {
    
            // 클라이언트 객체 생성, 생성자 호출
            new ChatGUIClient();
    
        }
    
    }

    ② 서버측

    • 서버는 클라이언트의 요청을 처리하는 역할을 수행
    • 클라이언트마다 가지고 있는 Socket과 연결할 ServerSocket 필요
    • ChatGUIServer: 접속 소켓을 관리하고 전체 소켓에 메세지를 출력하는 역할
      메세지 출력 자체는 ServerThread의 send()메소드가 수행하기 때문에
      ChatGUIServer에서는 전체 소켓에 send()메소드를 호출하는 broadCast() 메소드 작성 
    • ServerThread : 클라이언트의 요청을 직접적으로 처리하는 클래스
      클라이언트로부터 전송되는 메세지를 실시간으로 수신해서 출력하기 위해 스레드 상속
      클라이언트와의 직접적인 데이터 통신이 이루어짐
    package j200212;
    
    import java.net.*; // ServerSocket, Socket
    import java.io.*;  // 입출력
    
    // 동적 배열, 접속한 클라이언트의 정보를 실시간으로 저장하는 목적(고정 배열X)
    import java.util.Vector;
    
    public class ChatGUIServer {
    
        // 클라이언트와 연결할 때만 필요한 ServerSocket 클래스
        ServerSocket ss;
    
        // 서버로 접속한 클라이언트 Socket을 저장할 멤버 변수
        Socket s;
    
        // 접속 클라이언트 정보 실시간 저장
        Vector v;
    
        // ServerThread 자료형 멤버 변수 선언, has-a 관계 설정을 위함
        ServerThread st;
    
        // 생성자, 멤버 변수 초기화
        public ChatGUIServer() {
            // 사용자 정보를 담을 v를 Vector 객체로 초기화
            v = new Vector();
    
            // 접속이 될 수도 있고 안 될 수도 있기 때문에 예외 처리
            try {
                // ServerSocket 객체 생성 → 포트 번호 생성(임의의 번호 부여)
                ss = new ServerSocket(5432);
                System.out.println("ss>>>" + ss);
                System.out.println("채팅 서버 가동중...");
    
                // 서버 가동: 클라이언트가 접속할 때까지 기다리는 것(무한 대기)
                while (true) {
                    // 접속 클라이언트 Socket을 s 변수에 저장
                    s = ss.accept();
                    System.out.println("Accepted from" + s);
    
                    // 접속 클라이언트와 서버로 st객체 생성
                    st = new ServerThread(this, s);
    
                    // 접속할 때마다 v에 접속 클라이언트 스레드 추가
                    this.addThread(st);
    
                    // Thread 가동 -> run() -> broadCast() -> send() 실시간 메소드 호출
                    st.start();
                }
    
            } catch (Exception e) {
                // 접속 실패시 간단한 Error 메세지 출력
                System.out.println("서버 접속 실패>>>" + e);
            }
        }
    
        // 벡터 v에 접속 클라이언트의 스레드 저장
        public void addThread(ServerThread st) {
            v.add(st);
        }
    
        // 퇴장한 클라이언트 스레드 제거
        public void removeThread(ServerThread st) {
            v.remove(st);
        }
    
        // 각 클라이언트에게 메세지를 출력하는 메소드, send() 호출
        public void broadCast(String str) {
            for (int i = 0; i < v.size(); i++) {
                // 각각의 클라이언트를 ServerThread 객체로 형 변환 
                ServerThread st = (ServerThread) v.elementAt(i);
    
                // 각 스레드 객체에 str 문자열을 전송
                st.send(str);
            }
        }
    
        public static void main(String[] args) {
    
            // 익명 객체 생성
            new ChatGUIServer();
    
        }
    
    }
    
    // ServerThread 클래스 생성 → 서버에서 각 클라이언트의 요청을 처리할 스레드
    class ServerThread extends Thread {
    
        // 클라이언트 소켓 저장
        Socket s;
    
        // ChatGUIServer 클래스의 객체를 멤버 변수로 선언, has-a 관계를 위함
        ChatGUIServer cg;
    
        // 입출력
        BufferedReader br;
        PrintWriter pw;
    
        // 전달할 문자열
        String str;
    
        // 대화명(ID)
        String name;
    
        // 생성자
        public ServerThread(ChatGUIServer cg, Socket s) {
            /* cg = new ChatGUIServer(); → 작성 불가, 서버가 두 번 가동되기 때문에 충돌이 일어남
            따라서 매개변수를 이용해서 객체를 얻어온(call by reference) 뒤에 cg와 s값을 초기화해야 함
            */
            this.cg = cg;
    
            // 접속한 클라이언트 정보 저장
            this.s = s;
    
            // 데이터 전송을 위한 입출력 스트림 생성
            try {
                // =========== 입력 ===========
                // s.getInputStream() => 접속 클라이언트(소켓 객체)의 InputStream을 얻어 옴
                br = new BufferedReader(new InputStreamReader(s.getInputStream()));
    
                // =========== 출력 ===========
                /*
                BufferedWriter의 경우 버퍼링 기능을 가지기 때문에 PrintWriter 스트림 사용
                PrintWriter 스트림의 경우 생성자의 두 번째 인자로 autoFlush 기능을 지정할 수 있음
                BufferedWriter를 사용하는 경우 flush() 메소드를 사용해야 함
                */
                pw = new PrintWriter(s.getOutputStream(), true);
            } catch (Exception e) {
                System.out.println("에러 발생>>>>>" + e);
            }
        }
    
        // 메세지(입력 문자열) 출력 메소드
        public void send(String str) {
            // 문자열 출력
            pw.println(str);
    
            // 혹시나 버퍼에 남아있는 것을 비워냄
            pw.flush();
        }
    
        // run()_ServerThread -> broadCast(str)_ChatGUIServer -> send(str)_ServerThread
        public void run() {
            try {
                // 대화명 입력 받기
                pw.println("대화명을 입력하세요");
                name = br.readLine();
    
                // 서버에서 각 클라이언트에 대화명 출력
                cg.broadCast("[" + name + "]" + "님이 입장했습니다.");
    
                // 무한 대기하며 입력한 메세지를 각 클라이언트에 계속 전달
                while ((str = br.readLine()) != null) {
                    cg.broadCast("[" + name + "]: " + str);
                }
            } catch (Exception e) {
                // 접속자 퇴장시 v에서 해당 클라이언트 스레드 제거
                cg.removeThread(this); // this: ServerThread 객체, 접속 클라이언트
                 // 서버에서 각 클라이언트에 출력
                cg.broadCast("[" + name + "]" + "님이 퇴장했습니다.");
    
                // 콘솔에 퇴장 클라이언트 IP 주소 출력
                System.out.println(s.getInetAddress() + "의 연결이 종료됨!");
            }
        }
    
    }

    ※ port 번호와 ip 주소 같은 민감한 정보에 대해서는 Secure Coding을 해주어야 함


    지금까지 배운 java의 종합 예제라고 할 수 있는 채팅 프로그램 만들기를 해보았습니다.

    그래픽 관련 패키지 및 클래스(AWT, SWING)는 웹에서 사용할 일이 거의 없기 때문에
    클라이언트와 서버의 관계, 스레드와 스트림의 역할에 초점을 맞추고 보시면 좋을 것 같습니다.

    주석을 제외하면 양이 그렇게 많지는 않기 때문에 한 번 따라서 해보시기를 권장해 드립니다.

    'JAVA > JAVA' 카테고리의 다른 글

    Java | JDBC with Oracle  (0) 2020.02.13
    Java | Properties Class  (0) 2020.02.12
    Java | Network (2) ServerSoket & Socket  (0) 2020.02.11
    Java | Network (1) InetAddress & URL  (0) 2020.02.11
    Java | Singleton Pattern 싱글톤 패턴  (0) 2020.02.11

    댓글

Designed by Tistory.