step5 채팅 서버 클라이언트 구현 (GUI 활용 + synchronized)
Chatting Program의 UML
< Server >
다수에 클라이언트에게 동시에 서비스(통신)를 제공해야 하기 때문에, 한 서버에서 Thread를 여러 개 생성해준다.
→ multi-Threading
< Client >
step4와 다른 점은 client에도 multi-Threading을 해준다는 점이다.
그 이유는, client끼리도 '서로 메세지를 주고 받고'기능 통신 서비스를 동시에 실행되어야 하기 때문이다.
(다른 사람의 메세지도 보고, 나의 메세지도 뿌려주고 → broadcast() 함수로 구현)
[ ChattingServer ]
list
→ Client와 통신하는 객체를 리스트에 저장
다수의 스레드(serverWorker Thread)에 공유되는 자원이므로, 동기화처리가 필요하다
( list 요소에 대한 추가 및 삭제 작업이 진행되므로 )
Collection.synchronizedList(new ArrayList<ServerWorker>());
- >thread safe한 list 반환 받을 수 있다
ChattingServer의 go() 메서드
→ accept(), serverWorker(socket) 객체 생성, list에 데이터 추가, 메세지를 출력하는Thread 생성, start
main 메서드 내에 broadcast() 메서드
→ 접속한 모든 클라이언트에게 메세지를 출력하는 메서드 (for loop)
ServerWorker() Thread 내의 run() 메서드
→ finally에서 list에 list.remove(this);
ServerWorker() Thread 내의 chatting() 메서드
→ 클라이언트 메세지를 입력받아 접속한 모든 클라이언트에게 메세지를 보낸다
[ ChattingClient ]
ChattingClient()의 go()메서드
→ 소켓을 생성하고, 스캐너, 각 스트림을 생성,
→ 친구들의 메세지를 입력받는 Thread를 생성,start시킨다(daemon thread이용)
→ 스캐너로부터 데이터를 입력받아 서버에 출력하는 작업을 지속
친구들의 메세지를 입력받아 콘솔에 출력하는 역할은
inner class인 ReceiverWorker Thread가 전담한다.
ChattingServer
/ChattingServer.java
package step5.inst;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ChattingServer {
//ChattingServer의 instance variable
//list : 접속한 클라이언트와 통신할 ServerWorker 객체들이 저장된 리스트
private List<ServerWorker> list =
Collections.synchronizedList(new ArrayList<ServerWorker>());
public void go() throws IOException {
ServerSocket serverSocket = null;
try {
//1. ServerSocket 생성
serverSocket = new ServerSocket(5432);
System.out.println("** Chatting Server 실행 중 - Client 접속 대기 **");
while(true)
{
//2. accept()로 socket 리턴(연결)
Socket socket = serverSocket.accept();
ServerWorker serverWorker = new ServerWorker(socket);
//3. list에 추가
list.add(serverWorker);
System.out.println(socket.getInetAddress()+"님 입장하셨습니다");
//4. Thread, start
new Thread(serverWorker).start();
}
} finally {
if (serverSocket != null)
serverSocket.close();
}
}//go method
// serverWorker에서 message를 받으면, broadcast를 통해
// 접속한 모든 클라이언트에게 메세지를 출력하는 메서드
public void broadcast(String message) {
for(int i = 0; i < list.size(); i++) {
list.get(i).pw.println(message);
}
}//broadcast method
public static void main(String[] args) {
try {
new ChattingServer().go();
} catch (IOException e) {
e.printStackTrace();
}
}//main method
//inner class (Thread 대상)
class ServerWorker implements Runnable{
private Socket socket;
private BufferedReader br;
private PrintWriter pw;
private String clientIp;
public ServerWorker(Socket socket) {
super();
this.socket = socket;
clientIp = socket.getInetAddress().toString();
}
//stream 만들기
public void chatting() throws IOException {
br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
pw = new PrintWriter(socket.getOutputStream(), true);
//친구 메세지를 읽고 출력하는 것을 반복
while(true)
{
//클라이언트의 메세지를 입력받음
String message = br.readLine();
// 종료될 때 조건문
if(message == null | message.equals("null")|
message.trim().equals("종료"))
break;
// 모든 클라이언트의 메세지를 콘솔창에 출력
broadcast(clientIp + " 님의 메세지: " + message);
}
}//chatting method
@Override
public void run() {
try {
chatting();
} catch (IOException e) {
System.out.println(clientIp + "님이 강제종료하여 나감");
} finally {
//현재 객체를 remove하여 삭제
list.remove(this);
// 퇴장 정보 서버 콘솔에 출력
System.out.println(clientIp+" 님이 퇴장하였습니다");
//다른 접속자에게도 정보 알리기
broadcast(clientIp+" 님이 퇴장하였습니다");
if (socket != null)
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}//run method
}//inner class - ServerWorker
}//outer class - ChattingServer
ChattingClient
/ChattingClient.java
package step5.inst;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
import common.IP;
public class ChattingClient {
private Socket socket;
private BufferedReader br;
private PrintWriter pw;
private Scanner scanner;
public void go() throws UnknownHostException, IOException {
try {
// 1. 소켓 생성
socket = new Socket(IP.LOCAL, 5432);
// 2. stream 생성
br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
pw = new PrintWriter(socket.getOutputStream(), true);
// 3. 스캐너 생성
scanner = new Scanner(System.in);
//4. Thread 생성
Thread thread = new Thread(new ReceiverWorker());
//5. thread daemon & start
thread.setDaemon(true); //현 ChattingClient가 종료되면, ReceiverWorker thread도 종료
thread.start();
//6. 데이터 받고 출력 작업 반복
while(true)
{
//서버에 보낼 메세지
String message = scanner.nextLine();
//서버에 메세지 출력(보내기)
pw.println(message);
//종료 조건
if (message == null | message.trim().equals("종료"))
{
System.out.println("**채팅을 종료합니다**");
break;
}
}
} finally {
closeAll();
}
}//go method
//다 닫기!
public void closeAll() throws IOException {
if (scanner != null)
scanner.close();
if (socket != null)
socket.close();
}//closeAll method
public static void main(String[] args) {
try {
new ChattingClient().go();
} catch (IOException e) {
e.printStackTrace();
}
}
//서버에서 오는 메세지를 입력받는 스레드
class ReceiverWorker implements Runnable{
@Override
public void run() {
try {
receive();
} catch (IOException e) {
e.printStackTrace();
}
}//run method
public void receive() throws IOException {
while(true)
{
String message = br.readLine();
if (message == null)
break; //읽을 메세지가 없으면 끝!
System.out.println(message);
}
}//receive method
}//inner class - ReceiverWorker
}//outer class - ChattingClient
ChattingClient GUI
/ChattingGUIClient.java
package step6;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.text.DefaultCaret;
import common.IP;
public class ChattingGUIClient {
private JFrame frame;
private JTextArea textArea;
private JPanel panel;
private JTextField textField;
private JButton button;
/*
* 네트워크 통신을 위한 변수를 선언
*/
private Socket socket;
private BufferedReader br;
private PrintWriter pw;
/*
* 스트림과 소켓을 닫는 메서드를 정의한다
*/
public void closeAll() throws IOException{
if (br != null)
br.close();
if (pw != null)
pw.close();
if (socket != null)
socket.close();
}
public void startUI() {
frame = new JFrame("kostatok");
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
/*
* ServerWorker Thread에서 종료할것임을 알린다
*/
pw.println("종료");
try {
closeAll();
} catch (IOException e1) {
e1.printStackTrace();
}
System.exit(0);//시스템 종료
}
});
textArea = new JTextArea();
textArea.setBackground(Color.YELLOW);
frame.add(textArea, BorderLayout.CENTER);
// 스크롤바 - 업데이트
DefaultCaret caret = (DefaultCaret) textArea.getCaret();
caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
JScrollPane sp = new JScrollPane(textArea, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
frame.add(sp, BorderLayout.CENTER);// 스크롤적용 JTextArea 갱신
// textField와 button 을 생성한 후 panel에 두 요소를 추가하고
// 이 panel을 frame의 south 위치에 추가한다
textField = new JTextField(25);
textField.addKeyListener(new KeyHandler());
button = new JButton("전송");
button.addActionListener(new ButtonHandler());
panel = new JPanel();
panel.add(textField);
panel.add(button);
frame.add(panel, BorderLayout.SOUTH);
frame.setSize(400, 400);
frame.setLocation(500, 200);
frame.setVisible(true);
// textField에 포커스를 준다
textField.requestFocus();
}
public class ButtonHandler implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
/* 친구들에게 보낼 메세지 쓴 내용을 textField 의 getText() 를
* 이용해 가져와서 서버로 출력한다
* 출력 후 setText("") 과 requestFocus()를 호출해
* 입력란을 비워주고 커서를 준다
* 이러한 작업을 아래 별도의 메서드sendMessage에서 작업해서
* 엔터키 이벤트시에도 재사용할 수 있도록 한다
*/
sendMessage();
}
}//inner class - ButtonHandler
//sendMessage()를 ButtonHandler와 KeyHandler에서 공유하여 사용
public void sendMessage() {
pw.println(textField.getText());
textField.setText("");
textField.requestFocus();
}
public class KeyHandler extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
// Enter key 를 눌렀을 때 이벤트 처리
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
/* 서버에 출력하는 sendMessage() 를 호출한다
*
*/
sendMessage();
}
}
} //inner class - KeyHandler
public void go() throws UnknownHostException, IOException {
/* GUI가 화면에 보이기 전에 통신에 필요한
* 소켓과 입,출력 스트림을 생성한다
* 또한 지속적으로 친구들의 메세지를 입력받을
* ReceiverWorker Thread 를 생성하고 start 시킨다
* (start 시키기 전에 데몬스레드로 설정한다)
*
*/
socket = new Socket(IP.INST, 5432);
br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
pw = new PrintWriter(socket.getOutputStream(), true);//auto flush
//Thread 생성 - daemon thread로 생성
Thread thread = new Thread(new ReceiverWorker());
thread.setDaemon(true);
//화면을 구성하는 메서드 (UI 띄우기)
startUI();
// thread를 실행가능 상태로 보내 jvm에 의해 실행하게 한다
thread.start();
}
class ReceiverWorker implements Runnable {
@Override
public void run() {
/* 서버에서 오는 친구들의 메세지를 입력받아
* 화면 상단 TextArea에 출력한다
* textArea.append(message+"\n");
*/
try {
while(true) {
String message = br.readLine();
// 상대 쪽 서버장애로 null이 반환되면 읽기 종료
if (message == null ) {
break;
}
textArea.append(message+"\n");
}//while
} catch (Exception e) {
e.printStackTrace();
}
}//run
}//ReceiverWorker
public static void main(String[] args) {
ChattingGUIClient client = new ChattingGUIClient();
try {
client.go();
} catch (IOException e) {
e.printStackTrace();
}//catch
}//main
}//class
GUI 창