[UMC Ewha 5th] Server - SpringBoot

[UMC Server] Chapter 1. 서버란 무엇인가(소켓&멀티 프로세스) (1)

wlalsu_u 2023. 9. 27. 11:18

1.0 1주차 워크북 학습 목표

 

 

서버가 하는 일이 정확히 무엇이고,

 

어떻게 클라이언트와 서버가 데이터를 주고 받을 수 있는지에 대해 정확하게 이해하기!

 

 

 

1) 서버의 정의와 역할을 이해한다.
2) 서버가 어떻게 구축되는지 이해한다.

 

 

 

 

아래는 1주차 워크북을 수행하기 위해 알아야 하는 사전 개념을 간단하게 정리해보았다.

 

 

 

OS / 네트워크 수업에서 다루었던 내용이지만, 

 

까먹은 부분도 많고, 해당 개념에 대해 깊이 있게 알지 못하는 것 같아서

 

나중에 블로그에 자세히 정리하며 복습해야 할 것 같다.

 

 

 

[ 사전 개념 (1) - 시스템 콜 ]

- 응용 프로그램의 경우 유저 모드에서는 수행되지 못하는, 반드시 커널의 도움을 받아야 하는 작업이 있음

- 응용 프로그램이 특권 명령 (디스크 파일 접근, 화면 결과 출력 등) 을 수행해야 하는 경우,
OS에 명령 대행을 요청하는 것

- 시스템콜 인터페이스가 시스템 콜 테이블 (콜 번호, 콜 핸들러 함수 주소) 을 구성하고,
OS가 콜 번호에 따른 루틴을 차레대로 수행

- 명령 작업이 완료되면, CPU에 인터럽트를 발생시켜 종료를 알림

 

[ 사전 개념 (2) - 프로세스와 스레드 ]

- Process : OS로부터 자원을 할당받은 작업의 단위 (메모리에서 실행중인 프로그램) 

- Thread : 프로세스가 할당받은 자원을 이용하는 실행 흐름의 단위 (CPU 최소 작업 단위)

- 프로세스는 하나 이상의 스레드를 가지고 있음

- 프로세스는 다른 프로세스의 변수/자료에 접근할 수 없지만, 스레드는 메모리 공유 가능

 

[ 사전 개념 (3) - TCP/IP 계층 ]

- OSI 7계층을 간소화하여 TCP/IP 4계층으로 분류함

- 우리의 컴퓨터와 다른 사람의 컴퓨터가 통신할 때, TCP/IP 계층 모델에 따라 주로 동작함

- 네트워크 인터페이스 / 인터넷 / 전송 / 응용 계층으로 나뉘고, 각각의 프로토콜에 따라 통신

 

[ 사전 개념 (4) - TCP와 UDP의 차이 ]

- TCP/IP 전송계층에서 패킷을 다른 컴퓨터로 전달하기위해 사용하는 프로토콜

- TCP(Transmission Control Protocol) 는 차례대로, 안정성있게 데이터를 보내 신뢰성을 보장 (연결형, 일대일)

- UDP(User Datagram Protocol)는 신뢰성을 보장해주지는 않지만, 간단한 데이터를 빠르게 보냄 (비연결형, 다대다)

 

 

 

 


 

 

 

1.1 IP 주소와 포트번호

 

 

 

네트워크 상에는 수 많은 장치들이 서로 얽혀 있기 때문에,

 

각각의 컴퓨터가 누구인지 식별할 수 있는 방법이 필요하다.

 

 

 

이를 위해서는 각 컴퓨터마다 겹치지 않고 고유한 주소를 부여해야 하는데,

 

이는 'IP주소' 를 통해 가능하다.

 

 

 

 

IP 주소 값은 cmd 창에서

 

'nslookup [ ]' 명령어를 통해 확인할 수 있다.

 

 

 

 

 

 

 

 

[ IP(Internet Protocol) 주소 ]

