11장에서 echo 서버를 구현하던 중에,
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
이 부분에서 나처럼 “clientaddr은 값도 없는데 왜 실행되지?”
하고 잠깐 멈칫한 분들이 분명 있을 거라고 생각한다.
전체 코드를 보면,
clientaddr
는 선언만 해주고 값을 넣어준 적이 없다.
일반적인 함수라면,
정의되지 않은 변수의 값을 함수가 가져가려고 하면 오류가 뜬다.
예를 들어 아래 코드를 보자:
void example(int val);
int yumin;
example(yumin); // ❌ 초기화 안 됐음
변수 yumin이 초기화되지 않았기 때문에 에러가 난다.
하지만,
int yumin = 2;
example(yumin); // ✅ 정상
처럼 초기화된 값을 넘기면 당연히 오류 없이 작동한다.
그렇다면 다시 보자.
Accept(listenfd, (SA *)&clientaddr, &clientlen);
여기서 clientaddr은 값을 넘긴 게 아니라,
주소값을 넘겼다. 즉, "비어있는 통"을 건넨 것이다.
Accept()
는 그 주소로 찾아가서, 거기에 직접 값을 써준다.
즉, clientaddr
는 우리가 값을 "주지 않아도",
함수가 채워서 반환해주는 방식이다.
이런 방식을 call by reference,
즉, “참조에 의한 전달”이라고 부른다.
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
→ 두 번째, 세 번째 인자인
addr
,addrlen
이
함수 내부에서 직접 수정되는 출력 변수들이다.
📌 우리는 “이 통에 주소 받아와줘~” 라고 빈 통을 건네고,
Accept()
는 그 통에 도착한 클라이언트의 주소를 채워준다.
이쯤에서 헷갈릴 수 있는 두 개념을 정리해보자.
구분 | 설명 | 예시 | 결과 |
---|---|---|---|
Call by Value | 값만 복사해서 넘김 | example(int x) | 원본 변경 ❌ |
Call by Reference | 주소를 넘겨서 함수가 직접 원본 수정 | example(int *x) | 원본 변경 ✅ |
void example(int x) {
x = 10;
}
int main() {
int a = 5;
example(a);
printf("%d", a); // 5 (변하지 않음)
}
void example(int *x) {
*x = 10;
}
int main() {
int a = 5;
example(&a);
printf("%d", a); // 10 (값 변경됨)
}
→ Accept()는 두 번째 방식, Call by Reference 방식이다!
이 시점에서 또 하나의 개념을 꼭 짚고 가야 한다.
블로킹 함수란, 결과가 나올 때까지 멈춰있는 함수를 뜻한다.
int n = read(fd, buf, 100);
위 함수에서, read()
는 읽을 데이터가 없으면 기다린다.
→ 결과가 올 때까지 “멈춰 있는(read: 블로킹되는)” 것이다.
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
❗️ 참고로 실제로 "멈추는" 것은 Accept() 함수가 아니라,
그 안에서 호출된accept()
시스템 콜이다.
Accept()는 단지 그걸 감싸는 에러 처리용 wrapper 함수일 뿐이다.
accept()
도 마찬가지다.
누군가 문을 열고 들어올 때까지
accept()
는 문 앞에서 묵묵히 대기 중인 셈이다.
❗️ 클라이언트가 접속하지 않으면, 이 줄에서 프로그램은 "멈춘 채 기다린다."
이제 이런 의문이 생길 수 있다.
“그럼 여러 클라이언트가 동시에 접속하면 어떻게 되는 걸까?”
답은 간단하다.
지금 구조에서는 하나씩밖에 못 받는다.
(교재에 있는 echosrveri.
는 한 번에 한 개씩의 클라이언트를 반복해서 실행하는 반복서버를 예시로 들고있다.)
왜냐면...
echo(connfd); // 블로킹
Close(connfd); // 이후에만 다음 Accept
즉, 한 명을 다 처리하고 나서야
다음 사람을 받을 수 있다.
멀티 클라이언트를 처리하려면
fork()
를 써서 클라이언트마다 별도 프로세스를 만들어줘야 한다.
"그렇다면 Accept()가 블로킹 함수라는 건 어떻게 알 수 있었을까?"
"누가, 언제 그렇게 정의해놓은 걸까?"
정답은 이렇다:
✅ accept()는 운영체제가 기본적으로 블로킹 함수로 동작하도록 설계해놓은 함수다.
즉,
운영체제 커널과 POSIX 표준에서
“클라이언트가 접속하지 않으면 기다리게 만든다”
라고 기본 동작을 정의해놓은 것이다.
accept()
외에도, 운영체제나 시스템 프로그래밍에서
대표적인 블로킹 함수들은 다음과 같다:
함수 | 언제 블로킹될까? |
---|---|
read() | 읽을 데이터가 없을 때 (예: 키보드 입력 기다림) |
write() | 버퍼가 가득 찼을 때 (특히 파이프, 소켓 등) |
accept() | 연결 요청이 없을 때 |
connect() | 연결이 완료될 때까지 (TCP handshake 진행 중) |
wait() | 자식 프로세스가 종료될 때까지 |
fgets() | 사용자 입력이 끝날 때까지 (엔터 입력 전까지) |
손님맞이를 저런 표정으로 하고있어도 되나..