2009년 1월 21일 수요일

haskell 로 바이너리 패킷을 다루는 간단한 예제

지금 테스트툴을 haskell 로 작성중인데 바이너리 프로토콜을 써야 하는 상황이 왔네.

제일 먼저..네트웍 통신에 String 을 그냥 쓰는건 너무 효율이 안좋을것 같아서 바이트스트링을 쓰기로 하고 Bits 모듈의 시프트를 가져다가 Int -> (Word8,Word8) 등등의 무식한 로우레벨 함수로 바이트를 쪼개 만들어서 cons 로 이어 붙여서 패킷을 만들고, 또 이걸 읽어서 index, take, drop 등 아주 생기초 함수들(그외는 잘 모르니까)을 이용해서 파싱을 했다.

짜고 나니 이게 패킷구조가 좀더 복잡해지면 답이 안나오겠더라.. 결국 좀 뒤져보니 바이너리 모듈이란게 존재하네. 모나드 덩어리라 아직 이해할수는 없지만! 대강 써먹는데는 별문제가 없길래 이놈으로 갈아타서 작성을 해봤다.

그런데 맘에 안드는게... 아래 주석에도 적었지만 바이트스트링이 워드/캐릭터 에 따라서 또 스트릭트/레이지 에 따라서 나뉜다는게 거참... 이런 구조는 정말 납득이 안되는데.

뭐 아직 쌩초짜니까 그냥 선입견이지만 haskell 의 모듈시스템은 쓰는건 편한데 모듈을 만들어 제공하려면 정말 더러울것 같다.



{-
바이너리 패킷이 있다고 가정을 하고 이걸 읽고 써보자.
바이너리 패킷의 모양은

먼저 매직넘버 두바이트
[0] 0xAB
[1] 0xCD
그리고 두바이트 짜리 값 하나. 뭐.. packet id 정도?
[2]
[3]
다시 두바이트 짜리 값 하나. 뒤에 나올 payload 길이
[4]
[5]
이제 payload len 만큼 바이트가 나온다.
[6]
...

http://www.haskell.org/haskellwiki/DealingWithBinaryData
문서를 참고하자

아직 개념이 없어서 바이트스트링은 다 비슷한 부류로 느껴지는데 Word8
이냐 Char 에 따라 갈리고 다시 Strict 와 Lazy 에 의해 갈리는건 정말
부조리하게 느껴지는데... 결국 비슷한 일을 하는놈들인데 모듈이 그만큼
생긴 거니까..

네트웍쪽 모듈만 해도 바이트스트링용 모듈이 따로 존재하고 이 바이너리
모듈도 스트릭트 버전이 따로 있는등.. 이건 정말 이해할수가 없군.

더 공부하다 보면 알게 되려나.
-}


import Data.Binary
import Data.Binary.Get
import Data.Binary.Put
import Data.ByteString.Internal(c2w,w2c)
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as L

-- String 하고 ByteString 간의 전환은 정말 어색하네. 이렇게 번거롭게
-- 해야 할것 같지는 않은데... 다른 방법을 찾아봐야 겠다. IsString 이란
-- 클래스가 있긴 헌데.. 이걸 쓰려면 뭐 ghc 옵션을 줘야 하고 (확장) 영
-- 복잡하네. IsString 은 배워두면 쓸모 많을것 같으니 빨리 공부해야 할것
-- 같다.
toByteString :: String -> B.ByteString
toByteString x = B.pack $ map c2w x

fromByteString :: B.ByteString -> String
fromByteString x = map w2c (B.unpack x)


-- 음 Put 모나드를 리턴을 하고.. runPut 으로 그걸 실행하나 보네? 모나드
-- 공부를 아직 안했으니 뭐가뭔지 모르겠다. writer 모나드라고 문서엔
-- 나와있는데..
makePacket :: Int -> String -> Put
makePacket id payload = do
putWord8 0xAB
putWord8 0xCD
putWord16be $ fromIntegral id
putWord16be $ fromIntegral bodylen
putByteString body
where
body = toByteString payload
bodylen = B.length body

-- 실행은 요렇게. runPut 이 결국 ByteString.Lazy 를 주는데 이건 좀 맘에
-- 안드네. lazy 가 정말 좋은 스타일일까
test_makePacket = runPut $ makePacket 100 "hahaha"
-- 그냥 저장한번 해봤다.
test_store = L.writeFile "/tmp/hahaha" test_makePacket

-- 이건 Get 모나드.. 어떻게 돌아가는지는 몰라도 쓰는데는 문제 없다.
-- Word 와 Int 간의 캐스팅은 다른 언어에서라면 그냥 해주는건데 여기선
-- 그걸 안해주니 좀 답답하다. 뭐 장점도 많지만.
readPacket :: Get (Int, String)
readPacket = do
magic0 <- getWord8
magic1 <- getWord8
id <- getWord16be
len <- getWord16be
body <- getByteString (fromIntegral len)
return ((fromIntegral id), (fromByteString body))

-- 위에서 만든 test_makePacket 의 결과를 읽어봤다.
test_readPacket = runGet readPacket test_makePacket


댓글 없음: