CS

[c언어] TCP/IP Socket 통신 흐름

2024. 10. 7. 16:17

IP / Port numbers

  • IP 주소: 네트워크에서 장치를 식별하는 고유한 주소
  • 포트 번호: 특정 프로세스나 서비스를 식별하는 숫자
  • 일반적으로, IP 주소는 패킷이 어떤 시스템(컴퓨터 등)에 라우팅될 것인가를 결정한다면, Port number는 이 시스템의 어떤 어플리케이션에 패킷을 라우팅할 지 결정
    • 192.168.1.1:80은 IP 주소 192.168.1.1의 80번 포트를 통해 HTTP 서비스를 제공하는 서버

 

sockets

  • 네트워크 프로그래밍에서 클라이언트와 서버 간의 통신을 위한 엔드포인트(통신을 하기 위한 마지막 부분)
  • 네트워크 통신을 하는 방법들을 추상화하여 사용자에게 제공된 것
    • 모든 네트워크 통신은 소켓 기반

 

[소켓의 구성 요소]

  1. Local IP 주소
  2. Local Port
  3. Remote IP address (목적지 주소)
  4. Remote port (목적지 포트)
  5. Protocol (UDP, TCP)

 

※ 금융권에서 HTTP(application layer) 대신 TCP(transport layer) 통신을 사용하는 이유

  • 보안성: TCP는 세션 기반의 통신을 제공하여 연결이 안정적이다. 금융 거래에서는 데이터의 무결성과 보안을 최우선으로 하기 때문에, TCP를 통해 암호화된 데이터 전송(예: TLS/SSL)을 쉽게 구현할 수 있다.
  • 신뢰성: TCP는 데이터 전송 중 손실된 패킷을 자동으로 재전송하는 기능이 있어, 데이터의 신뢰성을 보장한다. 금융 거래에서는 데이터가 정확하게 전달되는 것이 매우 중요하다.
  • 성능: TCP는 상태 정보를 유지하기 때문에, 지속적인 연결을 통해 여러 번의 요청을 처리할 수 있다. 이는 대량의 데이터를 빠르게 전송해야 하는 금융 서비스에 유리하다.
  • 통신 제어: TCP는 흐름 제어와 혼잡 제어 기능을 제공하여 네트워크 상태에 따라 데이터 전송 속도를 조절할 수 있다. 이는 금융 서비스가 안정적으로 운영될 수 있도록 도와준다.
  • 실시간 처리: 금융 거래는 실시간 처리가 중요하므로, TCP의 지속적인 연결을 통해 지연을 최소화할 수 있다.

 

전체적인 구조

  1. 소켓 생성: 전송 레이어와 네트워크 레이어의 소켓을 생성
  2. 주소 설정 및 바인딩: 서버는 IP 주소와 포트를 설정하고 소켓에 바인딩
  3. 연결 수립: 서버는 클라이언트의 연결 요청을 수락하고, 클라이언트는 서버에 연결
  4. 데이터 전송: 클라이언트가 데이터를 전송하고, 서버가 이를 수신
  5. 응답 처리: 필요 시 서버가 클라이언트에 응답
  6. 소켓 닫기: 통신이 끝난 후 소켓을 닫음.

 

  • 서버가 클라이언트 연결 요청을 대기하고 수락하는 과정에서 3-way handshaking 발생
  • read()와 write()는 파일 디스크립터와 관련된 일반적인 함수로 소켓에서도 사용 가능

 

TCP/IP 통신 흐름

  1. Application Layer: 클라이언트와 서버의 애플리케이션이 데이터 생성. 예를 들어, 클라이언트는 메시지를 작성하고 서버에 전송.
  2. Transport Layer: 클라이언트는 TCP 프로토콜을 사용하여 데이터를 세그먼트로 나누고 TCP 헤더를 추가. 이 세그먼트는 TCP 소켓을 통해 네트워크 레이어로 전달.
  3. Network Layer: TCP 세그먼트는 IP 패킷으로 캡슐화되어 네트워크 레이어로 이동. 이때 IP 헤더가 추가. 패킷은 라우터 등을 통해 목적지 서버로 전송됨.
  4. Network Layer(서버): 서버는 수신한 IP 패킷을 네트워크 레이어에서 처리하고, 패킷을 TCP 레이어로 전달.
  5. Transport Layer(서버): 서버의 TCP 레이어는 IP 패킷에서 TCP 세그먼트를 추출하고, TCP 헤더를 검사하여 데이터의 무결성을 확인. 이후, 서버의 애플리케이션 레이어로 데이터를 전달.
  6. Application Layer에서 처리: 서버의 애플리케이션은 수신된 데이터를 처리하고 필요에 따라 응답을 생성.
  7. 응답 흐름: 서버가 응답 데이터를 생성하면, 다시 전송 레이어에서 TCP 세그먼트로 나누고, 네트워크 레이어에서 IP 패킷으로 캡슐화하여 클라이언트에게 전송. 클라이언트는 이 과정을 역으로 수행하여 애플리케이션 레이어에서 응답 데이터를 수신함.

 

