구글 두들에 나온 Hedy Lamarr 글을 읽고

며칠 전 구글 두들 (2015-11-09) 로 Hedy Lamarrs 라는 배우, 발명가인 여성에 대한 글을 읽었다. 트위터에 올라오는 것들 보다가 예전에 전공 과목 배울 때 내용이랑 섞여있는 게 있길레 간단히 정리.

흔히 약어로 쓰는 SS (spread spectrum), DS (direct sequence), FH (frequency hopping) 같은 걸 쓰고 있으니 참고해서 읽어주시길.

Hedy Lamarrs 와 FH

위키백과 항목에 따르면

Lamarr and Antheil realized that radio-controlled torpedoes, while important in the naval war, could easily be jammed, which caused the torpedo to go off course. With the knowledge she had gained about torpedoes from her first husband, and using a method similar to the way piano rolls work, they designed a frequency-hopping system that would continually change the radio signals sent to the torpedo.

즉, Lamarr 와 공동 발명자가 고안해 낸 것은 일종의 FHSS 시스템. Spread spectrum 방식 자체가 a. 전체 대역을 다 관찰하지 않고는 노이즈 플로어만 상승시키는 걸로 보이고 b. 특정 (좁은) 대역의 재밍으로는 통신을 재밍하기 어려운 장점 떄문에 풀려던 문제 (어뢰의 유도 신호를 재밍당하지 않게 하는 것) 에 적합하다는 것.

Wireless LAN, …

현재 사용하고 있는 무선랜 표준 (WLAN; 802.11) 과 발명 사이에 관계 있다는 설명글들이 쭉 올라오는데, 아주 거칠게 말하면 큰 관련은 없다 — 작게는 있다(?). 무선랜 대역인 2.4 GHz (다른 대역도 있지만) 에는 전자레인지가 동작 중인 경우 엄청난 양의 노이즈가 생기기 때문에 이를 피하기 위한 방법을 사용한다.

링크한 글에서도 얘기하지만 대부분의 사람이 경험해보지 못한 legacy 802.11 표준을 제외하곤 FHSS는 사용하지 않는다. (대부분의 경우 802.11b, g, n, ac 순서로 경험했으리라)
간단히 정리하자면

  • Legacy 802.11: DSSS or FHSS
  • 802.11a: OFDM
  • 802.11b: DSSS
  • 802.11g, ac: OFDM

그래서 (현재의) 무선랜 기술과 크게 관련있는 건 아니라고 적었던 것.

2G/3G 무선 통신

한국에서 사용하는 2G 무선 기술이 IS-95 인데 (혹은 그 이후의 IS-2000; 3G), 이 기술은 CDMA 를 써서 여러 사용자의 통신을 지원한다. 그리고 이 CMDA가 사용하는 방식이 DSSS.

GPS?

이건 왜지. P코드 때문이라고 하기엔 너무 먼데 (…….)

Bluetooth

이게 왜 안나왔는지는 정말 모르겠지만. Bluetooth 통신은 FHSS를 쓴다. 차라리 직접적으로 연관관계가 있는 이쪽을 위주로 정리해줬어도 좋았을지도.

음 그렇지만 FHSS도 spread-spectrum 자체가 맞기도하고, 넓은 대역에 pseudo-random number sequence를 써서 특정 형태의 재밍을 피할 수 있는 무선 통신 방식…을 생각하면 뭐 죄다 적용될 수는 있을지도?

상경길에 느끼는 무선 인터넷 지연 시간과 응용 프로그램 차이

설 연휴에 처가 방문하고 집으로 올라가는 중.

KTX의 무선 인터넷을 쓰는데 감상이 묘하다.

회사 일이 좀 밀려있어서 코드 리뷰하고 내 코드도 merge하고 그러는 중인데, 현대의 웹 기술(?)에 감탄하는 중이다.

vim (혹은 고전적인 vi) 같은 도구는 network latency가 커져도 매우 잘 동작하는 편인데[1] 노트북 — (wifi) — VPN — 회사에 있는 내 PC — 테스트 용 가상 서버군 으로 연결했더니 좀 괴롭더라.

반대로 링크나 하나 짧긴 하지만 “노트북 — (wifi) — VPN — 서버”로 연결한 gerrit code review 툴은 그다지 느리지 않단느낌. 아무래도 ajax로 거의 모든 요청을 보내서 네트웍 지연 시간 자체를 잘 숨기게 되는듯?

