네트워크

멀티캐스팅을 이용한 채팅 프로그램

충 민 2022. 9. 6. 17:13

앞서 만들었던 유니캐스팅을 이용한 채팅 프로그램에서 더 나아가 멀티캐스팅을 이용하여 한 서버안에 여러명이 대화가 가능한 채팅 프로그램을 만들어 보았다. 

 

멀티 캐스팅이란?

멀티 캐스팅
유니 캐스트 모델은 실시간 프로그램에서 서버의 정보를 모든 클라이언트가 공유할 때 문제점이 있다.
이런 문제를 해결할 수 있는 방법이 일대 다 전송을 지원하는 멀티 캐스팅 방법이다.
한명의 클라이언트가 서버의 정보를 변경했을 경우 모든 클라이언트에게 전송함으로써 서로가 변경된 정보를 공유할 수 있는 애플리케이션을 만들 때 적합하다.
멀티 캐스팅 프로그램을 작성하기 위해서는 유니캐스트에서 생성된 스레드를 저장하기 위한 공간(ArrayList)이 필요하며, 클라이언트에서는 자신이 보낸 메시지나 다른 클라이언트가 보낸 메시지를 받기 위한 스레드가 필요하다.

팀원들과 Java Swing으로 Gui를 먼저 구축한 후 각자 기능을 한 가지씩 맡아 만들어보았다. Swing을 처음 만져보는 입장이라 어려움이 있었지만 구글링과 유튜브를 통해 공부를 하면서 하나씩 해결하였다. 

 

 

팀 회의때 결정한 GUI

팀 회의시간에 위 사진처럼 GUI를 구축하기로 하였고 각자 Swing을 공부하면서 만들고 구축한 후 기능을 만들어보았다. 

 

    기능

  • 스킨(다크모드)
  • 내보내기(강퇴기능)
  • 귓속말
  • 이벤트마다 시간 띄우기
  • 현재 접속자 목록 
  • 특정언어에 일정시간 동안 채팅금지 
  • 엔터키로 채팅 보내기
  • 팝업창 띄우기

  클래스는 총 6개로 구성하였다. 

 

    클래스

  • 클라이언트 클래스
  • 서버 클래스
  • 로그인 클래스
  • 각 클라이언트의 쓰레드 클래스
  • 리시버 클래스
  • 샌더 클래스 

실행방법

  1. 서버 클래스를 켠다. 
  2. 원하는 명수대로 로그인 클래스를 켠다.
  3. 채팅 프로그램의 기능들을 실행한다.

 

 

로그인 클래스 실행시 

닉네임을 입력한 후 로그인 버튼을 누른다. 

 

홍길동으로 로그인을 하게되면  들어왔다는 메시지와 시간이 뜨게된다. 

또한 각자 채팅방에 자신의 이름에는 (나)라고 표시가 된다. 

 

이렇게 모두에게 채팅이 원활하게 된다.

다크모드 // 라이트모드

 

사용자를 클릭하게되면 귓속말과 강퇴하기 기능을 실행할 수 있다.

귓속말을 통해 홍길동이 똥개에게 보내어 (귓)으로 둘의 채팅창에만 대화가 이루어지는것을 확인할 수 있다.

강퇴하기를 누르게되면 팝업창과 함께 강퇴를 시킬수있다.

또한 나가기 버튼을 누르면 나가게 된다.

 

금지어 사용시 5초간 보내기 금지와 함께 팝업창이 뜬다. 

 

[소스코드]

  • 클라이언트 클래스
package Multicast;

import java.net.*;
class ClientExample4 {
    public static void start(String name) {
       try {
           // 서버와 연결
                Socket socket = new Socket("127.0.0.1", 9002);
                 // 메시지 송신 쓰레드와 수신 쓰레드 생성해서 시작
                SenderThread thread1 = new SenderThread(socket, name);
                Thread thread2 = new ReceiverThread(socket, thread1, name); 
                
                thread1.start();
                thread2.start();
            }
            catch (Exception e) {
                System.out.println(e.getMessage());
            }
    }
    
    
}
  • 서버 클래스
package Multicast;

