2017. 2. 22. 19:30ㆍJava
자바로 서버-클라이언트 채팅 프로그램을 구현해보자
클라이언트 측에서는 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();
}
}
/**
* 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) ...
'Java' 카테고리의 다른 글
| [Spring] Migrating from HttpClient 4 to HttpClient 5 (for self-signed certificates) (0) | 2024.12.04 |
|---|---|
| [Java] 데이터베이스 연동하기 (0) | 2017.02.23 |
| [Java 예제] 로또(a lottery) (0) | 2017.02.13 |
| [Java 예제] 홀수 마방진(a magic square of odd order) (3) | 2017.02.11 |
| [Java 예제] 다이아몬드(a diamond) (0) | 2017.02.11 |