#2_ 채팅방 프로토콜 설계(패킷설계)

마자나다·2023년 9월 2일

채팅방 프로젝트

목록 보기
3/6

채팅방 패킷

내가 만들려고하는 클라이언트-서버는 소켓통신으로 이루어진다.

앞서 말했지만 모든 클라이언트,서버가 자바언어로 이루어져있으면 Object로 서로 통신을 하기에 원할하겠지만 그런 경우는 거의 없다

그래서 나는 byte단위로 통신을 할 수 있도록 만들예정이다

패킷의 구성

byte단위로 패킷을 주고받기 때문에 그냥 툭 던져서는 받는 서버입장에서 뭐가뭔지 모른다.
처음엔 그냥 byte로 직렬화를 하여 던지니까 해석을 하는데 너무 복잡하고 어려움을 겪었다.
그래서 해결하기 위한 방안으로 헤더+바디의 구조로 만들었다.
헤더로 해석할 수 있는 정보를 준 다음에 바디를 해석할 수 있도록 설계할 예정이다.

1. 헤더
헤더는 어떠한 유형의 패킷이더라도 같은 방식을 가지고 있다.
헤더의 구성 : PacketType + 패킷의 전체 길이

2. 바디
바디는 각각의 패킷마다 내용이 모두 다르다. 그래서 헤더의 정보로 바디를 해석 할 것이다. 패킷마다 이름,메세지,서버공지 등등의 정보를 담고있다.

채팅방 프로젝트는 서버 모듈과 클라이언트 모듈 2개로 나뉘었었는데, 서버와 클라이언트가 상호간에 알아야하는 프로토콜 모듈이 필요하기 때문에 같이 공유하는 Share모듈을 따로 만들어서, 서버와 클라이언트가 둘다 공유할 수 있도록 설계하였다.

패킷 종류

처음엔 메세지타입(이름, 메세지), 커넥트타입(이름), 디스커넥트타입(이름),서버공지(메세지) 이정도로 패킷을 나누어서 만들었다. 이렇게 설계하니 서버에서 필요한 정보와 클라이언트가 필요한 정보 등 단점이 너무 많고 여러모로 통제가 안되었다...
그래서 클라이언트->서버 패킷, 서버->클라이언트 패킷 크게 2종류로 나누었다.
1. 클라이언트->서버

  • ClientConnectPacket : 클라이언트가 처음 접속을 하면 연결해주는 역할을 한다.
  • ClientDisconnectPacket : 클라이언트가 나가기를 원할때 정보를 담은 패킷이다.
  • ClientMessagePacket : 클라이언트가 보내는 메세지를 담고, 보내는 사람이 누구인지 정보를 담은 패킷이다.
    그 외에도 귓속말 기능, 닉네임변경, 파일패킷 등이 있다. (파일전송 부분은 내 입장에서 너무 까다로워서 따로 다루겠다..)
  1. 서버->클라이언트
  • ServerMessagePacket : 클라이언트가 보낸 메세지를 다른 클라이언트들에게 뿌리는 역할의 패킷이다.
  • ServerExceptionPacket : 문제가 생긴 클라이언트에게 오류보고를 위해 만든 패킷이다.
  • ServerNotifyPacket : 서버가 클라이언트에게 전체공지를 하기위해 만든 패킷이다.
    그 외에도 마찬가지로 파일패킷, 이름변경 공지 등 다양한 패킷이 있다.

패킷 설계

공통적인 형식을 갖춘 헤더, 모두 다른역할을 하고 있는 바디를 구성하기 위해 디자인 패턴에서 템플릿 메서드를 찾아봐서 활용하였다.
바디에게 헤더를 상속해주기 위해 abstract로 헤더클래스를 만들었다.

헤더


@AllArgsConstructor
@Getter
public abstract class HeaderPacket {
    protected PacketType packetType;
    protected int bodyLength; //타입을뺀 나머지 바디의 길이
    
	public byte[] getHeaderBytes() { //패킷타입 + 바디length byte로 변환(0~3, 4~7)
        byte[] headerBytes = new byte[8];
        System.arraycopy(intToByteArray(packetType.getValue()), 0, headerBytes, 0, 4);
        System.arraycopy(intToByteArray(bodyLength), 0, headerBytes, 4, intToByteArray(bodyLength).length);
        return headerBytes;
    }

Lombok을 사용했기 때문에 Get,Set은 따로 만들지 않았다
대신 getHeaderBytes를 사용하면 0 ~ 3은 int형태로 바꾼 Type을 넣어주었다, 4~7은 패킷의 전체길이를 int로 담고 있다.
int를 byte로 바꾸는 과정은 엔디안으로 메서드를 직접 같은 클래스내에 구현했으나 나중에 엔디안 파트로 따로 다루겠다.

System.arraycopy(intToByteArray(packetType.getValue()), 0, headerBytes, 0, 4);

이처럼 byte로 바꾸는 방식으로 나는 arraycopy를 사용했다.

- arraycopy 메서드

자바에서 배열의 요소를 다른 배열로 복사하는데 사용되는 메서드이다.

System.arraycopy(Object src,int srcPos,Object dest, int destPos, int length);

src : 복사할 원본 배열이다.
srcPos : 원본 배열에서 복사를 시작할위치(인덱스) 이다.
dest : 복사한 데이터를 저장할 대상 배열이다.
destPost : 대상 배열에서 데이터를 저장할 위치이다.
length : 복사할 요소의 개수이다.

바디

바디는 헤더를 상속받은 뒤 템플릿 메서드 방식을 사용하였기 때문에, 원하는 패킷이 만들어질때마다 공통된 헤더의 내용을 바디패킷에 맞춰 만들고, 바디는 패킷마다 다른 데이터가 필요하기 때문에 그에 맞춰 만들어졌다.
패킷이 너무 여러개있어서... 다 비슷한 형식이기 때문에 하나만 적겠다.

@Getter
public class ClientMessagePacket extends HeaderPacket {
    private final String message;
    private final String name;

