개발 블로그

TCP/IP Socket 통신 프로그래밍 본문

시스템/시스템 프로그래밍

TCP/IP Socket 통신 프로그래밍

갹둥 2024. 9. 2. 21:12

네트워크 통신의 기초를 이해하는 것은 모든 개발자에게 중요한 단계입니다. TCP 소켓 통신은 데이터 전송의 신뢰성과 순서를 보장하는 핵심 기술입니다. 이번 블로그에서는 TCP와 소켓의 기본 개념에 대해 작성하였습니다.

 

 

1. TCP(Transmission Control Protocol) 

TCP(Transmission Control Protocol)는 네트워크 통신에서 데이터 전송의 신뢰성을 보장하는 프로토콜로, 주로 인터넷과 같은 네트워크에서 데이터 패킷을 송수신하는 데 사용됩니다. TCP/IP 소켓 통신은 서버와 클라이언트 모델을 기반으로 합니다. TCP의 특징은 다음과 같습니다 

 

주요 특징

  1. 연결 지향성 (Connection-Oriented)
    • 데이터 전송 전에 송신 측과 수신 측 간의 연결을 설정
    • 연결이 설정된 후 데이터가 송수신되며, 연결 종료 전까지 유지
  2. 신뢰성 (Reliability)
    • 데이터가 손실되거나 손상된 경우 이를 감지하고 재전송을 요청하여 데이터의 신뢰성을 보장.
  3. 순서 보장 (Ordering)
    • 데이터가 송신 순서와 동일하게 수신될 수 있도록 패킷의 순서를 재조정
  4. 흐름 제어 (Flow Control)
    • 송신 측이 데이터 전송 속도를 조절하여 수신 측이 처리할 수 있는 속도로만 데이터를 송신
    • 수신 측의 버퍼 오버플로우를 방지
  5. 혼잡 제어 (Congestion Control)
    • 네트워크 혼잡 상태를 감지하고 데이터 전송 속도를 조절하여 네트워크의 과부하를 방지

 


2. 소켓(Socket)

소켓은 네트워크 통신의 기본 단위로, 데이터 전송의 양 끝단을 의미합니다. 소켓은 네트워크 상의 두 프로세스 간의 데이터 송수신을 가능하게 합니다. 간단히 말해서, 소켓은 네트워크에서 통신을 수행하는 ‘인터페이스’입니다.

 

 

*cf) 소켓의 유형:

  • 스트림 소켓 (Stream Socket): TCP를 사용하는 소켓으로, 연결 지향적이며 신뢰성 있는 데이터 전송을 제공
  • 데이터그램 소켓 (Datagram Socket): UDP를 사용하는 소켓으로, 비연결 지향적이며 데이터 전송의 신뢰성을 보장

 

소켓과 포트

  1. 소켓 (Socket):
    • 소켓은 네트워크에서 데이터 통신의 엔드포인트로 작용하는 소프트웨어 구조체
    • 소켓은 클라이언트와 서버 간의 데이터 전송을 위해 사용됨
    • 일반적으로 네트워크 프로그램에서 소켓은 IP 주소와 포트 번호를 갖고 있으며, 양방향 네트워크 통신이 가능한 객체
  2. 포트 (Port):
    • 포트는 특정 애플리케이션이나 서비스와 연결된 네트워크 소켓의 논리적 엔드포인트
    • 포트는 16비트 숫자로, 0에서 65535까지의 범위를 가지며, 특정 서비스나 애플리케이션에 대한 통신을 식별하는 데 사용

 

 

TCP/IP 소켓의 주요 유형과 역할

  • 서버 소켓 (Listener Socket)
    • 클라이언트의 연결 요청을 수신하기 위한 소켓
    • 서버는 리스너 소켓을 통해 여러 클라이언트로부터의 연결 요청을 처리
    • 이 소켓은 데이터 전송이 아닌 연결 수립을 목적으로 함
  • 클라이언트 소켓 (Client Socket)
    • 서버에 연결 요청을 보내고, 서버와의 데이터 송수신을 담당
    • 클라이언트 소켓은 서버 소켓에 연결을 요청하여 데이터를 주고받음
    • 소켓 생성 후 서버의 IP 주소와 포트 번호를 지정하여 연결 요청을 보냄
  • 데이터 소켓 (Data Socket)
    • 클라이언트와의 데이터 통신을 담당하는 소켓
    • 리스너 소켓이 클라이언트의 연결 요청을 수락(accept())하면 생성됨
    • 이 소켓을 통해 서버와 클라이언트는 데이터를 주고받음
  •  

 

 

 

소켓 통신 간단한 동작 흐름

  1. 서버:
    • 리스너 소켓을 생성하고 포트에 바인딩
    • 클라이언트의 연결 요청을 대기하고, 수락 후 데이터 소켓 생성
    • 데이터 소켓을 통해 클라이언트와 데이터 송수신
  2. 클라이언트:
    • 서버에 연결 요청을 보내기 위해 소켓 생성
    • 연결이 수립되면 데이터 송수신

 

 

리스너 소켓(서버 소켓)의 동작 과정

  1. 소켓 생성 (socket):
    • 먼저, 서버는 리스너 소켓을 생성
    • socket() 함수를 사용하여 소켓을 생성
    • int listener_fd = socket(AF_INET, SOCK_STREAM, 0);
  2. 주소 할당 (bind):
    • 리스너 소켓에 IP 주소와 포트 번호를 할당
    • bind(listener_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
  3. 연결 대기 상태 (listen):
    • 서버는 이제 클라이언트의 연결 요청을 대기하기 위해 리스너 소켓을 listen() 상태로 전환
    • listen(listener_fd, SOMAXCONN);
  4. 클라이언트 연결 수락 (accept):
    • 클라이언트가 서버에 연결을 요청하면, 리스너 소켓은 이 요청을 받아들여 새로운 소켓(일반적으로 데이터 소켓)을 생성
    • 이 새로운 소켓은 클라이언트와 서버 간의 실제 데이터 통신에 사용
    • int client_fd = accept(listener_fd, (struct sockaddr*)&client_addr, &client_len);
    • 리스너 소켓은 다시 연결 요청을 받기 위해 대기( LISTEN) 상태로 남아있고, accept() 함수는 새로 생성된 소켓(데이터 소켓) 파일 디스크립터를 반환
      • Data socket은 CONNECTED 상태
    • accept() 함수는 클라이언트의 연결 요청이 들어올 때까지 블록(대기) 상태로 있음
      • 클라이언트가 서버에 연결 요청을 하지 않으면, accept는 클라이언트의 연결 요청이 있을 때까지 계속 대기
/*
* C로 간단히 구현한 소켓 통신
*/

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

#define BUFFER_SIZE 1024

int main() {
   // Listener socket 생성
    int listener_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listener_fd < 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 서버 주소 구조체 초기화
    struct sockaddr_in server_addr; //  네트워크 주소를 표현하기 위한 구조체
    server_addr.sin_family = AF_INET; // 주소 패밀리 설정, IPv4 주소를 사용할 것임을 명시
    server_addr.sin_port = htons(8080); // port를 8080으로 지정
    server_addr.sin_addr.s_addr = INADDR_ANY; // ip 지정, 모든 네트워크 인터페이스에서 수신

    // Litener socket에 주소 binding
    if (bind(listener_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { 
        perror("bind failed");
        close(listener_fd);
        exit(EXIT_FAILURE);
    }

    // Litener socket을 Listen 상태로
    if (listen(listener_fd, SOMAXCONN) < 0) { 
        perror("listen failed");
        close(listener_fd);
        exit(EXIT_FAILURE);
    }

    printf("Server is listening on port 8080...\n");

    // Client 연결, Data Socket fd 반환 
    int client_fd = accept(listener_fd, NULL, NULL); 
    if (client_fd < 0) {
        perror("accept failed");
        close(listener_fd);
        exit(EXIT_FAILURE);
    }

    printf("Client connected.\n");

    char buffer[BUFFER_SIZE];
    ssize_t bytes_read;

    // 데이터 읽기
    bytes_read = read(client_fd, buffer, sizeof(buffer) - 1);
    if (bytes_read < 0) {
        perror("read failed");
        close(client_fd);
        close(listener_fd);
        exit(EXIT_FAILURE);
    }
    buffer[bytes_read] = '\0'; // Null-terminate the received data
    printf("Received: %s\n", buffer);

    // 데이터 쓰기
    const char *response = "Hello, client!";
    ssize_t bytes_written = write(client_fd, response, strlen(response));
    if (bytes_written < 0) {
        perror("write failed");
        close(client_fd);
        close(listener_fd);
        exit(EXIT_FAILURE);
    }
    printf("Sent: %s\n", response);

    // Data Socket 닫기
    close(client_fd);
    // Listener Socket 닫기
    close(listener_fd);

    return 0;
}