서버 측 코드

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

#define PORT 8080 // 포트 번호 정의

int main() {
    int server_fd, new_socket; // 서버 소켓 및 클라이언트 소켓
    struct sockaddr_in address; // 주소 구조체
    int addrlen = sizeof(address);
    char buffer[1024] = {0}; // 수신 버퍼

    // 1. 소켓 생성 (전송 레이어)
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    
    // 2. 주소 구조체 설정 (네트워크 레이어)
    address.sin_family = AF_INET; // IPv4
    address.sin_addr.s_addr = INADDR_ANY; // 모든 인터페이스에서 수신
    address.sin_port = htons(PORT); // 포트 설정

    // 3. 바인딩 (전송 레이어)
    bind(server_fd, (struct sockaddr *)&address, sizeof(address));
    
    // 4. 연결 대기 (전송 레이어)
    listen(server_fd, 3);
    
    // 5. 클라이언트 연결 수락 (전송 레이어) - 3-way handshaking 발생
    new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
    
    // 6. 데이터 수신 (전송 레이어)
    read(new_socket, buffer, 1024);
    printf("Received: %s\n", buffer); // 수신된 데이터 출력

    // 7. 소켓 닫기 (전송 레이어)
    close(new_socket);
    close(server_fd);
    return 0;
}

 

 

클라이언트 측 코드

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

#define PORT 8080 // 포트 번호 정의

int main() {
    int sock = 0; // 클라이언트 소켓
    struct sockaddr_in serv_addr; // 서버 주소 구조체
    char *message = "Hello from client"; // 전송할 메시지
    
    // 1. 소켓 생성 (전송 레이어)
    sock = socket(AF_INET, SOCK_STREAM, 0);
    
    // 2. 서버 주소 설정 (네트워크 레이어)
    serv_addr.sin_family = AF_INET; // IPv4
    serv_addr.sin_port = htons(PORT); // 포트 설정
    inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr); // 서버 IP 주소 설정
    
    // 3. 서버에 연결 (전송 레이어) - 3-way handshaking 발생
    connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    
    // 4. 데이터 전송 (전송 레이어)
    send(sock, message, strlen(message), 0);
    printf("Message sent\n"); // 전송 완료 메시지 출력

    // 5. 소켓 닫기 (전송 레이어)
    close(sock);
    return 0;
}
  • 위의 코드에서 socket() 함수와 bind(), listen(), accept(), connect() 등의 호출은 네트워크 레이어에서 패킷을 처리하고, TCP/IP 스택을 통해 데이터가 전송되도록 함.
    • 클라이언트: send()를 통해 TCP 세그먼트를 생성하고 서버로 전송
    • 서버: accept()를 통해 클라이언트의 연결 요청을 수락하고, read()로 클라이언트가 보낸 데이터를 TCP 세그먼트로 받아옴.

 

※ 주소 구조체를 사용하는 이유

  • 주소 정보 저장: struct sockaddr_in은 IPv4 주소와 포트 번호를 저장하는 구조체. 소켓 프로그래밍에서 서버와 클라이언트 간의 통신을 위해서는 IP 주소와 포트 정보를 알아야 하므로, 이를 구조체로 캡슐화하여 관리.
  • 유연한 데이터 구조: 소켓 프로그래밍에서는 다양한 주소 체계(IPv4, IPv6 등)와 프로토콜(TCP, UDP 등)을 지원해야 함. sockaddr 구조체는 이러한 다양한 주소 구조체를 일반화하여 사용할 수 있게 해주어 코드의 유연성을 높일 수 있음.
  • 함수 인자 전달: 소켓 관련 함수들은 struct sockaddr 형식의 포인터를 인자로 받아 다양한 주소 구조체를 동일한 방식으로 처리할 수 있음.

 

바인딩: 소켓을 특정 주소(IP 주소 + 포트 번호)에 연결하는 과정

 

서버 측 주소 바인딩

  • 서버의 역할: 서버는 클라이언트의 요청을 수신하고 처리 → 특정 IP 주소와 포트 번호에 바인딩되어 있어야 클라이언트의 연결 요청을 수신할 수 있음.
  • 네트워크 구조: TCP/IP 프로토콜에서 서버는 고정된 주소와 포트로 클라이언트의 연결을 수신하는 반면, 클라이언트는 연결을 수립할 때 서버의 주소와 포트를 지정
  • 자원 관리: 서버가 바인딩된 주소와 포트를 유지함으로써, 여러 클라이언트가 동시에 연결할 수 있도록 관리할 수 있음. 클라이언트는 각 연결에 대해 새로운 소켓을 생성하여 서버와 통신