#4_채팅방 프로토콜 설계(서버)

마자나다·2023년 10월 5일

채팅방 프로젝트

목록 보기
5/6

서버 모듈

앞서 클라이언트가 어떻게 구성되어있는지 알아보았다 이번엔 서버가 어떻게 알아볼 차례인데 대부분 클라이언트와 얼추 비슷한 구성을 이루고 있다.

서버 모듈 구성

서버는 마찬가지로 클라이언트가 접속을 시도할때마다, 서버 스레드를 만들고 역직렬화와 직렬화 동시에 수행하며, 클라이언트에게 받을 데이터는 받고, 보낼 데이터는 보내는 역할을 하고 있다.

1. Server
서버 부분은 클라이언트의 연결 수락을 하고 들어올때마다 새로운 소켓을 생성, 그리고 새로운 스레드를 생성하고 있다.

public void start() {
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(SERVER_PORT); // 포트번호와 서버 소켓 생성
            System.out.println("[Server Start]");
            while (true) {
                System.out.println("[Client Waiting]");
                Socket socket = serverSocket.accept(); //클라이언트 연결 수락
                //연결이 들어올때마다 새로운 소켓 생성
                //클라이언트가 접속하면 새로운 스레드 생성.
                ServerThread ServerThread = new ServerThread(socket);
                ServerThread.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

2. ServerThread
서버 스레드는 클라이언트가 보낸 메세지를 헤더부분의 데이터를 추출한뒤 그 정보를 기반으로 역할을 수행한다.

public void run() {
        try {
            while (true) {
                byte[] clientbytedata = new byte[MAXBUFFERSIZE];
                int clientbytelength = in.read(clientbytedata);
                PacketType clientpackettype = byteToPackettype(clientbytedata); //헤더부분 타입추출
                int clientpacketlength = byteToBodyLength(clientbytedata);// 헤더부분 길이추출
                boolean disconnectcheck = true;
                if (clientbytelength >= 0) {
                    HeaderPacket packet = makeClientPacket(clientbytedata, clientpackettype);
                    disconnectcheck = packetCastingAndSend(packet, clientpackettype);
                }
                if(!disconnectcheck){
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("[" + clientName + "Disconnected]");
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

위와 같은 코드로 헤더부분의 데이터를 추출한 다음. 나중에 소개할 makeClientPacket이나 PacketCasting에서 다시 클라이언트에게 보낼 데이터를 정리한다.

byteToPackettype,byteToBodyLength 이곳에서 앞서 바이트의 위치를 알고있기때문에 다시 재조합하여 타입과 길이를 파악한다.
이 메서드는 HeaderPacket클래스에 존재한다.

public static PacketType byteToPackettype(byte[] headerByte) {
        int type = byteArrayToInt(headerByte, 0, 3);
        return clientFindByValue(type);
    }

    public static int byteToBodyLength(byte[] headerByte) {
        return byteArrayToInt(headerByte, 4, 7);
    }
  • connetClient 메서드

커넥트 클라이언트 패킷이 들어오게 되면 클라이언트의 이름이 중복되지 않았는지 확인한 후, 이름을 저장한다. 스레드마다 사용자의 이름을 변수로 저장하고, Map을 만들어서 key는 out소켓을(나중에 전송을 위해서) value는 클라이언트의 이름으로 저장하였다.

private synchronized void connectClient(ClientConnectPacket connectPacket) throws IOException { // 커넥트 요청 들어올시 동작
        if (clientMap.containsValue(connectPacket.getName())) {
            exceptionMessage(out, "Duplicate name. Please enter another name");
            return;
        }
        clientName = connectPacket.getName();
        clientMap.put(out, clientName);
        sendAllNotify(clientName + " is Connected");
        System.out.println("[" + clientName + " Connected]"); //서버에 띄우는 메세지.
    }

sendAllNotify나 exceptionMessage메서드 같은 경우엔 내가 공지나 경고 메세지를 클라이언트에게 따로 보낼때 만든 메서드이다.

  • makeClientPacket, packetCastingAndSend 메서드

타입에 따라 다시 클라이언트가 보낸 패킷으로 만드는 makeClientPacket와 상속받은 HeaderPacket으로 되어있는 객체를 원래의 세부 패킷으로 캐스팅을 한 뒤 역할을 수행하는 packetCastingAndSend이다.

private HeaderPacket makeClientPacket(byte[] bytedata, PacketType clienttype) throws IOException {
        if (clienttype == CLIENT_MESSAGE) {
            return byteToClientMessagePacket(bytedata);
        } else if (clienttype == CLIENT_CONNECT) {
            return byteToClientConnectPacket(bytedata);
        } else if (clienttype == CLIENT_DISCONNECT) {
            return byteToClientDisconnectPacket(bytedata);
        } else if (clienttype == CLIENT_CHANGENAME) {
            return byteToClientChangeNamePacket(bytedata);
        } else if (clienttype == CLIENT_WHISPERMESSAGE) {
            return byteToClientWhisperPacket(bytedata);
        } else if (clienttype == CLIENT_FILE) {
            return byteToClientFilePacket(bytedata);
        } else return null;
    }

    public synchronized boolean packetCastingAndSend(HeaderPacket packet, PacketType clientpackettype) throws IOException {
        if (packet != null) {
            if (clientpackettype == PacketType.CLIENT_CONNECT) {
                ClientConnectPacket connectPacket = (ClientConnectPacket) packet;
                connectClient(connectPacket);
            } else if (clientpackettype == PacketType.CLIENT_MESSAGE) {
                ClientMessagePacket messagePacket = (ClientMessagePacket) packet;
                sendAllMessage(messagePacket);
            } else if (clientpackettype == CLIENT_CHANGENAME) {
                ClientChangeNamePacket changeNamePacket = (ClientChangeNamePacket) packet;
                boolean containsValue = clientMap.containsValue(changeNamePacket.getChangename());
                if (containsValue) {
                    exceptionMessage(out, "Duplicate name. Please enter another name");
                } else {
                    clientChangeName(changeNamePacket);
                    clientName = changeNamePacket.getChangename();
                    exceptionMessage(out, "Your name has been changed to " + changeNamePacket.getChangename());
                }
            } else if (clientpackettype == CLIENT_WHISPERMESSAGE) {
                ClientWhisperPacket whisperPacket = (ClientWhisperPacket) packet;
                sendWhisperMessage(whisperPacket, clientName);
            }
            else if (clientpackettype == CLIENT_FILE) {
                ClientFilePacket filePacket = (ClientFilePacket) packet;
                System.out.println("packetCasting chunk :" + filePacket.getChunk().length);
                sendFile(filePacket, clientName);
                return true;
            }
            else if (clientpackettype == PacketType.CLIENT_DISCONNECT) {
                ClientDisconnectPacket disconnectPacket = (ClientDisconnectPacket) packet;
                disconnectClient(disconnectPacket);
                if (disconnectPacket.getName().equals(clientName)) {
                    clientMap.remove(out);
                    return false;
                }
            }
        }
        return true;
    }

조금 switch문으로 조금 간단하게 만들수있을거 같은데 지금 보니까 후회스럽다.. 너무 패킷을 많이 만든감도 없지않아 있고 여튼! 이 코드의 역할은 받은 패킷마다 다양한 역할을 캐스팅하고 보내는 역할을 수행한다.

2. ServerMessageHandler
ServerMessageHandler클래스는 서버가 클라이언트에게 수행할 역할을 메서드를 모아놓은 클래스이다. 다양한 메서드가 있는데 간략한 요약을 아래에 적었다.

  1. sendAllMessage : 클라이언트가 모두에게 보내는 일반적인 메세지 채팅 내용을 보낸다
  2. sendWhisperMessage : 클라이언트가 귓속말 할 대상 한명에게 메세지를 보낼때 사용하는 메서드이다.
  3. clientChangeName : 클라이언트가 자신의 닉네임을 바꾸고싶을때 사용하는 메서드이다.
  4. sendAllNotify : sendAllMessage는 클라이언트가 다른 클라이언트 모두에게 보내는 메서드였다면, sendAllNotify는 서버가 모두에게 보내는 공지를 전송하는 메서드이다.
  5. disconnectClient : 클라이언트가 채팅방을 나갈때 사용되는 메서드이다.
  6. exceptionMessage : 서버가 한명의 클라이언트에게만 공지를 보낼때 사용되는 메서드이다.
  7. sendFile : 클라이언트가 이미지나 텍스트 파일등을 보낼때 사용되는 메서드이다.
  • sendAllMessage 메서드

클라이언트가 모두에게 보내는 전체메세지를 다른 클라이언트들에게 전송하는 기능을 가지고 있다.

public static void sendAllMessage(ClientMessagePacket messagepacket) throws IOException { //모두에게 전송하는 메세지 (lock 걸어야함)
        byte[] sendAllbyte = null;
        ServerMessagePacket serversendpacket = new ServerMessagePacket(messagepacket.getMessage(), messagepacket.getName());
        sendAllbyte = packetToByte(serversendpacket);
        try {
            for (Map.Entry<OutputStream, String> entry : clientMap.entrySet()) {
                String receiverName = entry.getValue();
                OutputStream clientStream = entry.getKey();
                if (messagepacket.getName().equals(receiverName)) {
                    continue;
                }
                try {
                    clientStream.write(sendAllbyte);
                    clientStream.flush();
                } catch (IOException e) {
                    // 클라이언트와의 연결이 끊어진 경우, 해당 클라이언트를 제거합니다.
                    clientMap.remove(clientStream);
                    out.println("[" + receiverName + " Disconnected]");
                }
            }
        } catch (ConcurrentModificationException e) {
            e.printStackTrace();
        }
    }

모두에게 보낼 메세지와 보내는사람이 이름을 담아서 패킷을 만들고.
packetToByte 패킷을 byte단위로 바꿔주는 메서드를 사용하여 byte단위로 바꿔준다. 그리고 Map안에 out소켓과 이름을 넣어놨던 데이터를 사용하여 모든 클라이언트들에게 메세지를 전달한다.

  • sendWhisperMessage 메서드

앞서 클라이언트가 /w명령어를 사용하여 특정한 한명의 원하는 클라이언트에게 메세지를 보내고싶을때 사용되는 메서드이다.

public static void sendWhisperMessage(ClientWhisperPacket whisperPacket, String sendName) throws IOException {
        byte[] sendAllbyte = null;
        ServerMessagePacket serversendpacket = new ServerMessagePacket(whisperPacket.getMessage(), sendName);
        sendAllbyte = packetToByte(serversendpacket);
        try {
            for (Map.Entry<OutputStream, String> entry : clientMap.entrySet()) {
                String receiverName = entry.getValue();
                OutputStream clientStream = entry.getKey();
                if (receiverName.equals(whisperPacket.getWhispername())) {
                    try {
                        clientStream.write(sendAllbyte);
                        clientStream.flush();
                    } catch (IOException e) {// 클라이언트와의 연결이 끊어진 경우, 해당 클라이언트를 제거합니다.
                        clientMap.remove(clientStream);
                        out.println("[" + receiverName + " Disconnected]");
                    }
                    return;
                }
            }
            for (Map.Entry<OutputStream, String> entry : clientMap.entrySet()) { //만약 전송되지 않았을때 예외처리
                String receiverName = entry.getValue();
                OutputStream clientStream = entry.getKey();
                if(receiverName.equals(sendName)){
                    try {
                        exceptionMessage(clientStream,"There is no user with that name.");
                    } catch (IOException e) {// 클라이언트와의 연결이 끊어진 경우, 해당 클라이언트를 제거합니다.
                        clientMap.remove(clientStream);
                        out.println("[" + receiverName + " Disconnected]");
                    }
                    return;
                }

            }
        } catch (ConcurrentModificationException e) {
            e.printStackTrace();
        }
    }

마찬가지로 보내는사람의 이름과 메세지를 담아 패킷을 만든다음, clientMap에 있는 이름중 whisperPacket.getWhispername()와 일치하는 클라이언트에게만 메세지 데이터를 전송한다.
for문이 2개가 있는데 첫번째 for문이 성공적으로 원하는 클라이언트에게 전송이 된다면 종료를한다.
하지만 존재하지않은 클라이언트에게 전송했다면 밑에 for문이 동작하며, 메세지를 보냈던 클라이언트에게 "There is no user with that name."를 전송하여 오류를 알린다.

  • clientChangeName 메서드

클라이언트가 자신의 이름을 바꾸고 싶을때 사용하는 메서드이다.

public static void clientChangeName(ClientChangeNamePacket clientChangeNamePacket) throws IOException {
        ServerNameChangePacket serverNameChangePacket = new ServerNameChangePacket(clientChangeNamePacket.getName(),clientChangeNamePacket.getChangename());
        byte[] serverNameChangePacketbyte = packetToByte(serverNameChangePacket);
        for (Map.Entry<OutputStream,String> entry : clientMap.entrySet()) {
            String receiverName = entry.getValue();
            OutputStream clientStream = entry.getKey();
            try {
                clientStream.write(serverNameChangePacketbyte);
                clientStream.flush();
            } catch (IOException e) {
                // 클라이언트와의 연결이 끊어진 경우, 해당 클라이언트를 제거합니다.
                clientMap.remove(clientStream);
                out.println("[" + receiverName + " Disconnected]");
            }
            if(clientChangeNamePacket.getName().equals(receiverName)){
                clientMap.put(clientStream,clientChangeNamePacket.getChangename());
            }
        }
    }

패킷 내부에는 원래의 이름과, 바꾸고싶은 이름 2개가 있다. clientMap안에 있는 원래의 이름과 동일한 데이터를 찾은 뒤,
이름을 수정한다.
그리고 바꾸는 닉네임이 중복될수 있는데 중복에 대한 확인은 앞서 보여준 packetCastingAndSend 메서드에서 수행한다.

  • sendAllNotify 메서드

서버로서 모두에게 공지하고싶은 내용이 있다면 사용되는 메서드이다.

 public static void sendAllNotify(String message) throws IOException { //서버 공지 (lock 걸어야함)
        ServerNotifyPacket packet = new ServerNotifyPacket(message);
        byte[] sendNotifybyte = packetToByte(packet);

        try {
            for (Map.Entry<OutputStream, String> entry : clientMap.entrySet()) {
                String receiverName = entry.getValue();
                OutputStream clientStream = entry.getKey();
                try {
                    clientStream.write(sendNotifybyte);
                    clientStream.flush();
                } catch (IOException e) {
                    // 클라이언트와의 연결이 끊어진 경우, 해당 클라이언트를 제거합니다.
                    clientMap.remove(receiverName);
                    out.println("[" + receiverName + " Disconnected]");
                }
            }
        } catch (ConcurrentModificationException e) {
            e.printStackTrace();
        }
    }

sendAllMessage와 마찬가지로 모두에게 메세지를 전송한다.

  • disconnectClient 메서드

클라이언트가 대화방을 나가고싶다는 명령어를 보내면 서버에서 클라이언트의 정보를 삭제 후 다른 클라이언트들에게 나갔다는 메세지를 보내는 메서드이다.

public static synchronized void disconnectClient(ClientDisconnectPacket disconnectPacket) throws IOException {
        ServerDisconnectPacket disconnectpacket = new ServerDisconnectPacket(disconnectPacket.getName());
        byte[] disconnectpacketbyte = packetToByte(disconnectpacket);

        try {
            for (Map.Entry<OutputStream, String> entry : clientMap.entrySet()) {
                String receiverName = entry.getValue();
                OutputStream clientStream = entry.getKey();
                try {
                    clientStream.write(disconnectpacketbyte);
                    clientStream.flush();
                } catch (IOException e) {
                    // 클라이언트와의 연결이 끊어진 경우, 해당 클라이언트를 제거합니다.
                    clientMap.remove(clientStream);
                    out.println("[" + receiverName + "Disconnected]");
                }
            }
        } catch (ConcurrentModificationException e) {
            e.printStackTrace();
        }
        out.println("[" + disconnectpacket.getName() + " Disconnected]"); //서버에 띄우는 메세지.
    }

clientMap을 돌면서 같은 이름의 클라이언트를 찾은다음 삭제를 한다. 그리고 나간 클라이언트의 이름을 말하여 다른 클라이언트들에게 나갔다고 공지한다.

  • exceptionMessage 메서드

exceptionMessage메서드는 서버가 특정한 한명의 클라이언트에게 공지를 해야할때 사용되는 메서드이다.

public static synchronized void exceptionMessage(OutputStream out,String message) throws IOException { //원하는 사람 한명에게만 서버 공지전송
        ServerExceptionPacket exceptionpacket = new ServerExceptionPacket(message);
        byte[] exceptionpacketbyte = packetToByte(exceptionpacket);
        out.write(exceptionpacketbyte);
        out.flush();
    }


마무리

서버는 클라이언트와 다르게 하나의 스레드 내에서 인풋과 아웃풋을 모두 수행한다. 전체적으로 아쉽긴한 설계였다. 줄줄이 if문을 사용할 바엔 switch문을 사용했다면 조금 깔끔할것 같기도 했고, 비슷한 기능은 조금더 유연하게 묶어서 메서드를 재사용 할 수 있을꺼 같다는 아쉬움도 있다.
그리고 패킷을 너무 세분화해서 그런지 조금 지저분하다는 생각이든다.
앞선 문제들에 생각하면서 계속해서 수정해보면 좋겠다.

다음으론 클라이언트와 서버가 파일을 전송할때(ex: jpeg,txt,png) 어떠한 방식을 사용했는지 작성 해보겠다.

profile
우왕좌왕 개발

0개의 댓글