Java/Network

8편 : TCP 소켓통신을 통한 멀티플레이 기능 구현 - 클라이언트 구현

아, 그래요? 2022. 1. 23. 10:51
Client 구현

클라이언트는 기존의 MainGUI 객체를 기본으로 하여 구현하였다. 소켓 통신을 위해 필요한 메서드들을 추가해주었다.

  • startClient() : 채널그룹 객체와 소켓채널 객체를 생성한다. 서버채널에 연결을 요청하고 클라이언트를 시작한다.
  • stopClient() : 작업 과정에 예외가 발생했거나 모든 작업을 마쳤을 경우, 클라이언트를 종료한다. 채널그룹과 소켓채널을 닫는다.
  • receive() : 서버채널로부터 데이터를 받는다.
  • send(String data) : data를 서버채널로 보낸다.
  • handleResult(List<Integer> args) : receive()에 대한 버튼 별 콜백함수로 읽은 데이터를 기반으로 GUI를 변경하거나 유저에게 알림을 보낸다.

 

startClient()
public class Client extends JFrame {

    private String name;
    private int id;

    private AsynchronousChannelGroup channelGroup;
    private AsynchronousSocketChannel socketChannel;

    private JButton[] deco = new JButton[17];

    // control button
    private Ground ground;
    private EndTurnButton endTurnButton;
    private DeclareImpButton declareImpButton;


    void startClient() {
        try {
            channelGroup = AsynchronousChannelGroup.withFixedThreadPool(
                    Runtime.getRuntime().availableProcessors(),
                    Executors.defaultThreadFactory()
            );
            socketChannel = AsynchronousSocketChannel.open(channelGroup);

            ground = new Ground(socketChannel);
            endTurnButton = new EndTurnButton(socketChannel);
            declareImpButton = new DeclareImpButton(socketChannel);

            socketChannel.connect(new InetSocketAddress("localhost", 5001), name, new CompletionHandler<Void, String>() {
                @Override
                public void completed(Void result, String attachment) {

                    new AlertWindow("alert", "연결이 되었습니다!");
                    receive();
                }

                @Override
                public void failed(Throwable exc, String attachment) {
                    // TODO: handle Exception
                    new AlertWindow("error", "서버와의 연결에 실패했습니다");
                    stopClient();
                }
            });
        } catch (Exception e) {
            if (socketChannel.isOpen()) { stopClient(); }
            return;
        }
    }
}
  • 18-23 : 채널그룹과 소켓채널을 생성한다.
  • 25-27 : 생성한 소켓채널을 필드로 갖는 컨트롤 버튼들을 생성한다.
  • 29 : 서버에 연결을 요청한다.
  • 33-34 : 연결에 성공했을 경우, 연결 되었음을 유저에게 알리고, 데이터 받기를 시작한다.
  • 38-41 / 45-46 : 연결에 실패하거나, 연결과정 중 예외 발생시 클라이언트를 종료한다. 

 

stopClient()
public class Client extends JFrame {

    void stopClient() {
        try {
            if (channelGroup != null && !channelGroup.isShutdown()) { 
            	channelGroup.shutdownNow(); 
            }
        } catch (IOException e1) {}
    }
}
  • 5-7 : 채널그룹이 열려있을 경우, 종료한다.

 

receive()
public class Client extends JFrame {

