[JAVA] 다중 채팅 프로그램

남우진·2022년 1월 17일
2
post-thumbnail

다중 채팅 프로그램?? 💡

  • JAVA 언어의 소켓 프로그래밍을 활용한 채팅 프로그램이다.
  • Swing을 활용해 프로그램의 GUI를 만들었다.
  • Server와 Client(s)간의 소통한다.

프로그램 작동 원리

  • Input과 Output을 통해 서로 값을 주고 받는다.
  • Client(s)는 메세지를 서버로 Output(전송) 해주고,
  • Server는 그 값을 각 Clients(s)로 Output(전송) 시켜준다.

실행 순서

  1. Server_GUI.java를 실행한다.

  2. 실행 후 ManagerLogin 클래스가 실행되어 로그인GUI가 켜진다.

  3. 0 ~ 65535까지 입력을 하면, 채팅 화면으로 넘어간다.
    또한, 콘솔창에 "현재 아이피와 포트넘버는 [ip주소], [port번호]입니다."가 출력되는 것을 확인할 수 있다.

  4. 채팅화면은 아래의 사진과 같으며 하단의 텍스트필드 통해 Enter키 혹은 전송버튼을 누르면 입력된 메세지들이 전송된다.

  5. 이후론 Client_GUI.java를 실행한다.

  6. 접속하고자하는 IP주소와 포트 그리고 본인의 닉네임을 입력한다.

  7. 접속 후 채팅 화면으로 넘어간다.

  8. 이후 추가적인 "6번" 실행이 있으면 그에 맞춰 추가적인 클라이언트가 접속한다.

  9. "8번"을 통해 생성된 클라이언트들간의 다중 채팅이 가능해진다.

실행 화면

  1. Server_GUI.java를 실행하면 아래와 같은 화면을 통해 포트를 입력하게 된다.

  2. 포트 입력 후 메인 서버용 채팅창이 켜지게 된다.
    구성 : 채팅창(좌측), 유저목록(우측), 입력창(하단) / 아래는 콘솔창

  3. 이후 Client_GUI.java를 실행하여 로그인 화면을 켠다.

  • 로그인 화면

  • 잘못된 입력을 기입할 시

  1. 입력이 완료되어 정상 접속이 되면 채팅창이 열린다.
  • 입장하면, 닉네임 + 입장하셨습니다. 가 출력이 됩니다.
  • 또한, 퇴장시 닉네임 + 퇴장하셨습니다. 가 출력이 됩니다.

실행 코드

1. Server_GUI.java

