- backlog queue에서 SYN을 보내 대기 중인 요청을, 큐에서 하나씩 연결 수립
- 리턴 값은 새로운 소켓의 파일 디스크립터 (* 아래 내용 참조)
[ accept 시스템 콜 형식 : accept( sockfd, sockaddr, socklen_t ); ]
- sockfd : 백로그 큐의 요청을 받아들이기 위한 소켓의 파일 디스크립터
- backlog : 백로그 큐에서 뺀 연결 요청을 통해 확인한 클라이언트 주소 정보 (IP주소, port 번호)
- socklen_t : backlog 구조체의 메모리 크기
#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<string.h>#include<sys/socket.h>#include<netinet/in.h>intmain(){
// 1. socket() 시스템 콜// IPv4, TCP, 시스템이 프로토콜 선택int server_socket = socket(AF_INET, SOCK_STREAM, 0);
// 2. bind() 시스템 콜// 소켓에 바인딩할 IP주소, Port 번호를 담은 구조체structsockaddr_inserver_address;// IPv4 주소 체계 사용
server_address.sin_family = AF_INET;
// 모든 가능한 IP 주소
server_address.sin_addr.s_addr = INADDR_ANY;
// 포트 번호 80
server_address.sin_port = htons(80);
bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address));
// 3. listen() 시스템 콜// TCP 백로그 큐의 크기 = 5listen(server_socket, 5);
print("Server: Waiting for client's connection...\n");
// 4. accept() 시스템 콜// 백로그 큐에서 빼온 클라이언트의 주소 정보를 담은 구조체structsockaddr_inclient_address;socklen_t slient_addrlen = sizeof(client_address);
int client_socket = accept(server_socket, (struct sockaddr *)&client_address, &client_addrlen);
print("Server: Accepted connection from $s:%d\n",
inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
// 5.3-way handshake의 나머지 두 단게 수행// 아래의 1.5장에서 이에 대해 자세히 설명char buffer[1024];
// Client ACK 받기ssize_t bytes_received = recv(client_socket, buffer, sizeof(buffer), 0);
if (bytes_received > 0) {
printf("Server: Received ACK from client.\n");
}
}
[ * 코드 함수 ]
- inet_nota() 함수 : 네트워크 바이트 순서의 32bit 를 Dotted-Decimal Notation 주소값으로 변환
- ntohs() 함수 : 네트워크 바이트 순서를 IP 포트 번호로 변환
1.5 TCP 3-way handshake
앞서 살펴보았듯이 TCP는 연결지향성, 신뢰성을 보장한다.
이는 TCP 3-way handshake 과정을 통해
클라이언트와 서버가 준비되었음을 확인하는 과정이 있기 때문이다.
위의 TCP 3-way handshake 과정을 하나씩 살펴보자.
1) SYN : 서버가 listen() 상태일 때 소켓 연결 요청을 보내는 것 ( = connect() system call )
2) SYN / ACK : accept() 시스템콜 이후 클라이언트에 요청이 잘 수행되었음을 반환
3) ACK : 서버의 SYN/ACK에 대한 반환
여기서 2와 3번 과정은 OS의 system call을 통해 이루어지며,
accept() 콜 이후에 established 상태를 수립하는 과정이다.
해당 과정에 대한 자세한 그림은 다음과 같다.
하지만 accept() 시스템 콜이 수행되더라도,
곧바로 3WHS를 통해 데이터 송수신이 이루어지는 것은 아니다.
서버는 하나의 프로세스이므로,
수많은 클라이언트의 요청이 들어있는 백로그 큐의 가장 앞에 있던 요청의
수행과 응답을 모두 끝낸 후, 다음 요청을 수행하려면 병목 현상이 발생할 것이다.
따라서 서버는 연결 요청을 받는 부분과, 응답을 보내는 부분을 구분한다.
#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<string.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>intmain(){
int server_socket = socket(AF_INET, SOCK_STREAM, 0);
structsockaddr_inserver_address;
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = INADDR_ANY;
server_address.sin_port = htons(80); // 웹 서버 포트인 80bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address));
listen(server_socket, 5);
printf("Server: Listening on port 80...\n");
while (1) {
structsockaddr_inclient_address;socklen_t client_addrlen = sizeof(client_address);
// accept() 시스템 콜int client_socket = accept(server_socket, (struct sockaddr *)&client_address, &client_addrlen);
// fork() 시스템 콜// 자식 프로세스인 경우// exec() 시스템 콜은 생략if (fork() == 0)
printf("Server: Accepted connection from %s:%d\n",
inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
// 3-way handshake의 나머지 두 단계 수행// 여기서는 ACK를 보내는 과정만 간단히 보여줍니다.sleep(1); // 실제로는 필요한 로직 수행// 서버의 응답 전송char response[] = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, world!";
send(client_socket, response, strlen(response), 0);
printf("Server: Sent response to client.\n");
close(client_socket);
exit(0);
}
close(client_socket);
}
close(server_socket);
return0;
}
위의 코드를통해 accept() 시스템 콜에 대한 리턴을 받은 것은
SYN 요청을 보낸 클라이언트가 백로그 큐에 있은 후, 이를 위한 응답을 위해 새로운 소켓을 만드는 것임을 알 수 있다.
그렇다면, 여기서 fork() 시스템 콜에 대해 조금 더 자세히 알아보자.
[ fork() 시스템 콜 : 자식 프로세스 생성 ]
- return값 0 : 자식 프로세스
- return 값 0이 아님 : 부모 프로세스
앞선 코드를 부모 프로세스와 자식 프로세스의 입장으로 나누어보면, 다음과 같다.
1) 부모 프로세스의 입장
부모 프로세스의 경우 accept() 시스템 콜로 연결 요청을 받아준 후,
나머지 작업은 자식 프로세스에게 맡기는 것을 확인할 수 있다.
부모 프로세스는 새로운 클라이언트의 연결 요청을 받아주는 역할!
#include<stdio.h#include <stdio.h>#include<stdlib.h>#include<unistd.h>#include<string.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>intmain(){
int server_socket = socket(AF_INET, SOCK_STREAM, 0);
structsockaddr_inserver_address;
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = INADDR_ANY;
server_address.sin_port = htons(80); // 웹 서버 포트인 80bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address));
listen(server_socket, 5);
printf("Server: Listening on port 80...\n");
while (1) {
structsockaddr_inclient_address;socklen_t client_addrlen = sizeof(client_address);
int client_socket = accept(server_socket, (struct sockaddr *)&client_address, &client_addrlen);
if (fork() == 0 -> false ) {
실행안함
}
}
close(server_socket);
return0;
}
2) 자식 프로세스의 입장
자식 프로세스의 경우 부모 프로세스가 새롭게 만든 소켓을 받아서,
나머지 작업 (3WHS의 2,3단계) 을 수행한 후, 클라이언트와 데이터 통신을 하는 것을 확인할 수 있다.
이때 해당 작업을 모두 수행하면 exit() 시스템 콜을 호출하여,
새로운 연결 요청을 받지 않고 종료한다.
자식 프로세스는 3WHS의 나머지 두 단계를 수행한 후 ACK를 보내는 역할!
#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<string.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>intmain(){
int server_socket = socket(AF_INET, SOCK_STREAM, 0);
structsockaddr_inserver_address;
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = INADDR_ANY;
server_address.sin_port = htons(80); // 웹 서버 포트인 80bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address));
listen(server_socket, 5);
printf("Server: Listening on port 80...\n");
while (1) {
structsockaddr_inclient_address;socklen_t client_addrlen = sizeof(client_address);
int client_socket = accept(server_socket, (struct sockaddr *)&client_address, &client_addrlen);
if (fork() == 0 -> true) { // 자식 프로세스printf("Server: Accepted connection from %s:%d\n",
inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
// 3-way handshake의 나머지 두 단계 수행// 여기서는 ACK를 보내는 과정만 간단히 보여줍니다.sleep(1); // 실제로는 필요한 로직 수행// 서버의 응답 전송char response[] = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, world!";
send(client_socket, response, strlen(response), 0);
printf("Server: Sent response to client.\n");
close(client_socket);
// 자식 프로세스 종료// 새로운 연결 요청 받지 않고, 응답만 준 후 종료exit(0);
}
close(client_socket);
}
close(server_socket);
return0;
}
[ 정리 ]
서버는 연결을 받는 부분 (부모 프로세스) 과 응답을 주는 부분 (자식 프로세스) 이 병렬적으로 이루어져 있다!