Go언어

[Go언어] 구조체 패딩 & 패킹 (Golang struct padding & packing)

아무일도없었다 2022. 11. 16. 01:10

Go 프로젝트를 진행하면서 다른 프로세스와의 TCP 통신 구조를 만들어야했다.

 

그래서 Go언어의 구조체는 패딩바이트가 들어가는지 확인을 해보았다.

 

(패딩이 뭔지 모른다면 아래 글을 참조)

https://hackerpark.tistory.com/entry/CC-%EA%B5%AC%EC%A1%B0%EC%B2%B4%ED%81%B4%EB%9E%98%EC%8A%A4-%ED%8C%A8%EB%94%A9-struct-class-padding

 

[C/C++] 구조체(클래스) 패딩 (struct, class padding)

패딩 (padding) 패딩이란 CPU의 효율을 높이기 위해서 효율적으로 메모리를 사용하는 기법 중에 하나이다. (여기서의 효율이란 저장 공간 효율이 아닌 데이터 처리 속도 효율을 의미한다.) 어떤 식

hackerpark.tistory.com

 

 

Go언어 패딩 (padding)


Go언어의 패딩 유무를 확인하기 위해 아래 예시코드를 작성해서 확인해보았다.

 

package main

import (
	"fmt"
	"unsafe"
)

type test struct {
	a int32 // 4
	b int64 // 8
	c int16 // 2
} // total = 4 + 8 + 2 = 14 byte

func main() {
	var t test
	size := unsafe.Sizeof(t)
	fmt.Println("size:", size)
}

 

< 결과 >

size: 24

 

 

결과는 Go언어에도 패딩바이트가 적용된다.

 

따라서 TCP 통신을 하기 위해서는 보낼 패킷데이터를 패킹하고 받은 패킷데이터를 언패킹하는 작업을 해줘야한다.

(패킹이 뭔지 모른다면 아래 글을 참조)

https://hackerpark.tistory.com/entry/CC-%EA%B5%AC%EC%A1%B0%EC%B2%B4-%ED%8C%A8%ED%82%B9-struct-packing

 

[C/C++] 구조체 패킹 (struct packing)

패킹을 이해하기 전 패딩의 개념을 알아야 한다. https://hackerpark.tistory.com/entry/CC-%EA%B5%AC%EC%A1%B0%EC%B2%B4%ED%81%B4%EB%9E%98%EC%8A%A4-%ED%8C%A8%EB%94%A9-struct-class-padding [C/C++] 구조체(클래스) 패딩 (struct, class paddi

hackerpark.tistory.com

 

 

Go언어 구조체 Packing 크기 구하기


먼저 보낼 데이터를 패킹하는 방법과 받은 데이터를 언패킹하는 방법에 대해 구글링을 통해 찾아봤는데 쉽지는 않았다.

(사실 이 포스팅을 하는 가장 큰 이유다.)

 

코드를 올리기에 앞서 몇몇 유틸은 직접 개발하였고, 오픈소스를 많이 참고하였다.

(한글화된 문서가 많이 있으면 좋을꺼같다..)

 


 

우선 패킹을 위해서는 구조체에서 패딩바이트를 뺀 실제 struct 의 크기를 구해야한다.

Unsafe 패키지에서 제공하는 Sizeof 함수는 패딩바이트를 포함한 크기를 반환해주기 때문에 직접 개발해야했다.
(개발 환경이 go1.19 버전을 사용하기 때문에 제네릭을 지원하지 않는 go 버전에서는 해당 코드를 직접 수정해야한다.)

 

또한 몇몇 type은 제외하였으니 필요하다면 직접 구현해야한다. (case 첫번째 목록이 제외된 type)

 

package util

import (
	"fmt"
	"reflect"
	"unsafe"
)

// packing sizeof
func Sizeof(a any) int {
	return getSizeType(reflect.ValueOf(a))
}