    public ClientMessagePacket(String message, String name) {
        super(PacketType.CLIENT_MESSAGE, 8 + name.getBytes().length + message.getBytes().length);
        this.message = message;
        this.name = name;
    }

생성자만 보면 super, 헤더부분에 타입을 넣어주고, 전체길이를 넣어주었다 그리고 서버에서 쓰일 message,name에 데이터를 넣어준다.

public byte[] getBodyBytes() {// 이름길이 + 이름 + 메세지길이 + 메세지를 바이트로 변환
        byte[] nameBytes = name.getBytes();
        byte[] messageBytes = message.getBytes();
        byte[] bodyBytes = new byte[bodyLength];
        System.arraycopy(intToByteArray(nameBytes.length), 0, bodyBytes, 0, 4);
        System.arraycopy(nameBytes, 0, bodyBytes, 4, nameBytes.length);
        System.arraycopy(intToByteArray(messageBytes.length), 0, bodyBytes, 4 + nameBytes.length, 4);
        System.arraycopy(messageBytes, 0, bodyBytes, 8 + nameBytes.length, messageBytes.length);
        return bodyBytes;
    }

    public static ClientMessagePacket byteToClientMessagePacket(byte[] bodyBytes) {
        int nameLength = byteArrayToInt(bodyBytes, 8, 11);
        String name = new String(bodyBytes, 12, nameLength); //인덱스 12부터 nameLength만큼 문자열로 변환

        int messageLength = byteArrayToInt(bodyBytes, 12 + nameLength, 15 + nameLength);
        String message = new String(bodyBytes, 16 + nameLength, messageLength); //인덱스 15 + nameLength부터 messageLength만큼 문자열로 변환

        return new ClientMessagePacket(message, name);
    }

이 부분은 byte 배열로 직렬화했다.
중요한 부분은 서버는 어디서부터 어디까지가 이름이고 메세지인지 알려주지 않으면 알수가 없다.
그래서 이름 byte길이 + 이름 + 메세지 byte길이 + 메세지 이렇게 이름과 메세지의 원래 데이터가 나오기전 어디 길이까지가 이름이고 메세제인지 알수 있도록 넣어 주었다.
getBodyBytes는 모든 길이를 byte로 반환 할수 있도록 만든 메서드이고,
byteToClientMessagePacket은 byte단위로 들어온 데이터를 다시 패킷으로 변환해서 리턴해주는 메서드이다.

이처럼 모든 패킷이 byte단위로 바꾸는 메서드, byte를 다시 패킷으로 바꾸는 메서드를 갖고있도록 만들어 주었다.

enum클래스 PacketType

PacketType을 그냥 int로 내가 알아서 넣어주는게 아닌 enum으로 누가봐도 알수 있도록 만들어주었다.

enum

enum이란 열거형(Enumeration)을 나타내는 특별한 데이터 형식이다. 상수들의 집합을 정의하고 이를 하나의 자료형으로 다룰수 있다.
상수 컬렉션을 정의하는데 쓰이는 특수한 타입이다. 열거형에는 상수, 메서드 등이 포함될수 있다.

@Getter
@AllArgsConstructor
public enum PacketType {
    CLIENT_MESSAGE(1),
    CLIENT_CONNECT(2),
    CLIENT_DISCONNECT(3),
    SERVER_NOTIFY(4),
    SERVER_MESSAGE(5),
    SERVER_EXCEPTION(6),
    SERVER_DISCONNECT(7),
    CLIENT_CHANGENAME(8),
    SERVER_CHANGENAME(9),
    CLIENT_WHISPERMESSAGE(10),
    CLIENT_FILE(11),
    SERVER_FILE(12);

    private final int value;

    public static PacketType clientFindByValue(int value) {
        return switch (value) {
            case 1 -> CLIENT_MESSAGE;
            case 2 -> CLIENT_CONNECT;
            case 3 -> CLIENT_DISCONNECT;
            case 4 -> SERVER_NOTIFY;
            case 5 -> SERVER_MESSAGE;
            case 6 -> SERVER_EXCEPTION;
            case 7 -> SERVER_DISCONNECT;
            case 8 -> CLIENT_CHANGENAME;
            case 9 -> SERVER_CHANGENAME;
            case 10 -> CLIENT_WHISPERMESSAGE;
            case 11 -> CLIENT_FILE;
            case 12 -> SERVER_FILE;
            default -> null;
        };
    }

그냥 패킷의 이름을 정하고 int를 할당해주면 다른사람이 보기에 불편할수 있기때문에 enum을 사용하여 상수마다 패킷의 이름을 할당해주었다.
그리고 clientFindByValue를 사용하면 패킷에마다 해당하는 int값을 넣어주면 packettype을 찾아줄 수 있도록 만들었다.


이런식으로 패킷을 클라이언트와 서버가 주고받을 데이터를 정해주고 그 데이터를 byte로 바꾸고 byte를 다시 패킷으로 바꾸는 메서드까지 모두 구현하였다. 다음으로 server에서 받아온 byte를 어떻게 역직렬하고 사용했는지 적어보겠따.
profile
우왕좌왕 개발

0개의 댓글