JAVA/JAVA
Java | Socket & Thread 채팅 프로그램 만들기
pathas
2020. 2. 12. 17:10
Socket & Thread 채팅 프로그램 만들기
지금까지 학습했던 거의 모든 내용이 나오는 예제
클라이언트끼리 실시간으로 채팅할 수 있는 프로그램 작성
① 클라이언트측
- 사용자가 직접 사용하는 UI 디자인
- 서버에 접속할 수 있도록 클라이언트 소켓 생성
- 채팅은 실시간으로 이루어지기 때문에 각 클라이언트마다 스레드를 가져야 함
- 채팅 메세지는 입출력 스트림으로 서버와 송수신할 수 있도록 함
package j200212;
// Socket 클래스
import java.net.*;
// 입출력 클래스
import java.io.*;
// 그래픽 관련 클래스
import java.awt.*; // GUI
import javax.swing.*; // JFrame, JTextField, JTextArea, JScrollPane
// Event 처리
import java.awt.event.*; // ActionListener
public class ChatGUIClient extends JFrame implements ActionListener, Runnable {
// ======== GUI =========
JTextField tf; // 전송할 텍스트 입력창
JTextArea ta; // 전송받은 텍스트 출력
JScrollPane js; // 스크롤바 생성
// ======== Socket =======
Socket s; // 서버와의 통신을 위함
// ======== Stream =======
BufferedReader br; // 클라이언트에서의 문자열 입력 스트림
PrintWriter pw; // 문자열 출력 스트림
// 서버로 전송할 문자열과 서버에서 받아올 문자열 변수
String str, str1;
// ======== 생성자 ========
public ChatGUIClient() {
// 창, 부착할 컴포넌트 생성 및 연결
tf = new JTextField();
ta = new JTextArea();
// 텍스트 출력창에 스크롤 바 연결
js = new JScrollPane(ta);
// BorderLayout 배치관리자, JTextArea를 정중앙에 부착
add(js, "Center");
// 텍스트 필드를 하단에 부착
add(tf, BorderLayout.SOUTH);
// 텍스트 필드에서 이벤트(enter)를 입력받고 해당 객체에서 이벤트 처리
tf.addActionListener(this);
// 창 크기 지정
setBounds(200, 200, 500, 350);
// 창이 보이도록 설정
setVisible(true);
// 텍스트 필드에 커서 입력
tf.requestFocus();
// X버튼 클릭시 정상 종료되도록 설정
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 서버와 연결, 연결되지 않을 수도 있기 때문에 예외 처리 필수
try {
// 클라이언트 측 소켓 정보 초기화
// Socket(host, port), host: 접속 서버 IP 주소, port: 서버 포트 번호
s = new Socket("192.168.0.145", 5432);
System.out.println("s>>>" + s);
// ========== Server와 Stream 연결 ===========
br = new BufferedReader(new InputStreamReader(s.getInputStream()));
// PrintWriter 스트림의 autoFlush 기능 활성화
pw = new PrintWriter(s.getOutputStream(), true);
} catch (Exception e) {
System.out.println("접속 오류>>>" + e);
}
// Thread 객체 생성, Runnable 인터페이스를 구현하기 때문에 this 작성
Thread ct = new Thread(this);
// 클라이언트 스레드 실행 → run() 호출
ct.start();
}
// Runnable 인터페이스 run() 메소드 오버라이딩
public void run() {
// 더 이상 입력을 받을 수 없을 때까지 JTextArea(채팅창)에 출력
try {
while ((str1 = br.readLine()) != null) {
ta.append(str1 + "\n"); // 상대방이 보낸 문자를 채팅창에 세로로 출력
}
} catch (Exception e) {
e.printStackTrace();
;
}
}
// ActionListener 메소드 오버라이딩, 입력란에서 enter입력시 실행할 코드
public void actionPerformed(ActionEvent e) {
// 내가 쓴 메세지를 str 변수에 저장
str = tf.getText();
// 변수에 저장 후 텍스트필드 초기화
tf.setText("");
// 내가 쓴 메세지 출력 -> 상대방은 br.readLine()으로 읽어들임
pw.println(str);
pw.flush();
}
public static void main(String[] args) {
// 클라이언트 객체 생성, 생성자 호출
new ChatGUIClient();
}
}
② 서버측
- 서버는 클라이언트의 요청을 처리하는 역할을 수행
- 클라이언트마다 가지고 있는 Socket과 연결할 ServerSocket 필요
- ChatGUIServer: 접속 소켓을 관리하고 전체 소켓에 메세지를 출력하는 역할
메세지 출력 자체는 ServerThread의 send()메소드가 수행하기 때문에
ChatGUIServer에서는 전체 소켓에 send()메소드를 호출하는 broadCast() 메소드 작성 - ServerThread : 클라이언트의 요청을 직접적으로 처리하는 클래스
클라이언트로부터 전송되는 메세지를 실시간으로 수신해서 출력하기 위해 스레드 상속
클라이언트와의 직접적인 데이터 통신이 이루어짐
package j200212;
import java.net.*; // ServerSocket, Socket
import java.io.*; // 입출력
// 동적 배열, 접속한 클라이언트의 정보를 실시간으로 저장하는 목적(고정 배열X)
import java.util.Vector;
public class ChatGUIServer {
// 클라이언트와 연결할 때만 필요한 ServerSocket 클래스
ServerSocket ss;
// 서버로 접속한 클라이언트 Socket을 저장할 멤버 변수
Socket s;
// 접속 클라이언트 정보 실시간 저장
Vector v;
// ServerThread 자료형 멤버 변수 선언, has-a 관계 설정을 위함
ServerThread st;
// 생성자, 멤버 변수 초기화
public ChatGUIServer() {
// 사용자 정보를 담을 v를 Vector 객체로 초기화
v = new Vector();
// 접속이 될 수도 있고 안 될 수도 있기 때문에 예외 처리
try {
// ServerSocket 객체 생성 → 포트 번호 생성(임의의 번호 부여)
ss = new ServerSocket(5432);
System.out.println("ss>>>" + ss);
System.out.println("채팅 서버 가동중...");
// 서버 가동: 클라이언트가 접속할 때까지 기다리는 것(무한 대기)
while (true) {
// 접속 클라이언트 Socket을 s 변수에 저장
s = ss.accept();
System.out.println("Accepted from" + s);
// 접속 클라이언트와 서버로 st객체 생성
st = new ServerThread(this, s);
// 접속할 때마다 v에 접속 클라이언트 스레드 추가
this.addThread(st);
// Thread 가동 -> run() -> broadCast() -> send() 실시간 메소드 호출
st.start();
}
} catch (Exception e) {
// 접속 실패시 간단한 Error 메세지 출력
System.out.println("서버 접속 실패>>>" + e);
}
}
// 벡터 v에 접속 클라이언트의 스레드 저장
public void addThread(ServerThread st) {
v.add(st);
}
// 퇴장한 클라이언트 스레드 제거
public void removeThread(ServerThread st) {
v.remove(st);
}
// 각 클라이언트에게 메세지를 출력하는 메소드, send() 호출
public void broadCast(String str) {
for (int i = 0; i < v.size(); i++) {
// 각각의 클라이언트를 ServerThread 객체로 형 변환
ServerThread st = (ServerThread) v.elementAt(i);
// 각 스레드 객체에 str 문자열을 전송
st.send(str);
}
}
public static void main(String[] args) {
// 익명 객체 생성
new ChatGUIServer();
}
}
// ServerThread 클래스 생성 → 서버에서 각 클라이언트의 요청을 처리할 스레드
class ServerThread extends Thread {
// 클라이언트 소켓 저장
Socket s;
// ChatGUIServer 클래스의 객체를 멤버 변수로 선언, has-a 관계를 위함
ChatGUIServer cg;
// 입출력
BufferedReader br;
PrintWriter pw;
// 전달할 문자열
String str;
// 대화명(ID)
String name;
// 생성자
public ServerThread(ChatGUIServer cg, Socket s) {
/* cg = new ChatGUIServer(); → 작성 불가, 서버가 두 번 가동되기 때문에 충돌이 일어남
따라서 매개변수를 이용해서 객체를 얻어온(call by reference) 뒤에 cg와 s값을 초기화해야 함
*/
this.cg = cg;
// 접속한 클라이언트 정보 저장
this.s = s;
// 데이터 전송을 위한 입출력 스트림 생성
try {
// =========== 입력 ===========
// s.getInputStream() => 접속 클라이언트(소켓 객체)의 InputStream을 얻어 옴
br = new BufferedReader(new InputStreamReader(s.getInputStream()));
// =========== 출력 ===========
/*
BufferedWriter의 경우 버퍼링 기능을 가지기 때문에 PrintWriter 스트림 사용
PrintWriter 스트림의 경우 생성자의 두 번째 인자로 autoFlush 기능을 지정할 수 있음
BufferedWriter를 사용하는 경우 flush() 메소드를 사용해야 함
*/
pw = new PrintWriter(s.getOutputStream(), true);
} catch (Exception e) {
System.out.println("에러 발생>>>>>" + e);
}
}
// 메세지(입력 문자열) 출력 메소드
public void send(String str) {
// 문자열 출력
pw.println(str);
// 혹시나 버퍼에 남아있는 것을 비워냄
pw.flush();
}
// run()_ServerThread -> broadCast(str)_ChatGUIServer -> send(str)_ServerThread
public void run() {
try {
// 대화명 입력 받기
pw.println("대화명을 입력하세요");
name = br.readLine();
// 서버에서 각 클라이언트에 대화명 출력
cg.broadCast("[" + name + "]" + "님이 입장했습니다.");
// 무한 대기하며 입력한 메세지를 각 클라이언트에 계속 전달
while ((str = br.readLine()) != null) {
cg.broadCast("[" + name + "]: " + str);
}
} catch (Exception e) {
// 접속자 퇴장시 v에서 해당 클라이언트 스레드 제거
cg.removeThread(this); // this: ServerThread 객체, 접속 클라이언트
// 서버에서 각 클라이언트에 출력
cg.broadCast("[" + name + "]" + "님이 퇴장했습니다.");
// 콘솔에 퇴장 클라이언트 IP 주소 출력
System.out.println(s.getInetAddress() + "의 연결이 종료됨!");
}
}
}
※ port 번호와 ip 주소 같은 민감한 정보에 대해서는 Secure Coding을 해주어야 함
지금까지 배운 java의 종합 예제라고 할 수 있는 채팅 프로그램 만들기를 해보았습니다.
그래픽 관련 패키지 및 클래스(AWT, SWING)는 웹에서 사용할 일이 거의 없기 때문에
클라이언트와 서버의 관계, 스레드와 스트림의 역할에 초점을 맞추고 보시면 좋을 것 같습니다.
주석을 제외하면 양이 그렇게 많지는 않기 때문에 한 번 따라서 해보시기를 권장해 드립니다.