구조체로 책임 나누기

왜 나누는데요?

  • 현재까지 코드는 진짜 쌩 버퍼 주고 함수를 일일히 거쳐서 내용을 직접 해석함
  • 문제는 그러면 중간 중간에 내가 어디까지 파싱했는지 알 수가 없음
  • 그리고 함수로만 존재하다보니 워크플로우 파악이 너무 복잡함
  • 그래서 구조체를 만들어서 상태를 표현하고 책임을 분리할 것임

어떻게 나누나요?

  • PacketReader : 바이트를 받아서 패킷 단위로 잘라주는 구조체
  • PacketWriter : 패킷을 받아서 바이트로 바꿔주는 구조체
  • Packet : “내용이 있는” 구조체 (메시지 타입과 페이로드)
  • Connection : 클라이언트 하나를 표현하는 구조체 - 연결과 reader/writer를 가짐
  • Dispatcher : Packet의 메시지 타입을 보고 Handler를 호출하는 구조체
  • Handler : Dispatcher에 의해 호출됨. 게임 로직을 실행하는 구조체
  • Session : Connection에 대한 정보를 포함하는 구조체 (ID, Player)

전체 구조도 (확정은 아님)

- 서버 디렉토리 구조 - 

Server/
├─ go.mod
├─ main.go
│
├─ protocol/        
│  └─ game.proto
│  └─ game.pb.go
│
├─ packet/          
│  ├─ reader.go
│  └─ writer.go
│
├─ network/         
│  ├─ connection.go
│  └─ server.go
│
├─ dispatch/        
│  └─ dispatcher.go
│
└─ handler/         
   └─ ping.go

PacketReader

  • 바이트를 받아서 패킷 단위로 잘라주는 구조체
  • 구조체 내부에 바이트 배열을 가지고 있어서, 완성되지 못한 패킷을 가지고 다음 스트림을 받을 수 있음
type PacketReader struct {
    buffer []byte // 참고 : 변수명이 대문자로 시작하면 public, 아니면 private임
}
 
// PacketReader 초기화. 빈 버퍼를 포함함
func NewPacketReader() *PacketReader {
    return &PacketReader{buffer: make([]byte, 0)}
}
 
// PacketReader에 남은 버퍼에 새 데이터를 붙여서 패킷화 시도
func (r *PacketReader) Read(data []byte) ([][]byte, error) {
    frames, remain := ParseFrames(append(r.buffer, data...))
    r.buffer = remain
    return frames, nil
}

PacketWriter

  • 패킷을 받아서 바이트 배열로 바꿔주는 구조체
  • 얘는 구조체 안에 딱히 뭐가 있지는 않음
type PacketWriter struct{}
 
// PacketWriter 초기화
func NewPacketWriter() *PacketWriter {
    return &PacketWriter{}
}
 
// 직렬화 된 패킷 반환
func (w *PacketWriter) Write(pkt *protocol.Packet) ([]byte, error) {
    return SerializePacket(pkt)
}
 
// 패킷 구조체를 바이트 배열로 변경
func SerializePacket(pkt *protocol.Packet) ([]byte, error) {
    // 1. 내부에서 돌던 패킷을 Protobuf에 규정한 패킷으로 바꿈
    protoPkt := &protocol.Packet{
        Type:    pkt.Type,
        Payload: pkt.Payload,
    }
 
    // 2. 패킷을 바이트 배열로 변경
    packetBytes, err := proto.Marshal(protoPkt)
    if err != nil {
        return nil, err
    }
 
    // 3. 빈 바이트 배열을 만듬
    // 이때 빈 바이트 배열의 길이는 4 + 바이트 배열로 변경한 패킷의 길이
    frame := make([]byte, 4+len(packetBytes))
 
    // 4. 빈 바이트 배열의 앞 4바이트를 바이트 배열로 변경한 패킷 길이로 정함
    binary.BigEndian.PutUint32(frame[:4], uint32(len(packetBytes)))
 
    // 5. 뒤에 바이트 배열로 변경한 패킷을 붙임
    copy(frame[4:], packetBytes)
 
    return frame, nil
}
 

다음에 할 것

  • Packet + codec 폴더에 있어야 하는 건 다 만들었음
  • 그러니 이번에는 dispatch와 handler를 만들어 볼 예정
  • 이에 더해 시간 되면 connection 구조체까지 만들어서 간단하게 핑퐁 테스트 할 예정