컴퓨터 네트워크에서 장치간 서로를 인식하고 통신을 하기 위한 특수한 번호

 

 

 

 

그렇다면 IP 주소를 통해 구분한 한 대의 컴퓨터 내에서,

 

어떤 프로세스에게 데이터를 보내야 하는지는 어떻게 알 수 있을까?

 

 

 

이를 식별하기 위해 '포트 번호' 가 사용된다.

 

 

 

 

[ 포트 번호 ]

컴퓨터 네트워크에서 장치간 서로를 인식하고 통신을 하기 위한 특수한 번호

ex) HTTP : 80 / HTTPS : 443

 

 

 

 

여기서 네트워크 통신은 컴퓨터가 직접 참여하는 것이 아닌,

 

현재 동작하고 있는 프로그램인 '프로세스' 간의 통신임을 기억하자!

 

( IPC : Inter-Process Communication)

 

 

 

 

 

앞선 내용을 요약해보면,

 

서버와 클라이언트가 통신을 하기 위해서는

 

[ 상대 프로세스가 동작하는 컴퓨터 IP ] : [ 상대 프로세스의 포트번호 ] 로 식별하는 것을 알 수 있다.

 

 

 

 

다시 한번 정리해보면, IP 주소는 각 컴퓨터를 식별하고 접속할 수 있는 주소이고
Port 번호는 해당 컴퓨터 내의 서버에 접속하는주소이다!

 

 

 

 

 

 

 


 

 

 

1.2 데이터 송신 과정

 

 

 

앞선 과정에서 IP 주소와 Port 번호를 통해 프로세스를 확인했다면,

 

해당 프로세스에서 어떻게 데이터를 송신할 수 있는 것일까?

 

 

 

프로세스간 데이터 전송 과정은 다음과 같다.

 

 

 

 

송신 과정

 

 

1) 서버 프로세스 (Application) 가 송신하고자 하는 데이터를 가지고 있음

2) 응용 프로그램이 OS에 write 시스템 콜을 호출하여 Socket에 데이터를 보냄

3) 네트워크 스택 (TCP/UDP, IP 계층, Ethernet 등) 에서 라우팅

4) NIC(랜 카드)를 통해 데이터가 외부로 나감

 

 

 

 

 

수신 과정

 

 

5) 수신자의 NIC에서 데이터를 송신받음

6) interrupt를 발생시켜서 Driver로 데이터를 옮긴 후, 네트워크 스택에서 라우팅

7) Socket에 데이터가 전달됨

8) 응용 프로그램에 데이터가 도달됨

 

 

 

 

 

자세한 내용은 아래의 HTTP 웹 기본 지식 게시글에 작성하였다.

 

 

 

https://wlalsu.tistory.com/100

 

[HTTP 웹 기본 지식] 섹션 1. 인터넷 네트워크

1.1 인터넷 통신 인터넷에서 두 대의 컴퓨터는 어떻게 통신할까? 만약 클라이언트와 서버가 가까이 있다면, 단순히 케이블을 연결하여 통신할 수 있을 것이다. 하지만 메시지를 전달해야하는 PC

wlalsu.tistory.com

 

 

 

 

 


 

 

 

1.3 소켓 (Socket) 이란?

 

 

 

앞서 프로세스간 데이터 전송 과정에서,

 

중간 역할을 했던 소켓에 대해 더 자세하게 알아보자.

 

 

 

[ 소켓 (Socket) ]

- OSI 7계층에서 응용 프로그램과 전송 계층 사이의 인터페이스 

- 방식에 따라 TCP 소켓 / UDP 소켓으로 나뉨

 

 

 

TCP 소켓 (stream 소켓)

 

 

- 연결 지향형 (3-way handshaking) 으로 데이터 전송에 대한 신뢰성 보장

 

- 흐름제어와 혼잡제어를 지원하여 데이터가 순차적으로 이동

 

