JAVA > LIBRARY > Java 소켓통신예제 계산기 채팅프로그램
 
JAVA
Library
Tip&Tech
Q&A
java공식사이트
Java  Platform Standard Edition 6 의 API 스펙
LIBRARY
  HOME > JAVA > LIBRARY
 
Java 소켓통신예제 계산기 채팅프로그램
작성일 : 10-02-04
조회 : 6,739  

프로젝트3. 채팅프로그램

지금까지 프로젝트를 어떻게 진행해왔는지를 생각해봅시다. 첫번째 프로젝트는 그냥 서버를 하나 띄우고, 클라이언트를 실행시키면 서버에 데이터를 한 번 보낸 후 프로그램을 종료했습니다. 두번째 프로젝트는 하나의 서버에 여러 클라이언트가 연결할 수 있었지만, 데이터 전송에 있어서는 하나가 가면 하나가 오고 하는 식이었습니다. 이번 프로젝트가 가지는 차이점은 채팅 프로그램을 생각하면 쉽게 알 수 있는 것들인데, 다음과 같습니다.

이전에는 패킷을 한 번 보내고서 응답을 기다렸지만, 이번 프로젝트에서는 서버에서 비동기적으로(아무때나) 데이터를 보냅니다. 왜냐하면 다른 클라이언트에서 서버로 보낸 메시지도 받아야 하기 때문입니다.
서버에서는 이전에는 패킷을 보낸 클라이언트에게만 결과값(패킷)을 전송했지만, 지금은 하나의 클라이언트에서 데이터가 보내지면 다른 모든 클라이언트에게 메시지를 보내야만 합니다.
서버에서 생성한 thread가 이전에는 독립적으로 클라이언트와 통신하는 모든 역할을 담당했지만, 이번에는 서버로부터의 요구에 의해 언제라도 클라이언트에게 메시지를 보낼 수 있어야 하고, 역으로 클라이언트로부터 메시지를 받으면 서버에게 다른 모든 클라이언트에게 메시지를 전송하라고 요청해야 합니다. 따라서 서버가 생성한 thread가 서버와 통신을 할 수 있어야 합니다.

따라서, 프로젝트2에서 했던 것과 같이 데이터를 보내고서 바로 결과를 기다리거나(클라이언트), 반대로 데이터를 받고 바로 그 클라이언트에게 응답을 보내는(서버) 방식이 아닌 다른 방식이 요구됩니다. 다음과 같은 방식으로 채팅 프로그램을 작성할 수 있습니다.

클라이언트에서도 서버로부터의 데이터를 받기 위해 Thread를 사용합니다. 데이터를 보내는 것은 사용자가 내용을 입력했을 때이지만, 서버로부터의 데이터는 아무때나 올 수 있기 때문에 서버로부터 응답을 받는 부분은 thread로 작성되어야만 서버의 데이터 전송에 바로 응답할 수 있습니다.
서버에서는 클라이언트가 연결되면 단지 Thread를 만들고 thread에게 모든 역할을 일임하는 것이 아니라, 만든 thread의 목록을 가지고서 하나의 thread가 메시지를 받으면, thread는 서버에 메시지를 받았다는 신호를 보내고, 서버는 thread 목록에 의해서 모든 클라이언트에게 데이터를 전송해야 합니다.
thread가 서버와 교신할 수 있도록, thread를 생성할때 서버 자신의 레퍼런스를 넘겨줍니다. 그러면 thread에서는 서버에 메시지를 보낼때, 서버의 메소드를 호출할 수 있습니다.
계산기 프로젝트까지 성실히 따라 왔다면 클라이언트에서도 thread를 생성하는 부분은 어렵지 않을 것입니다. 클라이언트에서 서버로 메시지를 보내는 것은 전과 같지만 메시지를 받는 부분은 thread를 생성하여 넘겨주어야 합니다. 이를 위해 thread를 생성할때, 소켓 또는 Reader를 함께 넘겨주어서 데이터를 받도록 해야 합니다. 또한 클라이언트에 있는 화면에 메시지를 출력하기 위해서는 클라이언트의 레퍼런스도 함께 넘겨주어야 합니다.

 

 

