* 개요
- 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 프로그래밍 -