- 웹 HTTP 통신, 이메일, 파일 전송 등에 사용되며 속도가 느림

 

 

 

 

UDP 소켓 (datagram 소켓)

 

 

- 비연결형 이므로 신뢰성을 보장하지 않음

 

- 혼잡제어를 지원하지 않으므로, 데이터 전송 속도가 빠름

 

 

 

 

 

 

 

아래의 그림은 Client Socket과 Server Socket 의

 

소켓 API 실행 흐름을 보여주고 있다.

 

 

 

 

 

 

 

 

소켓 API의 대략적인 실행 흐름은 다음과 같다.

 

 

 

 

 

클라이언트 소켓 (Client Socket)

 

 

1) Create : 맨 처음 소켓을 생성

 

2) Connect : 서버 소켓에 연결을 요청

 

3) Send / Receive : 서버가 연결 요청을 받으면, 데이터를 송수신

 

4) Close : 모든 데이터 처리가 완료되면 소켓을 닫음

 

 

 

 

 

서버 소켓 (Server Socket)

 

 

1) Create : 맨 처음 소켓을 생성

 

2) Bind : 소켓에 서버가 사용할 IP주소 / Port 번호를 결합

 

3) Listen : 클라이언트에서 연결 요청이 오는지 계속 듣고 있음 

 

4) Accept : 클라이언트에서 연결 요청이 오면, 이를 받아들임

 

5) Send / Receive : 클라이언트에 데이터를 송수신

 

6) Close : 모든 데이터 처리가 완료되면 소켓을 닫음

 

 

 

 

[ 참고 ]

여기서 TCP 소켓의 핵심은 'Accept 시스템 콜' 임을 알 수 있다.

( TCP는 데이터를 송수신하기 전, 3-way handshaking을 통한 연결을 지향하기 때문에 )

 

 

 

 


 

 

 

1.4 클라이언트와 서버에서 사용하는 시스템콜 살펴보기

 

 

 

앞서 소켓 API 실행 흐름으로 대략적으로 살펴본 시스템 콜을

 

하나씩 자세하게 살펴보자. 

 

 

 

 

1) socket() 시스템 콜

 

 

- 맨 처음 소켓을 만드는 시스템 콜

 

- 연결 대상에 대한 정보가 없는 껍데기 socket (소켓 형태를 미리 만들어 두는 것)

 

- TCP 소켓의 경우 stream, UDP 소켓의 경우 datagram으로 지정

 

- 리턴 값으로 파일 디스크립터(* 아래 참고 )를 주어서, 소켓에 데이터를 작성할지 읽어들일지 결정

 

 

 

 

[ Socket 시스템 콜 형식 : socket( domain, type, protocol); ]

- domain : IPv4, IPv6 결정

- type : TCP(stream), UDP(datagram) 소켓 선택

- protocol : 0 (시스템이 프로토콜 선택)  / 6 (TCP) / 17 (UDP)

 

int socket_descriptor;
// domain, type, protocol
// IPv4, TCP, 시스템이 프로토콜 선택
socket_descriptor = socket(AF_INET, SOCK_STREAM, 0);

 

 

 

 

 

[ 참고 ] 파일 디스크립터(FD) 란?

- 리눅스에서는 소켓 조작이 파일 조작과 동일하게 간주

- FD는 시스템으로부터 할당 받은 파일, 소켓에 부여된 정수

- FD는 0이 아닌 정수 값을 가짐 ( 0:표준입력 / 1:표준출력 / 2:표준에러 )

- 파일 디스크립터를 파라미터로 전달해서,OS의 어떤 파일에 데이터를 쓸지, 요청할지 결정

 

 

 

 

 

 

2) bind() 시스템 콜

 

 

- socket() 을 통해 생성한 소켓에 IP주소와 Port 번호를 바인딩

 

- 어떤 소켓에 바인딩할지 OS에 알려주기 위해 파일 디스크립터 포함

 