1) 프로토콜 정의

이 프로그램에서는 별다른 프로토콜 정의가 필요하지 않을 것 같네요. 아무것도 정의하지 않는 것도 하나의 정의라고 할 수 있겠지만서도... 아무튼 클라이언트에서 한 줄 문장을 보내면, 서버에서는 보낸 사람 아이디와 합쳐서 다른 클라이언트들에게 보내주면 되겠죠. 아이디를 바꾸는 것이나, 도움말 보기 같은 기능들은 서버나 클라이언트에서 받은 텍스트를 분석하는 기능을 넣으면 되겠죠.

이번에도 한 줄 씩의 메시지를 주고 받게 되니까 readLine() 메소드가 있는 BufferedReader와 BufferedWriter를 사용하는 것이 좋겠네요.

 

 

2) 서버 설계1: 소켓 준비와 Thread리스트 만들기

이 부분도 계산기 프로젝트와 별반 다르지 않습니다. 단지 thread를 생성할때, 인자로 자기 자신을 넘겨주는 것과, 생성한 thread를 목록에 넣어두는 것이 다르죠. 일단 프로젝트2에서와 같이 서버 부분의 소켓과 thread 구동까지 구현하고서, 수정하도록 해봅시다. 다음과 같겠죠.

ServerSocket srv=new ServerSocket(port);      // 서버 소켓을 만듭니다. port는 포트 번호
while (true) {
  Socket socket=srv.accept();         // 클라이언트의 연결을 기다립니다.
                                      // 연결되면 다음행으로 진행됩니다.
  Thread t=new ServerThread(socket);  // Thread를 생성합니다. 이때 소켓을 인자로 넘겨줍니다.
  t.start();                          // 생성된 Thread를 구동시킵니다.
}

여기까지는 모두 알고 있겠죠? 여기서 더 추가되어야 할 내용은 생성한 thread의 목록에 넣는 것입니다. 목록은 배열로 만들어도 좋고, 리스트를 구현해도 좋겠죠. 배열은 만들기는 쉽지만, 처음부터 사용하고자하는 최대한의 공간을 잡아주어야 하기 때문에 메모리 낭비도 심하고, 중간에 사용자가 나가는 경우, 그 빈공간을 채우기 위해 뒤에 있는 내용을 하나씩 앞으로 당겨주어야 하는 불편이 있습니다. 리스트는 배열의 단점을 극복할 수 있지만, 포인터가 없는 자바에서 구현하는 것이 원칙적으로 불가능하죠. 하지만, 걱정할 것은 없습니다. Vector라는 클래스를 사용하면 간단하게 해결되기 때문입니다. 이 클래스의 세부적인 사양은 자바스펙을 보도록 하고, 지금은 중요한 메소드만 몇 개 알아봅시다.

class Java.util.Vector

  • void addElement(Object obj) : 내용(객체)을 추가할 때 사용합니다.
  • Object elementAt(int index) : 주어진 위치(index)에 있는 내용을 참조합니다.
  • boolean removeElement(Object o) : o와 같은 객체 목록에서 삭제합니다.
  • void removeElementAt(int index) : 주어진 위치에 있는 객체를 삭제합니다..

 

Vector v=new Vector(); 라고 선언되어 있다면 v에 추가하기 위해서는 v.addElement(obj);


Vector v=new Vector(); 라고 선언되어 있다면 v에 추가하기 위해서는 v.addElement(obj);를, 삭제하기 위해서는 v.removeElementAt(index)를, 참조하기 위해서는 v.elementAt(index)를 사용할 수 있겠죠. 위의 프로그램에서는 일단, 처음에 Vector클래스를 하나 만들고서, thread를 생성한 후 벡터에 추가해주면 됩니다. 그러면 프로그램은 다음과 같이 수정되겠죠. 내친 김에 exception처리까지 해서 코딩을 해봅시다.


ServerSocket srv=new ServerSocket(port);
Vector client=new Vector();

while (true) {
  try {
    Socket socket=srv.accept();
    ServerThread t=new ServerThread(socket, this);
    t.start();
    client.addElement(t);
  } catch (IOException e) {
    System.out.println("Connect to Fail!!");
  }

}
하나 주의할 점은 Vector가 이 메소드 밖에서 정의되어 있어야 한다는 점입니다. 그렇지 않으면 이 벡터는 이 메소드 안에서만 사용할 수 있게 되겠죠. 또 하나는 thread를 생성할 때 자기 자신(this)를 인자로 함께 넘겨준다는 점입니다. 이것은 개별 thread에서 다른 thread로 메시지를 보낼 때, 이 Server클래스를 사용하여 메시지를 보낼 수 있도록 하기 위해서입니다. 즉 thread가 다른 모든 thread들의 레퍼런스를 가지고서 메시지를 보내는 것이 아니라 그냥 server에 메시지를 전송하도록 요청하면 서버는 목록에 있는 모든 클래스에게 메시지를 보내는 구조입니다. 아래 그림을 다시 보면 좀 이해가 빠를 듯합니다.

 

 

3) 서버 설계2: 리스트를 이용하여 모든 thread에 메시지 전송하기

그림에서 보면, 모든 thread에 메시지를 전송하는 메소드가 하나 더 필요하다는 것을 알 수 있죠. 이것의 구현은 아주 간단합니다. 그냥 for문으로 처음부터 끝까지 돌리면서 객체에 하나씩 하나씩 전송해주면 되죠.


public void message(String str) {
  for (int i=0; i<client.size(); i++) {
    ((ServerThread)client.elementAt(i)).sendMessage(str);
  }
}
client.elementAt(i)는 아까 만든 벡터에서 i째에 있는 객체니까, i번째에 만든 thread를 의미하겠죠. sendMessage(str)는 ServerThread에 구현해 놓은 메소드입니다. 이것을 호출하면 인자로 주어진 String을 자신이 연결된 클라이언트에 전송하게 됩니다. 이렇게 서버에서는 일일히 모든 클라이언트에 메시지를 전송하는 것이 아니라, 그냥 연결된 Thread에 있는 메소드를 호출하기만 하면, Thread가 알아서 메시지를 전송합니다. 위의 그림을 보면 이해가 쉬우리라 생각합니다.

 

그런데, 굵게 표시되어있는 (ServerThread)는 무엇인지 궁금할 것입니다. 이것은 클래스의 상속과 관계된 형변환입니다. 뒤에서 만들겠지만, 우리는 Thread 클래스를 상속하여 ServerThread를 작성할 것입니다. 자바의 모든 클래스는 Object라는 클래스에서 파생됩니다. Thread또한 마찬가지이고, 따라서 ServerThread 또한 Object로부터 파생된 것(Object를 상속한 것)입니다. SuperClass와 SubClass가 있을때, 좋은 용어는 아니지만, 부모 클래스와 자식클래스라고도 하죠. 아무튼 SuperClass를 상속하여 SubClass를 만들었다면, SuperClass로 선언된 변수는 자식 클래스를 받을 수 있습니다. 예를 들어, 다음 코드는 컴파일이 가능하다는 이야기죠.

Superclass s1=new Superclass();
Subclass s2=new Subclass();
s1=s2;

하지만, subclass s2가 s1에 넣어지면, s2가 새로 만든 메소드는 사용할 수 없게 됩니다. 즉, print()라는 메소드가 s2에만 있고, s1에만 있다면 s1=s2라고 s1에는 s2가 들어있지만, s1.print()와 같이 사용할 수 없다는 의미입니다. 따라서 print()메소드를 사용하기 위해서는 s1을 다시 s2로 형변환(type casting)을 해주어야 합니다. ((s2)s1).print()는 가능하다는 이야기입니다.

