본문 바로가기
프로젝트/1. 세미_바나나톡

멀티 채팅 프로그램_1주차(12/26~01/01)

by 루팽 2023. 1. 1.

1) 프로젝트 시작

국비학원 5주 차만에 세미 프로젝트로 멀티 채팅 프로그램을 DB에 연결해서 구현해보기로 했다.

선생님이 멀티 채팅이라는 주제와 참고용 코드들을 알려주시고 가까이 앉은 6명이 한 팀이 되었다.

처음 역할 분담을 어떻게 해야 할지 애매해서 그냥 프론트와 백을 반반 나눠서 사다리 타기로 뽑았는데, 난 프론트엔드쪽 담당+조장을 맡게 됐다.

사실 조장이라고 무슨 권한이 있는 건 아니고 오히려 의무만 생겼지만, 다른 팀들과 견줄 수 있는 결과물을 만들고 싶고 나중에 발표도 해야 하니 프로젝트의 전반적인 것을 설명할 수 있을 만큼 이해해두자는 각오를 했다.

팀 명은 바나나, 채팅 프로그램 이름은 바나나톡.

딱히 의미가 있는 건 아니고 그냥 선생님이 토마토 언급을 자주 하셔서 우리는 바나나하자 하면서 결정하게 됐다.

 

2.1) 프로젝트 기획 - 프로젝트 공정표

첫 프로젝트이고 이제 막 자바를 배운 시점이라 약간 막막하기도 했지만 이전 팀들의 PPT를 참고하면서 프로젝트 공정표를 작성했다.

평일엔 학원 수업과 복습으로 프로젝트까지 하기엔 무리가 있을 것 같아 주말에 zoom으로 회의하며 조금씩 만들고, 만약 마감이 가까운데 구현이 다 안 됐다면 그때 평일 시간도 투자하기로 했다.

또한 아직 복잡한 설계는 잘 모르고 직접 코드를 치면서 이해하는 부분이 중요하다고 생각돼서 설계 부분을 최대한 빠르게 끝내고 구현을 길게 잡는 일정으로 잡았다.

 

2.2) 프로젝트 기획 - 화면 설계도

화면 설계는 평일에 남는 시간을 이용해 비교적 빠르게 끝났는데,

요구사항 정의서만 보면서 내용을 추가하다 보니 직관적이지 않아서 그림을 그리면서 대략적인 구상도를 완성시켰다.

앞으로 구현하면서 수정할 부분이 계속 생길 것 같아서 이 정도로만 그려놓기로 했다.

 

2.3) 프로젝트 기획 - 요구사항 정의서

요구사항 정의서는 화면 설계도를 참고하며 하나씩 추가하고, 필수와 선택을 나누어서 1차 구현 때 필수적인 부분을 집중적으로 작업하고, 만약 필수적인 것들을 다 했는데 시간이 남으면 그때 선택 부분을 하나씩 추가하기로 했다.

평일 남는 시간에 조금씩 작성한 후 1주 차 회의 때 다 함께 요구사항과 기능을 하나씩 확인하고, 클래스명과 변수명까지 확정 지었다.

 

대부분 JFrame 화면 전환이 많아서 어떻게 처리해야할까 고민하다 아래 코드와 같이 테스트해본 후

레이아웃 변화가 심해서 JFrame만 두고 JPanel자체도 지우고 싶을 때 this.removeAll()을 사용해서 지우면 제대로 실행되지 않으니 Container con = this.getContentPane();을 써서 처리하기로 했다.

package dev_java.week5;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.Color;
import java.awt.Container;

public class repaintEx extends JFrame implements ActionListener {
  Container con = this.getContentPane();
  JPanel jp_north = new JPanel();
  JPanel jp_center = new JPanel();
  JTextArea jta = new JTextArea("기본 JTextArea", 10, 20);
  JButton jbtn1 = new JButton("기본창");
  JButton jbtn2 = new JButton("대화방");
  JButton jbtn3 = new JButton("con");

  public repaintEx() {
    initDisplay();
  }

  public void initDisplay() {
    jbtn1.addActionListener(this);
    jbtn2.addActionListener(this);
    jbtn3.addActionListener(this);
    jp_north.add("North", jbtn1);
    jp_north.add("North", jbtn2);
    jp_north.add("North", jbtn3);
    jp_center.add(jta);
    this.add("North", jp_north);
    this.add("Center", jp_center);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setBackground(Color.GREEN);
    this.setSize(400, 300);
    this.setVisible(true);
  }

  void view1() {
    jp_center.removeAll();
    jp_center.setBackground(Color.MAGENTA);
    this.revalidate();
  }

  void view2() {
    jp_center.removeAll();
    jp_center.setBackground(Color.ORANGE);
    this.revalidate();
  }

