Java

[Java] 멀티 소켓 통신 채팅 프로그램 tcp 서버/클라이언트 구조 - Swing GUI(2)

igkrap 2021. 6. 30. 10:16

 전 포스팅에서 어느 정도 윤곽이 잡힌 멀티 채팅 프로그램을 만들었었는데요. 해당 프로그램에는 커다란 문제점이 존재했습니다. 바로 클라이언트 측에서 갑자기 인터넷 통신이 끊긴다던지, 비정상적으로 프로그램을 종료한다던지 해서 클라이언트 측에서 소켓이 끊겼을 때 서버 측에서 이를 감지하지 못하고 해당 소켓을 열어둔 채 대기하고 있는 상황이 발생하는 것입니다. 그것으로 인해 종료된 클라이언트(유저)가 유령처럼 남아서 다른 클라이언트가 들어왔을 때 채팅방에 존재하는 것으로 인식하는 섬뜩한(?) 문제가 발생하였습니다.

 

 그래서 이를 해결하기 위하여 저같은 경우 특정한 방법을 사용해보았습니다. 먼저 서버 측 프로그램에

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);
	}
}