이런 이유로 위에서도 형변환이 필요합니다. 위에 있는 Vector클래스의 메소드들을 보면 알 수 있지만, addElement(Object obj)와 같이 컴포넌트를 추가할 때 Object로 받게 됩니다. 즉 벡터 안에서는 Object클래스로 변환되어 저장된다는 뜻입니다. elementAt()도 역시 Object로 반환합니다. Object클래스에 sendMessage()라는 메소드가 있을 리가 없죠. 따라서 반환된 클래스를 다시 처음대로 ServerThread로 만들어주기 위해서 앞에서와 같이 형변환을 하는 것이죠.


4) 서버 설계3: ServerThread구현

이 부분 또한 앞의 계산기 프로그램과 비슷합니다. 생성자에서 Server클래스를 인자로 받는 것과 sendMessage()메소드를 따로 구현해주어야 하는 차이점만 있습니다. 먼저 생성자를 만들어봅시다.


ChattingServer server=null;
BufferedReader br=null;
BufferedWriter bw=null;

public ServerThread(Socket socket, ChattingServer c) {
  server=c;
 
  try {
    br=new BufferedReader(new InputStreamReader(socket.getInputStream()));
    bw=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
  } catch (IOException e) {
  }
}
인자로 받은 ChattingServer클래스와 소켓을 클래스 내의 다른 메소드들이 쓸 수 있도록 클래스 인스턴스에 넣어줍니다. 다음에는 run()을 만들어주어야 합니다. 이미 알고 있겠지만, ChattingServer클래스에서 생성자를 사용하여 ServerThread를 만들고, start()를 사용하여 이 Thread를 구동시키면 run()메소드가 실행이 됩니다.


public void run() {
  String str="";

  while (true) {
    try {
      str=br.readLine();
      server.message(username+":"+str);
    } catch (IOException e) {
      server.removeClient(this);
      break;
    }
  }
}

아주 단순하죠? 그냥 한 줄을 읽어서, 그 내용 그대로 ChattingServer에 있는 message()메소드를 호출하였습니다. 앞에서 설명한 듯이 message()메소드에서는 리스트에 있는 모든 Thread들에게 클라이언트로 메시지를 보내라는 요청을 하게 되죠. 클라이언트로부터 데이터를 읽다가 exception이 나면 thread를 종료하도록 하였습니다. ChattingServer의 removeClient라는 메소드를 호출하도록 했네요. 이 메소드에서는 removeElement()를 사용해서 리스트에서 이 Thread를 제거하도록 하면 되겠죠. 그 구현은 각자에게 맡기겠습니다.

서버의 message()메소드를 호출하면 이 메소드에서는 각 thread에 있는 sendMessage(String str)을 호출하여 클라이언트에게 실제로 메시지를 전송하도록 하였던 것을 기억할 겁니다. sendMessage()메소드도 구현해주어야 하겠죠.

 

 public void sendMessage(String str) {
  try {
   bw.write(str);
   bw.newLine();
   bw.flush();
  } catch (IOException e) {
  }
 }

이렇게 하면 메시지를 클라이언트에 보낼 수 있게 됩니다. 이렇게 하면 ChattingServer가 일단 완성되었습니다. 하지만 채팅 프로그램에 꼭 필요한 기능들은 더 추가해야 합니다. 예컨대 사용자 이름을 어떻게든 받아서, 메시지를 보낼때마다 사용자 이름과 함께 보내도록 해야 하겠죠. 제가 테스트할 때는 클라이언트와 연결되면 클라이언트는 제일 먼저 사용자 이름을 전송하고, ServerThread에서는 그것을 받고 나서 루프를 돌도록 했습니다. 사실 이것은 그냥 클라이언트에서 자신의 이름을 처음부터 붙여서 전송하는 것도 괜찮겠죠.
제일 먼저 클라이언트가 접속을 하면 모든 사용자들에게 사용자가 들어왔다는 메시지를 전송하는 기능은 기본입니다. 또는 사용자 이름을 바꾸거나, 다른 사용자의 리스트를 보거나 하는 등의 기능들은 각자 추가해보시기 바랍니다.

 

 