- 클라이언트는 Port 번호가 자동으로 부여되므로, 서버에서만 사용 ( * 아래 참고 )

 

 

 

 

[ bind 시스템 콜 형식 : bind( sockfd, sockaddr, socklen_t ); ]

sockfd : 바인딩 할 소켓의 파일 디스크립터

- sockaddr : 소켓에 바인딩할 IP 주소, Port 번호를 구조체로 담음

- socklen_t : sockaddr 구조체의 메모리 크기

 

#include <sys/socket.h>
#include <netinet/in.h>

int main() {

	// 1. socket() 시스템 콜
    // IPv4, TCP, 시스템 프로토콜 선택
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    
    // 파일 디스크립터 값이 -1인 경우 -> socket 생성 실패
    if (sockfd == -1) {
    	perror("Socket creation failed");
    }
    
    // 소켓 바인딩할 IP, Port 담은 구조체
    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET // IPv4
    server_address.sin_addr.s_addr = INADDR_ANY; // 모든 가능한 IP 주소
    server_address.sin_port = htons(80); // 포트 번호 80
    
    if (bind(sockfd, (struct sockaddr *)&server_address, sizeof(server_address)) == -1) {
    	perror("Bind Failed");
        return 1;
    }
    
    return 0;
}

 

 

 

 

[ 참고 ] 클라이언트는 통신 시 Port 번호가 자동으로 부여되는 이유!

- 서버 프로그램의 경우, 보통 지정된 포트 번호를 가지고 있음

- 하지만 클라이언트 프로그램은 자신의 IP 주소와 port 번호를 다른 클라이언트 / 서버가 미리 알고 있을 필요 X

- 또한 클라이언트 프로그램이 여러 컴퓨터와 병렬적으로 연결되는 경우, 하나의 포트 번호를 사용하면 에러 발생

- 따라서 클라이언트는 bind()를 사용하는 것이 더 범용성을 떨어뜨리므로 임의로 부여된 포트 번호 사용

 

 

 

 

 

3) listen() 시스템 콜

 

 

- 서버가 클라이언트에서 연결요청이 오고 있는지 듣고 있는 함수

 

- 연결 지향인 TCP 에서만 사용

 

- 최대로 받아주는 크기인 backlog를 설정하여, 클라이언트의 연결 요청을 받아들임

 

- 이때 backlog는 TCP의 backlog queue ( *아래 참고 )의 크기 

 

 

 

 

[ listen 시스템 콜 형식 : listen( sockfd, backlog ); ]

sockfd : 소켓의 파일 디스크립터

- backlog : 연결 요청을 받아줄 크기 ( = TCP의 백로그 큐 크기 )

 

#include <sys/socket.h>

int main() {

	// 1. socket() 시스템 콜
    // IPv4, TCP, 시스템 프로토콜 선택
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    
    // 파일 디스크립터 값이 -1인 경우 -> socket 생성 실패
    if (sockfd == -1) {
    	perror("Socket creation failed");
    }
    
    // 앞서 작성한 서버 소켓 주소, 바인딩 설정 코드 생략---->
    
 
    int backlog = 10; // 최대 대기열 크기
    if (listen(sockfd, backlog) == -1) {
    	perror("Listen failed");
        return 1;
    }
    
    
    return 0;
}

 

 

 

[ 참고 ] TCP backlog queue 알아보기

- listen() 시스템 콜은 파라미터의 backlog 크기만큼 backlog queue를 생성!

- 서버는 backlog queue를 가진 채로 클라이언트의 연결 요청을 받기 위해 대기 상태 유지

- 수많은 클라이언트가 요청을 보내면 이 요청들은 모두 backlog queue에 저장

- 커널은 listen() 소켓에 대해 2개의 큐를 가지고 있음 (complted connection queue, incompete connection queue)

- backlog는 이 두가지 큐의 합에 대한 최대값을 규정 ( 가득 찬 경우 요청 거절 )