import java.net.*;
class ServerExample4 {
    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(9002);
            while (true) {
                Socket socket = serverSocket.accept();
                Thread thread = new PerClinetThread(socket);
                thread.start();
            }
        }
        catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}
  • 로그인 클래스
package Multicast;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.text.SimpleDateFormat;
import java.util.Date;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;

public class Login {
   static JTextField text;
   static JFrame frame;
   public static void login() {
       frame = new JFrame("Login");
       frame.setBounds(400, 300, 400, 240);
       frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
       frame.setLayout(new BorderLayout());// 동,서,남,븍 나뉨
       frame.setResizable(false);    // 크기 고정
       
       JPanel panel = new JPanel();
       panel.setLayout(null);
       
       JLabel la = new JLabel("닉네임");
       la.setBounds(65, 70, 80, 35);   // x, y, width, height
       panel.add(la);
       
       text = new JTextField();
       text.setBounds(125, 73, 160, 30);
       text.addKeyListener(new KeyAdapter() {
            public void keyPressed(KeyEvent e) {
               if(e.getKeyCode()==KeyEvent.VK_ENTER) {//엔터키 누를시
                  action();
               }
            }
         });
       panel.add(text);
       
       JButton btn = new JButton("로그인");
       btn.setBackground(Color.lightGray);
       btn.setBounds(125, 110, 160, 30);
       Font font = new Font("맑은 고딕", Font.BOLD, 10);
       btn.setFont(font);
       btn.addActionListener(new ActionListener() {
          @Override
          public void actionPerformed(ActionEvent e) {
             if(e.getActionCommand().equals("로그인")) {
                action();
             }
          }
       });
       
       panel.add(btn);
       
       frame.setContentPane(panel);
       frame.setVisible(true);
    }
   
   public static void action() {
      String[] chars = new String[] {"~", "`", "!", "@", "#", "$", "%", "^",
              "&", "*", "(", ")", "-", "_", "+",
              "=", "'", "<", ">", "?", "/", ";", ":", "|"};
      String name = text.getText();
      if(name.length() < 1) {
         JOptionPane.showMessageDialog(null,"닉네임을 입력해주세요");
      }else{
         int cnt = 0;
         for(int i = 0; i < chars.length; i++) {
            if(name.contains(chars[i])) {
               cnt++;
            }
         }
         if(cnt > 0) {
            JOptionPane.showMessageDialog(null,"특수문자는 포함하실 수 없습니다");
         }else {
            frame.setVisible(false);
            ClientExample4.start(name);
         }
      }
   }
   
   
    public static void main(String[] args) {
        if (args.length != 1) {
           login();
        }
    }
}
  • 각 클라이언트의 쓰레드 클래스
package Multicast;
//각 클라이언트 접속에 대해 하나씩 작동하는 스레드 클래스 
import java.io.*;
import java.net.*;
import java.util.*;
class PerClinetThread extends Thread {
 
  // ArrayList 객체를 여러 스레드가 안전하게 공유할 수 있는 동기화된 리스트로 만듭니다.
  static List<PrintWriter> list = Collections.synchronizedList(new ArrayList<PrintWriter>());
  static ArrayList<String> nameList = new ArrayList<String>();
  Socket socket;
  PrintWriter writer;
  boolean kicked=false;
  PerClinetThread(Socket socket) {
      this.socket= socket;
      try {
          writer = new PrintWriter(socket.getOutputStream());
          list.add(writer);
      }
      catch (Exception e) {
          System.out.println(e.getMessage());
      }
  }
  public void run() {
      String name = null;
      try {
          BufferedReader reader = new BufferedReader(
              new InputStreamReader(socket.getInputStream()));
     
          // 수신된 첫번째 문자열을 대화명으로 사용하기 위해 저장
          name = reader.readLine(); 
          nameList.add(name);
          
          sendAll("#" + name + "#님이 들어오셨습니다");
          while (true) {
             String nameStr = "";
              for(int i = 0; i < nameList.size(); i++) {
                 nameStr += nameList.get(i) + "&";
              }
              sendAll(nameStr);
              String str = reader.readLine();
              if (str == null)
                  break;
              if (str.contains("#kick")) {
                 String[] strArr = str.split("#");
                 sendAll("#kick"+name+"님이#" +strArr[2]+"#님을 추방하였습니다");
              }else if (str.contains("#redCard")) {   
                 kicked=true;
              }else {
                 sendAll(name + ">" + str);  // 수신된 메시지 앞에 대화명을 붙여서 모든 클라이언트로 송신}
              }
          }
      }
      catch (Exception e) {
          System.out.println(e.getMessage());
      }
      finally {
         if(kicked == true) {
              list.remove(writer);
              nameList.remove(name);
              sendAll("#" + name + "#님이 추방당하셧습니다"); // 사용자가 채팅을 종료했다는 메시지를 모든 클라이언트로 보냅니다.
              try {
                  socket.close();
              }
              catch (Exception ignored) {
              }
         }else {
              list.remove(writer);
              nameList.remove(name);
              sendAll("#" + name + "#님이 나가셨습니다"); // 사용자가 채팅을 종료했다는 메시지를 모든 클라이언트로 보냅니다.
              try {
                  socket.close();
              }
              catch (Exception ignored) {
              }
         }
      }
  }
 