5) 클라이언트1: 소켓 연결, 메시지 전송

소켓 연결과 메시지 전송은 따로 설명이 필요 없을 것 같네요. 그냥 소스만 보도록 하겠습니다.


Socket socket=null;
BufferedReader br=null;
BufferedWriter bw=null;

try {
 socket=new Socket(ip, port);
 br=new BufferedReader(new InputStreamReader(socket.getInputStream()));
 bw=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
 
} catch (IOException e) {
 System.out.println("Fail to connection!!");
 throw e;
}
  
ReceiveMessage r=new ReceiveMessage(br, this);
r.start();

다른 부분은 ReceiveMessage 를 선언해서 구동시키는 부분입니다. 이미 짐작했겠지만, ReceiveMessage는 Thread로 구현되어 있습니다. 이 부분은 앞에서 설명했듯이 계속해서 돌고 있다가 서버로부터 메시지를 받으면 클라이언트의 화면에 전송하는 역할을 합니다.

 

 

이번에는 실제와 비슷하게 클라이언트를 GUI로 만들도록 합니다. swing을 사용해서, TextArea와 TextField를 만들어서, TextField에 내용을 입력하면 서버에 전송하고, 서버로부터 메시지를 받으면 TextArea에 붙이도록 합니다. swing에 대한 자세한 내용은 Mahadevi의 Swing강좌를 참조하세요. 객체지향언어는 자료의 형식과 인자의 전달, 이벤트(메소드 호출)가 가장 중요하다고 할 수 있겠죠. 위 그림에서 클라이언트에서 사용자의 메시지를 기다리는 부분은 루프를 돌고 있는 것처럼 그려졌지만, 실제로는 이벤트에 의해서 일어납니다. TextField에 문자열을 입력하고 엔터를 치는 순간(이벤트가 발생하면) 메시지를 서버에 전송하게 되는 것이죠.

어떻게 텍스트필드를 달고 이벤트헨들링을 하는가에 대해서는 스윙이나 awt를 공부해야 하니까 여기서 설명을 하자면 한없이 길어질 것 같아서 생략해야 하겠네요. 아무튼 어떻게어떻게해서 텍스트 필드에서 데이터를 입력하면 sendMessage()라는 메소드가 실행되게 되었다고 칩시다. sendMessage()메소드에서는 텍스트 필드의 내용을 읽어서 서버에 전송하기만 하면 되겠죠.


public void sendMessage() {
  String str=textField.getText();   // 텍스트 필드의 내용을 얻는다.
  try {
    bw.write(str);                    // str을 버퍼에 넣는다.
    bw.newLine();                     // \n 을 추가한다.
    bw.flush();                       // 버퍼의 내용을 서버에 전송한다.
  } catch (IOException e) {
    // 서버에 전송할 수 없는 상황을 처리할 수 있는 코드.
  }
}

이렇게 하면 클라이언트는 텍스트 필드에 내용을 입력하고서 엔터를 치면 서버로 전송할 수 있게됩니다.

 

 

6) 클라이언트2: 메시지 받기

ReceiveMessage 클래스를 만들어야겠군요. 이 클래스는 당연히 Thread로 작성되어야 하겠죠. 인자로는 데이터를 받아야 할테니까, 소켓 같은 것을 넣어주어야 하겠죠. 하지만 받기만 하니까 소켓 말고 BufferedReader를 넘겨주기로 합시다. 그리고 데이터를 받아서 화면에 써주어야 하는데, 이 화면은 ChattingClient클래스에 있는 것이니까 ReceiveMessage가 직접 쓸 수는 없겠죠. 그냥 편하게 Thread를 생성할때 ChattingClient가 자기 자신(this)을 넘겨주는 걸로 합시다. 나머지는 별다는 설명이 필요 없을 것 같으니까 그냥 소스를 봅시다.


class ReceiveMessage extends Thread {
 
 BufferedReader br=null;
 ChattingClient client=null;

