다중 채팅 프로그램?? 💡
- JAVA 언어의 소켓 프로그래밍을 활용한 채팅 프로그램이다.
- Swing을 활용해 프로그램의 GUI를 만들었다.
- Server와 Client(s)간의 소통한다.
Server_GUI.java를 실행한다.
실행 후 ManagerLogin 클래스가 실행되어 로그인GUI가 켜진다.
0 ~ 65535까지 입력을 하면, 채팅 화면으로 넘어간다.
또한, 콘솔창에 "현재 아이피와 포트넘버는 [ip주소], [port번호]입니다."가 출력되는 것을 확인할 수 있다.
채팅화면은 아래의 사진과 같으며 하단의 텍스트필드 통해 Enter키 혹은 전송버튼을 누르면 입력된 메세지들이 전송된다.
이후론 Client_GUI.java를 실행한다.
접속하고자하는 IP주소와 포트 그리고 본인의 닉네임을 입력한다.
접속 후 채팅 화면으로 넘어간다.
이후 추가적인 "6번" 실행이 있으면 그에 맞춰 추가적인 클라이언트가 접속한다.
"8번"을 통해 생성된 클라이언트들간의 다중 채팅이 가능해진다.
Server_GUI.java를 실행하면 아래와 같은 화면을 통해 포트를 입력하게 된다.
포트 입력 후 메인 서버용 채팅창이 켜지게 된다.
구성 : 채팅창(좌측), 유저목록(우측), 입력창(하단) / 아래는 콘솔창
이후 Client_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) {
}
}
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();
}
}
}
}
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) {
}
}
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을 할 때 어떻게 해야 그 값을 구분할 수 있는지 정말 많은 고민을 했었던것 같습니다. 가장 어려웠던 것은 유저 목록을 받고 그 값을 다시 올려주는 과정이었지만, 해결을 통해 성취감과 자바의 많은 기능을 이해할 수 있던 좋은 기회였다고 생각합니다.
이거 참고해도 괜찮을까요?