-
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