Go 프로젝트를 진행하면서 다른 프로세스와의 TCP 통신 구조를 만들어야했다.
그래서 Go언어의 구조체는 패딩바이트가 들어가는지 확인을 해보았다.
(패딩이 뭔지 모른다면 아래 글을 참조)
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 통신을 하기 위해서는 보낼 패킷데이터를 패킹하고 받은 패킷데이터를 언패킹하는 작업을 해줘야한다.
(패킹이 뭔지 모른다면 아래 글을 참조)
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}
정상적으로 데이터가 패킹되고 언패킹되어 처리되는것을 확인할 수 있다.
'Go언어' 카테고리의 다른 글
[Golang] go binary 실행시 /lib64/libc.so.6: version `GLIBC_2.XX' not found 에러 해결 방법 (1) | 2024.05.30 |
---|---|
[Go언어] 알파인리눅스(Alpine Linux)에서 Go 1.20이상 실행하기 (__res_search: symbol not found, libresolv.so.2) (0) | 2023.10.23 |
[GO언어] Go 환경 변수 및 버전 관리하기 (GOROOT, GOPATH) (1) | 2022.10.04 |
[Golang] Go 언어 module (mod) 설정 (1) | 2022.09.29 |
[Golang] go 언어 개발 시작하기 (0) | 2022.09.29 |