소켓이란 무엇인가
소켓이라는 말은 원래 벽에 박힌 콘센트, 무언가를 꽂는 구멍을 뜻합니다. 네트워크에서도 비슷합니다. 두 프로그램이 데이터를 주고받기 위해 꽂는 양쪽 끝의 통신 구멍, 그것이 소켓입니다. 한쪽 끝의 소켓에 데이터를 넣으면 반대쪽 끝의 소켓으로 흘러나옵니다. 이 장에서는 그 소켓이 태어나서 사라지기까지의 일생을 따라가 봅니다.
전화 통화에 빗대어 보기
소켓의 동작은 전화 통화와 놀랍도록 닮았습니다. 이 비유를 머릿속에 넣어 두면 앞으로 나올 함수들이 훨씬 쉽게 이해됩니다.
전화를 받는 쪽을 생각해 봅시다. 먼저 전화기를 한 대 마련합니다. 그리고 전화국에서 번호를 받아 그 전화기에 부여합니다. 이제 전화기를 켜 두고 벨이 울리기를 기다립니다. 누군가 전화를 걸어오면 수화기를 들어 통화를 시작하고, 할 말이 끝나면 수화기를 내려놓습니다.
전화를 거는 쪽은 더 간단합니다. 전화기를 들고, 상대의 번호를 누르고, 연결되면 이야기를 나눈 뒤 끊습니다.
소켓 프로그래밍이 바로 이 순서 그대로입니다. 받는 쪽을 서버, 거는 쪽을 클라이언트라고 부를 뿐입니다.
소켓의 일생
서버 쪽 소켓이 거치는 단계를 함수 이름과 함께 늘어놓으면 이렇습니다.
전화기를 마련하는 일이 socket()입니다. 운영체제에게 "통신용 창구를 하나 만들어 달라"고 요청하면, 운영체제는 소켓을 하나 만들어 그 번호표를 돌려줍니다. 이 번호표를 파일 디스크립터라고 부르는데, 잠시 뒤에 다시 이야기하겠습니다.
번호를 부여하는 일이 bind()입니다. 만든 소켓을 특정 주소와 포트에 묶어, "이 창구는 이 번호로 들어오는 연락을 받는다"고 정합니다.
전화기를 켜 두고 기다리는 일이 listen()입니다. 이 순간부터 운영체제는 들어오는 연결 요청을 받아 대기열에 쌓아 둡니다.
수화기를 드는 일이 accept()입니다. 대기열에 쌓인 연결 하나를 꺼내 통화를 시작합니다. 흥미롭게도 이때 통화 전용 소켓이 새로 하나 생깁니다. 원래의 소켓은 계속 다음 전화를 받는 창구로 남고, 실제 대화는 새로 생긴 소켓으로 합니다. 전화기는 그대로 두고 통화만 따로 이어지는 셈입니다.
대화를 나누는 일이 recv()와 send()입니다. 받은 데이터를 읽고, 보낼 데이터를 씁니다.
수화기를 내려놓는 일이 close()입니다. 소켓을 닫아 통신을 끝냅니다.
클라이언트 쪽은 더 짧습니다. socket()으로 전화기를 마련하고, connect()로 상대 번호를 눌러 연결한 뒤, send()와 recv()로 대화하고, close()로 끊습니다. 거는 쪽은 번호를 부여받거나 벨을 기다릴 필요가 없으니 bind(), listen(), accept()가 없습니다.
소켓은 파일이다
유닉스 계열 운영체제에는 "모든 것은 파일"이라는 오래된 철학이 있습니다. 소켓도 예외가 아닙니다.
socket()이 돌려주는 번호표, 즉 파일 디스크립터는 사실 파일을 열었을 때 받는 번호와 같은 종류입니다. 그래서 소켓에서 데이터를 읽고 쓰는 일이 파일을 읽고 쓰는 일과 똑 닮았습니다. 이 점은 단순한 비유가 아니라 실제 설계입니다. 덕분에 우리는 파일을 다루듯 소켓을 다룰 수 있고, 뒤에서 배울 select나 epoll 같은 도구도 소켓과 파일을 똑같이 취급합니다.
이 "소켓은 곧 파일"이라는 감각은 C 예제에서 특히 또렷해집니다. C에서는 socket()이 돌려준 정수 하나를 들고 다니며 그 번호로 읽고 쓰고 닫습니다. 그 정수가 바로 파일 디스크립터입니다.
이제 소켓의 정체와 일생을 알았으니, 통신의 좌표가 되는 주소와 포트를 살펴볼 차례입니다.