[Java] 멀티 소켓 통신 채팅 프로그램 tcp 서버/클라이언트 구조 - Swing GUI(2)
전 포스팅에서 어느 정도 윤곽이 잡힌 멀티 채팅 프로그램을 만들었었는데요. 해당 프로그램에는 커다란 문제점이 존재했습니다. 바로 클라이언트 측에서 갑자기 인터넷 통신이 끊긴다던지, 비정상적으로 프로그램을 종료한다던지 해서 클라이언트 측에서 소켓이 끊겼을 때 서버 측에서 이를 감지하지 못하고 해당 소켓을 열어둔 채 대기하고 있는 상황이 발생하는 것입니다. 그것으로 인해 종료된 클라이언트(유저)가 유령처럼 남아서 다른 클라이언트가 들어왔을 때 채팅방에 존재하는 것으로 인식하는 섬뜩한(?) 문제가 발생하였습니다.
그래서 이를 해결하기 위하여 저같은 경우 특정한 방법을 사용해보았습니다. 먼저 서버 측 프로그램에
Socket.setSoTimeout(15000), 즉 소켓에 read() timeout을 15초 할당해 주었습니다. (현재 방식은 blocking IO 방식으로 setSoTimeout을 이용하여 timeout을 설정해주면 15초 동안 read 값이 아무 것도 들어오지 않았을 때 timeout 예외가 일어나도록 해준 것입니다.)
그런 다음 클라이언트 프로그램에선 Sender 스레드를 하나 만들어 주고 10초 주기마다 자신이 살아있다는 alive 신호를 날려주도록 했습니다.
이 원리를 이용하면 당연하게도 정상적인 상황에서는 해당 신호를 계속 주기 때문에 timeout 예외가 발생하지 않지만 클라이언트가 다운되어 신호를 주지 못하는 상황이 되었을 때 timeout처리가 되게 됩니다. (heartbeat방식과 유사합니다)
뿐만 아니라 메시지를 주고 받는 프로토콜 또한 해쉬 맵을 이용하여 주고받아 조금 더 쉽게 정리할 수 있도록 구조를 많이 바꿔보았습니다.
코드가 궁금하신 분들을 위해 아래에 서버, 클라이언트 코드를 남기겠습니다~ UI 쪽은 저번과 유사합니다
먼저 서버 코드입니다.
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Timer;
import java.util.TimerTask;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
public class msg_server extends Thread {
static ArrayList<msg_server> users = new ArrayList<msg_server>();
Socket socket;
String nick="";
HashMap<String, Object> Data=new HashMap<String,Object>();
InputStream in;
ObjectInputStream ois;
OutputStream out;
ObjectOutputStream oos;
static ServerSocket ss;
public msg_server(Socket socket) {
this.socket = socket;
nick="";
try {
in=socket.getInputStream();
out=socket.getOutputStream();
ois=new ObjectInputStream(in);
oos=new ObjectOutputStream(out);
this.socket.setSoTimeout(15000);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void broadCast(String msg) throws Exception {
for(msg_server user :users)
{ user.send(msg);}
}
public void broadCast(String[] userList) throws Exception {
for(msg_server user :users)
{ user.updateUsersList(userList);}
}
public void updateUsersList(String[] userList) throws Exception {
HashMap<String,Object> map = new HashMap<String,Object>();
map.put("protocol", "update");
map.put("userList", userList);
oos.writeObject(map);
}
public void send(String msg) throws Exception {
HashMap<String,Object> map = new HashMap<String,Object>();
map.put("protocol", "msg");
map.put("message", msg);
oos.writeObject(map);
}
@Override
public synchronized void run() {
try {
while (true) {
Data= (HashMap<String, Object>) ois.readObject();
if (Data.get("protocol")==null) break;
String protocol=(String)Data.get("protocol");
switch(protocol) {
case "setName":
this.nick=(String)Data.get("name");
broadCast("Welcome. "+this.nick);
ArrayList<String> names =new ArrayList<String>();
for(msg_server user : users) {
names.add(user.nick);
}
broadCast(names.toArray(new String[names.size()]));
break;
case "quit":
send("quit");
break;
case "msg":
broadCast(this.nick+": "+(String) Data.get("message"));
break;
case "alive":
break;
}
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
finally{
System.out.println("socket disconnect");
try {
in.close();
out.close();
ois.close();
oos.close();
socket.close();
users.remove(this);
ArrayList<String> names =new ArrayList<String>();
for(msg_server user : users) {
names.add(user.nick);
}
broadCast(names.toArray(new String[names.size()]));
broadCast(nick+ " has left the chat");
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
int port = 2400;
try {
ss = new ServerSocket(port);
System.out.println("Server Open");
while (true) {
Socket cs = ss.accept();
System.out.println("Client " + cs.getRemoteSocketAddress() + " : " + cs.getPort());
msg_server serverThread = new msg_server(cs);
serverThread.start();
users.add(serverThread);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
try {
ss.close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}
}
다음은 클라이언트 코드입니다.
import java.net.Socket;
import java.net.URL;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.swing.*;
import javax.swing.border.Border;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Scanner;
import java.util.regex.Pattern;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.lang.reflect.Field;
public class msg_client {
public static void main(String[] args) {
new StartFrame();
}
}
class StartFrame extends JFrame {
JTextField ipnum, name;
ImageIcon icon;
URL imgURL;
JPanel down_panel;
public StartFrame() {
setTitle("connect");
// setSize(500,300);
setSize((int) Toolkit.getDefaultToolkit().getScreenSize().getWidth() / 3,
(int) Toolkit.getDefaultToolkit().getScreenSize().getHeight() / 2);
setLocation(
(int) (Toolkit.getDefaultToolkit().getScreenSize().getWidth() / 2)
- (int) (this.getSize().getWidth() / 2),
(int) (Toolkit.getDefaultToolkit().getScreenSize().getHeight() / 2)
- (int) (this.getSize().getHeight() / 2));
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setResizable(true);
imgURL = getClass().getClassLoader().getResource("Title.png");
icon = new ImageIcon(imgURL);
Image img = icon.getImage();
img = img.getScaledInstance((int) getSize().getWidth(),
(int) getSize().getHeight() - (int) (getSize().getHeight() / 10), java.awt.Image.SCALE_SMOOTH);
icon = new ImageIcon(img);
JPanel background = new JPanel() {
public void paintComponent(Graphics g) {
g.drawImage(icon.getImage(), 0, 0, null);
setOpaque(false); // 그림을 표시하게 설정,투명하게 조절
super.paintComponent(g);
}
};
background.setBounds(0, 0, (int) this.getSize().getWidth(),
(int) (this.getSize().getHeight()) - (int) (getSize().getHeight() / 5));
background.setLayout(null);
down_panel = new JPanel();
JLabel ipnum_label = new JLabel("IP: ");
ipnum = new JTextField(15);
ipnum.setText("0.0.0.0");
JLabel name_label = new JLabel("NickName: ");
name = new JTextField(15);
name.setText("Name");
JButton start_button = new JButton("Connect");
int down_panel_X = (int) (background.getSize().getWidth() / 3) - (int) (background.getSize().getWidth() / 9);
int down_panel_Y = (int) (background.getSize().getHeight() - (int) background.getSize().getHeight() / 10);
int down_panel_width = (int) background.getSize().getWidth() / 2;
int down_panel_height = (int) background.getSize().getHeight() / 5;
down_panel.setBounds(down_panel_X, down_panel_Y, down_panel_width, down_panel_height);
down_panel.setLayout(new FlowLayout());
down_panel.add(ipnum);
down_panel.add(name);
down_panel.add(start_button);
down_panel.setBackground(new Color(255, 0, 0, 0));
background.add(down_panel);
add(background);
start_button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String ip = ipnum.getText();
String nick_name = name.getText();
if (isIPv4(ip)) {
dispose();
new Chater(ip, nick_name);
} else {
JOptionPane.showMessageDialog(null, "ip를 제대로 입력해주세요.");
}
}
});
// setUndecorated(true);
setVisible(true);
}
public static boolean isIPv4(String str) {
return Pattern.matches("((\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])([.](?!$)|$)){4}", str);
}
}
class Chater {
Receiver receiver;
Socket socket;
ChatFrame frame;
Sender sender;
public Chater(String ip, String name) {
try {
socket = new Socket(ip, 2400);
sender =new Sender(socket,name);
sender.start();
frame = new ChatFrame(sender);
receiver = new Receiver(socket, frame);
receiver.start();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
System.out.println("서버 접속 실패");
}
}
}
class ChatFrame extends JFrame {
Sound sound;
Socket socket;
Sender sender;
JTextArea receiveArea, usersListArea, sendArea;
JLabel users_label;
public ChatFrame(Sender s) {
sound=new Sound();
sender = s;
setTitle("ChatRoom");
setLayout(new BorderLayout());
setSize((int) Toolkit.getDefaultToolkit().getScreenSize().getWidth() / 4,
(int) Toolkit.getDefaultToolkit().getScreenSize().getHeight() / 2);
setLocation(
(int) (Toolkit.getDefaultToolkit().getScreenSize().getWidth() / 2)
- (int) (this.getSize().getWidth() / 2),
(int) (Toolkit.getDefaultToolkit().getScreenSize().getHeight() / 2)
- (int) (this.getSize().getHeight() / 2));
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setResizable(false);
addWindowListener(new WindowListener() {
@Override
public void windowClosing(WindowEvent e) {
try {
quit();
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
@Override
public void windowOpened(WindowEvent e) {
// TODO Auto-generated method stub
}
@Override
public void windowClosed(WindowEvent e) {
// TODO Auto-generated method stub
}
@Override
public void windowIconified(WindowEvent e) {
// TODO Auto-generated method stub
}
@Override
public void windowDeiconified(WindowEvent e) {
// TODO Auto-generated method stub
}
@Override
public void windowActivated(WindowEvent e) {
// TODO Auto-generated method stub
}
@Override
public void windowDeactivated(WindowEvent e) {
// TODO Auto-generated method stub
}
});
JButton send_button = new JButton("Send");
users_label = new JLabel("접속자 수: ");
receiveArea = new JTextArea(19, 24);
receiveArea.setEditable(false);
receiveArea.setLineWrap(true);
usersListArea = new JTextArea(19, 10);
usersListArea.setEditable(false);
usersListArea.setLineWrap(true);
sendArea = new JTextArea(1, 20);
sendArea.setLineWrap(true);
JPanel topPanel = new JPanel();
JPanel bottomPanel = new JPanel();
JScrollPane scrollPane = new JScrollPane(receiveArea);
JScrollPane scrollPane2 = new JScrollPane(usersListArea);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
topPanel.setLayout(new BorderLayout());
topPanel.add(users_label, BorderLayout.NORTH);
topPanel.add(scrollPane2, BorderLayout.WEST);
topPanel.add(scrollPane, BorderLayout.EAST);
add(topPanel, BorderLayout.NORTH);
bottomPanel.add(sendArea);
bottomPanel.add(send_button);
add(bottomPanel, BorderLayout.SOUTH);
sendArea.addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
// TODO Auto-generated method stub
}
@Override
public void keyReleased(KeyEvent e) {
// TODO Auto-generated method stub
}
@Override
public void keyPressed(KeyEvent e) {
// TODO Auto-generated method stub
if (e.getKeyChar() == KeyEvent.VK_ENTER) {
try {
sendChat();
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
e.consume();
}
}
});
send_button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
try {
sendChat();
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
});
setVisible(true);
}
void updateChat(String s) {
receiveArea.append(s + "\n");
receiveArea.setCaretPosition(receiveArea.getDocument().getLength());
toFront();
setAutoRequestFocus(false);
if(!isFocused()) sound.dingdong(false,"note.wav");
}
void updateUserList(String[] userList) {
usersListArea.setText("");
users_label.setText("접속자 수: " + (userList.length));
for (String user :userList) {
usersListArea.append(user + "\n");
}
}
void enterUser(String n, String user) {
users_label.setText("접속자 수: " + n);
usersListArea.append(user + "\n");
}
void exitUser(String n, String user) {
users_label.setText("접속자 수: " + n);
usersListArea.setText(usersListArea.getText().replaceFirst(user + "\n", ""));
}
void sendChat() throws Exception {
if (sendArea.getText().isEmpty()) {
return;
}
sender.send(sendArea.getText());
sendArea.setText(null);
}
void quit() throws Exception {
sender.quit();
}
}
class Receiver extends Thread {
Socket socket;
ChatFrame cFrame;
ArrayList<String> chatLogs = new ArrayList<String>();
InputStream in;// 읽는 stream
ObjectInputStream ois;
HashMap<String, Object> Data=new HashMap<String,Object>();
public Receiver(Socket s, ChatFrame frame) throws Exception {
socket = s;
cFrame = frame;
in=socket.getInputStream();
ois=new ObjectInputStream(in);
}
@Override
public void run() {
try {
while (true) {
Data= (HashMap<String, Object>) ois.readObject();
if (Data.get("protocol")==null) break;
String protocol=(String)Data.get("protocol");
switch(protocol) {
case "quit":
socket.close();
break;
case "msg":
chatLogs.add((String) Data.get("message"));
cFrame.updateChat((String) Data.get("message"));
break;
case "update":
cFrame.updateUserList((String[])Data.get("userList"));
break;
}
}
// try {
// while (true) {
// Data=(HashMap<String, Object>) ois.readObject();
// if ((s = reader.readLine()) != null) {
// if (s.equals("/quit")) {
// socket.close();
// break;
// } else if (s.startsWith("/enter_user "))
//
// {
// cFrame.enterUser(s.split(" ")[1], s.split(" ")[2]);
// } else if (s.startsWith("/exit_user ")) {
// cFrame.exitUser(s.split(" ")[1], s.split(" ")[2]);
// } else if (s.startsWith("/update_user_list ")) {
// cFrame.updateUserList(s.split(" ")[1]);
// s = reader.readLine();
// cFrame.enterUser(s.split(" ")[1], s.split(" ")[2]);
// } else {
// chatLogs.add(s);
// cFrame.updateChat(s);
// }
// }
// }
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
System.exit(0);
}
}
}
class Sound {
boolean isMute=false;
public Sound() {
}
public void dingdong(boolean LOOP,String filename) {
try (InputStream in = getClass().getResourceAsStream(filename)) {
InputStream bufferedIn = new BufferedInputStream(in);
try (AudioInputStream audioIn = AudioSystem.getAudioInputStream(bufferedIn)) {
Clip clip = AudioSystem.getClip();
clip.open(audioIn);
clip.start();
if(LOOP)clip.loop(-1);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class Sender extends Thread {
OutputStream out;
ObjectOutputStream oos;
Socket socket;
HashMap<String, Object> Data= new HashMap<String,Object>();
public Sender(Socket s, String n) throws Exception {
socket = s;
out = socket.getOutputStream();
oos = new ObjectOutputStream(out);
setNick(n);
}
@Override
public void run() {
try {
while (true) {
HashMap<String,Object> map = new HashMap<String,Object>();
map.put("protocol", "alive");
oos.writeObject(map);
sleep(10000);
}}
catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
void send(String msg) throws Exception {
HashMap<String,Object> map = new HashMap<String,Object>();
map.put("protocol", "msg");
map.put("message", msg);
oos.writeObject(map);
}
void setNick(String n) throws Exception {
HashMap<String,Object> map = new HashMap<String,Object>();
map.put("protocol", "setName");
map.put("name", n);
oos.writeObject(map);
}
void quit() throws Exception {
HashMap<String,Object> map = new HashMap<String,Object>();
map.put("protocol", "quit");
oos.writeObject(map);
}
}