 public ReceiveMessage(BufferedReader br, ChattingClient c) {
  this.client=c;
  this.br=br;
 }
 
 public void run() {
  
  String str="";
  
  while (true) {
   try {
    str=br.readLine();
    if (str!=null) {
     client.message(str);
    }
   } catch (IOException e) {
    
   }
    
  } // end of while
 }  // end of run ()
}  // end of class ReceiveMessage

소스는 매우 간단합니다. 생성자에서는 그냥 BufferedReader와 ChattingClient를 인자로 받기만 하고, run()메소드에서 무한 루프를 돌면서 한줄씩 받아서 ChattingClient의 message라는 메소드를 호출해주는 것이 다죠. 물론 ChattingClient에는 message()가 구현되어 있어야 하고, TextArea에 문자열을 출력해주는 일을 하겠죠.

 

 

7)참고 예제

제가 작성한 예제 화일입니다.

ChattingServer.java: 채팅 서버입니다. 컴파일 하시고, java ChattingServer [port-name/default=5777] 로 실행합니다.
ChattingClient.java: 클라이언트부입니다. 이것을 실행시키면 로컬호스트의 5777 포트로만 연결합니다. 이것 말고 아래의 RunClient를 실행시키세요.
RunClient.java: ChattingClient를 사용하여 실제 클라이언트 GUI를 만듭니다. java ChattingClient 라고 실행하면 ip와 port, username을 입력하는 창이 뜹니다.
먼저 서버를 실행시키시고, RunClient를 실행시키면 됩니다. IP입력창에서, 자신의 컴퓨터에 서버가 구동되고 있다면, 자신의 IP또는 127.0.0.1을 입력하시고, 다른 컴퓨터에서 실행된다면 그 컴퓨터의 IP를 입력하세요. port는 처음 서버를 구동시킬때 사용한 포트를 적어주어야 합니다. 별다른 옵션없이 서버를 구동했다면 포트번호는 5777번 입니다.

실제로 채팅 프로그램은 클라이언트가 애플릿일때 유용성이 클 것입니다. 하지만, 애플릿에서 소켓 통신을 하기 위해서는 보안정책파일을 편집해야 하기 때문에, 일단 그냥 어플리케이션으로 작성했습니다. ChattingClient가 JPanel로 작성되어있기 때문에 Applet으로 만들기 위해서는 애플릿을 상속한 적당한 클래스를 상속받아서 ChattingClient를 붙여주기만 하면 됩니다. 물론 보안정책파일도 편집해야 하겠죠.
 
퍼온 곳 : http://blog.naver.com/hopal78/100005269988 

[출처] Java 소켓통신예제|작성자 뮤즈


 
 

Total 22
번호 제   목 조회
22 Java SE 6 한글 API 문서입니다. 4122
21 Stream for Data Transmision 2552
20 setAutoCommit(false) 에 대해서... 7725
19 java.net 패키지 - Url 클래스 5480
18 소켓 연결 샘플 4877
17 Java 소켓통신예제 계산기 채팅프로그램 6740
16 Java 소켓통신예제 계산기 프로그램 5107
15 Java 소켓통신예제 Socket in TCP/IP protocol 14696
14 Java 소켓통신예제 13297
13 자바 컴파일과 실행 에 사용되어지는 javac, java 명령어의 옵션 6036
12 자바 api 메뉴얼 3994
11 java.util 패키지 5298
10 J2SE, J2EE, J2ME 3844
9 이클립스 사용방법 9375
8 리눅스 자바설치 4094
7 IBM 이클립스의 기본적인 사용법을 설명 4600
6 자바언어의 특징 4226
5 자바 API 문서에서 substring 메서드의 사용방법 입니다. 11140
4 JAVA MySQL 연동하기 9260
3 JAVA 데이터 베이스 드라이버 명칭과 데이터베이스 URL 3795
 1  2  
 
개인홈페이지 덤벙닷컴은 프로그래머와 디자이너위한 IT커뮤니티 공간입니다.
Copyright ⓒ www.dumbung.com. All rights reserved.