[책펌] Multicast(1:N 소켓스레드)프로그램 예제

By | 10월 22, 2008

* 개요

- Multicast 란? : 하나의 서버에서 모든 클라이언트에 동시 전송하는 개념, 서버를 통하여 모든 클라이언트가 실시간으로 상호 전송가능 (이 예제에서는 클라이언트를 선별하여 메세지를 날릴 수 없다. 그러기 위해서는 적당한 프로토콜을 만들어서 적용해야 한다.)

* 구성


- MultiServer.java : 모든 클라이언트의 TCP요청을 받아 소켓 객체를 생성한다. 소켓을 유지하기 위한 스레드를 생성하고, 이 스레드를 저장할 Collection(ArrayList)을 생성하는 클래스다.

- MultiServerThread.java : 각각의 클라이언트의 소켓 객체를 유지하기 위한 클래스다. 이 클래스는 멀티서버에 있는 컬렉션을 가지고 있기 때문에 다른 클라이언트에게 메시지를 보낼 수 있다.

- MultiClient.java : 스윙으로 구현된 클라이언트 클래스다. 이 클래스에서는 메시지를 보낼 때는 이벤트에서 처리했고, 다른 클라이언트가 보낸 메시지를 받기 위해 MultiClientThread 객체를 생성했다.

- MultiClientThread.java : 다른 클라이언트의 메시지를 받기 위한 클래스다.

* 실행방법

(1) MultiServer 실행
(2) MultiClient 실행
    - java socket.multicast.MultiClient "접속할호스트명" "사용자아이디"

MultiServer.java

package socket.multicast;

import java.io.*;
import java.net.*;
import java.util.*;

public class MultiServer {

 private ArrayList<MultiServerThread> list;
 private Socket socket;
 
//생성자
 public MultiServer() throws IOException{
  
  list = new ArrayList<MultiServerThread>();
  ServerSocket serverSocket = new ServerSocket(5000);   //서버소켓 생성
  MultiServerThread mst = null;
//accept() 메서드가 있는 while 루프에 대한 지속여부 boolean인  isStop
  boolean isStop = false;

  
  while(! isStop){
   
   System.out.println("Server ready...");
//ServerSocket.accept()메서드로 클라이언트의 접속을 기다림, 연결 후 리턴되는 Socket 객체를 멤버로 할당.
   socket = serverSocket.accept();   

//이 MultiServer 객체인자로 하여 Runnable객체인 MultiServerThread 객체를 생성한다.
//이는 클라이언트와 접속이 이루어졌을 때 지속적인 대화를 하기 위해서 이다.
   mst = new MultiServerThread(this);

//이렇게 만든 스레드 객체를 ArrayList 안에 넣는다.   
   list.add(mst);
//스레드 생성 및 시작
   Thread t = new Thread(mst);
   t.start();
  }
 }
  
 public ArrayList<MultiServerThread> getList(){
  return list;
 }
  
 public Socket getSocket(){
  return socket;
 }
  
 public static void main(String[] args)throws IOException{
  new MultiServer();
 }
 }

MultiServerThread.java

package socket.multicast;

import java.io.*;
import java.net.*;

public class MultiServerThread implements Runnable{

 private Socket socket;
 private MultiServer ms;
 private ObjectInputStream ois;
 private ObjectOutputStream oos;
 
//생성자
 public MultiServerThread(MultiServer ms){
  this.ms = ms;   //인자로 받은 MultiServer 객체를 멤버변수로 할당.
 }
 

 public synchronized void run(){
  
  boolean isStop = false;   //메세지를 읽고 쓰는 루프문의 지속에 대한 boolean
  
  try{
   
//인자로 넘어온 MultiServer로부터 연결된 TCP소켓을 가져온다.
   socket = ms.getSocket();
//소켓에 기록하기 위한 스트림 생성
   ois = new ObjectInputStream(socket.getInputStream());
   oos = new ObjectOutputStream(socket.getOutputStream());
   String message = null;
   
   while(! isStop){
    
//ObjectInputStream의 readObject()메서드는 객체를 역직렬화 하는데, 이 때 객체는 반드시 Serializable를 구현해야 한다String 클래스는 기본적으로 Serializable를 구현한 클래스이기 때문에 readObject() 메서드를 이용하여 String객체를 역직렬화 할 수 있다.
    message = (String)ois.readObject();

//클라이언트는 'id#메세지' 형태의 데이타를 보내도록 되어 있다.

    String[] str = message.split("#");

    
    if(str[1].equals("exit")){

//broadCasting메서드는 MultiServer 객체가 가지고 있는 ArrayList에서 모든 MultiServerThread 객체를 가져다가 각각의 연결된 TCP소켓에 message를 기록한다. (모든 클라이언트에 message 배포)
     broadCasting(message);

     isStop = true;   //루프에서 빠져나감
    }else{
     broadCasting(message);
    }
    
   }//end while

//while 루프를 빠져나오게 되면
//MultiServer객체가 가지고 있는 ArrayList에서 이 MultiServerThread 객체를 제거한다.
   ms.getList().remove(this);

   System.out.println(socket.getInetAddress()+" 정상적으로 종료하셨습니다.");
   
   System.out.println("list size : "+ms.getList().size());
   
   
  }catch(Exception e){

//클라이언트가 강제종료 했을 경우 현재 TCP socket 에서 Exception이 발생하게 되는데, 이런 경우에도 ArrayList에서 현재의 MultiServerThread 객체를 제거해 준다.
   ms.getList().remove(this);

   System.out.println(socket.getInetAddress()+"비정상적으로 종료하셨습니다.");
   System.out.println("list size : "+ms.getList().size());
  }
 }
 