  void view3() {
    con.remove(jp_center);
    jp_center.setBackground(Color.blue);

    // JPanel jp1 = new JPanel();
    // this.add("Center", jp1);
    // jp1.setBackground(Color.blue);
    this.revalidate();
  }

  public static void main(String[] args) {
    new repaintEx();
  }

  @Override
  public void actionPerformed(ActionEvent e) {
    Object obj = e.getSource();
    if (obj == jbtn1) {
      view1();
    } else if (obj == jbtn2) {
      view2();
    } else if (obj == jbtn3) {
      view3();
    }
  }
}

 

2.4) 프로젝트 기획 - DB설계

DB의 경우 학원에서 아직 배우지도 않았고 사전지식도 부족하기에 조금 막막했는데 관련해서 잘 아시는 팀원분들이 있어서 생각보다 수월하게 진행되었다.

마찬가지로 평일에 틈틈이 작성 후 1주 차 회의 때 모여서 다 같이 보면서 내용을 확정 짓고, 월요일에 선생님께 다시 한번 확인받기로 했다.

 

2.5) 프로젝트 기획 - 클래스 설계

클래스 설계는 어떻게 할지 감이 안 잡혀서 일단 각자 선생님이 올려주신 서버 클라이언트 단체방 코드 리뷰를 하면서 공부해오고 일요일 회의 때 다시 얘기해보기로 했다.

그렇게 일요일에 다시 모여서 공부한 것들을 코드 리뷰를 하면서 복습하고 클래스를 어떻게 설계할지 의견을 나눴는데,

클라이언트 스레드, 서버 스레드의 스레드를 CRUD와 그 외 기능으로 나눠서 2개씩 연결해보는 건 어떨까 하는 결론이 나왔다.

하지만 그렇게 해도 스레드끼리 꼬이지 않고 제대로 작동할지, 구현이 너무 복잡하거나 어렵지 않을지 확신이 없어서 월요일에 선생님께 조언을 구하기로 하고 1주 차 회의는 마쳤다.

 

3) 느낀 점

어쩌다 보니 조장을 맡게 돼서 최대한 조장의 역할을 하려고 이것저것 사전조사하고 길을 제시하려고는 하지만 첫 프로젝트에 첫 협업이다 보니 혼자만으론 부족한 점이 많은 것 같다.

그래도 다른 팀원분들도 적극적으로 참여하면서 먼저 자료를 만들고 방향을 제시해주는 부분이 있어서 프로젝트가 꽤 수월하게 진행되는 중이다.

1주 차 토요일 미팅을 마치고 클래스 설계를 하기 전 코드에 대한 이해가 부족한 것 같아 참고 코드들을 클론코딩하며 의미나 기능을 이해하고 주석을 달면서 공부했는데, 하다 보니 새벽 3시 반이 넘어버려서 결국 잠이 부족한 상태에서 일요일 미팅을 들어가게 되었다.

아무래도 주말이다 보니 토요일에 회의 끝나고 바로 코드분석에 들어가지 않고 조금 놀다가 오후 9시쯤에 2시간 정도만 공부하고 잘 생각으로 시작했는데 아주 크나큰 착오였다.

앞으로 주말에 공부시간과 휴식시간을 어떻게 배분할지 좀 더 생각해 봐야겠다.

그래도 몸이 피곤한 만큼 기본적인 로직 구성은 거의 이해가 돼서 이제 직접 코드를 작성하면서 응용만 하면 금방 익숙해질 것 같다.

가능하다면 Git 협업도 같이 진행하고 싶은데 익숙하지도 않은데 사용하려면 따로 공부해야 하는 부분도 있고 이번 프로젝트에 필수적으로 사용해야 하는 건 아니기에 일단 구현 단계에 들어가서 코드를 짜면서 생각해봐야 할 것 같다.

 

4) 2주 차에 해야 할 것

  1. 선생님께 조언구하고(DB설계, 클래스설계) 해당 부분 테스트해 보기
  2. 선생님이 슬랙에 새로 올려주신 코드들 분석하기
  3. 역할 분담 후 기능 구현 시작하기.

 

5) 1주 차 참고 코드

<바나나톡 프로젝트 깃허브 바로가기>

package dev_java.banana_chat.ex.one;

public class Protocol {
  public static final int TALK_IN = 100;
  public static final int MESSAGE = 200;
  public static final int WHISPER = 300;
  public static final int CHANGE = 400;
  public static final int TALK_OUT = 500;

  public static final int WAIT = 600;
  public static final int ROOM_LIST = 700;
  public static final int ROOM_CREATE = 710;
  public static final int ROOM_IN = 720;
  public static final int ROOM_INLIST = 730;
  public static final int ROOM_OUT = 740;
  public static final String seperator = "#";
}
/*
 * final이 클래스 앞에 오면 상속을 못함
 * 메소드 앞에 오면 메소드 오버라이딩을 못함
 * 변수 앞에 오면 상수
 */

 

package dev_java.banana_chat.ex.one;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.List;
import java.util.Vector;

public class Server extends JFrame implements ActionListener, Runnable {
  // 화면부 선언
  JButton jbtn_log = new JButton("로그 저장");
  JTextArea jta_log = new JTextArea();
  JScrollPane jsp_log = new JScrollPane(jta_log, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
      JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
  // 서버연결부 선언
  List<ServerTh> globalList = null; // 왜 타입이 sth?? -> 각 클라이언트와 이어진 서버 스레드를 담기위해
  ServerTh sth = null;
  ServerSocket server = null; // 서버소켓
  Socket socket = null; // 클라이언트를 받을 소켓

  // 화면부 메소드
  public void initDisplay() {
    jbtn_log.addActionListener(this);
    this.add("North", jbtn_log);
    this.add("Center", jsp_log);
    this.setSize(400, 300);
    this.setVisible(true);
  }

  public static void main(String[] args) {
    Server s = new Server();
    s.initDisplay(); // 화면
    Thread th = new Thread(s); // 스레드 생성하며 s객체 넘김 ->서버의 run() 호출위헤
    th.start(); // 서버의 run() 호츌
  }

  // 서버연결부 메소드
  @Override
  public void run() {
    globalList = new Vector<>();
    boolean isStop = false;
    try {
      server = new ServerSocket(5555);
      jta_log.append("서버 대기 중...\n");
      while (!isStop) {
        socket = server.accept(); // Client 정보 소켓에 대입
        jta_log.append("Client: " + socket + "\n");
        jta_log.append("Client: " + socket.getInetAddress() + "\n");
        ServerTh sth = new ServerTh(this);
        sth.start(); // 서버스레드의 run() 호출 -> Client의 소켓 정보 넘기기위해
      }
    } catch (Exception e) {
      e.printStackTrace();
    }

  }

  @Override
  public void actionPerformed(ActionEvent e) {
    Object obj = e.getSource();
    if (obj == jbtn_log) {
      // 로그를 파일로 저장하기
    }
  }
}

 

package dev_java.banana_chat.ex.one;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.StringTokenizer;

public class ServerTh extends Thread {
  // 선언부
  Server s = null;
  Socket client = null;
  ObjectInputStream ois = null;
  ObjectOutputStream oos = null;
  String nickName = null;