    void receive() {
        ByteBuffer byteBuffer = ByteBuffer.allocate(100);

        socketChannel.read(byteBuffer, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer result, ByteBuffer attachment) {
                attachment.flip();
                Charset charset = Charset.forName("UTF-8");
                String message = charset.decode(attachment).toString();
                System.out.println("[응답 받음] " + message);

                String[] messageList = message.split(" ");
                try {
                    if (messageList.length < 2) { throw new IOException(); }

                    String flag = messageList[0];
                    String command = messageList[1];

                    if (flag.equals("TRUE")) {
                        List<Integer> args = new ArrayList<>();
                        for (int i = 2; i < messageList.length; ++i) {
                            int arg = Integer.parseInt(messageList[i]);
                            args.add(arg);
                        }

                        switch (command) {
                            case "PutTile":
                                ground.handleResult(args);
                                break;
                            case "EndTurn":
                                endTurnButton.handleResult(args);
                                break;
                            case "DeclareImp":
                                declareImpButton.handleResult(args);
                                break;
                            case "Wait":
                                new AlertWindow("alert", "상대 플레이어를 기다리는 중입니다");
                                break;
                            case "BeginGame":
                                new AlertWindow("alert", "게임을 시작합니다");
                                id = args.get(0);
                                if (id == 1) { Mode.getInstance().openLock(); }
                                break;

                            default:
                                System.out.println("[잘못된 커멘드입니다.]");
                        }
                    }
                } catch (IOException e) {
                    System.out.println("[서버에서 보낸 정보가 부족합니다] " + message);
                }
                ByteBuffer _byteBuffer = ByteBuffer.allocate(100);
                socketChannel.read(_byteBuffer, _byteBuffer, this);
            }

            @Override
            public void failed(Throwable exc, ByteBuffer attachment) {
                // TODO: handle exception
            }
        });
    }
}
  • 4 : ByteBuffer를 생성한다.
  • 6 : 소켓채널이 서버에 읽기를 요청한다.
  • 9 : 읽기에 성공했을 경우, 버퍼의 position을 초기화한다.
  • 10-11 : 버퍼를 디코딩하여 메세지를 문자열 형태로 받는다.
  • 14-19 : 문자열을 파싱하여 메세지로 부터 요청결과(flag), 요청명령(command), 첨부인자(args)를 받는다.
  • 21-48 : 요청결과가 TRUE(성공)인 경우, 요청 명령에 따른 버튼별 콜백함수(handleResult)를 실행한다.
  • 54-55 : 다시 ByteBuffer를 생성해 read 요청을 보냄으로써 읽기를 무한반복할 수 있게 한다. 

 

send(String data) 

send() 메서드의 경우 버튼을 클릭 시 클릭 이벤트에 해당하는 요청을 서버에게 보내는 것이므로 추상 객체인 ControlButton의 디폴트 메서드로 선언하여 ControlButton을 상속한 각종 버튼들이 클릭 이벤트 발생시 호출할 수 있도록 구현하였다.

public abstract class ControlButton {

    AsynchronousSocketChannel socketChannel;
 
    void send(String data) {

        if (Mode.getInstance().getLock()) { return; }

        Charset charset = Charset.forName("UTF-8");
        ByteBuffer byteBuffer = charset.encode(data);

        socketChannel.write(byteBuffer, null, new CompletionHandler<Integer, Void>() {
            @Override
            public void completed(Integer result, Void attachment) {}

            @Override
            public void failed(Throwable exc, Void attachment) {}
        });
    }
}
  • : Mode의 lock 필드는 서버 접근 권한에 관한 필드로 이것이 true일 경우, 락이 걸려 서버에 접근이 안된다. 이경우 바로 메서드를 리턴한다.
  • 9-10 : 매게인자로 받은 data를 인코딩하여 byteBuffer로 변환한다.
  • 12 : 인코딩한 데이터를 서버채널로 보낸다.
  • 추가적인 콜백함수는 없음

 

handleResult(List<Integer> args)

handleResult() 메서드는 서버 채널에서 데이터를 클라이언트 채널로 보낼 경우, 그것을 Client 객체가 받고 이후, 버튼 별로 실행하는 콜백함수이다. 버튼 별로 다르게 정의되어야 하므로 ControlButton의 추상 메서드로 선언하였다.

public abstract class ControlButton {
    public abstract void handleResult(List<Integer> args);
}

 

Ground.handleResult()

public class Ground extends ControlButton {

    public void handleResult(List<Integer> args) {
        System.out.println(args);
        int x = args.get(0);
        int y = args.get(1);
        int type = args.get(2);

        groundBtn[17*x + y].setIcon(ImgStore.getInstance().getRail(type));
        groundBtn[17*x + y].setName("-1");
        selectMode.initTileType();
    }
}
  • 5-7 : 메게인자로 받은 데이터로 부터 타일을 놓을 위치와 철로 타입을 입력 받는다.
  • 9-11 : 해당 위치의 타일 이미지를 해당하는 철로로 변경하고, 선택된 철로 타입을 초기화한다.

EndTurnButton.handleResult()

public class EndTurnButton extends ControlButton {

    @Override
    public void handleResult(List<Integer> args) {
        curPlayer = (curPlayer + 1) % 2;

        if (Mode.getInstance().getLock()) { Mode.getInstance().openLock(); }
        else { Mode.getInstance().closeLock(); }

        turnPlayerButton.setIcon(
                (curPlayer == 0) ?
                        ImgStore.getInstance().getImg("P1Img") :
                        ImgStore.getInstance().getImg("P2Img")
        );
    }
}
  • 5 : 현재 턴인 플레이어를 바꿔준다.
  • 7 : 내가 현재 턴인 경우 락을 풀고, 나의 턴이 아닌경우 락을 걸어준다.
  • 10-14 : 턴 변경에 따라 현제 턴 이미지도 변경해준다.