 public void broadCasting(String message) throws IOException{
  for(MultiServerThread ct : ms.getList()){

//send메서드는 ObjectOutputStream을 사용하여 현재의 MultiServerThread 객체에 message 를 기록한다.

     ct.send(message);

  }
 }

 public void send(String message) throws IOException{
//writeObject()메서드는 객체를 직렬화 해서 스트림에 전송한다.  
   oos.writeObject(message);

 }
 
}

MultiClient.java

package socket.multicast;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import javax.swing.*;
import javax.swing.border.Border;

public class MultiClient implements ActionListener{

 private Socket socket;
 private ObjectInputStream ois;
 private ObjectOutputStream oos;
 private JFrame jframe;
 private JTextField jtf;
 private JTextArea jta;
 private JLabel jlb1, jlb2;
 private JPanel jp1, jp2;
 private String ip;
 private String id;
 private JButton jbtn;
 
//생성자 - IP(연결하려는 서버IP) 와 ID를 인자로 받는다.
 public MultiClient(String argIp, String argId){

  ip = argIp;
  id = argId;
  jframe = new JFrame("Multi Chatting");
  jtf = new JTextField(30);             //JTextField는 한줄짜리 입력창
  jta = new JTextArea("", 10, 50);   //JTextArea는 여러줄짜리 입력창
  jlb1 = new JLabel("Usage ID : [[" + id + "]]");
  jlb2 = new JLabel("IP : "+ ip);
  jbtn = new JButton("종료");
  jp1 = new JPanel();
  jp2 = new JPanel();
  jlb1.setBackground(Color.yellow);
  jlb2.setBackground(Color.green);
  jta.setBackground(Color.pink);
  jp1.setLayout(new BorderLayout());
  jp2.setLayout(new BorderLayout());
  
  jp1.add(jbtn, BorderLayout.EAST);
  jp1.add(jtf, BorderLayout.CENTER);
  jp2.add(jlb1, BorderLayout.CENTER);
  jp2.add(jlb2, BorderLayout.EAST);
  
  jframe.add(jp1, BorderLayout.SOUTH);
  jframe.add(jp2, BorderLayout.NORTH);
  
  JScrollPane jsp = new JScrollPane(jta, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,  JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
  jframe.add(jsp, BorderLayout.CENTER);
 
  jtf.addActionListener(this);     //JTextField에 ActionListener연결
  jbtn.addActionListener(this);  //JButton에 ActionListener 연결
 
//JFrame에 WindowListener를 연결하는데, WindowsAdapter 익명클래스를 생성하면서 필요한 메서드들(windowClosing(), windowOpened())을 오버라이딩 했다.
  jframe.addWindowListener(new WindowAdapter(){

   
   public void windowClosing(WindowEvent e){
    try{
//윈도우를 닫을 때 사용자가 종료 메세지를 보낸 것과 동일하게 문자열을 만들어 소켓에 기록한다.
     oos.writeObject(id+"#exit");
    }catch(IOException ee){
     ee.printStackTrace();
    }
    System.exit(0);   //어플리케이션 종료
   }
   
   public void windowOpened(WindowEvent e){
//윈도우가 열리면 JTextField에 커서를 둔다.
    jtf.requestFocus();

   }
   
  });
  

  jta.setEditable(false);   //JTextArea 를 읽기전용으로
//스크린의 사이즈를 얻어오기 위해서 Toolkit 객체를 생성한다.
  Toolkit tk = Toolkit.getDefaultToolkit();

//Toolkit의 getScreenSize() 메서드를 사용해서 스크린사이즈를 담은 Dimension 객체를 리턴받는다.
  Dimension d = tk.getScreenSize();

  int screenHeight = d.height;
  int screenWidth = d.width;
  
  jframe.pack();   //JFrame의 사이즈를 서브콤포넌트들에 맞게 자동으로 조정해준다.
  jframe.setLocation((screenWidth - jframe.getWidth())/2, (screenHeight - jframe.getHeight())/2);
  jframe.setResizable(false);
  jframe.setVisible(true);
  
 }
 
 
//ActionListener 인터페이스가 강제하는 메서드
 public void actionPerformed(ActionEvent e){
//ActionEvent의 getSource() 메서드는 이벤트를 발생시킨 객체 자체를 리턴시켜 준다.
  Object obj = e.getSource(); 

  String msg = jtf.getText();  //JTextField에서 값을 읽어오는 getText() 메서드
  
//ActionListener에 연결한 객체가 두 개 이상이므로 IF문을 사용해 구분하였다.
//JTextField의 요청일 경우
  if(obj == jtf){

   
   if(msg == null || msg.length()==0){   //메세지 내용이 없을 경우

//Alert창 : JOptionPane.showMessageDialog(소속되는 상위객체, 메세지객체, 메세지창제목, 메세지창종류)

    JOptionPane.showMessageDialog(jframe, "글을 쓰세요", "경고", JOptionPane.WARNING_MESSAGE);
   
   }else{   //메세지 내용이 있을 경우
   
    try{
     oos.writeObject(id+"#"+msg);   //메세지를 직렬화 하여 소켓에 기록
    }catch(IOException ee){
     ee.printStackTrace();
    }
    jtf.setText("");   //기록한 후에는 입력창을 지우고 다시 입력받을 준비를 한다.
   }
  
//JButton의 요청일 경우 (종료버튼)
  }else if(obj == jbtn){ 
   
   try{
    oos.writeObject(id+"#exit");  //종료메세지 생성후 전송
   }catch(IOException ee){
    ee.printStackTrace();
   }
   System.exit(0);  //어플리케이션 종료
  }
 }
 
 //걍 System.exit(0)를 줄인 메서드
 public void exit(){
  System.exit(0);
 }
  
 
//객체 생성과 동시에 실행시키고자 만든 메서드
 public void init() throws IOException{
  
  socket = new Socket(ip, 5000);   //서버에 연결하는 소켓 객체 생성
  System.out.println("connected...");
  oos = new ObjectOutputStream(socket.getOutputStream());
  ois = new ObjectInputStream(socket.getInputStream());

//MultiClientThread 객체를 생성하면서 자신(MultiClient)을 인자로 넘긴다.
  MultiClientThread ct = new MultiClientThread(this);

//MultiClientThread 스레드를 시작한다.
  Thread t = new Thread(ct);

  t.start();
  
 }
 
 
 public static void main(String[] args)throws IOException{
  
  JFrame.setDefaultLookAndFeelDecorated(true);
  MultiClient cc = new MultiClient(args[0], args[1]);
  cc.init();
 }
 
 
 public ObjectInputStream getOis(){
  return ois;
 }
 
 public JTextArea getJta(){
  return jta;
 }

 public String getId(){
  return id;
 }
  
}

MultiClientThread.java

package socket.multicast;

//본 Thread 클래스는 다른 클라이언트로부터의 메세지를 지속적으로 대기하며 받고 보여주기 위한 클래스이다.
public class MultiClientThread extends Thread{

