SSISO Community

시소당

다중 클라이언트 통신

다중  클라이언트를  지원하는  서버  구조는  프로그램의  요구에  따라  다양하게  설계된다.  서버를  설계할  때  무엇보다  중요한  것은  되도록  서버의  연산과  자원의  부담을  줄이는  것이다.  하나의  클라이언트가  서버에  많은  연산과  자원의  부담을  준다면  많은  클라이언트를  지원하는  서버는  언제든지  다운될  수  있기  때문이다.

  

각각의  클라이언트는  서버  측에  있는  하나의  소켓과  연결하여  독립적으로  데이터를  주고받기  때문에  클라이언트와  서버의  통신은  스레드로  구현해야한다.  스레드를  이용하지  않는  다른  방법을  생각해  볼  수  있겠으나  자바에서는  뾰족한  수가  없다.  설사  있다하더라도  스레드를  이용하는  방법보다  뛰어나지  못할  것이다.

  

여기서는  가위바위보  게임을  위한  간단한  서버/클라이언트  프로그램을  만들어보자.  아래는  클라이언트와  서버의  통신을  그림으로  표현한  것이다.  프로그램에  따라  다르겠지만  보통의  경우  상당히  복잡한  구조를  가진다.

  

[그림  21-10]  다중  클라이언트  ↔  서버

  

하나의  클라이언트는  서버  측  하나의  소켓과  통신하고  클라이언트끼리는  통신하지  않는다.  서버는  클라이언트가  접속하면  소켓을  생성하여  클라이언트와  연결시킨  후,  클라이언트와  데이터  입출력을  담당하는  스레드를  실행시킨다.  소켓  관리자는  소켓의  개수와  연결  상태를  관리하는  Vector이다.

  

  

  

가위바위보  게임  서버

서버는  접속해온  클라이언트와  소켓을  연결하고  입·출력을  담당하는  스레드를  실행시키는  것으로써  서버의  주요  역할은  끝난다.  실제  통신은  스레드가  담당한다.  스레드는  클라이언트가  보낸  "OK"  메시지를  받으면  가위,  바위,  보  중에서  하나를  선택하여  클라이언트  측으로  전송한다.  다른  말로,  클라이언트로부터의  신호가  없거나,  "OK"  이외의  다른  메시지를  받으면  서버  측에서는  어떤  응답도  보내지  않는다.

  

스레드는  가위,  바위,  보에  대응하는  정수  0,  1,  2를  보내는  것으로  약속한다.  예를  들면,  클라이언트가  서버로부터  0을  수신하면  서버가  가위를  선택한  것이  된다.

  

Random  rnd=new  Random();                //  난수  발생기

...

String  msg=reader.readUTF();        //  클라이언트가  보낸  데이터를  읽는다.

if(msg.equals("OK")){                          //  데이터가  "OK"  이면

    writer.writeInt(rnd.nextInt(3));      //  0~2사이  임의의  정수를  스트림에  출력한다.

    writer.flush();

}

  

서버  측에서는  데이터를  읽는  메소드로  readUTF를  사용하고  출력하는  메소드로는  writeInt를  사용해  보자.  그러면  클라이언트는  읽을  때  readInt를,  출력할  때  writeUTF를  사용해야  할  것이다.

  

소켓  관리자(Vector)는  클라이언트가  접속하면  그와  연결된  소켓을  리스트에  추가하고,  클라이언트가  나가면  그와  연결된  소켓을  리스트에서  제거한다.  그러면  접속자  수는  리스트에  추가된  소켓의  개수가  될  것이다.

  

Vector  sManager=new  Vector();

  

sManager.add(socket);                      //  소켓을  리스트에  추가한다.

  

sManager.remove(socket);              //  소켓을  리스트에서  제거한다.

  

sManager.size();                                  //  접속자  수를  얻는다.

  

  

승부의  결과는  서버가  처리하지  않고  클라이언트  측에서  처리하도록  하자.  이유는  서버의  부담을  줄이기  위함이다.

  

Server4.java


  

import  java.net.*;

import  java.io.*;

import  java.util.*;

public  class  Server4{

    private  ServerSocket  server;

    Vector  sManager=new  Vector();              //  소켓을  관리하는  벡터

    Random  rnd=new  Random();                    //  난수를  생성하는  변수

    public  Server4(){}

    void  startServer(){

        try{

            server=new  ServerSocket(7777);

            System.out.println("서버소켓이  생성되었습니다.");

            while(true){

                Socket  socket=server.accept();

                System.out.println("클라이언트와  연결되었습니다.");

  

                //  클라이언트와  통신하는  스레드를  생성하고  실행시킨다.

                new  KBBCom_Thread(socket).start();

  

                //  소켓  관리자  리스트에  소켓을  추가한다.

                sManager.add(socket);

  

                //  현재  접속하고  있는  클라이언트의  수를  화면에  출력한다.

                System.out.println("현재  클라이언트  수:  "+sManager.size());

            }

        }catch(Exception  e){

            System.out.println(e);

        }

    }

    public  static  void  main(String[]  args){

        Server4  server=new  Server4();

        server.startServer();          //  서버를  실행한다.

    }

  

    //  클라이언트와  통신하는  스레드  클래스

    class  KBBCom_Thread  extends  Thread{

        Socket  socket;                                              //  소켓의  레퍼런스

        private  DataInputStream  reader;              //  소켓의  입력  스트림

        private  DataOutputStream  writer;            //  소켓의  출력  스트림

        KBBCom_Thread(Socket  socket){          //  생성자

            this.socket=socket;

        }