IRC에서 대화한 내용을 토대로 좀 더 정리하면,

  • 이동 명령어 (hjkl 이동 혹은 ctrl + w + hjkl 류의 split window 이동) 사용에 대한 피드백이 바로 안와서 빡침
  • 웹은 이동 자체는 로컬 머신 안에서 하기 때문에 이동에 대한 피드백은 차이가 없음
  • 어차피 트랜잭션은 시간이 걸릴 수도 있다고 믿기 때문에 상대적으로 덜 빡침

정도.

  1. 애초에 baud rate가 매우 낮던 시절에 만든 도구라서 그렇다. 심지어 e-ink display 용 vi가 있을 정도다. []

지연시간 – 대역폭 곱

학부 데이터 통신 / 네트워크 수업과 대학원 석사 과정동안 배운 많은 개념 중에 내가 가장 감명 깊게(?) 생각했던 부분은 바로 이것,

대역폭 — 지연시간 곱 (bandwidth — delay product)

이게 무슨 의민가?

End-to-end에서 아주 간략하게 생각하자면 하나의 링크에 대해 양쪽 끝 단 사이에 “날아가고 있는” 데이터의 총량이다.[1] 그러니까 둘 사이에 존재하는, 보냈지만 아직 도착하지 않은 데이터의 ‘용량’이란 의미.  이 이상 넣어봐야 실제로 네트워크 위에 있는게 아니라 양쪽 시스템의 버퍼 위 어딘가가 될 것이란 것.
즉, 양 끝 단 사이의 네트워크 자원을 최대한 활용하려면 이 정도의 데이터가 네트워크 위에 계속 존재한단 소리.
지연 시간에 대해 상대적으로 신경을 덜 써도 되는 시스템이라면 약간의 지연 시간을 감수하고라도, 전송량을 극대화 하기 위해서는 이 값 만큼의 데이터가 네트워크로 계속 흐르게 해줘야 한다.

결론적으로, “best-effort” 트래픽을 처리할 때 여러가지 목표를 적당히 trade-off하는게 아니라, 이 것 단 하나만 최적화하면 된다는 것.

이번에 마무리 지은 파일 전송 프로토콜/프로그램에서도 이 개념만 충실히 지키는 형식으로 만들었다.
On-demand로 파일 chunk를 요청하긴 하는데, 특정 크기 — 앞에서 말한 대역폭 — 지연 시간 곱 만큼 — 의 윈도우를 두고, 이 윈도우 만큼의 데이터는 항상 미리 요청하게 한 것.어차피 RTT x 네트워크 대역폭만큼은 미리 요청해야, RTT 후에 요청한 데이터가 NIC를 통해 올라오고, 계속해서 대역폭을 채우게 된다.
실 세계에서도 이걸 최적화 못하면 “굉장히 느린 시스템”처럼 보이게 된다. 총 전송량이 적으면 대역폭 보다 지연 시간이 훨씬 크게 보이기 때문이다. 대역폭이 무한히 커 봐야 데이터는 RTT 후에나 오거든(…). 그래서 광대역 통신으로 해도 RTT가 크면 말짱 꽝.
예를 들어, HTTP같은 프로토콜에서는 한 번에 하나의 요청만 HTTP 연결 위로 오가기 때문에, 충분히 큰 단일 응답을 보내는 경우를 제외하고서는 요청을 보내고 응답을 받는 시간 (= RTT) 동안 이 링크를 채울래야 채울 수가 없다.
그래서 실제로 유저가 느끼는 대역폭이 매우 작다.[2] 덕분에(?) 매우 지연 시간에 예민한 시스템이된다.

그런 의미에서 Google의 HTTP 1.2 SPDY 같은 프로토콜이 나오는 것이기도 하다. 지연 시간이 더 중요하다는 의미로 비슷한 맥락에서 — 이 경우에는 HTTP의 한계가 아니라 TCP slow start의 한계 때문에 — TCP 알고리즘을 수정해서, Slow start의 구현을 사실 상 무시하고, 상대적으로 더 큰 slow-start window를 사용하는 변형(논문도 있다!)을 쓴다.

 

  1. 중간이 몇 hop인지는 중요치 않다 []
  2. 게다가 DNS이름 기준으로 한 호스트에 보내는 연결은 기껏해야 2개. 물론 수 많은 브라우져들이 이걸 어기긴한다 []

평범하지 않게 파일 복사하기

외부에서 버전을 붙이는 디렉터리가 있다. 이 디렉터리를 특정 컴퓨터 A에서 B로 복사하는 일을 해보자. 버전을 붙인다는 점을 좀 더 이용하면 – 즉, 이전 버전 데이터가 있는 디렉터리가 있다면 – 좀 더 적게 복사하고, 빠르게 복사할 수 있을 거다. 만약 이 데이터가 아주 간략하게 하기엔 좀 큰 수준 – 대충 10GiB에서 30GiB 정도? – 라면 뭔가 최적화하면 좋지 않을까?

대략 다음과 같은 정도로?

  • 이전 버전 디렉터리에 똑같은 파일이 있으면 해당 컴퓨터 안에서 복사한다.
  • 비슷한 파일이 있으면, rsync 처럼 부분부분 쪼개서 없거나 다른 부분만 복사한다.
  • 아예 새 파일이면 새 파일을 복사한다.

 

실제로 구현을 해보니 문제가 있었다. 그리고 그 원인은 "네트워크 전송이 충분히 빠르다"라는 것. 이제 하나씩 생각해보자.

한 컴퓨터 안에서 복사

이걸 하려면 하드 디스크에서 읽기 1번, 쓰기 1번이 필요하다. 근데 네트워크를 통해 전송하면 A에서 읽기 1번, B에서 쓰기 1번이면 족하다. 그래서 두 머신 모두에서 disk I/O를 최대 속도로 할 수 있고, 이게 SSD나 RAID 안 쓰고서야 대략 100MB/s 정도. 네트워크가 1Gbps라면 대략 이 데이터를 다 실어 나를 수 있다.

하지만 다른 방법도 있다. Windows에도 hard-link[1] 가 있는데, 이걸 써서 여러 문제를 한 큐에 푼다. 디스크 사용량도 안 늘고, 복사 시간은 실로 0이나 다름없고…

 

rsync 비슷한 방법

버전 별 차이가 정말 크지 않다면 – 내가 테스트한 환경에서는 대략 절반~90%정도가 비슷하더라 – 없거나 다른 부분만 가져오는 게 아마 빠를 거다. 그래서 rsync…랑 비슷한 zsync 에서 사용한 방법을 차용해서 해봤다. 그러니까 해시 계산 / 체크섬 계산을 많이 하는 쪽이 B가 되는 rsync? 근데 90% 가까이 가도 이 방법이 느리더라.

일단 앞에서 말한 이유 – 로컬 복사가 네트워크가 충분히 빠르면 그다지 빠른 방법이 아니란 것 – 와, 이게 정말 CPU-intensive하다는 점. 파일을 1bytes 단위마다 가벼운 체크섬 하나 구하고, 이 체크섬과 맞는 게 있을 때마다 MD5 계산을 하기 때문에 정말 오래 걸린다. 대략 이 방법으로 하면 초당 10 MiB 복사하는 게 힘든 수준이었음.. 그것도 Xeon 코어 4개(=8HT) 다 써서 한 건데. Orz[2] 결국 이게 더 빠른 건 네트워크 대역폭이 100 Mbps 이하 수준인 경우에나…

 

결론:

하드웨어 속도 (기반 네트워크, 저장소 I/O 속도) 가 충분하면, 그냥 단순한 게 짱임.

  1. http://en.wikipedia.org/wiki/Hard_link 참고; Windows API라 한정하면, CreateHardLink( ) 라는 Win32 API가 있다 []
  2. 동시에 8개 스트림을 처리하게 했음 []

병목은 어디에?

2월 말부터 시작해서, 게임 리소스를 네트워크 너머로 배포하는 유틸리티를 작성하고 있다. C# 공부를 겸해서(…) 틈틈이 짜고 있는데, 요 며칠간은 어떤 골치거리 에 매여있는 터라[1] 주로 성능 평가만 했는데, 병목현상이 생기는 곳이 좀 예상치 못한 곳이더라.

개념적으로 보내는 쪽(이하 S), 받는 쪽(이하 R) 두 개의 entity로 구성되며, 대략 다음과 같이 동작한다

  1. S에서 보낼 디렉터리 전체의 요약 정보 — 이름 크기 md5 hash — 를 담은 Merkle tree를 만든다.
  2. R 쪽에서도 이걸 구하고 이 tree를 비교해서 어떤 파일이 없고, 어떤 파일이 다른지 판단한다.
  3. R에서 없는 파일의 전체, 혹은 다른 파일의 일부를 요구한다.
  4. S에서 이런 파일의 전체/일부를 전송한다
  5. R과 S에서 전체 디렉터리의 Merkle tree를 다시 구한다; 단 이번에는 다른 hash 함수를 쓴다. 그리고 이 root node만 전송한다.
  6. 이걸 비교해서 전체 파일 전송이 성공했는지 판단한다.

처음 짠 코드에서 병목은 3,4 였고, 이 부분을 .net framework의 TPL을 이용해서 복수 request / 복수 sendfile 형식으로 처리하는 식으로 디스크 / 네트워크 사용률을 높여서 해결했다. 근데 오늘 결과를 측정해보니, 예상외의 장소에서 병목이 나오더라.

3.2 GiB / 1700 파일 정도인 모 프로그램의 설치 디스크를 통째로 전송해봤는데, 대략 11분이 걸렸다. S, R에서 각각 Merkle tree를 계산하는 게 약 40초[2] 걸린다. 다음은 전송(3, 4)이 약 5분 걸리더라. 그리고 마지막으로 5단계에서 양쪽에 각각 6분 정도(……….)의 시간이 걸리더라. 16bytes hash를 생성하는 MD5대신에 좀 더 큰 48 bytes (384bits) hash를 생성하는 SHA384를 썼는데 시간이 대략 Orz.

SHA384가 실제로는 SHA 512의 truncated 버전이라, 좀 느릴 거라 생각하긴 했는데, 생각보다 너무 느리다. 거의 똑같이 계산하는데 MD5는 1분도 안 걸리고, SHA384(512?)는 6분이 걸리다니 Orz.

다른 해시를 쓰거나, Merkle tree를 계산할 때 병렬로 – 아마 work-stealing queue를 만들고, I/O 와 CPU 계산에 들어가는걸 고려한 스레드 수를 써서 – 계산해야 하려나?

  1. 이전에 쓴 글을 보면 짐작하리라 []
  2. 물론 이건 양쪽 머신에서 동시에 진행 []

TCP segment 가 전송되지 않는다면 어딜 의심할까?

A서버에서 B서버로 대용량 데이터를 전송하는 네트워크 프로그래밍을 하나 한다고 치자.
이런 저런 삽질 끝에 프로그램을 한 쌍 — 보내는 쪽과 받는 쪽 — 만들었다. 이제 이 프로그램을 실험해 볼 차례다.

실험 결과, A 서버에서 네트워크 send() 호출하는 게 충분히 빠르지 않은 것 같다 – 사실 정말 그런진 모르겠고, send-queue가 너무 빨리 늘어나는데 반해, 실제 네트워크 사용률도 그리 높지 않아서 어느 쪽이 문제인지 모르겠다고 쳐보자.

어디가 문제라고 판단해야 할까? 근데 설상가상으로 B 서버에 직접적으로 붙어서 디버깅하거나 할 순 없는 상황이라고 치자(…).

A? B? 어느 쪽을 의심해야 할까? 그리고 그 판단의 근거는?

이제 이걸 판단할 근거를 찾아보자.

 

Plan A: TCP segment 를 캡쳐해서 까본다

TCP segment 는 항상 다음과 같은 window 안에서 전송된다.

TCP_Sliding_Windows

이 구조를 sliding window라고 한다.[1] 여기서 제일 중요한 부분이 advertised window 부분이다. 이 값은 받는 쪽의 TCP segment의 header에 있는 window 필드를 통해 전달된다. 모든 전송 관련된 메시지는 이 window를 기준으로 전송된다. 만약 받는 쪽에서 ACK을 보내면 이 window의 오른쪽이 ACK된 마지막 패킷 + window의 위치로 전진한다. 물론 retransmission을 원하는 경우 그냥 같은 값이 올 수도 있다. 그리고 보내는 쪽은 advertised window 안에 있는 segment 만 보낼 수 있다.

즉, 마지막 ack #부터 시작해서 ack # + window 까지 전송할 수 있는 상태다. 그리고 이 window는 0일 수 있다. 즉, 받는 쪽에서 더 이상 segment를 못 받는 경우 이걸 0으로 설정하고, 차후에 공간이 생기면 window update 메시지를 통해 이 값을 증가시킨다 — 같은 ack #에 window 값만 증가시켜서 보낸다.

이걸 가지고 서버 A와 B 중 어느 쪽이 병목인지 판단할 수 있다. 우선 이런 ACK segment의 header를 까봐야 하니 WireShark 같은 툴로 TCP를 캡쳐하고, B 서버의 advertised window와 ack 된 범위를 확인하면 된다.

  • 만약 크기 0인 window만 보내고, advertised window 크기를 늘리지 못하면, 이건 받는 쪽(서버 B)가 동작하는 방식이나, 구현체 자체의 성능 문제다. 즉 B가 TCP segment를 윗단의 응용 프로그램에 보내지 못해서 – 즉 윗 단이 느려서 — A는 보낼 여력이 있는데 B가 처리 못하는 것이다.
  • 반대로 advertised window 에는 공간이 있는 경우(can be sent로 표시된 부분이 0보다 큼)0) A가 느려서(?) 못 보내는 문제다.

근데 이거 생각 외로 잘 안 될 때가 있다. 왜냐하면 속도 문제를 고민할 정도의 상황이면 캡쳐 툴이 이걸 잘 캡쳐하지 못하고 일부를 놓칠 수 있고, 일일이 까보는 것도 그렇게 쉽진 않다. 그럼 좀 더 프로그래밍 적인 방법을 찾아보자.

 

Plan B: B 서버를 흉내 내는 툴을 짜고 패킷을 받고 바로 버린다

별도의 처리 없이 TCP 스트림을 받아서 버리는 툴(TCP sink)을 만든다. A-B 연결의 도입부분만 적당히 흉내(!)내고, 그 이후에는 받고 무시하는 프로그램을 짜자.
그리고 실제 B 서버 대신 이 TCP sink에 접속해서 속도가 얼마나 나오나 보면 된다. 실제로 전송되나 여부는 TCP sink 쪽에서 받은 bytes 수만 세면 된다.[2]

 

하여튼 내 경우엔 Plan B로 문제의 원인을 찾았다. 모처에 있던 B 서버의 성능 문제… 사실 Plan A로 해결하고 싶었지만, Windows 용 WireShark로 전부 다 캡쳐하질 못하더라. 이건 원인을 잘 모르겠음; 그냥 WinPcap이 구리다?

  1. 자세한 사항은 TCP/IP Illustrated Vol. 1, Chapter 20. TCP Bulk Data Flow 참조 []
  2. 데이터 무결성은 TCP 자체를 믿으면 된다. 정말 민감한 데이터가 아닌 이상… 그리고 그 경우엔 응용 프로그램 단에서 적당한 CRC 등을 이용할 생각을 하자. []

IPv6 를 대비한 코드를 짤 때 가끔 보는 실수

현재 인터넷의 주소 체계는 IPv4다. 이 IPv4로 쓸 수 있는 주소는 곧 동이 난다.[1]

그래서인지, 요즘 네트웍 쪽 코드를 읽다보면 몇 가지 방법으로 IPv4 –> IPv6 전환하기 위한 대비를 해둔걸 보게 된다. 크게 두 가지 방식을 보게 된다.

문자열로 주소를 전달한다 – IPv4 or IPv6 인지는 신경쓰지 않고, 그 밑의 네트웍 라이브러리나 코드 단에서 처리하게 한다.

미리 IPv6 일 때 사용할 인자 혹은 메모리 등을 예약(reserve)해 둔다 – IP 주소를 특정 데이터 타입으로 전달한다고 할 때, 추가로 공간을 할당한다면 여기에 IPv6 주소를 넣어서 보낼 수 있다.

하지만 요즘 본 코드 중 하나는 실소를 금할 수가 없더라. 두번째 형태로 IPv6를 대비한 것으로 보이는데, 예약한 공간이 4 bytes이다…

이게 왜 문제냐하면, IPv4에서 IPv6로 넘어갈 때, 주소로 사용하는 크기는 32bits에서 128bits으로 늘어난다는 점 때문. 64bits이 아니라… 그래서 추가로 공간을 예약한다면 char[12]를 쓰거나, 아예 주소 전달할 때 16bytes를 쓰고, IPv4쓰는 동안 4bytes을 쓴다.

