Bigfat

[Java 예제] 채팅(chatting) 본문

Java

[Java 예제] 채팅(chatting)

kyou 2017. 2. 22. 19:30

자바로 서버-클라이언트 채팅 프로그램을 구현해보자

클라이언트 측에서는 AWT와 Swing을 적절히 섞어 UI를 구현하였다. 웹 프로그래밍만 하다보니 자바 GUI에 대해 무지한데, 채용공고를 보니 Swing 개발자도 구하고 있었다. 지금으로서는 API를 읽는 습관을 들이기 위해 사용한다고 생각하자.


소켓, 스레드뿐만 아니라 오버라이딩과 지네릭스까지 한 번쯤은 생각해보고 구현해보면 좋을 것 같은 예제다.


/**
 * ChatClientMain.java
 * 채팅 클라이언트 Main 클래스
 * @Date 2017. 2. 22.
 */
package com.multichat.client;

import java.util.Random;

public class ChatClientMain {

	public static void main(String[] args) throws Exception {
		
		int randomNum = new Random().nextInt(1000);
		ChatClient ccd = new ChatClient(String.valueOf(randomNum));
		// setBounds(int x, int y, int width, int height)
		ccd.setBounds(400, 100, 300, 600);
		ccd.setVisible(true);
	}

}


/** * ChatClient.java * 채팅 클라이언트 기능 구현 클래스 * @Date 2017. 2. 22. */ package com.multichat.client; import java.awt.BorderLayout; import java.awt.Container; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.JTextField; public class ChatClient extends JFrame implements ActionListener, Runnable { private static final long serialVersionUID = 1L; JLabel topLine; // 상단부 JTextArea showArea; // 대화창 JPanel bottomLine; // 하단부 JTextField inputBox; // 입력창 JButton sendButton; // '보내기'버튼 BufferedReader br; PrintWriter pw; Socket chatSocket; boolean flag; String nickName; public ChatClient(String nickName) throws Exception { super(nickName+"의 채팅창"); this.nickName = nickName; design(); connect(); new Thread(this).start(); // Runnable 구현 // 익명클래스 > 이벤트 발생 시에만 메모리에 올라간다. this.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { stop(); System.exit(0); } @Override public void windowOpened(WindowEvent e) { // TODO Auto-generated method stub inputBox.requestFocus(); } }); } public void design() { Container ct = this.getContentPane(); // 상단부 topLine = new JLabel(nickName); ct.add(topLine, BorderLayout.PAGE_START); // 대화창 showArea = new JTextArea(""); showArea.setEditable(false); showArea.setLineWrap(true); // 자동 줄바꿈 ct.add(new JScrollPane(showArea), BorderLayout.CENTER); // scroll-bar 붙임 // 하단부 // bottomLine = new JPanel(new GridLayout(1,2)); // int rows, int cols bottomLine = new JPanel(); inputBox = new JTextField(15); sendButton = new JButton("보내기"); bottomLine.add(inputBox); bottomLine.add(sendButton); ct.add(bottomLine, BorderLayout.PAGE_END); // Action inputBox.addActionListener(this); // JTextField의 Enter키 이벤트 발생 sendButton.addActionListener(this); } // 소켓 연결 public void connect() { try { chatSocket = new Socket("127.0.0.1", 8889); br = new BufferedReader(new InputStreamReader(chatSocket.getInputStream())); pw = new PrintWriter(chatSocket.getOutputStream(), true); } catch (Exception e) { System.out.println("연결 실패"); } this.sendMessage("["+nickName+"] 님이 입장하셨습니다."); } // Runnable의 run() 메서드 구현 @Override public void run() { try { // Server로부터 받는 데이터를 읽어들임 while(!flag) { String msg = br.readLine(); if(msg != null && !msg.equals("")) { showArea.append(msg+"\n"); showArea.setCaretPosition(showArea.getText().length()); } } } catch (Exception e) { e.printStackTrace(); } } // 메시지 전송 public void sendMessage(String msg) { pw.println(msg); } @Override public void actionPerformed(ActionEvent e) { String msg = inputBox.getText(); if(!msg.equals("")) { sendMessage("[" + nickName + "]: " + msg); inputBox.setText(""); } } // 접속 종료 public void stop() { String endOfMsg = "!@#$"; // EOM : End of Message try { sendMessage("["+nickName+"] 님이 퇴장하셨습니다." + endOfMsg); chatSocket.close(); flag = true; } catch (Exception e) { e.printStackTrace(); } } }


/**
 * ChatServerMain.java
 * 채팅 서버 Main 클래스
 * @Date 2017. 2. 22.
 */
package com.multichat.server;

import java.net.ServerSocket;
import java.net.Socket;

public class ChatServerMain {

	private ServerSocket serverSocket;
	private Socket socket;
	static ChatManage chatManage = new ChatManage();
	
	public void startServer() {
		try {
			serverSocket = new ServerSocket(8889);
			while(true) {
				System.out.println("서버 대기중");
				socket = serverSocket.accept();
				ChatThread chatThread = new ChatThread(socket);
				chatManage.addChatThread(chatThread);
				chatThread.start();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public static void main(String[] args) {
		ChatServerMain chatServer = new ChatServerMain();
		chatServer.startServer();
	}
	
}


웹 개발에서는 스레드는 WAS가 알아서 해결해주니 개념적인 이해가 더 중요한 것 같다. 물론 안드로이드 프로그래밍에서는 자주 쓰인다.

/**
 * ChatThread.java
 * 각각의 클라이언트와 소켓 통신하도록 구현한 클래스
 * @Date 2017. 2. 22.
 */
package com.multichat.server;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class ChatThread extends Thread {
	Socket socket;
	PrintWriter pw;
	BufferedReader br;
	boolean flag;
	
	public ChatThread(Socket socket) {
		this.socket = socket;
	}
	
	@Override
	public void run() {
//		"] 님이 퇴장하셨습니다."를 사용해도 무방하다.
		String endOfMsg = "!@#$";
		try {
			br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			pw = new PrintWriter(socket.getOutputStream(), true);
			
			while(!flag) {
				String msg = br.readLine();
				if(msg != null && !msg.equals("")) {
					if(msg.endsWith(endOfMsg)) {
						ChatServerMain.chatManage.removeChatThread(this);
						msg = msg.substring(0, msg.length() - 4);
						br.close();
						pw.close();
						socket.close();
						flag = true;
					}
					System.out.println(msg);
					ChatServerMain.chatManage.sendAllMessage(msg);
				}
			}
		} catch (Exception e) {
			try {
				if(br != null) br.close();
				if(pw != null) pw.close();
				if(socket != null) socket.close();
			} catch (Exception e2) {
				e2.printStackTrace();
			}
		}
	}
	
	public void sendMessage(String msg) {
		pw.println(msg);
	}
}


/**
 * ChatManage.java
 * 클라이언트를 관리하는 클래스
 * @Date 2017. 2. 22.
 */
package com.multichat.server;

import java.util.Iterator;
import java.util.Vector;

public class ChatManage extends Vector<ChatThread> {

	private static final long serialVersionUID = 1L;

	public synchronized void addChatThread(ChatThread thread) {
		this.add(thread);
		System.out.println("[서버]: " + this.size() + "명 접속해있습니다.");
	}
	
	public synchronized void removeChatThread(ChatThread thread) {
		this.remove(thread);
		System.out.println("[서버]: " + this.size() + "명 접속해있습니다.");
	}	

	public synchronized void sendAllMessage(String msg) {
		// Iterator를 사용하는 방법
		Iterator<ChatThread> iterator= this.iterator();
		
		while(iterator.hasNext()) {
			ChatThread chatThread = iterator.next();
			chatThread.sendMessage(msg);
		}
	}
}


Server측에서 while(true)로 무한루프를 타기 때문에 아래와 같은 에러가 날 때도 있다. 정상적으로 출력되면서도 이런 에러가 나오길래 검색해봤더니, 스레드 덤프 분석하기 중 원격 서버로부터 메시지 수신을 받기 위해 계속 대기하는 경우에 해당하는 것 같다. 순간적으로 CPU 사용량도 50%까지 올라가는 것을 확인했다.


// 이런 문구들과 함께 Error가 발생할 때도 있다.
Software caused connection abort: recv failed
at java.net.SocketInputStream.socketRead0(Native Method)
...