func getSizeType(v reflect.Value) int {
	switch v.Kind() {
	case reflect.Invalid, reflect.Bool, reflect.Float32, reflect.Float64,
		reflect.Chan, reflect.Func, reflect.Interface, reflect.Map,
		reflect.Pointer, reflect.UnsafePointer, reflect.Complex64, reflect.Complex128:
		return 0
	case reflect.Array:
		if s := getSizeType(v.Index(0)); s >= 0 {
			return s * v.Len()
		} else {
			return 0
		}
	case reflect.Struct:
		sum := 0
		for i, n := 0, v.NumField(); i < n; i++ {
			s := getSizeType(v.Field(i))
			if s < 0 {
				return -1
			}
			sum += s
		}
		return sum
	case reflect.String:
		return len(v.String())
	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
		reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return int(v.Type().Size())
	case reflect.Slice:
		l := v.Len()
		if v.Index(0).Kind() == reflect.Uint8 || v.Index(0).Kind() == reflect.Int8 {
			return l
		} else {
			sum := 0
			for i := 0; i < l; i++ {
				s := getSizeType(v.Index(i))
				if s < 0 {
					return -1
				}
				sum += s
			}
			return sum
		}

	}

	return -1
}

 

위의 함수를 사용하여 다시 예시코드를 돌려보면 아래의 결과를 얻을 수 있다.

 

func main() {
	var t test
	size := unsafe.Sizeof(t)
	fmt.Println("unsafe size:", size)

	packSize := util.Sizeof(t)
	fmt.Println("packing size:", packSize)
}

 

< 결과 >

unsafe size: 24
packing size: 14

 

패킹된 구조체의 크기를 구하였으니 이제는 byte array를 만들어서 패킹을 하면된다.

 

 

Go언어 패킹과 언패킹 (packing, unpacking , 직렬화)


 

패킹과 언패킹의 경우 오픈소스를 참조하였고 아주 살짝 수정만 하였다.

(데이터 패킹 & 언패킹 소스)

 

추가로 네트워크의 패킷은 모두 빅엔디안으로 통신하기 때문에 송수신되는 패킷은 모두 빅엔디안으로 가정하고 개발하였다.

 

package packet

import (
	"encoding/binary"
	"errors"
)

type Packet struct {
	pos  int
	data []byte
}

func NewPacket(buffer []byte) Packet {
	return Packet{
		data: buffer,
	}
}

func (p *Packet) ReadByte() (byte, error) {
	if p.pos >= len(p.data) {
		return 0, errors.New("read byte failed")
	}
	ret := p.data[p.pos]
	p.pos++
	return ret, nil
}

func (p *Packet) ReadBytes(readSize int) []byte {
	ret := p.data[p.pos : p.pos+readSize]
	p.pos += readSize
	return ret
}

func (p *Packet) ReadBool() (bool, error) {
	b, err := p.ReadByte()
	if b != byte(1) {
		return false, err
	} else {
		return true, err
	}
}

func (p *Packet) ReadS8() (int8, error) {
	ret, err := p.ReadByte()
	return int8(ret), err
}

func (p *Packet) ReadU16() (uint16, error) {
	if p.pos+2 > len(p.data) {
		return 0, errors.New("read uint16 failed")
	}
	buf := p.data[p.pos : p.pos+2]
	p.pos += 2
	return binary.BigEndian.Uint16(buf), nil
}

func (p *Packet) ReadS16() (int16, error) {
	ret, err := p.ReadU16()
	return int16(ret), err
}

func (p *Packet) ReadU32() (uint32, error) {
	if p.pos+4 > len(p.data) {
		return 0, errors.New("read uint32 failed")
	}
	buf := p.data[p.pos : p.pos+4]
	p.pos += 4
	return binary.BigEndian.Uint32(buf), nil
}

func (p *Packet) ReadS32() (int32, error) {
	ret, err := p.ReadU32()
	return int32(ret), err
}

func (p *Packet) ReadU64() (uint64, error) {
	if p.pos+8 > len(p.data) {
		return 0, errors.New("read uint64 failed")
	}
	buf := p.data[p.pos : p.pos+8]
	p.pos += 8
	return binary.BigEndian.Uint64(buf), nil
}