  // 서버에 연결되어 있는 모든 클라이언트로 똑같은 메시지를 보냅니다.
  private void sendAll(String str) {  
      for (PrintWriter writer : list) {
          writer.println(str);
          writer.flush();
      }
  }
}
  • 리시버쓰레드 클래스
package Multicast;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;

import java.net.*;
class ReceiverThread extends Thread {
    Socket socket;
    SenderThread send;
    String name;
    ReceiverThread(Socket socket, SenderThread send, String name) {
        this.socket = socket;
        this.send = send;
        this.name = name;
    }
    public void run() {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            while (true) {
                Date now = new Date(System.currentTimeMillis());
                SimpleDateFormat simple= new SimpleDateFormat("(a hh:mm)");

      //서버로부터 수신된 메시지를 모니터로 출력
                String str = reader.readLine();
                if (str == null) 
                   break;
                if(str.contains("#")) {
                   String newStr = str.replace("#", "");
                   send.area.append(newStr+simple.format(now));
                   send.area.append("\n");
                   if(str.contains("님이 나가셨습니다")) {
                      String[] strArr = str.split("#");
                      send.arr.removeElement(strArr[1]);
                   }else if(str.contains("#kick")) {
                      String[] strArr = str.split("#");
                      if(name.equals(strArr[2])) {
                         send.sendMsg("#redCard");
                         System.exit(0); 
                      }
                   }else if(str.contains("님이 추방당하셧습니다")) {
                      String[] strArr = str.split("#");
                      send.arr.removeElement(strArr[1]);
                   }
                }else if(str.contains("->")) {   // 귓속말
                    String[] whisper = str.split("->");
                    if(whisper[1].equals(name)) {
                       send.area.append(whisper[0].replace(">", "(귓)>") + whisper[2]);
                       send.area.append("\n");
                    }else if(whisper[0].equals(name+">")) {
                       send.area.append(whisper[1]+"(에게)>" + whisper[2]);
                       send.area.append("\n");
                    }
                 }else if(!str.contains(">")) {
                   if(str.contains("&")) {
                      String[] strArr = str.split("&");
                      System.out.println(strArr.length);
                      send.arr.clear();
                      for(int i = 0; i < strArr.length; i++) {
                         if(strArr[i].equals(name)) {
                            send.arr.addElement(strArr[i] + "(나)");
                         }else {
                            send.arr.addElement(strArr[i]);
                         }
                      }
                   }
                }else {
                   String[] strArr = str.split(">");
                   if(strArr[0].equals(name)) {
                      str = strArr[0] + "(나)>" + strArr[1];
                      send.area.append(str);
                        send.area.append("\n");
                   }else {
                   send.area.append(str);
                    send.area.append("\n");}
                }
                System.out.println(str+simple.format(now));
            }
        }
        catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
}
  • 샌더쓰레드  클래스 
package Multicast;
import java.net.*;
import java.text.SimpleDateFormat;
import java.util.Date;


