패킷 길이 프레이밍

  • 내가 알기로 TCP 헤더에 이미 패킷의 크기가 있는 줄 알았는데… 아니었다!!!

  • 게다가 TCP는 UDP와 다르게 메시지를 따로따로 보내는게 아니고, 바이트 스트림을 그냥 보냄!

  • 출처 : RFC 9623, TCP 표준 문서

  • TCP는 메시지 경계가 없다!!! 그저 스트림을 보낼 뿐이다!

  • TCP는 순서, 신뢰성, 흐름제어는 보장하나 메시지 경계를 보장하지 않는다

  • 그래서 넣은게 패킷 길이 프레이밍!

  • 참고 : 순서는 보장하기 때문에, 중간에 갑자기 다른 패킷이 앞에 길이 프레이밍 없이 들어오는 경우는 없음. 그건 걱정 안해도 됨

  • 그래서, 패킷 구조가 이렇게 됨

코드로 구현하기

func SerializePacket(pkt *protocol.Packet) ([]byte, error) {
 
    // 1. 패킷을 바이트 배열로 변경
    packetBytes, err := proto.Marshal(pkt)
    if err != nil {
        return nil, err
    }
 
    // 2. 빈 바이트 배열을 만듬
    // 이때 빈 바이트 배열의 길이는 4 + 바이트 배열로 변경한 패킷의 길이
    
    frame := make([]byte, 4+len(packetBytes))
    
    // 3. 빈 바이트 배열의 앞 4바이트를 바이트 배열로 변경한 패킷 길이로 정함
    binary.BigEndian.PutUint32(frame[:4], uint32(len(packetBytes)))
 
    // 4. 뒤에 바이트 배열로 변경한 패킷을 붙임
    copy(frame[4:], packetBytes)
    
    return frame, nil
}
 
func DeserializePacket(frame []byte) (*protocol.Packet, error) {
 
    // 1. frame 자체의 길이가 4 바이트가 안되면 말이 안됨 (길이 프레이밍이 4 바이트임)
 
    if len(frame) < 4 {
        return nil, fmt.Errorf("Frame is too small")
    }
 
    // 2. 패킷의 길이를 받음
    packetLen := binary.BigEndian.Uint32(frame[:4])
 
    // 3. 전체 프레임 길이가 4 + 패킷 길이 보다 작으면 데이터가 오다 만거임
    if len(frame) < int(4+packetLen) {
        return nil, fmt.Errorf("Incomplate frame")
    }
 
 
    // 4. 실제 패킷 데이터 뽑기
    packetBytes := frame[4 : 4+packetLen]
 
    // 5. 역직렬화 해서 Packet 구조체로 변환 후, 문제 없으면 반환함
    var pkt protocol.Packet
    if err := proto.Unmarshal(packetBytes, &pkt); err != nil {
        return nil, err
    }
 
    return &pkt, nil
}
  • 실행 결과

하나의 바이트 슬라이스에, 여러 개의 패킷이 섞여있다면?

  • 결국 위에 만든 패킷 길이 프레이밍도 이걸 위해 했다.
  • 위에서 만든 길이를 토대로 파서를 만들면 됨
  • 아까 만든 역직렬화 로직에서 체크하던 부분을 파서로 옮김
// Buffer에 쌓인 byte 배열을 제대로 수신된 프레임들과 남은 프레임으로 쪼개서 반환
func ParseFrames(buffer []byte) ([][]byte, []byte) {
    var frames [][]byte
    offset := 0
    for {
        // 1. 앞에 붙는 길이 헤더를 검사함. 4보다 작으면 지금 처리 X
        if len(buffer[offset:]) < 4 {
            break
        }
        length := binary.BigEndian.Uint32(buffer[offset : offset+4])
        frameSize := int(4 + length)
 
        // 2. 프레임이 다 안왔으면 지금 처리 X
        if len(buffer[offset:]) < frameSize {
            break
        }
 
        // 3. 프레임 추출해서 프레임들에다가 넣음
        frame := buffer[offset : offset+frameSize]
        frames = append(frames, frame)
 
        offset += frameSize
    }
 
    remain := buffer[offset:] // 처리 못한것들
    return frames, remain
}
 
// 패킷 구조체를 바이트 배열로 변경
func SerializePacket(pkt *protocol.Packet) ([]byte, error) {
    // 1. 패킷을 바이트 배열로 변경
    packetBytes, err := proto.Marshal(pkt)
    if err != nil {
        return nil, err
    }
    
    // 2. 빈 바이트 배열을 만듬
    // 이때 빈 바이트 배열의 길이는 4 + 바이트 배열로 변경한 패킷의 길이
    frame := make([]byte, 4+len(packetBytes))
 
    // 3. 빈 바이트 배열의 앞 4바이트를 바이트 배열로 변경한 패킷 길이로 정함
    binary.BigEndian.PutUint32(frame[:4], uint32(len(packetBytes)))
 
    // 4. 뒤에 바이트 배열로 변경한 패킷을 붙임
    copy(frame[4:], packetBytes)
    return frame, nil
}
 
 
// 역직렬화 가능한 byte 배열을 받고, 패킷으로 반환
func DeserializePacket(frame []byte) (*protocol.Packet, error) {
    // 여기까지 왔으면 일단 역직렬화 가능한 바이트 배열이 들어왔다고 가정
    // 역직렬화 해서 Packet 구조체로 변환 후, 문제 없으면 반환함
    var pkt protocol.Packet
    if err := proto.Unmarshal(frame[4:], &pkt); err != nil {
        return nil, err
    }
 
    return &pkt, nil
}
func main() {
 
    // Frame 1
    frame1, _ := SerializePacket(makePingPacket(1))
    
    // Frame 2
    frame2, _ := SerializePacket(makePingPacket(2))
 
    // Frame 3 (일부만)
    frame3, _ := SerializePacket(makePingPacket(3))
 
    // 일부러 쪼개기
    
    combined := append(frame1, frame2...)
    combined = append(combined, frame3[:5]...) // 미완성
 
    frames, remain := ParseFrames(combined)
 
    for i, frame := range frames {
        fmt.Println("\n\n---- Frame ", i, " ----")
        fmt.Println(frame)
        pkt, err := DeserializePacket(frame)
        if err != nil {
            fmt.Println("Deserialize Error : ", err)
            continue
        }
 
        fmt.Printf("\nPacket Type : %v", pkt.Type)
        switch pkt.Type {
        case protocol.MessageType_PING:
            var ping protocol.Ping
            if err := proto.Unmarshal(pkt.Payload, &ping); err != nil {
                fmt.Println("Ping Unmarshal Error : ", err)
            }
            fmt.Printf(" | Ping Timestamp : %v", ping.Timestamp)
        }
    }
    fmt.Println("\n\nRemain Frames : ", remain)
}

  • 해냈음!
  • 두 개는 똑바로 보내고 하나는 보내다 마는걸 테스트했음

다음에 할 것

  • 구조체를 만들어서 따로 정리하기