JAVA/JAVA

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

pathas 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)는 웹에서 사용할 일이 거의 없기 때문에
클라이언트와 서버의 관계, 스레드와 스트림의 역할에 초점을 맞추고 보시면 좋을 것 같습니다.

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