본문 바로가기
JAVA SE/Code

CODE [20.08.06/Day_21] Java SE / Chatting 서버, 클라이언트 구현 (IO, Thread, GUI, synchronized list)

by 파프리카_ 2020. 8. 5.
728x90
반응형

step5   채팅 서버 클라이언트 구현 (GUI 활용 + synchronized)

 

 

Chatting Program의 UML

출처 : https://cafe.naver.com/kostakosta203?iframe_url=/MyCafeIntro.nhn%3Fclubid=30164756

 

< 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 창

 

728x90
반응형