1. 비 단일 바이트 캐릭터 셋
알파벳 이나 숫자, 몇가지 특수 기호 같은 경우는 다 합해 봐야 전체 문자 수가 256개를 넘지 않기 때문에, 1바이트 (8 bit)를 가지고서도 충분히 모두 표현이 가능했다. 예를들어 ASCII코드에서는, A는 0x41, a는 0x61 /는 0x2F로 표현이 가능했다. 그러나 컴퓨터가 한자나 한글 혹은 여러가지 문자들을 처리해야 함에 따라서 1바이트로 문자를 처리 하는 방식의 단점이 드러나기 시작했다.
1바이트 만 가지고 256개를 넘어가는 여러 문자들을 관리 할 수 없게 된 것이다. 그래서 이 문제를 해결하고자 2바이트 이상으로 각 문자를 표현하고자 하는 캐릭터 셋이 제안되었다. 캐릭터 셋은 표현하고자 하는 문자와, 그 문자를 어떻게 bit로 표현할지 미리 정해 놓은 테이블이다.
대표적으로 문자를 2바이트로 대신하여 표현하는 유니코드가 있다. A는 0x0041, a는 0x0061와 같이 1바이트로 표현할 수 있었던 알파벳도 2바이트로 표현하고 그외의 모든 문자도 모두 2바이트로 표현하는 캐릭터 셋이다. 이외에도 아스키 코드는 1바이트 기타 문자는 3바이트로 표현하는 UTF-8, 아스키 코드는 1바이트로 한글은 2바이트로 표현하는 KCS5601과 같이 저마다 다른 방식으로 문자를 표현하고자 하는 캐릭터셋들이 있다.
2. 수신 Byte의 프레이밍 혹은 파싱
그런데 이런 문자의 인코딩 방식이 TCP/IP 통신에서는 골치 아픈 걸림돌이 될 수 있다. 예를들어, 내가 "한국화" 라는 문자열을 가상의 HKH-1이라는 캐릭터 셋을 이용해서 인코딩 하고 TCP/IP를 이용하여 상대편에 전송한다고 생각해보자. 아래와 같은 바이트 배열을 연결된 반대쪽에 보낸다고 생각해보자.
한 |
0x0001 |
국 |
0x0203 |
화 |
0x0405 |
... |
... |
<HKH-1 캐릭터 셋의 일부. 글자를 2바이트로 인코딩한다.>
{0x00, 0x01, 0x02, 0x03, 0x04, 0x05} |
<인코딩 된 바이트>
리틀 엔디언이니, 빅엔디언이니 전송하는 순서가 바뀌긴 하지만 어찌되었든 간에 보낸 순서대로 받는다고 가정하고 수신하는 쪽에서 이 바이트들을 받아서 어떻게 처리 할지 생각해보자. 단, 받는 쪽에서는 위에 인코딩 된 6바이트를 순서대로 받기는 하나 한번에 받지 못할 수도, 혹은 받을 수도 있다. 왜냐하면 TCP/IP로 전송할 때 전송하기 가장 좋은 싸이즈로 조각내어 전송하기 때문이다.
즉, 받는 쪽에서는 {0x00, 0x01, 0x02, 0x03, 0x04, 0x05} 와 같이 한 무더기로 한번에 받을 수도, 혹은 아래와 같이 각 문자를 두번에 나누어, 혹은 문자를 표현하는 2바이트 중 각각의 바이트를 따로 받을 수도 있다.
{0x00, 0x01} |
{0x02, 0x03, 0x04, 0x05} |
<나눠서 받는 경우 1>
{0x00, 0x01, 0x02} |
{0x03, 0x04, 0x05} |
<나눠서 받는 경우 2>
나눠서 받는 경우 1의 경우를 보자. 받는 쪽에서도 HKH-1 캐릭터 셋을 알고 있으니까 이 경우 각각 "한", "국화" 라는 글자를 받아낸 바이트를 통해 알 수 있다. 그런데 문제가 있다. 받는 쪽에서 보낸 사람이 "한국화" 라는 문자열을 보낸건지 "한국화장품" 이라는 문자열을 보낸건지 알 수 있는 방도가 없다. 일단 이 문제를 어떻게 처리 할 것인지 나중에 다시 언급하고 나눠서 받는 경우 2의 경우를 보자.
나눠서 받는 경우 2에서는 문제가 더 심각하다. 1의 경우가 가지는 문제에 더불어 "국"을 의미했던 0x02 0x03이 따로 떨어져 있어서 한번 소켓으로 바이트를 받고 받은 바이트를 바로 HKH-1로 디코딩 (바이트를 문자로 바꿈) 할 수가 없다.
그럼 이 문제들을 어떻게 처리 해야 할까?
먼저 2번의 경우 생기는 문제는 아래와 같은 방법으로 풀 수 있다.
받은 바이트 데이터를 바이트 배열에 계속 차곡 차곡 쌓는다. |
이 방법을 통해 인코딩된 2바이트가 따로 수신되더라도 두 바이트를 연계해서 의미해석이 가능해진다. 1 경우 생기는 문제를 해결 하는 방법으로는 아래의 방법이 있다.
1. 미리 보내고 받는 쪽 모두 몇바이트를 보낼 것인지 사전에 약속하기 |
2. 몇바이트를 보낼 것인지 먼저 전송하기 (starter) |
3. 문자가 끝났음을 알리는 바이트 전송하기 (sperator, delimiter 혹은 지시자) |
먼저, 1은 미리 3글자만 보낸다고 알려주는 것이다. 그러면 받는 쪽에서 무조건 6바이트만 HKH-1로 디코딩하여 글자로 만들면 된다. 그런데 실제 TCP를 이용하는 곳에서는 써먹기가 어렵다. 보내고자 하는 정보가 3글자로 제한되기 때문이다.
2번째 방법은 미리 '<', '>' 와 같은 지시자를 이용하여 몇바이트를 보낼지 전송하는 것이다. 전송받은 바이트를 차곡차곡 쌓아가면서 이 바이트 배열에 '<' , '>' 사이에 들어있는 값을 HKH-1로 디코딩 하여 앞으로 추가로 더 오게되는 정보가 몇바이트 인지 알려주는 방법이다. 예를 들어, 아래와 같이 "한국화"를 전송할 수 있다.
"<6>한국화" == { 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05}
< |
0x0607 |
6 |
0x0809 |
> |
0x0A0B |
한 |
0x0001 |
국 |
0x0203 |
화 |
0x0405 |
... |
... |
그럼 수신하는 쪽에서는 0x0607과 0x0A0B를 찾아서 그 사이에 있는 값을 HKH-1로 디코딩하여 앞으로 오는 몇바이트가 실제로 송신단에서 보낸 문자열인지 알 수 있게 된다.
3번째 방법은 보내는 문자열 다음에 문자열의 끝을 알리는 지시자를 전송하는 방법이다. 예를 들어 지시자로 "<>"를 쓴다고 한다면 전체 전송하는 문자열은 "한국화<>" 가 되고 실제 전송되는 바이트는 아래와 같다.
한 |
0x0001 |
국 |
0x0203 |
화 |
0x0405 |
< |
0x0607 |
> |
0x0809 |
... |
... |
이렇게 2, 3번을 통해 실제 의미있는 데이터 범위를 정하는 과정을 프레이밍 또는 파싱 작업이라고 하는데, 이걸 실제로 구현하려고 하면 썩 생각만큼 녹록치가 않다. 아래와 같은 기능들을 직접 구현해야 하기 때문이다.
1. 나눠서 받는 데이터를 저장할 수 있는 byte 구조
2. byte구조에서 starter 혹은 delimiter를 검색할 수 있는 기능
3. 필요한만큼 byte구조에서 데이터를 빼고 나머지 데이터를 다시 byte구조에 옮기는 기능
Queue의 Dequeue 기능과 비슷하나 1바이트가 아닌 복수개의 바이트를 움직이는 점이
다르다.
링크를 통해 연습삼아 구현해 본 코드를 살펴 볼 수 있다. (비공개)