import java.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class Server_GUI {

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

class ManagerLogin extends JFrame implements ActionListener, KeyListener { 
	// 로그인 창
	Server_ChatGUI Server_chat = null;
	JPanel Port_Log = new JPanel();
	JLabel Port_Label = new JLabel("입력을 허용할 포트 번호를 입력하세요.");
	JLabel Port_Warning = new JLabel("(단, 포트 번호는 0 ~ 65535까지)");
	JTextField Port_Text = new JTextField(20);
	JButton Port_Enter = new JButton("접속!");

	public ManagerLogin() {
		setTitle("서버 메니저 창");
		setLocationRelativeTo(null);
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 닫았을 때 메모리에서 제거되도록 설정합니다.
		setSize(300, 120);
		setVisible(true);
		setResizable(false);
		Port_Enter.addActionListener(this);
		Port_Text.addKeyListener(this);
		Port_Log.add(Port_Label);
		Port_Log.add(Port_Warning);
		Port_Log.add(Port_Text);
		Port_Log.add(Port_Enter);
		add(Port_Log);
	}

	public void actionPerformed(ActionEvent e) { 
		// "접속!" 버튼을 누르면 작동이 됩니다.
		try {
			int Port = Integer.parseInt(Port_Text.getText().trim());
			if (e.getSource() == Port_Enter) {
				Server_chat = new Server_ChatGUI(Port);
				setVisible(false);
			}
		} catch (Exception a) {
			JOptionPane.showMessageDialog(null, "올바르지 않은 입력입니다!");
		}
	}

	public void keyPressed(KeyEvent e) { 
		// 텍스트필드에 값을 입력한 후 엔터키를 누르면 작동이 됩니다.
		try {
			if (e.getKeyCode() == KeyEvent.VK_ENTER) {
				int Port = Integer.parseInt(Port_Text.getText().trim());
				Server_chat = new Server_ChatGUI(Port);
				setVisible(false);
			}
		} catch (Exception a) {
			JOptionPane.showMessageDialog(null, "올바르지 않은 입력입니다!");
		}

	}

	public void keyTyped(KeyEvent e) { // 불필요
	}

	public void keyReleased(KeyEvent e) { // 불필요
	}

}

class Server_ChatGUI extends JFrame implements ActionListener, KeyListener {
	// 서버용 채팅창
	JPanel ServerGUI_Panel = new JPanel();
	JLabel ServerLabel = new JLabel("Main Server");
	JLabel UserLabel = new JLabel("유저 목록");
	JTextField Chat = new JTextField(45);
	JButton Enter = new JButton("전송");
	TextArea ServerChatList = new TextArea(30, 50);
	TextArea UserList = new TextArea(30, 15);
	Server_Back SB = new Server_Back();

	public Server_ChatGUI(int Port) {
		setTitle("메인 서버");
		setVisible(true);
		setLocationRelativeTo(null);
		setSize(750, 520);
		setResizable(false);
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 닫았을 때 메모리에서 제거되도록 설정합니다.

		ServerChatList.setEditable(false);
		UserList.setEditable(false);
		Chat.addKeyListener(this);
		Enter.addActionListener(this);

		ServerGUI_Panel.add(ServerLabel);
		ServerGUI_Panel.add(ServerChatList);
		ServerGUI_Panel.add(UserLabel);
		ServerGUI_Panel.add(UserList);
		ServerGUI_Panel.add(Chat);
		ServerGUI_Panel.add(Enter);
		add(ServerGUI_Panel);

		UserList.append("Admin\n"); // 실행과 동시에 서버주인(Admin)을 유저목록에 추가하도록 합니다.
		SB.setGUI(this);
		SB.Start_Server(Port);
		SB.start(); // 서버 채팅창이 켜짐과 동시에 서버소켓도 함께 켜집니다.
	}

	public void actionPerformed(ActionEvent e) { // 전송 버튼을 누르고, 입력값이 1이상일때만 전송되도록 합니다.
		String Message = Chat.getText().trim();
		if (e.getSource() == Enter && Message.length() > 0) {
			AppendMessage("서버 : " + Message + "\n");
			SB.Transmitall("서버 : " + Message + "\n");
			Chat.setText(null); // 채팅창 입력값을 초기화 시켜줍니다.
		}
	}

	public void keyPressed(KeyEvent e) { // 키보드 엔터키를 누르고, 입력값이 1이상일때만 전송되도록 합니다.
		String Message = Chat.getText().trim();
		if (e.getKeyCode() == KeyEvent.VK_ENTER && Message.length() > 0) {
			AppendMessage("서버 : " + Message + "\n");
			SB.Transmitall("서버 : " + Message + "\n");
			Chat.setText(null); // 채팅창 입력값을 초기화 시켜줍니다.
		}
	}

	public void AppendMessage(String Message) {
		ServerChatList.append(Message);
	}

	public void AppendUserList(ArrayList NickName) {
		String name;
		for (int i = 0; i < NickName.size(); i++) {
			name = (String) NickName.get(i);
			UserList.append(name + "\n");
		}
	}

	public void keyTyped(KeyEvent e) {
	}

	public void keyReleased(KeyEvent e) {
	}
}

2. Server_Back.java

import java.net.*;
import java.io.*;
import java.util.*;

public class Server_Back extends Thread {
	Vector<ReceiveInfo> ClientList = new Vector<ReceiveInfo>(); // 클라이언트의 쓰레드를 저장해줍니다.
	ArrayList<String> NickNameList = new ArrayList<String>(); // 클라이언트의 닉네임을 저장해줍니다.
	ServerSocket serversocket;
	Socket socket;
	private Server_ChatGUI serverchatgui;

	public void setGUI(Server_ChatGUI serverchatgui) {
		this.serverchatgui = serverchatgui;
	}

	public void Start_Server(int Port) {
		try {
			Collections.synchronizedList(ClientList); // 교통정리를 해준다.( clientList를 네트워크 처리해주는것 )
			serversocket = new ServerSocket(Port); // 서버에 입력된 특정 Port만 접속을 허가하기 위해 사용했습니다.
			System.out.println("현재 아이피와 포트넘버는 [" + InetAddress.getLocalHost() + "], [" + Port + "] 입니다.");
		} catch (Exception e) {
			System.out.println(e.getMessage());
		}
	}

	public void run() {
		try {
			NickNameList.add("Admin"); // 유저목록의 첫 번째 서버(Admin)을 추가합니다.
			while (true) {
				System.out.println("새 접속을 대기합니다...");
				socket = serversocket.accept(); // 포트 번호와 일치한 클라이언트의 소켓을 받습니다.
				System.out.println("[" + socket.getInetAddress() + "]에서 접속하셨습니다.");
				ReceiveInfo receive = new ReceiveInfo(socket);
				ClientList.add(receive);
				receive.start();
			}
		} catch (Exception e) {
			System.out.println(e.getMessage());
		}
	}

	public void Transmitall(String Message) {
		// 모든 클라이언트들에게 메세지를 전송해줍니다.
		for (int i = 0; i < ClientList.size(); i++) {
			try {
				ReceiveInfo ri = ClientList.elementAt(i);
				ri.Transmit(Message);
			} catch (Exception e) {
				System.out.println(e.getMessage());
			}
		}
	}

	public void removeClient(ReceiveInfo Client, String NickName) {
		// 퇴장한 유저 발생시 목록에서 삭제하는 역할을 합니다.
		ClientList.removeElement(Client);
		NickNameList.remove(NickName);
		System.out.println(NickName + "을 삭제 완료했습니다.");
		serverchatgui.UserList.setText(null);
		serverchatgui.AppendUserList(NickNameList);
	}

	class ReceiveInfo extends Thread { 
		// 각 네트워크(클라이언트)로부터 소켓을 받아 다시 내보내는 역할을 합니다.
		private DataInputStream in;
		private DataOutputStream out;
		String NickName;
		String Message;

		public ReceiveInfo(Socket socket) {
			try {
				out = new DataOutputStream(socket.getOutputStream()); // Output
				in = new DataInputStream(socket.getInputStream()); // Input
				NickName = in.readUTF();
				NickNameList.add(NickName);
			} catch (IOException e) {
				System.out.println(e.getMessage());
			}
		}

		public void run() {
			try {
				// 새로운 유저 발생시 유저목록을 초기화 합니다.
				// 후에 새롭게 유저목록을 입력해줍니다.
				// 또한, 새로운 유저가 입장했음을 모든 클라이언트에게 전송합니다.
				serverchatgui.UserList.setText(null); 
				serverchatgui.AppendUserList(NickNameList);
				Transmitall(NickName + "님이 입장하셨습니다.\n");
				for (int i = 0; i < NickNameList.size(); i++) {
					// +++닉네임의시작+++은 해당 값이 닉네임임을 알게해주는 식별자이며
					// 실전에서는 더욱 암호화된 값을 적용시켜 혼동 발생을 막아줍니다.
					Transmitall("+++닉네임의시작+++" + NickNameList.get(i));
				}
				serverchatgui.AppendMessage(NickName + "님이 입장하셨습니다.\n");
				while (true) {
					Message = in.readUTF();
					serverchatgui.AppendMessage(Message);
					Transmitall(Message);
				}
			} catch (Exception e) {
				// 유저가 접속을 종류하면 여기서 오류가 발생합니다.
				// 따라서 발생한 값을 다시 모든 클라이언트 들에게 전송시켜줍니다.
				System.out.println(NickName + "님이 퇴장하셨습니다.");
				removeClient(this, NickName);
				Transmitall(NickName + "님이 퇴장하셨습니다.\n");
				for (int i = 0; i < NickNameList.size(); i++) {
					Transmitall("+++닉네임의시작+++" + NickNameList.get(i));
				}
				serverchatgui.AppendMessage(NickName + "님이 퇴장하셨습니다.\n");
			}
		}

		public void Transmit(String Message) {
			// 전달받은 값(Message)를 각 클라이언트의 쓰레드에 맞춰 전송합니다.
			try {
				out.writeUTF(Message);
				out.flush();
			} catch (Exception e) {
				e.getStackTrace();
			}

		}
	}
}

3. Client_GUI.java

import java.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class Client_GUI {
	public static void main(String[] args) {
		LoginGUI LG = new LoginGUI();
	}
}

class LoginGUI extends JFrame implements ActionListener {
	// 유저의 로그인 창
	private JPanel Login_GUIPanel = new JPanel();
	private JTextField NickName_Text = new JTextField(20);
	private JTextField Port_Text = new JTextField("####", 20);
	private JTextField IPAddress_Text = new JTextField("###.###.###.###", 20);
	private JLabel NickName_Label = new JLabel("유저 입력");
	private JLabel Port_Label = new JLabel("포트 입력");
	private JLabel IPAddress_Label = new JLabel("주소 입력");
	private JButton Login_GUI_Button = new JButton("접속!");

	public LoginGUI() {
		setTitle("로그인 화면");
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		setLocationRelativeTo(null);
		setSize(300, 170);
		setResizable(false);
		setVisible(true);
		Login_GUI_Button.setPreferredSize(new Dimension(260, 40));
		Login_GUI_Button.addActionListener(this);
		Login_GUIPanel.add(NickName_Label);
		Login_GUIPanel.add(NickName_Text);
		Login_GUIPanel.add(Port_Label);
		Login_GUIPanel.add(Port_Text);
		Login_GUIPanel.add(IPAddress_Label);
		Login_GUIPanel.add(IPAddress_Text);
		Login_GUIPanel.add(Login_GUI_Button);
		add(Login_GUIPanel);
	}

	public void actionPerformed(ActionEvent e) {
		// 닉네임, 주소, 포트값을 버튼을 통해 입력받습니다.
		try {
			if (e.getSource() == Login_GUI_Button) {
				String NickName = NickName_Text.getText().trim();
				String IPAddress = IPAddress_Text.getText().trim();
				int Port = Integer.parseInt(Port_Text.getText().trim());
				new Client_ChatGUI(NickName, IPAddress, Port);
				setVisible(false);
			}
		} catch (Exception a) {
			// 만약 올바르지 않는 값이 입력되면 팝업창을 띄워줍니다.
			JOptionPane.showMessageDialog(null, "올바르지 않은 입력입니다!");
		}
	}
}

class Client_ChatGUI extends JFrame implements ActionListener, KeyListener {
	//클라이언트용 채팅창
	String NickName;
	Client_Back CB = new Client_Back();
	JPanel ClientGUIPanel = new JPanel();
	JLabel UserLabel = new JLabel("유저 목록");
	JLabel User = new JLabel(NickName);
	JTextField Chat = new JTextField(45);
	JButton Enter = new JButton("전송");
	TextArea ChatList = new TextArea(30, 50);
	TextArea UserList = new TextArea(30, 15);

	public Client_ChatGUI(String NickName, String IPAddress, int Port) {
		this.NickName = NickName;

		setTitle("고객 창");
		setVisible(true);
		setLocationRelativeTo(null);
		setSize(750, 530);
		setResizable(false);
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		ChatList.setEditable(false);
		UserList.setEditable(false);
		Chat.addKeyListener(this);
		Enter.addActionListener(this);

		ClientGUIPanel.add(User);
		ClientGUIPanel.add(ChatList);
		ClientGUIPanel.add(UserLabel);
		ClientGUIPanel.add(UserList);
		ClientGUIPanel.add(Chat);
		ClientGUIPanel.add(Enter);
		add(ClientGUIPanel);
		CB.setGui(this);
		CB.getUserInfo(NickName, IPAddress, Port);
		CB.start(); // 채팅창이 켜짐과 동시에 접속을 실행해줍니다.
	}

	public void actionPerformed(ActionEvent e) { 
		// 전송 버튼을 누르고, 입력값이 1이상일때만 전송되도록 합니다.
		String Message = Chat.getText().trim();
		if (e.getSource() == Enter && Message.length() > 0) {
			CB.Transmit(NickName + " : " + Message + "\n");
			Chat.setText(null);
		}
	}

	public void keyPressed(KeyEvent e) { 
		// 키보드 엔터키를 누르고, 입력값이 1이상일때만 전송되도록 합니다.
		String Message = Chat.getText().trim();
		if (e.getKeyCode() == KeyEvent.VK_ENTER && Message.length() > 0) {
			CB.Transmit(NickName + " : " + Message + "\n");
			Chat.setText(null);
		}
	}

	public void AppendMessage(String Message) {
		ChatList.append(Message);
	}

	public void AppendUserList(ArrayList NickName) {
		// 유저목록을 유저리스트에 띄워줍니다.
		String name;
		UserList.setText(null);
		for (int i = 0; i < NickName.size(); i++) {
			name = (String) NickName.get(i);
			UserList.append(name + "\n");
		}
	}

	public void keyTyped(KeyEvent e) {
	}

	public void keyReleased(KeyEvent e) {
	}
}

4. Client_Back.java

import java.io.*;
import java.net.*;
import java.util.*;

public class Client_Back extends Thread {
	private String NickName, IPAddress;
	private int Port;
	private Socket socket;
	private String Message;
	private DataInputStream in;
	private DataOutputStream out;
	private Client_ChatGUI chatgui;
	ArrayList<String> NickNameList = new ArrayList<String>(); // 유저목록을 저장합니다.

	public void getUserInfo(String NickName, String IPAddress, int Port) {
		// Client_GUI로부터 닉네임, 아이피, 포트 값을 받아옵니다.
		this.NickName = NickName;
		this.IPAddress = IPAddress;
		this.Port = Port;
	}

	public void setGui(Client_ChatGUI chatgui) {
		// 실행했던 Client_GUI 그 자체의 정보를 들고옵니다.
		this.chatgui = chatgui;
	}

	public void run() {
		// 서버 접속을 실행합니다.
		try {
			socket = new Socket(IPAddress, Port);
			out = new DataOutputStream(socket.getOutputStream());
			in = new DataInputStream(socket.getInputStream());
			out.writeUTF(NickName);
			while (in != null) { 
				// 임의의 식별자를 받아 닉네임 혹은 일반 메세지인지 등을 구분시킵니다.
				Message = in.readUTF();
				if (Message.contains("+++닉네임의시작+++")) { 
					// +++닉네임의시작+++이라는 수식어가 붙어있을 경우엔 닉네임으로 간주합니다.
					chatgui.UserList.setText(null);
					NickNameList.add(Message.substring(12));
					chatgui.AppendUserList(NickNameList);
				} else if (Message.contains("님이 입장하셨습니다.")) {
					// ~~ 님이 입장하셨습니다. 라는 식별자를 받으면 기존의 닉네임 리스트 초기화 후 새로 입력시킵니다.
					NickNameList.clear();
					chatgui.UserList.setText(null);
					chatgui.AppendMessage(Message);
				} else if (Message.contains("님이 퇴장하셨습니다.")) {
					// ~~ 님이 퇴장하셨습니다. 라는 식별자를 받으면 기존의 닉네임 리스트 초기화 후 새로 입력시킵니다.
					NickNameList.clear();
					chatgui.UserList.setText(null);
					chatgui.AppendMessage(Message);
				} else {
					// 위 모든 값이 아닐 시엔 일반 메세지로 간주합니다.
					chatgui.AppendMessage(Message);
				}
			}
		} catch (Exception e) {
			System.out.println(e.getMessage());
		}
	}

	public void Transmit(String Message) {
		// 입력받은 값을 서버로 전송(out) 해줍니다.
		try {
			out.writeUTF(Message);
			out.flush();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

후기

소켓 프로그래밍은 저에게 다소 생소한 작업이었습니다. 이번 프로젝트에서는 특히 각 클라이언트들로부터 In/Output을 할 때 어떻게 해야 그 값을 구분할 수 있는지 정말 많은 고민을 했었던것 같습니다. 가장 어려웠던 것은 유저 목록을 받고 그 값을 다시 올려주는 과정이었지만, 해결을 통해 성취감과 자바의 많은 기능을 이해할 수 있던 좋은 기회였다고 생각합니다.

profile
Passion and Growth!!!

3개의 댓글

comment-user-thumbnail
2023년 2월 16일

이거 참고해도 괜찮을까요?

1개의 답글
comment-user-thumbnail
2023년 5월 21일

안녕하세요. 혹시 노트북 2대로는 사용이 안 되는 건가요? 두 대로 해서 연결해보려 하니 오류가 떠서요!

답글 달기