func (p *Packet) ReadS64() (int64, error) {
	ret, err := p.ReadU64()
	return int64(ret), err
}

func (p *Packet) ReadString(readSize int) (string, error) {
	if p.pos+readSize > len(p.data) {
		return "", errors.New("read string Data failed") 
	}
	bytes := p.data[p.pos : p.pos+readSize]
	p.pos += readSize
	return string(bytes), nil
}

func (p *Packet) WriteS8(data int8) {
	p.data[p.pos] = (byte)(data)
	p.pos++
}

func (p *Packet) WriteU16(data uint16) {
	binary.BigEndian.PutUint16(p.data[p.pos:], data)
	p.pos += 2
}

func (p *Packet) WriteS16(data int16) {
	p.WriteU16(uint16(data))
}

func (p *Packet) WriteBytes(data []byte) {
	copy(p.data[p.pos:], data)
	p.pos += len(data)
}

func (p *Packet) WriteU32(data uint32) {
	binary.BigEndian.PutUint32(p.data[p.pos:], data)
	p.pos += 4
}

func (p *Packet) WriteS32(data int32) {
	p.WriteU32(uint32(data))
}

func (p *Packet) WriteU64(data uint64) {
	binary.BigEndian.PutUint64(p.data[p.pos:], data)
	p.pos += 8
}

func (p *Packet) WriteS64(data int64) {
	p.WriteU64(uint64(data))
}

func (p *Packet) WriteString(data string) {
	copyLen := copy(p.data[p.pos:], data)
	p.pos += copyLen
}

 

위의 코드를 사용하여 TCP 로 수신받은 데이터를 언패킹(Unpack) 할 수 있다.

 

아래는 위의 코드를 사용하여 데이터를 Pack & UnPack 하는 예시코드다.

 

< 예시 코드 >

type ReadPacket struct {
	a int32 // 4
	b int64 // 8
	c int16 // 2
} // total = 4 + 8 + 2 = 14 byte

func NewReadPacket() ReadPacket {
	return ReadPacket{}
}

// Unpacking
func (r *ReadPacket) UnPack(readData []byte) (err error) {
	p := NewPacket(readData)

	r.a, err = p.ReadS32()
	if err != nil {
		return err
	}

	r.b, err = p.ReadS64()
	if err != nil {
		return err
	}

	r.c, err = p.ReadS16()
	if err != nil {
		return err
	}

	return nil
}

type WritePacket struct {
	a int32 // 4
	b int64 // 8
	c int16 // 2
} // total = 4 + 8 + 2 = 14 byte

func NewWritePacket() WritePacket {
	return WritePacket{}
}

func (w WritePacket) Pack() []byte {
	pktData := make([]byte, Sizeof(w))

	p := packet.NewPacket(pktData)
	p.WriteS32(w.a)
	p.WriteS64(w.b)
	p.WriteS16(w.c)

	return pktData
}

// Packing
func (t TestPacket) Pack() []byte {
	pktData := make([]byte, Sizeof(t))

	p := packet.NewPacket(pktData)
	p.WriteS32(t.a)
	p.WriteS64(t.b)
	p.WriteS16(t.c)

	return pktData
}

func main() {
	////////////// Packing Data //////////////
	// Make Struct
	p := NewTestPacket()

	// Input Data
	p.a = 1
	p.b = 2
	p.c = 3

	// Struct Packing
	pktData := p.Pack()

	// TODO: Write Socket pktData

	// Debugging
	fmt.Println("Write:", pktData)
    
	////////////// UnPacking Data //////////////
	// Read Buffer (TCP로 받았다고 가정..)
	ReadData := writeData

	// Make Struct
	r := NewReadPacket()

	// Unpacking
	err := r.UnPack(ReadData)
	if err != nil {
		log.Fatal(err)
	}

	// Debugging
	fmt.Println("Read:", r)
}

 

< 결과 >

Write: [0 0 0 1 0 0 0 0 0 0 0 2 0 3]
Read: {1 2 3}

 

정상적으로 데이터가 패킹되고 언패킹되어 처리되는것을 확인할 수 있다.

반응형