import java.util.ArrayList;

import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
class SenderThread extends Thread implements ActionListener{
    Socket socket;
    String name, whispering, my;
    PrintWriter writer;
    JTextField tf;
    JTextArea area = new JTextArea();
    DefaultListModel<String> arr = new DefaultListModel<>();
    JList list;
    JLabel peopleList, label;
    JFrame frame;
    String str = "";
    JButton btn3;
    SimpleDateFormat simple;
    Date now;
    JPanel panel;
    //JButton whbtn;
    SenderThread(){}
    
    SenderThread(Socket socket, String name) { 
        this.socket = socket;
        this.name = name;
        
        frame = new JFrame("Chat");
        frame.setBounds(400, 140, 600, 540);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setResizable(false);    // 크기 고정
       
        panel = new JPanel();
        panel.setLayout(null);
       
        // 채팅
        JScrollPane scrollPane = new JScrollPane(area);
        scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);   // 스크롤바 항상 보이게
        scrollPane.setBorder(null);
        scrollPane.setBounds(10, 10, 360, 440);
        panel.add(scrollPane);
        area.setEditable(false);
      
       // 사용자 목록
        label = new JLabel("사용자 목록");
        label.setBounds(440, 10, 80, 20);
        panel.add(label);
       
        list = new JList(arr);
        //list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
       
        list.addMouseListener(new MouseAdapter() {
           public void mouseClicked(MouseEvent e) {
              if(list.getSelectedValue() !=null) {
                System.out.println("d"+list.getSelectedValue());
                  JPopupMenu menu = new JPopupMenu();
                  JMenuItem whbtn  = new JMenuItem("귓속말");
                  whbtn.addActionListener(new ActionListener() {
                         public void actionPerformed(ActionEvent e) {
                            peopleList.setText(whispering + "에게");
                            tf.requestFocus();
                         }
                     });                  
                  
                  JMenuItem kick = new JMenuItem("강퇴하기");
                  kick.addActionListener(new ActionListener() {
                         public void actionPerformed(ActionEvent e) {
                           int result=JOptionPane.showConfirmDialog(null,"진짜로 강퇴하시겠습니까?","강퇴확인",JOptionPane.YES_NO_OPTION);
                          if(result==JOptionPane.YES_OPTION) {
                               String kickCommand = "#kick#"+whispering;
                              writer.println(kickCommand);
                              writer.flush();
                          }else {
                             JOptionPane.showMessageDialog(null,"취소하였습니다");
                          }
                           
                         }
                     });   
                  
                  menu.addSeparator();
                     menu.add(whbtn);
                     menu.add(kick);
                     menu.show(e.getComponent(),e.getX(),e.getY());
              }
           }
       });
        list.addListSelectionListener(new ListSelectionListener() {
            @Override
            public void valueChanged(ListSelectionEvent e) {
               whispering = (String) list.getSelectedValue();
               if(whispering != null) {
                  // 자기 자신에게는 귓속말하지 못하도록함
                  if(whispering.equals(name+"(나)")) {
                     list.clearSelection();
                     whispering = null;
                     
                  }else {

                  }
               }
            }
             
          });
       
       JScrollPane scrollPane2 = new JScrollPane(list);
       scrollPane2.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
       scrollPane2.setBorder(null);
       scrollPane2.setBounds(380, 35, 190, 385);
      panel.add(scrollPane2);
       peopleList = new JLabel("모두에게");
       peopleList.setBounds(10, 464, 50, 20);
       panel.add(peopleList);
       
       tf = new JTextField();
       tf.setBounds(70, 460, 200, 30);
       panel.add(tf);
       tf.addKeyListener(new KeyAdapter() {
         public void keyPressed(KeyEvent e) {
               
               now = new Date(System.currentTimeMillis());
               simple= new SimpleDateFormat("(a hh:mm)");
            if(e.getKeyCode()==KeyEvent.VK_ENTER) {//엔터키 누를시
               action(btn3);
            }
         }
      });
       
      JButton btn1 = new JButton("다크모드");
      btn1.setBounds(380, 425, 190, 30);
      btn1.setBackground(Color.lightGray);
      btn1.addActionListener(this);
       panel.add(btn1);
       