 private MultiClient mc;
 
 public MultiClientThread(MultiClient mc){  //생성시 MultiClient 객체를 인자로 받아 생성

  this.mc = mc;
 }
 

 public void run(){
  
  String message = null;
  
//message가 id#메세지 의 형태로 오기 때문에 split()으로 분리하여 문자열배열로 받는다.
  String[] receivedMsg = null;

  boolean isStop = false;   //true 이면 다른 서버로부터의 메세지 대기상태가 해제된다.   

 while(! isStop){

   
   try{
    
//서버와 연결된 소켓으로부터 역직렬화 하며 메세지를 읽어 변수에 할당한다.
//이 readObject() 메서드는 서버에서 객체를 전송할 때까지 기다리는 블로킹메서드이다.
    message = (String)mc.getOis().readObject();
    receivedMsg = message.split("#");
  
   }catch(Exception e){
    e.printStackTrace();
    isStop = true;
   }
   
   System.out.println(receivedMsg[0]+", "+receivedMsg[1]);
   

   if(receivedMsg[1].equals("exit")){   //exit메세지를 서버로부터 받았을 경우
    
//서버에서 온 메세지가 자신이 보냈던 exit 요청이라면 MultiClient 어플리케이션을 종료한다.
    if(receivedMsg[0].equals(mc.getId())){
     
     mc.exit();
     
    }else{  //다른 사람의 exit 요청이라면 내 swing 창에 누가 종료했는지 표시한다.
     
     mc.getJta().append(receivedMsg[0]+"님이 종료하셨습니다."+System.getProperty("line.separator"));

//JTextArea에 append 되는 경우는 스크롤바가 내려가지 않기 때문에 setCaretPosition() 메서드를 이용하여 캐릿의 위치를 JTextArea에 쓰여있는 문자열의 총 길이를 얻어와서 변경한다.
     mc.getJta().setCaretPosition(mc.getJta().getDocument().getLength());


    }

    
   }else{   //서버에서 온 메세지가 exit 가 아닐 경우
    
//메세지의 내용을 JTextArea에 출력한다.
    mc.getJta().append(receivedMsg[0]+" : "+receivedMsg[1]+System.getProperty("line.separator"));


//이후 JTextArea에 append 시킬 위치를 문장의 끝으로 조정한다.

    mc.getJta().setCaretPosition(mc.getJta().getDocument().getLength());


   }

   
  }//end while
 
 }//end run()
 
 
}

- 출처 : [한빛미디어] 자바 5.0 프로그래밍 -

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments