요약

  • Windows Sockets 1.1은 Windows 개발자에게 Berkeley 방식의 TCP/IP 인터페이스를 제공했지만, Berkeley의 차단 동작을 16비트 Windows에 그대로 옮길 수는 없었다.
  • 차단된 Winsock 호출도 Windows 메시지를 계속 처리해야 했다. 협력형 데스크톱은 응답성을 유지했지만, 중첩 호출과 재진입, 버퍼 수명에는 엄격한 한계가 생겼다.

소켓 호출은 네트워크를 감추기 위한 것이었다

소켓은 프로그램이 밑단의 네트워크 스택 공급자에 얽매이지 않고 데이터를 보내고 받을 수 있는 안정적인 방법이다. Windows 개발자들이 원한 것은 바로 그 약속이었다. 출발점도 익숙하기를 바랐다. Berkeley 소켓은 이미 Unix에서 TCP/IP를 프로그래밍하는 표준 모델이었다.

Windows Sockets 프로젝트는 1991년 10월 10일 산호세 Interop ’91의 Birds of a Feather 세션에서 시작됐다. 1.1 명세는 이후 과정을 기록한다. 위원회가 만들어졌고, 이메일 토론이 이어졌으며, JSB와 NetManage의 제안을 상당 부분 반영한 초안이 나왔다. 12월에는 Microsoft가 회의를 주최했고, 뒤이어 작업 그룹이 문서를 계속 다듬었다. 이 인터페이스는 여러 참가자가 함께 만든 결과였지, 한 공급자가 강제한 TCP/IP 프로토콜이 아니었다.

1.0은 네트워크 소프트웨어 공급자와 애플리케이션 개발자가 공통 계약을 기준으로 구현과 프로그램을 만들기 시작하도록 공개됐다. 1993년 1월 20일자 1.1은 1.0을 구현한 여러 회사의 경험을 바탕으로 해설과 소규모 수정을 보탰다. 이 순서가 중요하다. 다음 버전의 문구를 확정하기 전에 명세는 이미 실제 코드와 부딪혀 봤다.

명세는 Unix 개발자에게 익숙하고 소스 코드 이식을 쉽게 하려고 Berkeley 소켓 모델을 채택했다. 범위는 Internet 프로토콜 모음으로, TCP 스트림 소켓과 UDP 데이터그램 소켓을 정의했다. 1.1이 가능한 모든 네트워크를 표준화한다고 가장하지 않았다. 이 경계는 유용했다. 개발자는 익숙한 인터페이스를 얻었고, 그 아래 모든 시스템이 똑같이 동작한다는 약속은 받지 않았다.

그다음 recv가 16비트 데스크톱과 만났다

어려움은 프로그램이 기다릴 때 드러난다. 차단형 recv는 데이터가 도착하거나 작업이 다른 방식으로 끝날 때까지 반환하지 않는다. 선점형 스케줄링을 쓰는 시스템이라면 프로세스가 기다리는 동안 운영체제가 다른 작업을 실행할 수 있다. Windows Sockets 1.1은 16비트 Windows가 그런 방식으로 스케줄링한다고 가정할 수 없었다.

Microsoft 문서는 우회 방법을 놀라울 만큼 자세히 설명한다. 차단 작업이 바로 끝나지 않으면 서비스 공급자가 작업을 시작하고 루프에 들어간다. Windows 메시지를 처리하고, 소켓 호출이 끝났는지 확인한 뒤 다시 반복한다. 네트워크 읽기가 대기 중이어도 메시지 기반 애플리케이션은 창 이벤트를 계속 처리할 수 있었다.

이 설계가 16비트 프로그램을 선점형 프로세스로 바꾼 것은 아니다. 프로그래머가 단순한 대기라고 여길 수 있는 호출 안에 Windows 메시지 처리를 끼워 넣었다. 기다리는 중에 들어온 메시지는 애플리케이션이 다시 Winsock을 호출하게 만들 수 있다. 1.1은 이런 중첩을 금지했다. 한 작업에는 완료되지 않은 Winsock 함수 호출이 하나만 허용됐다. 두 번째 중첩 호출은 WSAEINPROGRESS 오류로 실패했다.

API는 두 가지 작은 탈출구를 제공했다. WSAIsBlocking은 차단 호출이 진행 중인지 확인했고, WSACancelBlockingCall은 호출을 취소할 수 있었다. 여러 문서 창을 다루는 복잡한 앱은 WSASetBlockingHook을 설치해 기본 메시지 처리 절차를 바꿀 수 있었다. 제어권은 앱으로 넘어갔고, 메시지를 제대로 처리할 책임도 함께 넘어갔다.

인터페이스는 일을 옮겼다. 없애지는 않았다

비차단·비동기 호출은 차단 훅의 재진입 문제를 피했다. 대신 다른 의무가 생겼다. 작업이 버퍼나 다른 메모리 객체를 가리키는 포인터를 보유한다면, 완료될 때까지 애플리케이션이 그 객체를 유효하게 유지해야 했다. 네트워크 라이브러리가 메모리 수명 결정을 대신해 줄 수는 없었다.

익숙한 API와 이식 가능한 프로그램 사이에는 작지만 중요한 틈이 있다. 함수 이름을 맞추면 이식 착수 비용을 낮출 수 있다. 하지만 호스트의 스케줄링 모델, 메시지 루프, 메모리 수명까지 지워 주지는 않는다. Microsoft는 이후 BSD 소스 코드의 이식을 돕기 위한 헤더 파일을 문서화했다. Winsock 2가 Winsock2.h를 도입한 뒤에도 기존 애플리케이션과의 호환을 위해 옛 Winsock.h 선언은 남았다. 호환성 자체가 계속 들고 가야 하는 소프트웨어가 됐다.

1.1은 인터페이스와 여기서 지원하는 TCP/IP 소켓 유형에 경계를 그었다. 공급자는 구현을 제공했고, 애플리케이션 개발자는 대기·취소·메시지 처리·메모리 보호 방식을 선택했다. 공통 명세는 각 구성요소를 연결하기 쉽게 만들었다. 각자의 책임까지 서로 바꿔 맡을 수 있게 하지는 않았다.

눈에 띄는 설계 결정은 Windows가 Berkeley를 베꼈다는 데 있지 않다. 작업 그룹은 이식이 매력적으로 보일 만큼 Berkeley의 프로그래밍 어휘를 보존하면서, Windows가 Unix인 척할 수 없는 지점을 문서로 남겼다. 메시지 루프가 영수증이었다. 개방형 인터페이스는 한 비용을 줄였고 다음 비용이 어디서 발생했는지 정확히 보여줬다.

출처