이러면 예약해두나 마나 허망한 짓. 나중에 IPv6처리하려면 결국 8bytes를 추가로 넣어야한다. 게다가 이게 더 안습(…)했던건, 저게 일종의 프로토콜 명세의 일부라서, 나중에 고치려면 모든 이해 당사자들에게 이걸 얘기하고, 수정해가야 한다는 점인데… 내가 나중에 수정도 하게 될텐데, 무진장 귀찮구만…

개인적인 선호(?)는, 아예 첫번째 방법으로 문자열을 쓰는 것. 이러면 프로토콜 자체에 영향을 줄 일은 매우 적고, 대부분의 Network Library/API에서도 문자열로 된 주소를 쉽게 인식한다는 크나큰 장점이 있다.

여튼 세 줄 요약

  • IPv6는 128bits을 쓴다. 메모리에 나타내려면 IPv4 주소에 비해 4배의 공간이 필요하다
  • 코드를 짤 때 공부하는 습관은 소중한 것이다(…).
  • 네트웍 주소처리는 가장 밑 단에 해당하는 코드가 아니고서야 문자열 기준으로 하는게 편하다. 실수할 여지도 적고.
  1. 2010년 9월 19일 현재 추정치로, IANA 기준으로는 내년 5월 22일, 각 지역별 레지스트리 기준으로는 내후년 6월 12일이면 주소가 바닥난다. []

Adobe FlashSocketPolicyDaemon (python) 코드의 버그

예~~~~전에 작성한 ActionScript 코드 및 이것과 통신하는 C++ 서버 코드가 있는데, 이걸 다시 창고(…)에서 꺼낼 일이 생겼다. 전부 다시 빌드하고 – 그동안 라이브러리는 업데이트 되었는데, 이 녀석은 손 안 대서 수정할 게 좀 있더라 – 테스트 코드를 돌리기 위해서 python 서버들을 띄우고, C++ 서버를 띄우고, 테스트 클라이언트(.swf)를 연결했다.

그런데 소켓 정책 파일(flash socket policy file)을 얻어오지 못하는 게 아닌가?[1]

한참 고민하고 + 코드 리뷰하고 나서야 문제를 파악했는데, 내 C++ 코드나, ActionScript 적응단 문제가 아니라 Adobe 에서 제공하는 flashsocketpolicyd 의 python 구현체 일부에 문제가 있는 코드가 있었다.

class policy_server(object):
  def __init__(self, port, path):
    self.port = port
    self.path = path
    self.policy = self.read_policy(path)
    self.log(‘Listening on port %d\n % port)
    try:
      self.sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
    except AttributeError, socket.error:
      # AttributeError catches Python built without IPv6
      # socket.error catches OS with IPv6 disabled
      self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

IPv6 (AF_INT6) 로 소켓을 열어보고, 안되면 IPv4 (AF_INET) 로 소켓을 여는 구현인데, 문제는 Windows 나  일부 UNIX 구현체 들 중 일부는, IPv6 소켓이 IPv6 스택만 지원하는 형태로 여는 게 기본 값이라는 것. 덕분에 IPv4 스택을 이용하는 클라이언트 프로그램(=flash player) 랑 통신 못하는 것. 야 임마 ㅠㅠ

이게 예전에는 잘 되었던 이유는, WinSock을 사용하는 경우, 듀얼 스택 자체를 XP/2003 서버까지 지원하지 않았고 예전에는 Windows Server 2003(x64) 에서 작업했기 때문. 덕분에 try 에서 항상 예외가 나서 IPv4 소켓을 생성했기에 잘 돌았지만, Vista x64를 쓰는 지금에 와서 빵 터진 듯 Orz

듀얼 스택 지원 부분을 저렇게 안일하게 구현하면 안되지. 아무리 reference code 수준이 될까 말까한 거라곤 해도 이건 좀 아니지 않나?

  1. 게다가 예전에도 언급했지만, 소켓 정책 서버에서 파일을 못 가져오면, 처음 붙는 소켓에 또 요청을 날리기에 C++ 서버는 예상치 못한 XML 요청을 받게 된다. =_= []