        public  void  run(){

            try{

  

                //  소켓의  입·출력  스트림을  얻는다.

                reader=new  DataInputStream(socket.getInputStream());

                writer=new  DataOutputStream(socket.getOutputStream());

  

                String  msg;

                //  입력  스트림으로부터  메시지를  얻는다.

                while((msg=reader.readUTF())!=null){

                    if(msg.equals("OK")){        //  받은  메시지가  "OK"이면

  

                        //  0에서  2사이의  정수형  난수를  클라이언트로  전송한다.

                        writer.writeInt(rnd.nextInt(3));

                        writer.flush();

                    }    

                }

            }catch(Exception  e){

            }finally{      //  클라이언트와  접속이  끊겼을  때의  처리

                try{

                    sManager.remove(socket);    //  소켓  관리자  리스트에서  소켓을  제거한다.

  

                    //  입·출력  스트림과  소켓을  닫는다.

                    if(reader!=null)  reader.close();

                    if(writer!=null)  writer.close();

                    if(socket!=null)  socket.close();

                    reader=null;  writer=null;  socket=null;

  

                    //  적당한  정보를  화면에  출력한다.

                    System.out.println("클라이언트가  나갔습니다.");

                    System.out.println("현재  클라이언트  수:  "+sManager.size());

                }catch(Exception  e){}

            }

        }

    }

}

  

  

  

가위바위보  게임  클라이언트

클라이언트는  사용자가  가위,  바위,  보  중에서  하나를  선택하면  서버로  "OK"를  전송하고  서버의  응답을  기다린다.  서버로부터  응답이  오면  사용자가  선택한  것과  서버가  선택한  것을  서로  비교하여  승부의  결과를  화면에  보여준다.

  

클라이언트는  다음과  같은  상수를  선언한다.  각각  가위,  바위,  보를  나타내는  것이다.

  

public  static  int  KAWI=0;

public  static  int  BAWI=1;

public  static  int  BO=2;

  

사용자가  선택한  것을  player라고  하고  서버가  선택한  것을  server라고  하면,  먼저  비긴  경우는  쉽게  알  수  있다.  바로  player==server  일  경우이다.  사용자가  이긴  경우도  쉽게  알  수  있다.  아래는  승부를  알아내는  부분이다.

  

if(player==server)msgView.append("비겼습니다.\n");

else  if(player>server  ||  server-player==2)msgView.append("이겼습니다.\n");

else  msgView.append("졌습니다.\n");

  

다음  그림은  3개의  클라이언트가  서버에  접속하여  게임을  즐기는(?)  화면이다.  독자는  여기에  점수도  보여주는  코드를  삽입할  수  있을  것이다.

  

[그림  21-11]  서버와  다중  클라이언트  실행화면

  

  

Client4.java


  

import  java.awt.*;

import  java.net.*;

import  java.io.*;

import  java.awt.event.*;

public  class  Client4  extends  Frame  implements  ActionListener{

    private  TextArea  msgView=new  TextArea();

    private  Button  kawi,  bawi,  bo;                //  가위,  바위,  보에  대한  버튼

    private  DataInputStream  reader;

    private  DataOutputStream  writer;

    public  static  int  KAWI=0;                            //  가위를  나타내는  상수

    public  static  int  BAWI=1;                            //  바위를  나타내는  상수

    public  static  int  BO=2;                                //  보를  나타내는  상수

    Socket  socket;

    public  Client4(String  title){                        //  생성자

        super(title);

        msgView.setEditable(false);

  

        //  필요한  버튼  객체를  생성하고  배치한다.

        kawi=new  Button("가위");

        bawi=new  Button("바위");

        bo=new  Button("보");

        add(msgView,"Center");

        Panel  p=new  Panel();

        p.add(kawi);  p.add(bawi);  p.add(bo);

        add(p,  "South");

  

        //  버튼들의  이벤트를  처리한다.

        kawi.addActionListener(this);

        bawi.addActionListener(this);

        bo.addActionListener(this);

  

        pack();

    }

    private  void  connect(){

        try{

            msgView.append("서버와의  연결을  시도합니다.\n");

            socket=new  Socket("127.0.0.1",  7777);

            msgView.append("게임을  시작합니다.\n");

            

            //  소켓의  입·출력  스트림을  얻는다.

            reader=new  DataInputStream(socket.getInputStream());

            writer=new  DataOutputStream(socket.getOutputStream());

        }catch(Exception  e){

            msgView.append("연결  실패..");

        }

    }

    public  void  actionPerformed(ActionEvent  ae){    //  액션  이벤트  처리

        //  사용자가  선택한  것과  서버가  선택한  것.

        int  player=-1,  server=-1;                  //  -1은  선택되지  않은  상태를  나타낸다.

  

        //  사용자가  누른  버튼에  해당하는  값을  기억한다.

        if(ae.getSource()==kawi)

            player=KAWI;

        else  if(ae.getSource()==bawi)

            player=BAWI;

        else  if(ae.getSource()==bo)

            player=BO;

  

        //  다른  컨트롤에서  발생한  이벤트이면  메소드를  빠져  나온다.

        if(player==-1)return;

        

        try{

            //  "OK"를  서버로  전송한다.

            writer.writeUTF("OK");

            writer.flush();

  

            //  서버의  응답을  얻는다.  0~2  사이의  정수

            server=reader.readInt();

        }catch(IOException  ie){}

        

        //  승부  결과를  계산하여  msgView에  나타낸다.

        if(player==server)msgView.append("비겼습니다.\n");

        else  if(player>server  ||  server-player==2)msgView.append("이겼습니다.\n");

        else  msgView.append("졌습니다.\n");

        

    }

  

    public  static  void  main(String[]  args){

        Client4  client=new  Client4("가위바위보  게임");

        client.setVisible(true);

        client.connect();

    }

}

출처  :  http://java.pukyung.co.kr/Lecture/Chapter21.php

8610 view

4.0 stars