웹 개발을 하다 보면 스프링(Spring)이나 노드(Node.js) 같은 고수준 프레임워크가 네트워크의 복잡한 부분을 모두 숨겨줍니다. 하지만 대규모 트래픽을 다루거나 "Address already in use", "Connection refused"와 같은 치명적인 네트워크 에러를 마주하게 되면, 그 이면에 숨겨진 저수준의 네트워크 통신 원리를 이해해야만 문제를 해결할 수 있습니다.
네트워크 애플리케이션 프로그래밍 인터페이스의 가장 대중적인 표준은 버클리 소켓(Berkeley sockets) API입니다 [1]. 이 가이드에서는 단순한 코드 작성을 넘어 서버와 클라이언트가 어떻게 연결을 맺고 데이터를 주고받는지, 소켓(Socket) 통신의 핵심 함수인 `connect`, `bind`, `listen`, `accept`의 내부 메커니즘과 수학적/구조적 존재 이유를 심도 있게 파헤쳐 보겠습니다.
---
1. 소켓(Socket)이란 무엇인가? 전화기 비유로 이해하기
소켓은 애플리케이션 계층과 TCP/IP 네트워크 계층 사이를 연결해 주는 소프트웨어 인터페이스(엔드포인트)입니다. 복잡한 컴퓨터 과학 용어를 쓰기 전에, 일상생활의 '전화 시스템'에 비유하면 이 핵심 함수들의 역할을 매우 직관적으로 이해할 수 있습니다 [2].
* **`socket()`**: 전화기를 장만하는 것과 같습니다. 통신을 위한 가장 기본적인 도구를 준비합니다.
* **`bind()`**: 내 전화기에 특정 '전화번호'를 부여하는 과정입니다.
* **`listen()`**: 전화기의 벨소리를 켜두어, 외부에서 오는 전화를 들을 수 있게 대기 상태로 만드는 것입니다.
* **`connect()`**: 상대방의 전화번호를 알고, 전화를 거는 행위입니다.
* **`accept()`**: 벨이 울릴 때 수화기를 들어 상대방과 통화를 시작하는 것입니다.
이 직관적인 흐름을 바탕으로, 각 단계가 운영체제 내부에서 실제로 어떻게 작동하는지 상세히 알아보겠습니다.
2. 서버를 여는 첫걸음: `socket()`과 `bind()`
### 통신의 시작: `socket()`
네트워크 I/O를 수행하기 위해 프로세스가 가장 먼저 해야 할 일은 `socket` 함수를 호출하는 것입니다 [3]. 이 함수는 통신에 사용할 프로토콜의 종류(예: IPv4인지 IPv6인지, TCP 같은 스트림 소켓인지 UDP 같은 데이터그램 소켓인지)를 지정하여 메모리상에 빈 소켓 객체를 생성하고, 파일 디스크립터(File Descriptor)를 반환합니다.
### 주소 할당의 미학: `bind()`
소켓이 생성되었다면 이 소켓이 어떤 IP 주소와 포트(Port) 번호로 들어오는 데이터를 처리할지 지정해야 합니다. 이를 수행하는 함수가 `bind()`입니다 [4]. `bind`는 소켓에 특정한 프로토콜 주소를 부여합니다 [5].
여기서 많은 초보 개발자들이 궁금해하는 점이 있습니다. **"왜 클라이언트는 `bind()`를 거의 호출하지 않을까?"**
클라이언트 프로그램의 경우 굳이 고정된 포트 번호가 필요하지 않습니다. 따라서 클라이언트가 `bind`를 명시적으로 호출하지 않으면, 나중에 `connect`를 호출할 때 커널이 알아서 남는 임시 포트(Ephemeral port)와 적절한 소스 IP 주소를 자동으로 할당해 줍니다 [6, 7].
반면, 웹 서버(80번 포트)처럼 항상 정해진 위치에서 클라이언트의 요청을 기다려야 하는 서버는 반드시 자신의 잘 알려진 포트(Well-known port)를 `bind`를 통해 고정해야 합니다 [6].
또한, 서버 설정에서 IP 주소로 `INADDR_ANY`라는 와일드카드 주소를 사용하는 것을 자주 보셨을 것입니다 [8]. 서버에 여러 개의 네트워크 인터페이스(예: 유선 LAN, 무선 LAN 등)가 있을 때, 특정 IP 하나만 바인딩하면 다른 네트워크 카드로 들어오는 요청은 무시됩니다. `INADDR_ANY`를 사용하면 커널이 해당 서버의 어떤 인터페이스로 들어오든 지정된 포트의 연결 요청을 모두 수용하게 됩니다 [8, 9].
3. 연결 대기열의 관리자: `listen()`의 두 가지 큐(Queue)
서버 소켓에 번호까지 부여했다면, 이제 외부의 연결을 받아들일 수 있는 '수동적 소켓(Passive Socket)'으로 변환해야 합니다. `listen()` 함수는 닫혀있던 소켓의 상태를 `CLOSED`에서 `LISTEN` 상태로 변경합니다 [10].
이 단계에서 가장 중요하게 알아야 할 최적화 개념은 `listen()` 함수가 관리하는 **두 가지의 백로그 큐(Backlog Queue)**입니다 [11, 12].
1. **미완료 연결 큐 (Incomplete connection queue):** 클라이언트로부터 연결 요청(SYN)이 도착했지만, 아직 TCP의 3-way handshake가 완전히 끝나지 않은 상태(SYN_RCVD 상태)의 소켓들이 머무는 대기열입니다.
2. **완료된 연결 큐 (Completed connection queue):** 3-way handshake가 성공적으로 완료되어 서버와 클라이언트가 연결된 상태(ESTABLISHED 상태)의 소켓들이 대기하는 곳입니다.
`listen(sockfd, backlog)` 함수에서 `backlog` 매개변수는 이 큐들의 최대 크기를 제한합니다 [10, 13]. 만약 대규모 트래픽이 갑자기 몰려 큐가 가득 차게 되면, 서버 측 TCP는 새로운 클라이언트의 SYN 요청을 무시하게 됩니다 [14]. 이렇게 무시함으로써 클라이언트가 재전송 타이머에 의해 다시 요청을 보내도록 유도하여 서버의 과부하를 막는 것입니다.
4. 능동적 연결과 3-way Handshake: `connect()`
클라이언트가 서버와 통신하기 위해 호출하는 `connect()` 함수는 TCP의 상징인 **3-way handshake**를 발생시킵니다 [15]. 이 과정은 다음과 같이 진행됩니다 [16-18].
1. 클라이언트는 초기 순서 번호(ISN, Initial Sequence Number)를 담은 **SYN** 세그먼트를 서버로 보냅니다.
2. 서버는 자신의 ISN을 담은 **SYN**과, 클라이언트의 요청을 확인했다는 **ACK**를 합쳐서 응답합니다.
3. 클라이언트는 서버의 SYN에 대해 **ACK**를 보냅니다.
`connect()` 함수는 이 세 번의 패킷 교환이 완전히 끝나고 서버의 ACK를 받을 때까지 (즉, 최소 한 번의 왕복 시간인 RTT 동안) 블로킹(대기)됩니다 [19, 20]. 성공적으로 완료되면 클라이언트의 소켓은 `ESTABLISHED` 상태로 전환됩니다 [21].
5. 새로운 소켓의 탄생: `accept()`의 분업 원리
서버는 `listen`을 통해 큐에 연결을 쌓아두기만 할 뿐, 실제로 데이터를 주고받으려면 `accept()` 함수를 호출해야 합니다. `accept()`는 완료된 연결 큐의 맨 앞에서 완전히 연결된 항목을 꺼내어 반환합니다 [22].
여기서 네트워크 프로그래밍의 가장 아름다운 구조적 특징이 등장합니다. **`accept()` 함수는 기존의 서버 소켓을 그대로 반환하는 것이 아니라, 커널이 생성한 "완전히 새로운 연결된 소켓(Connected Socket)의 식별자"를 반환합니다 [23].**
왜 굳이 새로운 소켓을 만들까요?
역할을 철저히 분리하기 위해서입니다. 처음 만들었던 **'리스닝 소켓(Listening Socket)'**은 서버가 살아있는 내내 오직 클라이언트의 새로운 연결 요청을 감지하는 역할만 수행합니다 [23, 24]. 반면, `accept`가 뱉어낸 **'연결된 소켓'**은 방금 연결된 특정 클라이언트와의 데이터 송수신만을 전담합니다.
이러한 소켓의 분리 덕분에, 서버는 부모 프로세스가 리스닝 소켓으로 계속 새로운 전화를 받고, 자식 프로세스나 스레드(Thread)를 분기(Fork)하여 연결된 소켓을 넘겨주는 방식(Concurrent Server)으로 수만 명의 클라이언트를 동시에 처리할 수 있는 것입니다 [25].
6. 안전한 연결 종료: `close()`와 `TIME_WAIT`의 철학
데이터 통신이 끝나면 자원을 반환하기 위해 연결을 종료해야 합니다. TCP는 양방향 통신이므로 종료할 때도 4-way handshake라는 꼼꼼한 4단계 과정을 거칩니다 [26-28].
1. 연결을 먼저 끊고자 하는 쪽(Active closer)이 **FIN** 패킷을 보냅니다. (`close()` 호출)
2. 상대방(Passive closer)은 이를 확인하는 **ACK**를 보냅니다.
3. 상대방도 자신의 남은 데이터를 모두 처리한 후 **FIN**을 보냅니다.
4. 처음 연결을 끊으려 했던 쪽이 마지막으로 **ACK**를 보냅니다.
여기서 가장 골치 아픈 상태인 **`TIME_WAIT`**가 등장합니다. 먼저 연결을 끊으려 했던(Active close) 소켓은 마지막 ACK를 보낸 직후 완전히 종료되지 않고, `TIME_WAIT` 상태에 빠져 최대 세그먼트 수명(MSL, Maximum Segment Lifetime)의 2배 시간 동안 기다립니다 [29, 30].
이유는 두 가지입니다. 첫째, 자신이 보낸 마지막 ACK가 네트워크 유실로 상대방에게 도달하지 못하면 상대방은 FIN을 재전송할 텐데, 이때 재전송된 FIN을 처리하고 ACK를 다시 보내주기 위해서입니다 [30]. 둘째, 과거의 연결에서 길을 잃고 떠돌던 지연된 패킷이 우연히 새롭게 생성된 연결에 섞여 들어와 데이터를 오염시키는 것을 막기 위해 잠시 포트를 묶어두는 것입니다.
덧붙여, `close()` 함수는 소켓의 참조 카운트를 1 감소시킬 뿐, 즉시 FIN을 날리지 않을 수도 있습니다 [31]. 하지만 `shutdown()` 함수를 사용하면 참조 카운트와 무관하게 즉시 강제로 절반의 연결을 끊고 FIN을 전송할 수 있어(Half-close), 내가 보낼 데이터는 다 보냈지만 상대방의 응답은 마저 기다려야 하는 고급 제어가 가능해집니다 [32, 33].
---
요약
데이터베이스나 웹 개발의 아키텍처가 아무리 발전하더라도, 인터넷을 통한 모든 통신은 결국 운영체제 레벨의 소켓(Socket) API를 통해 이루어집니다.
* 통신 엔드포인트를 만드는 `socket()`
* 주소를 할당하는 `bind()`
* 대기 큐를 생성하는 `listen()`
* 3-way handshake를 시작하는 `connect()`
* 1:1 전담 소켓을 복제해 내는 `accept()`
* 그리고 우아한 종료를 보장하는 4-way handshake와 `TIME_WAIT`까지.
이러한 내부 흐름과 프로토콜의 디자인 철학을 깊이 이해한다면, 애플리케이션에 발생하는 원인 모를 네트워크 지연이나 연결 거부 오류 앞에서도 당황하지 않고 운영체제의 관점에서 시스템 트러블슈팅을 해낼 수 있을 것입니다.
참고문헌
[1] tcpip-illustrated-volume-1-2nd-edition — 1.5.3 Application Programming Interfaces (APIs) Applications, whether p2p or client/server, need to express their desired network operations (e.g., make a connection, write or read data). This is usually supported by a host operating system using a networking application programming interface (API).…
[2] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — sequence number plus one. Similarly, the ACK of each FIN is the sequence number of the FIN plus one. An everyday analogy for establishing a TCP connection is the telephone system [Nemeth 1997]. The socket function is the equivalent of having a telephone to use. bind is telling other people your tele…
[3] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — notification to the server. The server then closes its end of the connection and either terminates or waits for a new client connection. Figure 4.1. Socket functions for elementary TCP client/server. Addison Wesley : UNIX Network Programming Volume 1, Third Edition: The Sockets Networking API 137 4.…
[4] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — in a loop, trying each IP address for a given host until one works, each time connect fails, we must close the socket descriptor and call socket again. 4.4 'bind' Function The bind function assigns a local protocol address to a socket. With the Internet protocols, the protocol address is the combina…
[5] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — Returns: 0 if OK,-1 on error Historically, the man page description of bind has said "bind assigns a name to an unnamed socket." The use of the term "name" is confusing and gives the connotation of domain names (Chapter 11) such as foo.bar.com. The bind function has nothing Addison Wesley : UNIX Net…
[6] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — argument is the size of this address structure. With TCP, calling bind lets us specify a port number, an IP address, both, or neither. Servers bind their well-known port when they start. We saw this in Figure 1.9. If a TCP client or server does not do this, the kernel chooses an ephemeral port for…
[7] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — belong to an interface on the host. For a TCP client, this assigns the source IP address that will be used for IP datagrams sent on the socket. For a TCP server, this restricts the socket to receive incoming client connections destined only to that IP address. Normally, a TCP client does not bind an…
[8] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — 145 If we specify a port number of 0, the kernel chooses an ephemeral port when bind is called. But if we specify a wildcard IP address, the kernel does not choose the local IP address until either the socket is connected (TCP) or a datagram is sent on the socket (UDP). With IPv4, the wildcard addre…
[9] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — the HTTP server is started for each organization and each copy binds only the IP address for that organization. An alternative technique is to run a single server that binds the wildcard address. When a connection arrives, the server calls getsockname to obtain the destination IP address from the cl…
[10] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — 147 1. When a socket is created by the socket function, it is assumed to be an active socket, that is, a client socket that will issue a connect. The listen function converts an unconnected socket into a passive socket, indicating that the kernel should accept incoming connection requests directed t…
[11] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — connections the kernel should queue for this socket. #include <sys/socket.h> #int listen (int sockfd, int backlog); Returns: 0 if OK, -1 on error This function is normally called after both the socket and bind functions and must be called before calling the accept function. To understand the backlog…
[12] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — three-way handshake. These sockets are in the SYN_RCVD state (Figure 2.4). 2. A completed connection queue, which contains an entry for each client with whom the TCP three-way handshake has completed. These sockets are in the ESTABLISHED state (Figure 2.4). Figure 4.7 depicts these two queues for a …
[13] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — placed onto the completed queue. There are several points to consider regarding the handling of these two queues. The backlog argument to the listen function has historically specified the maximum value for the sum of both queues. There has never been a formal definition of what the backlog means.…
[14] tcpip-illustrated-volume-1-2nd-edition — We try to start a third whose SYN appears as segment 7 (port 2463), but the server-side TCP ignores the SYNs because the queue for this listening endpoint is full. The client retransmits its SYN in segments 8–12 using binary exponential backoff. In FreeBSD and Solaris, TCP ignores the incoming SYN w…
[15] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — Section 3.3. The socket address structure must contain the IP address and port number of the server. We saw an example of this function in Figure 1.5. Addison Wesley : UNIX Network Programming Volume 1, Third Edition: The Sockets Networking API 141 The client does not have to call bind (which we wil…
[16] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — connections are established and terminated, and TCP's state transition diagram. Three-Way Handshake The following scenario occurs when a TCP connection is established: 1. The server must be prepared to accept an incoming connection. This is normally done by calling socket, bind, and listen and is ca…
[17] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — Normally, there is no data sent with the SYN; it just contains an IP header, a TCP header, and possible TCP options (which we will talk about shortly). Addison Wesley : UNIX Network Programming Volume 1, Third Edition: The Sockets Networking API 74 3. The server must acknowledge (ACK) the client's S…
[18] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — 4. The client must acknowledge the server's SYN. The minimum number of packets required for this exchange is three; hence, this is called TCP's three-way handshake. We show the three segments in Figure 2.2. Figure 2.2. TCP three-way handshake. We show the client's initial sequence number as J and th…
[19] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — that connect can be used with UDP, but it does not cause a "real" connection to be established; it just causes the kernel to store the peer's IP address and Addison Wesley : UNIX Network Programming Volume 1, Third Edition: The Sockets Networking API 525 port number.) We showed in Section 2.6 that t…
[20] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — fork code, we recommend the simple approach. 16.3 Nonblocking 'connect' When a TCP socket is set to nonblocking and then connect is called, connect returns immediately with an error of EINPROGRESS but the TCP three-way handshake continues. We then check for either a successful or unsuccessful comple…
[21] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — As with the ETIMEDOUT error, in this example, connect returns the EHOSTUNREACH error only after waiting its specified amount of time. In terms of the TCP state transition diagram (Figure 2.4), connect moves from the CLOSED state (the state in which a socket begins when it is created by the socket fu…
[22] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — interpretation, as does BSD/OS 3.0, then the application need not specify huge backlog values just because the server handles lots of client requests (e.g., a busy Web server) or to provide protection against SYN flooding. The kernel handles lots of incomplete connections, regardless of whether they…
[23] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — socket address structure pointed to by cliaddr; on return, this integer value contains the actual number of bytes stored by the kernel in the socket address structure. If accept is successful, its return value is a brand-new descriptor automatically created by the kernel. This new descriptor refers …
[24] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — and listen), and we call the return value from accept the connected socket. It is important to differentiate between these two sockets. A given server normally creates only one listening socket, which then exists for the lifetime of the server. The kernel creates one connected socket for each client…
[25] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — Close(connfd); /* done with this client */ exit(0); /* child terminates */ } Addison Wesley : UNIX Network Programming Volume 1, Third Edition: The Sockets Networking API 159 Close(connfd); /* parent closes connected socket */ } When a connection is established, accept return…
[26] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — Chapter 24 of TCPv1 contains more details on these options. TCP Connection Termination Addison Wesley : UNIX Network Programming Volume 1, Third Edition: The Sockets Networking API 76 While it takes three segments to establish a connection, it takes four to terminate a connection. 1. One application…
[27] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — FIN is acknowledged by TCP. The receipt of the FIN is also passed to the application as an end-of-file (after any data that may have already been queued for the application to receive), since the receipt of the FIN means the application will not receive any additional data on the connection. 3. Some…
[28] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — Since a FIN and an ACK are required in each direction, four segments are normally required. We use the qualifier "normally" because in some scenarios, the FIN in Step 1 is sent with data. Also, the segments in Steps 2 and 3 are both from the end performing the passive close and could be combined int…
[29] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — 2.7 TIME_WAIT State Undoubtedly, one of the most misunderstood aspects of TCP with regard to network programming is its TIME_WAIT state. We can see in Figure 2.4 that the end that performs the active close goes through this state. The duration that this endpoint remains in this state is twice the ma…
[30] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — interpreted by the server as an error. If TCP is performing all the work necessary to terminate both directions of data flow cleanly for a connection (its full-duplex close), then it must correctly handle the loss of any of these four segments. This example also shows why the end that performs the a…
[31] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — as well as when it contains one or more complete lines (which we can consume). We will address these buffering concerns in the improved version of str_cli shown in Section 6.7. 6.6 'shutdown' Function The normal way to terminate a network connection is to call the close function. But, there are two …
[32] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — function calls in this scenario. Figure 6.12. Calling shutdown to close half of a TCP connection. Addison Wesley : UNIX Network Programming Volume 1, Third Edition: The Sockets Networking API 226 #include <sys/socket.h> int shutdown(int sockfd, int howto); Returns: 0 if OK, –1 on error The action of…
[33] UNIX Network Programming, Volume 1, Third Edition, The Sockets Networking API — discarded. The process can no longer issue any of the read functions on the socket. Any data received after this call for a TCP socket is acknowledged and then silently discarded. By default, everything written to a routing socket (Chapter 18) loops back as possible input to all routing sockets on t…