      btn3 = new JButton("보내기");
      btn3.setBounds(270, 460, 95, 30);
      btn3.setBackground(Color.lightGray);
      btn3.addActionListener(this);
      panel.add(btn3);
       
       JButton btn2 = new JButton("나가기");
      btn2.setBounds(380, 460, 190, 30);
      btn2.setBackground(Color.lightGray);
      btn2.addActionListener(this);
       panel.add(btn2);
       
       frame.setContentPane(panel);
       frame.setVisible(true);
    }
    public void run() {
       try {
            //BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
            writer = new PrintWriter(socket.getOutputStream());
            
          // 제일 먼저 서버로 대화명 송신한다.
          writer.println(name);
           writer.flush();
           
           while (true) {
               if (str.equals("bye"))
                   break;
           }
        }
        catch (Exception e) {
            System.out.println(e.getMessage());
        }
        finally {
            try { 
                socket.close(); 
            } 
            catch (Exception ignored) {
            }
        }
    }
    
    public void action(JButton button) {
       Timer timer = new Timer(5000, new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
               tf.setEnabled(true);
               tf.requestFocus();
                button.setEnabled(true);
                button.setText("보내기");
            }
        });
       timer.setRepeats(false);
       String chat = tf.getText();
        if(chat.length() > 0) {
           String[] chars = new String[] {"시발", "병신", "개새끼", "바보", "멍청이", "존나"};
           int cnt = 0;
           for(int i = 0; i < chars.length; i++) {
              if(chat.contains(chars[i])) {
                 chat = chat.replace(chars[i], "*".repeat(chars[i].length()));
                 cnt++;
              }
           }
           if(cnt > 0) {
              tf.setEnabled(false);
              button.setEnabled(false);
               button.setText("채금");
               timer.start();
               JOptionPane.showMessageDialog(null,"비속어를 사용하셨습니다. 5초간 채팅 금지입니다.");
           }
           if(peopleList.getText().equals("모두에게")) {   // 귓속말 보내는중
                String[] to = peopleList.getText().split("에게");
                writer.println(chat+simple.format(now));
                tf.setText("");
                 writer.flush();
             }else {
                String[] to = peopleList.getText().split("에게");
                writer.println("->" + to[0] + "->" + chat+simple.format(now));
                tf.setText("");
                writer.flush();
                peopleList.setText("모두에게");
             }
        }
    }
    

   @Override
   public void actionPerformed(ActionEvent e) {
         now = new Date(System.currentTimeMillis());
         simple = new SimpleDateFormat("(a hh:mm)");
      if(e.getActionCommand().equals("보내기")) {
        JButton button = (JButton)e.getSource();
         action(button);
      }else if(e.getActionCommand().equals("다크모드")) {
          JButton btn = (JButton)e.getSource();
          area.setBackground(Color.DARK_GRAY);
          area.setForeground(Color.WHITE);
          list.setBackground(Color.DARK_GRAY);
          list.setForeground(Color.WHITE);
          panel.setBackground(Color.GRAY);
          tf.setBackground(Color.DARK_GRAY);
          tf.setForeground(Color.WHITE);
          label.setForeground(Color.white);
          peopleList.setForeground(Color.white);
          btn.setText("라이트 모드");
          
          }else if(e.getActionCommand().equals("라이트 모드")) {
             JButton btn = (JButton)e.getSource();
             area.setBackground(Color.WHITE);
             area.setForeground(Color.BLACK);
             list.setBackground(Color.WHITE);
             list.setForeground(Color.BLACK);
             panel.setBackground(new Color(238, 238, 238));
             tf.setBackground(Color.WHITE);
             tf.setForeground(Color.BLACK);
             label.setForeground(Color.BLACK);
             peopleList.setForeground(Color.BLACK);
             btn.setText("다크모드");      
          }else if(e.getActionCommand().equals("나가기")) {
         //str = "bye";
         frame.dispose();
         System.exit(0);
      }
   }
   void sendMsg(String msg) {
      writer.println(msg);
      writer.flush();
   }
   
}