1) 완전 연결 큐 : 3HSK가 맺어진 클라이언트 항목 저장 (established 상태)

2) 불완전 연결 큐 : 3HSK를 맺기 위해 보낸 SYN* 패킷을 저장하는 큐 (SYN_RCVD 상태)




* SYN  : Client가 소켓을 통해 서버에 요청을 하여 backlog queue에 들어갈 때 보내는 요청

* 참고 블로그 : https://plummmm.tistory.com/60

 

 

 

 

 

4) accept() 시스템 콜 ( 매우 중요 !!! )

 

 

- 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>

int main() {

	// 1. socket() 시스템 콜
    // IPv4, TCP, 시스템이 프로토콜 선택
	int server_socket = socket(AF_INET, SOCK_STREAM, 0);
    
    // 2. bind() 시스템 콜
    // 소켓에 바인딩할 IP주소, Port 번호를 담은 구조체
    struct sockaddr_in server_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 백로그 큐의 크기 = 5
    listen(server_socket, 5);
    print("Server: Waiting for client's connection...\n");
    
    // 4. accept() 시스템 콜
    // 백로그 큐에서 빼온 클라이언트의 주소 정보를 담은 구조체
    struct sockaddr_in client_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>

int main() {
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = INADDR_ANY;
    server_address.sin_port = htons(80); // 웹 서버 포트인 80

    bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address));

    listen(server_socket, 5);

    printf("Server: Listening on port 80...\n");

    while (1) {
        struct sockaddr_in client_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);

    return 0;
}

 

 

 

 

 

위의 코드를통해 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>

int main() {
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = INADDR_ANY;
    server_address.sin_port = htons(80); // 웹 서버 포트인 80

    bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address));

    listen(server_socket, 5);

    printf("Server: Listening on port 80...\n");

    while (1) {
        struct sockaddr_in client_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);

    return 0;
	}

 

 

 

 

 

 

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>

int main() {
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_address;
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = INADDR_ANY;
    server_address.sin_port = htons(80); // 웹 서버 포트인 80

    bind(server_socket, (struct sockaddr *)&server_address, sizeof(server_address));

    listen(server_socket, 5);

    printf("Server: Listening on port 80...\n");

    while (1) {
        struct sockaddr_in client_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);

    return 0;
}

 

 

 

 

 

[ 정리 ]

서버는 연결을 받는 부분 (부모 프로세스) 과 응답을 주는 부분 (자식 프로세스) 이 병렬적으로 이루어져 있다!

 

 

 

 


 

 

 

1.6 HTTP 웹 서버 코드

 

 

 

앞선 소켓 프로그래밍 코드를 바탕으로

 

아파치의 간략화된 코드는 다음과 같다.

 

 

 

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    const char* server_ip = "127.0.0.1";
    int server_port = 8080;

    int server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        perror("Socket creation failed");
        return 1;
    }

    struct sockaddr_in server_addr, client_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(server_port);
    server_addr.sin_addr.s_addr = inet_addr(server_ip);

    if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("Binding failed");
        return 1;
    }

    if (listen(server_socket, 5) == -1) {
        perror("Listening failed");
        return 1;
    }

    printf("Server listening on %s:%d\n", server_ip, server_port);

    while (1) {
        socklen_t client_addr_len = sizeof(client_addr);
        int client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_len);
        if (client_socket == -1) {
            perror("Accepting client failed");
            continue;
        }

        printf("Accepted connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

        char request[1024];
        recv(client_socket, request, sizeof(request), 0);
        printf("Received request:\n%s\n", request);

        char response[] = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\nHello, World!";
        send(client_socket, response, sizeof(response), 0);

        close(client_socket);
    }

    close(server_socket);
    return 0;
}

 

 

 

 

 

 

 

 

 

 

최용욱 'UMC Server 1주차 워크북' 내용을 기반으로 작성하였습니다.