  // 생성부
  public ServerTh(Server s) {
    this.s = s;
    this.client = s.socket; // 서버에서 받아온 소켓 대입
    try {
      oos = new ObjectOutputStream(client.getOutputStream()); // 말하기
      ois = new ObjectInputStream(client.getInputStream()); // 듣기
      String msg = (String) ois.readObject(); // 들은 것 msg에 저장(입장시 말한 100#닉네임)
      s.jta_log.append(msg + "\n"); // 로그에 msg 출력
      s.jta_log.setCaretPosition(s.jta_log.getDocument().getLength()); // 맨아래로 자동 스크롤
      StringTokenizer st = null;
      if (msg != null) { // 만약 msg가 있다면
        st = new StringTokenizer(msg, "#"); // 구분자 #를 기준으로 msg 문자열 분리
      }
      if (st.hasMoreTokens()) { // 만약 남아있는 토큰이 있다면
        st.nextToken(); // 첫번째 것, 프로토콜 스킵처리
        nickName = st.nextToken(); // 닉네임 저장
        s.jta_log.append(nickName + "님이 입장하였습니다.\n");
        for (ServerTh sth : s.globalList) { // globalList에 저장된 sth에 100#닉네임 반복 전송
          this.send(Protocol.TALK_IN + Protocol.seperator + sth.nickName);
        }
        s.globalList.add(this); // 현재 서버에 입장한 클라이언트 스레드 추가
        this.broadCasting(msg); // 클라이언트의 말을 전체 사람들에게 전달
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  // 클라이언트에게 말하기(말 전달하기)
  public void send(String msg) {
    try {
      oos.writeObject(msg); // 클라이언트가 말한 메세지를 다시 전달하기
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  // 현재 입장해있는 사람들 모두에게 메세지 전송
  public void broadCasting(String msg) {
    for (ServerTh sth : s.globalList) { // globalList에 저장된 sth의 msg 반복 전송
      sth.send(msg);
    }
  }

  @Override
  public void run() {
    String msg = null;
    boolean isStop = false;
    try {
      run_start: while (!isStop) { // while문 라벨붙임
        msg = (String) ois.readObject(); // 듣기
        s.jta_log.append(msg + "\n");
        s.jta_log.setCaretPosition(s.jta_log.getDocument().getLength());
        StringTokenizer st = null;
        int protocol = 0;
        if (msg != null) {
          st = new StringTokenizer(msg, "#"); // msg를 #로 구분
          protocol = Integer.parseInt(st.nextToken()); // 읽어들인 프로토콜 인트로 전환
        }
        switch (protocol) {
          // 클라이언트가 말한 것 -> 200#닉네임#입력한메세지
          case Protocol.MESSAGE: {
            String nickName = st.nextToken();
            String message = st.nextToken();
            broadCasting(Protocol.MESSAGE // 모두에게 말 전달하기
                + Protocol.seperator + nickName
                + Protocol.seperator + message);
          }
            break;

          // 클라이언트가 말한 것 -> 300#닉네임#상대이름#보내는메세지
          case Protocol.WHISPER: {
            String nickName = st.nextToken(); // 보내는 사람
            String otherName = st.nextToken(); // 받는 사람
            String privateMsg = st.nextToken();
            for (ServerTh sth : s.globalList) {
              if (otherName.equals(sth.nickName)) { // 상대이름이 sth에 있는 닉네임과 같다면 말 전달하기
                sth.send(Protocol.WHISPER
                    + Protocol.seperator + nickName
                    + Protocol.seperator + otherName
                    + Protocol.seperator + privateMsg);
                break;
              }
            }
            this.send(Protocol.WHISPER // 나(보내는 사람)에게도 해당 메세지 출력하기
                + Protocol.seperator + nickName
                + Protocol.seperator + otherName
                + Protocol.seperator + privateMsg);
          }
            break;

          // 클라이언트가 말한 것 -> 400#닉네임#새 대화명#닉네임의 대화명이 새 대화명으로 변경되었습니다
          case Protocol.CHANGE: {
            String nickName = st.nextToken();
            String newName = st.nextToken();
            String message = st.nextToken();
            this.nickName = newName;
            broadCasting(Protocol.CHANGE // 모두에게 대화명 변경 말하기
                + Protocol.seperator + nickName
                + Protocol.seperator + newName
                + Protocol.seperator + message);
          }
            break;
        }
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

 

package dev_java.banana_chat.ex.one;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.table.DefaultTableModel;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.awt.GridLayout;
import java.awt.BorderLayout;

public class Client extends JFrame implements ActionListener {
  // 화면부 선언부
  JButton jbtn_changeName = new JButton("대화명 변경");
  JButton jbtn_fontColor = new JButton("글자색 변경");
  JButton jbtn_privateChat = new JButton("1:1 대화");
  JButton jbtn_groupChat = new JButton("단체 대화");
  JButton jbtn_send = new JButton("전송");
  JTextField jtf_msg = new JTextField(20);
  JPanel jp_first = new JPanel();
  JPanel jp_first_south = new JPanel();
  JPanel jp_second = new JPanel();
  JPanel jp_second_south = new JPanel();
  String cols[] = { "대화명" };
  String data[][] = new String[0][1]; // 왜 0일까??
  DefaultTableModel dtm = new DefaultTableModel(data, cols);
  JTable jtb = new JTable(dtm);
  JScrollPane jsp = new JScrollPane(jtb);
  JTextArea jta_display = new JTextArea(15, 38);
  JScrollPane jsp_display = new JScrollPane(jta_display, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
      JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
  // 서버연결부 선언
  String nickName = null;
  Socket socket = null;
  ObjectOutputStream oos = null; // 말하기
  ObjectInputStream ois = null; // 듣기

  // 화면부 메소드
  public void initDisplay() {
    jbtn_changeName.addActionListener(this);
    jbtn_fontColor.addActionListener(this);
    jbtn_privateChat.addActionListener(this);
    jbtn_groupChat.addActionListener(this);
    jbtn_send.addActionListener(this);
    jtf_msg.addActionListener(this);
    // 사용자 닉네임 받기
    nickName = JOptionPane.showInputDialog("닉네임을 입력하세요");
    this.setLayout(new GridLayout(1, 2));
    // 왼쪽 화면
    jp_first.setLayout(new BorderLayout());
    jp_first.add("Center", jsp_display); // 채팅 출력 부분
    jp_first.add("South", jp_first_south); // 아래 입력 부분
    jp_first_south.setLayout(new BorderLayout());
    jp_first_south.add("Center", jtf_msg); // 입력 jtf
    jp_first_south.add("East", jbtn_send); // 전송 버튼
    jta_display.setLineWrap(true); // 자동 줄바꾸기
    // 오른쪽 화면
    jp_second.setLayout(new BorderLayout());
    jp_second.add("Center", jsp); // 유저 목록 부분
    jp_second.add("South", jp_second_south); // 버튼 부분
    jp_second_south.setLayout(new GridLayout(2, 2));
    jp_second_south.add(jbtn_changeName);
    jp_second_south.add(jbtn_fontColor);
    jp_second_south.add(jbtn_privateChat);
    jp_second_south.add(jbtn_groupChat);
    // 창 기본 세팅
    this.setTitle(nickName + "님의 대화창");
    this.add(jp_first);
    this.add(jp_second);
    this.setSize(700, 400);
    this.setVisible(true);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  }

  // 서버연결부 메소드
  public void init() {
    try {
      socket = new Socket("127.0.0.1", 5555);
      oos = new ObjectOutputStream(socket.getOutputStream());
      ois = new ObjectInputStream(socket.getInputStream());
      // 입장시 100#닉네임 형태로 서버에 말하기
      oos.writeObject(Protocol.TALK_IN + Protocol.seperator + nickName);
      ClientTh cth = new ClientTh(this); // 클라이언트 스레드와 연결
      cth.start(); // ct의 run() 호출
    } catch (Exception e) {
      System.out.println(e.toString()); // 예외원인 클래스명 출력
    }
  }

  public static void main(String[] args) {
    Client c = new Client();
    c.initDisplay();
    c.init();
  }

  @Override
  public void actionPerformed(ActionEvent e) {
    Object obj = e.getSource();
    String msg = jtf_msg.getText();

    // 엔터 혹은 전송버튼
    if (jtf_msg == obj || jbtn_send == obj) {
      try {
        // 200#닉네임#입력한메세지 형태로 서버에 말하기
        oos.writeObject(Protocol.MESSAGE
            + Protocol.seperator + nickName
            + Protocol.seperator + msg);
        jtf_msg.setText(""); // 입력창 비우기
      } catch (Exception e2) {
        e2.printStackTrace();
      }
    }

    // 1:1 채팅 버튼
    else if (jbtn_privateChat == obj) {
      int row = jtb.getSelectedRow(); // 선택한 상대
      // 선택한 상대가 없다면
      if (row == -1) {
        JOptionPane.showMessageDialog(this, "귓속말 상대를 선택하세요.", "info", JOptionPane.INFORMATION_MESSAGE);
        return;
      }
      // 상대를 선택했을 경우
      else {
        String otherName = (String) dtm.getValueAt(row, 0); // 상대 이름
        // 자신을 선택했을 경우
        if (nickName.equals(otherName)) {
          JOptionPane.showMessageDialog(this, "다른 상대를 선택하세요.", "info", JOptionPane.INFORMATION_MESSAGE);
          return;
        }
        String privateMsg = JOptionPane.showInputDialog(otherName + "님에게 보낼 메시지");
        try {
          // 300#닉네임#상대이름#보내는메세지 형태로 서버에 말하기
          oos.writeObject(Protocol.WHISPER
              + Protocol.seperator + nickName
              + Protocol.seperator + otherName
              + Protocol.seperator + privateMsg);
        } catch (Exception e2) {
          e2.printStackTrace();
        }
      }
      jtb.clearSelection(); // 선택한 상대 초기화
    }

    // 대화명 변경 버튼
    else if (jbtn_changeName == obj) {
      String newName = JOptionPane.showInputDialog("변경할 대화명을 입력하세요.");
      // 대화명을 입력하지 않았을 경우
      if (newName == null || newName.trim().length() < 1) {
        JOptionPane.showMessageDialog(this, "변경할 대화명을 입력하세요", "INFO", JOptionPane.INFORMATION_MESSAGE);
        return;
      }
      try {
        // 400#닉네임#새 대화명#닉네임의 대화명이 새 대화명으로 변경되었습니다 형태로 서버에 말하기
        oos.writeObject(Protocol.CHANGE
            + Protocol.seperator + nickName
            + Protocol.seperator + newName
            + Protocol.seperator + nickName + "의 대화명이 " + newName + "으로 변경되었습니다.");
      } catch (Exception e2) {
        e2.printStackTrace();
      }
    }

    // 단체 대화 버튼
    else if (jbtn_groupChat == obj) {
    }

    // 글자색 변경 버튼
    else if (jbtn_fontColor == obj) {
    }
  }
}

 

package dev_java.banana_chat.ex.one;

import java.util.StringTokenizer;
import java.util.Vector;

public class ClientTh extends Thread {
  Client c = null;

  public ClientTh(Client c) {
    this.c = c;
  }

  @Override
  public void run() {
    boolean isStop = false;
    while (!isStop) {
      try {
        String msg = "";
        msg = (String) c.ois.readObject(); // 서버에서 클라이언트에게 전송한 메세지
        StringTokenizer st = null;
        int protocol = 0;
        if (msg != null) {
          st = new StringTokenizer(msg, "#");
          protocol = Integer.parseInt(st.nextToken());
        }
        switch (protocol) {
          // 서버스레드가 말한 것 -> 100#닉네임
          case Protocol.TALK_IN: {
            String nickName = st.nextToken();
            c.jta_display.append(nickName + "님이 입장하였습니다.\n"); // 서버로부터 전달받은 메세지 실제 대화창에 출력
            Vector<String> vName = new Vector<>();
            vName.add(nickName);
            c.dtm.addRow(vName); // 대화방 참가자 목록에 추가
          }
            break;

          // 서버스레드가 말한 것 -> 200#닉네임#입력한메세지
          case Protocol.MESSAGE: {
            String nickName = st.nextToken();
            String message = st.nextToken();
            c.jta_display.append("[" + nickName + "]: " + message + "\n");
            c.jta_display.setCaretPosition(c.jta_display.getDocument().getLength()); // 맨아래로 자동 스크롤
          }
            break;

          // 서버스레드가 말한 것 -> 300#닉네임#상대이름#보내는메세지
          case Protocol.WHISPER: {
            String nickName = st.nextToken();
            String otherName = st.nextToken();
            String message = st.nextToken();
            c.jta_display.append(nickName + "님이 " + otherName + "님에게> " + message + "\n");
            c.jta_display.setCaretPosition(c.jta_display.getDocument().getLength());
          }
            break;

          // 서버스레드가 말한 것 -> 400#닉네임#새 대화명#닉네임의 대화명이 새 대화명으로 변경되었습니다
          case Protocol.CHANGE: {
            String nickName = st.nextToken();
            String newName = st.nextToken();
            String message = st.nextToken();
            // 대화방 참가자 목록의 이름도 변경
            for (int i = 0; i < c.dtm.getRowCount(); i++) {
              String temp = (String) c.dtm.getValueAt(i, 0);
              if (nickName.equals(temp)) {
                c.dtm.setValueAt(newName, i, 0);
                break;
              }
            }
            // JFrame의 타이틀도 변경
            if (nickName.equals(c.nickName)) {
              c.setTitle(newName + "님의 대화창");
              c.nickName = newName;
            }
            c.jta_display.append("<<" + message + ">>\n");
          }
            break;
          default:
            System.out.println("해당하는 프로토콜이 존재하지 않습니다.");
        }
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  }
}

 

package dev_java.banana_chat.ex.two;

import java.util.List;
import java.util.Vector;

public class RoomCopy {
  List<ServerTh> userList = new Vector<>();
  List<String> nameList = new Vector<>();
  String title = null; // 단톡방 이름
  String state = null; // 대기실 혹은 참여중
  int maxNum = 0; // 단톡방 최대 정원 수
  int currentNum = 0; // 헌재 정원 수

  public RoomCopy() {
  }

  public RoomCopy(String title, int currentNum) {
    this.title = title;
    this.currentNum = currentNum;
  }

  public RoomCopy(String title, String state, int currentNum) {
    this.title = title;
    this.state = state;
    this.currentNum = currentNum;
  }

  public String getTitle() {
    return this.title;
  }

  public void setTitle(String title) {
    this.title = title;
  }

  public String getState() {
    return this.state;
  }

  public void setState(String state) {
    this.state = state;
  }

  public int getMaxNum() {
    return this.maxNum;
  }

  public void setMaxNum(int maxNum) {
    this.maxNum = maxNum;
  }

  public int getCurrentNum() {
    return this.currentNum;
  }

  public void setCurrentNum(int currentNum) {
    this.currentNum = currentNum;
  }
}

 

package dev_java.banana_chat.ex.two;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.List;
import java.util.Vector;

public class Server extends JFrame implements ActionListener, Runnable {
  // 화면부 선언
  JButton jbtn_log = new JButton("로그 저장");
  JTextArea jta_log = new JTextArea();
  JScrollPane jsp_log = new JScrollPane(jta_log, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
      JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
  // 서버연결부 선언
  List<ServerTh> globalList = null; // 왜 타입이 sth?? -> 각 클라이언트와 이어진 서버 스레드를 담기위해
  ServerTh sth = null;
  ServerSocket server = null; // 서버소켓
  Socket socket = null; // 클라이언트를 받을 소켓
  // 대화방 선언
  List<RoomCopy> roomList = null;

  // 화면부 메소드
  public void initDisplay() {
    jbtn_log.addActionListener(this);
    this.add("North", jbtn_log);
    this.add("Center", jsp_log);
    this.setSize(400, 300);
    this.setVisible(true);
  }

  public static void main(String[] args) {
    Server s = new Server();
    s.initDisplay(); // 화면
    Thread th = new Thread(s); // 스레드 생성하며 s객체 넘김 ->서버의 run() 호출위헤
    th.start(); // 서버의 run() 호츌
  }

  // 서버연결부 메소드
  @Override
  public void run() {
    globalList = new Vector<>();
    boolean isStop = false;
    try {
      server = new ServerSocket(5555);
      jta_log.append("서버 대기 중...\n");
      while (!isStop) {
        socket = server.accept(); // Client 정보 소켓에 대입
        jta_log.append("Client: " + socket + "\n");
        jta_log.append("Client: " + socket.getInetAddress() + "\n");
        ServerTh sth = new ServerTh(this);
        sth.start(); // 서버스레드의 run() 호출 -> Client의 소켓 정보 넘기기위해
      }
    } catch (Exception e) {
      e.printStackTrace();
    }

  }

  @Override
  public void actionPerformed(ActionEvent e) {
    Object obj = e.getSource();
    if (obj == jbtn_log) {
      // 로그를 파일로 저장하기
    }
  }
}

 

package dev_java.banana_chat.ex.two;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.StringTokenizer;
import javax.swing.JOptionPane;

public class ServerTh extends Thread {
  // 선언부
  Server s = null;
  Socket client = null;
  ObjectInputStream ois = null;
  ObjectOutputStream oos = null;
  String nickName = null;
  String g_title = null; // 이 스레드가 들어간 단톡방 이름
  int g_current = 0; // 이 스레드가 들어간 단톡방 현재 인원 수

  // 생성부
  public ServerTh(Server s) {
    this.s = s;
    this.client = s.socket; // 서버에서 받아온 소켓 대입
    try {
      oos = new ObjectOutputStream(client.getOutputStream()); // 말하기
      ois = new ObjectInputStream(client.getInputStream()); // 듣기
      String msg = (String) ois.readObject(); // 들은 것 msg에 저장(입장시 말한 100#닉네임)
      s.jta_log.append(msg + "\n"); // 로그에 msg 출력
      s.jta_log.setCaretPosition(s.jta_log.getDocument().getLength()); // 맨아래로 자동 스크롤
      StringTokenizer st = null;
      if (msg != null) { // 만약 msg가 있다면
        st = new StringTokenizer(msg, "#"); // 구분자 #를 기준으로 msg 문자열 분리
      }
      if (st.hasMoreTokens()) { // 만약 남아있는 토큰이 있다면
        st.nextToken(); // 첫번째 것, 프로토콜 스킵처리
        nickName = st.nextToken(); // 닉네임 저장
        g_title = st.nextToken(); // 단톡방 이름
        s.jta_log.append(nickName + "님이 입장하였습니다.\n");
        s.jta_log.append("위치: " + g_title + "\n"); // 현재 어느 대화방에 있는지 표시
      }
      for (ServerTh sth : s.globalList) { // globalList에 저장된 sth에 반복
        String currentName = sth.nickName; // sth의 닉네임
        String currentState = sth.g_title; // sth의 단톡방 이름
        // this 사용 -> 이 서버 스레드(와 연결된 특정 클라이언트)
        // sth 사용 -> globalList에 담긴 모든 서버 스레드(와 클라이언트)
        this.send(Protocol.WAIT // WAIT#sth닉네임#sth단톡방이름 -> WAIT는 뭘까??
            + Protocol.seperator + currentName
            + Protocol.seperator + currentState);
      } // 대화목록관리(닉네임, 단톡방이름) 전송
      for (int i = 0; i < s.roomList.size(); i++) { // 방 개수
        RoomCopy room = s.roomList.get(i);
        String title = room.title; // 단톡방 이름
        g_title = title;
        int current = 0; // 단톡방 현재 정원
        if (room.userList != null && room.userList.size() != 0) { // 단톡방에 사람이 있을 경우
          current = room.userList.size();
        }
        g_current = current;
        this.send(Protocol.ROOM_LIST // 700#단톡방이름#단톡방멤버수
            + Protocol.seperator + g_title
            + Protocol.seperator + g_current);
      } // 대화방 관리(단톡방이름, 단톡방 멤버 수) 전송
      s.globalList.add(this); // 현재 서버에 입장한 클라이언트 스레드 추가
      this.broadCasting(msg); // 클라이언트의 말을 전체 사람들에게 전달
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  // 클라이언트에게 말하기(말 전달하기)
  public void send(String msg) {
    try {
      oos.writeObject(msg); // 클라이언트가 말한 메세지를 다시 전달하기
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  // 현재 입장해있는 사람들 모두에게 메세지 전송
  public void broadCasting(String msg) {
    synchronized (this) { // 동기화, 이 코드가 실행되는동안 sth의 다른 동기화 메소드는 기다림
      for (ServerTh sth : s.globalList) { // globalList에 저장된 sth의 msg 반복 전송
        sth.send(msg);
      }
    }
  }

  // 이 서버스레드가 속한 단톡방에서 말하기
  protected void broadcast(String msg) {
    synchronized (this) {
      for (int i = 0; i < s.roomList.size(); i++) {
        RoomCopy room = s.roomList.get(i);
        if (g_title.equals(room.title)) { // 단톡방 이름이 같다면
          for (int j = 0; j < room.userList.size(); j++) {
            ServerTh sth = room.userList.get(j);
            try {
              sth.send(msg);
            } catch (Exception e) {
              room.userList.remove(j--); // 유저리스트 삭제 -> 그럼 g_title을 사람마다 다르게 설정?
            }
          }
          break;
        }
      }
    }
  }

  // 단톡방에서 말하기
  public void roomCasting(String msg, String roomTitle) {
    for (int i = 0; i < s.roomList.size(); i++) {
      RoomCopy room = s.roomList.get(i);
      if (roomTitle.equals(room.title)) {
        for (int j = 0; j < room.userList.size(); j++) {
          ServerTh sth = room.userList.get(j);
          try {
            sth.send(msg);
          } catch (Exception e) {
            e.printStackTrace();
          }
        }
      }
    }
  }

  @Override
  public void run() {
    String msg = null;
    boolean isStop = false;
    try {
      run_start: while (!isStop) { // while문 라벨붙임
        msg = (String) ois.readObject(); // 듣기
        s.jta_log.append(msg + "\n");
        s.jta_log.setCaretPosition(s.jta_log.getDocument().getLength());
        StringTokenizer st = null;
        int protocol = 0;
        if (msg != null) {
          st = new StringTokenizer(msg, "#"); // msg를 #로 구분
          protocol = Integer.parseInt(st.nextToken()); // 읽어들인 프로토콜 인트로 전환
        }
        switch (protocol) {
          // 710#방이름#인원수
          case Protocol.ROOM_CREATE: {
            String roomTitle = st.nextToken();
            String curretNum = st.nextToken();
            RoomCopy room = new RoomCopy(roomTitle, Integer.parseInt(curretNum));
            room.setTitle(roomTitle);
            room.setCurrentNum(Integer.parseInt(curretNum));
            this.broadCasting(Protocol.ROOM_CREATE
                + Protocol.seperator + roomTitle
                + Protocol.seperator + curretNum);
          }
            break;

          // 720#단톡방이름#단톡방멤버수#닉네임#temp??
          case Protocol.ROOM_IN: {
            String roomTitle = st.nextToken();
            String nickName = st.nextToken();
            StringBuffer names = new StringBuffer(); // 문자열 추가, 변경 -> ???????
            String temp = null; // ????????????
            for (int i = 0; i < s.roomList.size(); i++) {
              RoomCopy room = s.roomList.get(i);
              if (roomTitle.equals(room.title)) { // 단톡방 이름이 같다면
                g_title = roomTitle;
                g_current = room.currentNum + 1;
                room.setCurrentNum(g_current); // 인원수 업데이트
                room.userList.add(this); // 이 서버스레드(클라이언트)를 단톡방에 넣음
                JOptionPane.showMessageDialog(s, "s.globalList" + s.globalList + " room.userList" + room.userList);
                room.nameList.add(nickName); // 유저리스트에 닉네임 추가
              }
            }
            for (int i = 0; i < s.roomList.size(); i++) { // 방 개수
              RoomCopy room = s.roomList.get(i);
              String title = room.title;
              g_title = title;
              int current = 0;
              if (room.userList != null && room.userList.size() != 0) { // 단톡방에 사람이 있을 때
                current = room.userList.size();
              }
              for (int j = 0; j < room.nameList.size(); j++) {
                // 닉네임은 다르고 사람 중 방 이름만 같은 경우 클라이언트로 전송
                if (!nickName.equals(room.nameList.get(j))) { // 닉네임과 같지 않고
                  if (roomTitle.equals(room.title)) { // 단톡방 이름은 같은 경우
                    ServerTh sth = room.userList.get(j);
                    // 730#단톡방이름#단톡방멤버수#닉네임
                    sth.send(Protocol.ROOM_INLIST
                        + Protocol.seperator + g_title
                        + Protocol.seperator + g_current
                        + Protocol.seperator + nickName);
                  }
                }
              }
            } // 대화방 관리
            broadCasting(Protocol.ROOM_IN
                + Protocol.seperator + g_title
                + Protocol.seperator + g_current
                + Protocol.seperator + this.nickName
                + Protocol.seperator + temp);
          }
            break;

          // 단톡방에서 말하기
          case Protocol.MESSAGE: {
            String roomTitle = st.nextToken();
            String nickName = st.nextToken();
            String message = st.nextToken();
            String fontColor = st.nextToken();
            String imgChoice = "";
            while (st.hasMoreTokens()) {
              imgChoice = st.nextToken();
            }
            // 200#닉네임#메세지#폰트컬러#이미지초이스?
            this.roomCasting(Protocol.MESSAGE
                + Protocol.seperator + nickName
                + Protocol.seperator + message
                + Protocol.seperator + fontColor
                + Protocol.seperator + imgChoice, roomTitle);
          }
            break;

          // 단톡방 나가기
          case Protocol.ROOM_OUT: {
            String nickName = st.nextToken();
            s.globalList.remove(this); // 이 스레드를 제거
            String message = Protocol.ROOM_OUT
                + Protocol.seperator + nickName;
            this.broadCasting(message);
          }
            break run_start;
        }
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

댓글