0202

학교

[컴퓨터 네트워크] Socket programming (Part 2) - (2)

일자 : 24-2 11주차 1차시

[3] select()

C
#include <sys/select.h> // select 함수 사용을 위한 헤더 파일
#include <sys/time.h> // timeval 구조체 사용을 위한 헤더 파일

// select 함수 선언
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

struct timeval {
  long tv_sec; /* 초 단위 */
  long tv_usec; /* 마이크로초 단위 */
}

1) 역할

: 동기 I/O 다중화 (Synchronous I/O multiplexing)

2) 파라미터 설명

  • nfds : 세트 내에서 가장 높은 번호의 파일 디스크립터에 1을 더한 값

    • select()는 FD_SETSIZE 미만의 파일 디스크립터 번호만 모니터링할 수 있다.

  • readfds : 읽기 준비 상태를 모니터링할 파일 디스크립터가 있는 세트

  • writefds : 쓰기 준비 상태를 모니터링할 파일 디스크립터가 있는 세트

  • exceptfds : "예외 조건"을 모니터링할 파일 디스크립터가 있는 세트

  • timeout : 파일 디스크립터가 준비될 때까지 select()가 대기할 시간 간격

    • timeout이 Null일 경우, 무한 대기

    • timeout이 0일 경우, 대기하지 않고 즉시 반환

3) 반환 값

  • 반환 값

    • 성공 시: 성공한 세트 내 파일 디스크립터 수

    • 타임아웃 발생 시: 0

[4] Call procedures of select()

  1. Step One.

    • Set file descriptors: 모니터링할 파일 디스크립터를 설정하는 것이다. 예를 들어, 읽기, 쓰기, 예외 처리를 위한 파일 디스크립터 세트를 구성하는 작업이다.

    • Set check range: 확인할 파일 디스크립터의 범위를 설정하는 것이다. nfds 파라미터에 해당하며, 파일 디스크립터의 최대값에 1을 더한 값을 지정한다.

    • Set timeout: select() 함수가 대기할 시간(타임아웃)을 설정하는 것이다. 타임아웃 값에 따라 select() 함수는 파일 디스크립터의 상태 변화를 기다리는 시간을 결정한다.

  2. Step Two.

    • Call select(): 설정한 파일 디스크립터 세트와 타임아웃 값을 기반으로 select() 함수를 호출하는 것이다. select() 함수는 파일 디스크립터가 읽기, 쓰기, 또는 예외 조건에 대해 준비 상태인지 확인한다.

  3. Step Three.

    • Check result of select(): select() 함수의 결과를 확인하는 것이다. 파일 디스크립터가 준비 상태인지 여부를 판별하며, 각 파일 디스크립터가 읽기 가능(Readable), 쓰기 가능(Writable), 예외(Exception) 상태인지 확인하는 과정이다.

[5] Descriptor sets

: 디스크립터 세트

  • fd_set: 정수 배열

    • 각 정수의 각 비트는 하나의 디스크립터에 해당합니다.

    • 관련 매크로 함수들이 존재한다.

1) 함수 / 설명 표

매크로 함수

설명

void FD_ZERO(fd_set *set)

세트에서 모든 항목을 제거한다

void FD_SET(int fd, fd_set *set)

fd를 세트에 추가한다

void FD_CLR(int fd, fd_set *set)

fd를 세트에서 제거한다

int FD_ISSET(int fd, fd_set *set)

fd가 세트에 포함되어 있으면 true를 반환한다

2) 예시

이 예시는 FD_ZERO, FD_SET, FD_CLR, FD_ISSET 함수를 사용하여 fd_set 세트를 관리하는 과정을 보여준다. FD_ZERO는 세트를 초기화하고, FD_SET은 특정 파일 디스크립터를 추가하며, FD_CLR는 특정 파일 디스크립터를 제거하고, FD_ISSET은 세트에 해당 파일 디스크립터가 포함되어 있는지 확인하는 기능을 한다.

  • fd_set: array of integers

    • Each bit in each integer corresponds to a descriptor

    • The related macro functions

1) 표로 보는 function / description

Macro function

Description

void FD_ZERO(fd_set *set)

Clear all entities from the set

void FD_SET(int fd, fd_set *set)

Add fd to the set

void FD_CLR(int fd, fd_set *set)

Remove fd to the set

int FD_ISSET(int fd, fd_set *set)

Return true if fd is in the set

2) 예시

[6] Detecting changes of file descriptor

: 파일 디스크립터의 변화 감지

  • 질문: 어떤 파일 디스크립터가 준비되었는지 어떻게 알 수 있나요?

  • 답변: select() 함수를 호출하기 전과 후의 fd_set 상태를 확인함으로써 알 수 있습니다.

과정 설명

  1. Before calling select()

    • fd_set 내 모든 파일 디스크립터는 준비 상태가 아닌(0) 상태입니다.

    • 예를 들어, fd0, fd1, fd2, fd3의 값이 모두 0으로 설정됩니다.

  2. When fd1 and fd3 change

    • select() 호출 이후, 파일 디스크립터 중 fd1fd3가 준비 상태로 변경됩니다.

  3. After calling select()

    • fd_set 내에서 준비된 파일 디스크립터는 1로 표시됩니다.

    • 위의 예시에서 fd1fd3의 값이 1로 변경된 것을 통해 준비 상태임을 확인할 수 있습니다.

이와 같은 방식으로 select() 함수는 파일 디스크립터의 상태 변화를 감지하며, 각 파일 디스크립터의 준비 여부를 판단할 수 있습니다.

[7] Detecting changes of file descriptor

: 파일 디스크립터의 변화 감지

  • 소켓이 준비 상태가 되는 조건들

조건

읽기 가능?

쓰기 가능?

예외?

읽을 데이터가 있을 때

O

새로운 연결이 대기 중인 소켓에 준비되었을 때

O

읽기 반이 닫혔을 때(FIN 수신됨)

O

쓰기를 위한 공간이 있을 때

O

쓰기 반이 닫혔을 때

O

TCP의 대역 외(out-of-band) 데이터가 있을 때

O

물론입니다! 아래는 요청하신 내용을 한국어로 번역한 것입니다.

2. 다양한 입출력 함수

[1] recv() 및 send()

C
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

1) flags

옵션

설명

recv

send

MSG_DONTROUTE

패킷을 전송할 때 게이트웨이를 사용하지 않고, 직접 연결된 네트워크의 호스트에게만 전송

O

?MSG_DONTWAIT?

논블로킹 작업을 활성화

O

O

MSG_OOB

긴급(Out-of-band) 데이터를 전송하거나 수신

O

O

?MSG_PEEK?

수신 큐의 시작에서 데이터를 반환하지만 해당 데이터를 큐에서 제거하지 않음

O

MSG_WAITALL

모든 데이터를 대기(전체 요청이 만족될 때까지 블록)

O

[2-1] readv() & writev()

1) readv()와 writev() 시스템 호출의 작동 방식

  1. readv() 함수의 경우

    • read function: 입력 버퍼(Input buffer)에서 각각의 Array 1, 2, 3으로 개별적인 read() 호출을 통해 데이터를 읽어온다.

    • readv function: 입력 버퍼에서 단일 readv() 호출로 여러 Array(1, 2, 3)에 동시에 데이터를 읽어올 수 있다.

  2. writev() 함수의 경우

    • write function: Array 1, 2, 3에서 출력 버퍼(Output buffer)로 각각 개별적인 write() 호출을 통해 데이터를 쓴다.

    • writev function: 여러 Array(1, 2, 3)에서 출력 버퍼로 단일 writev() 호출로 동시에 데이터를 쓸 수 있다.

  3. 주요 차이점:

  • 일반 read/write는 각각의 배열마다 개별적인 시스템 콜이 필요하다.

  • readv/writev는 한 번의 시스템 콜로 여러 배열의 입출력을 처리할 수 있어 더 효율적이다.

  • 이는 특히 여러 메모리 버퍼를 다룰 때 시스템 콜의 오버헤드를 줄일 수 있다는 장점이 있다.

[2-2] readv() & writev()

C
#include <sys/uio.h> // iovec 구조체를 사용하기 위해 필요한 헤더 파일

ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

1) 역할

  • 여러 개의 버퍼에 데이터 읽기 또는 쓰기

    • readv와 writev는 단일 함수 호출로 여러 버퍼에 대해 읽거나 쓸 수 있게 해준다.

2) 반환 값

: 하나 이상의 버퍼에서 읽거나 쓰기

  • 성공

    • readv: 읽은 바이트 수

    • writev: 쓴 바이트 수

  • 오류: -1

3) 파라미터 설명

  • fd: 파일 디스크립터

  • iov: iovec 구조체의 주소

    struct iovec {

    code
    void *iov_base; /* 시작 주소 */
    size_t iov_len; /* 전송할 바이트 수 */

    };

  • iovcnt: 배열의 개수

[3] 소켓과 표준 입출력

  • 표준 입출력 스트림은 소켓과 함께 사용할 수 있지만, 고려해야 할 몇 가지 사항이 있다.

    • fdopen 함수를 호출하여 어떤 디스크립터에서든 표준 입출력 스트림을 생성할 수 있다.

    • 마찬가지로, 표준 입출력 스트림이 주어지면 fileno 함수를 호출하여 해당 디스크립터를 얻을 수 있다.

    • fseek, fsetpos, rewind 함수는 모두 lseek을 호출하는데, 이는 소켓에서 실패한다.

    • 읽기와 쓰기 문제를 처리하는 가장 쉬운 방법은 주어진 소켓에 대해 두 개의 표준 입출력 스트림을 여는 것이다: 하나는 읽기를 위한 것이고, 다른 하나는 쓰기를 위한 것이다.

[4] 표준 입출력

  • 표준 입출력 함수

    • 파일 포인터(FILE *)를 사용한다.

    • 예: fopen(), fclose(), fwrite(), fread()

  • 시스템 함수

    • 파일 디스크립터(int)를 사용한다.

    • 예: open(), close(), write(), read()

[5-1] fdopen()

C
#include <stdio.h>
FILE *fdopen(int fildes, const char *mode);

1) 역할

: 파일 디스크립터와 스트림을 연결한다.

  • 파일 디스크립터와 스트림을 연결한다.

2) 파라미터 설명

  • filedes: 파일 디스크립터

  • mode:

    • r: 파일을 읽기 위해 연다.

    • w: 파일을 쓰기 위해 연다.

    • a: 파일의 끝에서 쓰기 위해 연다.

    • r+: 파일을 읽기와 쓰기 위해 연다.

    • w+: 파일을 읽기와 쓰기 위해 연다.

    • a+: 파일의 끝에서 읽기와 쓰기 위해 연다.

3) 반환 값

  • 성공: 스트림에 대한 포인터

  • 오류: 널 포인터

[5-2] fileno()

C
#include <stdio.h>
int fileno(FILE *stream);

1) 역할

: 스트림 포인터를 파일 디스크립터에 매핑한다.

  • stream이 가리키는 스트림에 연결된 정수 파일 디스크립터를 반환한다.

2) 파라미터 설명

  • stream: 파일 디스크립터

3) 반환 값

  • 성공: 스트림과 연결된 파일 디스크립터의 정수 값

  • 오류: 널 포인터

[6] 입력과 출력 스트림 분리

  • 주어진 소켓에 대해 읽기와 쓰기를 위한 두 개의 표준 입출력 스트림을 여는 것이 일반적이다.

    • 편리한 처리를 위해

    • 그러나 연결을 닫을 때 중복된 파일 디스크립터를 신중하게 처리해야 한다.

[7-1] Problem when closing 소켓

1) 핵심 문제:

  • 하나의 소켓에 대해 read와 write용 FILE 포인터를 각각 얻더라도, 둘 중 하나에 대해 fclose()를 호출하면 소켓이 완전히 닫히게 된다.

  • 즉, half-close가 아닌 완전 종료가 발생한다.

2) 예시 코드 분석

서버 측 코드 (sep_serv.c):

C
readfp = fdopen(clnt_sock, "r");
writefp = fdopen(clnt_sock, "w");

fputs("FROM SERVER: Hi~ client? \n", writefp);
fputs("I love all of the world \n", writefp);
fputs("You are awesome! \n", writefp);
fflush(writefp);

fclose(writefp);    // writefp를 닫으면
fgets(buf, sizeof(buf), readfp);    // 이 부분은 실행되지 않음

클라이언트 측 코드 (sep_clnt.c):

C
readfp = fdopen(sock, "r");
writefp = fdopen(sock, "w");

while (1)
{
    if (fgets(buf, sizeof(buf), readfp)==NULL) 
        break;
    fputs(buf, stdout);
    fflush(stdout);
}

fputs("FROM CLIENT: Thank you! \n", writefp);
fflush(writefp);
fclose(writefp); fclose(readfp);

(1) 문제점 설명:

  1. 서버가 writefp를 fclose()로 닫으면 소켓 자체가 완전히 닫힌다.

  2. 이로 인해 서버의 fgets() 함수는 실행되지 않는다.

  3. 소켓이 완전히 닫히면서 File Descriptor와 모든 FILE 포인터들이 함께 닫힌다.

(2) 영향:

  • 이러한 동작은 half-close를 구현하기 어렵게 만든다.

  • 한쪽 방향의 통신만 닫으려고 해도 양방향 통신이 모두 종료된다.

  • 이는 소켓 프로그래밍에서 주의해야 할 중요한 특성이다.

[7-2] dup() 및 dup2()

C
#include <unistd.h>

// oldfd의 파일 디스크립터를 복제하는 함수
int dup(int oldfd);

// oldfd의 파일 디스크립터를 newfd로 복제하는 함수
int dup2(int oldfd, int newfd);

1) 역할

: 파일 디스크립터를 복제한다.

  • dup() 시스템 호출은 oldfd 파일 디스크립터의 복사본을 생성하며, 새로운 디스크립터를 위해 사용되지 않는 가장 낮은 번호의 파일 디스크립터를 사용한다.

  • dup2() 시스템 호출은 dup()와 동일한 작업을 수행하지만, 사용되지 않는 가장 낮은 번호의 파일 디스크립터 대신 newfd에 지정된 파일 디스크립터 번호를 사용한다.

2) 반환 값

  • 반환 값

    • 성공: 새로운 파일 디스크립터

    • 오류: -1

[7-3] Solution when closing

1) 핵심 솔루션

  • Half-close는 파일 디스크립터를 복제함으로써 달성할 수 있다.

2) 개선된 코드 분석:

서버 측 코드 (sep_serv2.c):

C
readfp = fdopen(clnt_sock, "r");
writefp = fdopen(dup(clnt_sock), "w");    // 파일 디스크립터 복제

fputs("FROM SERVER: Hi~ client? \n", writefp);
fputs("I love all of the world \n", writefp);
fputs("You are awesome! \n", writefp);
fflush(writefp);

shutdown(fileno(writefp), SHUT_WR);    // 출력 스트림만 종료
fclose(writefp);

fgets(buf, sizeof(buf), readfp); fputs(buf, stdout);    // 정상 실행됨
fclose(readfp);

클라이언트 측 코드 (sep_clnt.c):

C
readfp = fdopen(sock, "r");
writefp = fdopen(sock, "w");

while (1)
{
    if (fgets(buf, sizeof(buf), readfp)==NULL) 
        break;
    fputs(buf, stdout);
    fflush(stdout);
}

fputs("FROM CLIENT: Thank you! \n", writefp);
fflush(writefp);
fclose(writefp); fclose(readfp);

(1) 주요 변경사항과 특징:

  1. dup()를 사용하여 파일 디스크립터를 복제한다.

  2. shutdown() 함수를 사용하여 출력 스트림만 종료한다.

  3. 소켓은 모든 관련 파일 디스크립터가 닫힐 때까지 완전히 종료되지 않는다.

(2) 장점:

  • half-close 구현이 가능해진다.

  • 읽기와 쓰기 스트림을 독립적으로 제어할 수 있다.

  • 데이터 송수신의 완전성을 보장할 수 있다.

(3) 동작 원리:

  1. 파일 디스크립터를 복제하여 독립적인 read/write 스트림을 생성한다.

  2. shutdown() 함수로 특정 스트림만 종료가 가능하다.

  3. 모든 관련 파일 디스크립터가 닫혀야 소켓이 완전히 종료된다.

(4) 실행 결과:

  • 서버와 클라이언트 간의 half-close가 성공적으로 구현되어 데이터 송수신이 정상적으로 이루어진다.

  • fgets() 함수가 정상적으로 실행되어 클라이언트의 응답을 받을 수 있다.

3. 멀티 스레드 프로그래밍

[1] 스레드

  • 경량 프로세스

    • 스레드는 프로세스보다 시스템 자원을 덜 요구한다.

    • 프로세스 내의 모든 스레드는 스택 공간을 제외하고 동일한 주소 공간을 공유한다.

    • 스레드 간 통신은 프로세스 간 통신보다 더 효율적이고 사용하기 쉽다.

[2] 스레드 vs. 프로세스

  • 프로세스는 서로 별도의 메모리 공간을 유지한다.

  • 스레드는 스택을 제외하고 동일한 메모리 공간을 공유한다.

[3] Pthread

  • Pthread는 1995년에 IEEE POSIX(휴대용 운영 체제 인터페이스)에 의해 규정된 표준화된 스레드 프로그래밍 인터페이스이다.

  • Pthreads는 pthread.h 헤더/포함 파일과 스레드 라이브러리를 사용하여 구현된 C 언어 프로그래밍 타입과 프로시저 호출의 집합으로 정의된다.

[4] Pthread API

  • ?스레드 관리?

    • 스레드 생성, 종료, 조인 등 스레드에 직접 작용하는 함수들이다.

  • ?뮤텍스?

    • 뮤텍스 생성, 파괴, 잠금, 잠금 해제를 제공한다.

  • 세마포어

    • 세마포어 생성, 파괴, 대기, 포스트를 제공한다.

  • 조건 변수

    • 특정 변수 값에 따라 생성, 파괴, 대기 및 신호를 주는 함수를 포함한다.

[5] 스레드 생성

C
#include <pthread.h>

// 새로운 스레드를 생성하는 함수
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);

1) 역할

: 새로운 스레드를 생성한다.

  • 호출 프로세스에서 새로운 스레드를 시작한다.

2) 파라미터 설명

  • thread: 성공 시 새로운 스레드 ID

  • attr: 스레드 속성을 설정하기 위한 값. 기본값은 NULL.

  • start_routine: 스레드가 생성될 때 실행할 루틴

  • arg: 전달할 인수

3) 반환 값

  • 성공: 0

  • 오류: 오류 번호

[6] 스레드 조인

C
#include <pthread.h>

// 종료된 스레드와 조인하는 함수
int pthread_join(pthread_t thread, void **retval);

1) 역할

: 종료된 스레드와 조인한다.

  • 지정된 스레드가 반환될 때까지 호출 스레드를 차단한다.

  • 부모 스레드가 반환되면 모든 자식 스레드는 종료된다.

2) 파라미터 설명

  • thread: 종료할 지정된 스레드

  • retval: 종료된 스레드의 반환 값(참조로 반환되는 포인터)

3) 반환 값

  • 성공: 0

  • 오류: 오류 번호

[7] 스레드 분리 (Thread Detach)

C
#include <pthread.h>
int pthread_detach(pthread_t thread);

1) 역할

: 스레드를 분리한다

  • thread로 식별된 스레드를 분리 상태로 설정한다.

  • 분리된 스레드가 종료되면, 해당 스레드의 자원이 자동으로 시스템에 반환된다. 이는 다른 스레드가 종료된 스레드와 조인(join)할 필요가 없다.

  • 비차단(non-blocking) 함수이다.

2) 파라미터 설명

  • thread: 종료할 스레드를 지정한다.

3) 반환 값

  • 성공: 0

  • 오류: 오류 번호

[8] 스레드 취소 (Thread Cancel)

C
#include <pthread.h>
int pthread_cancel(pthread_t thread);

1) 역할

: 스레드에 취소 요청을 보낸다

  • 이 함수는 스레드에 취소 요청을 보낸다.

  • 타겟 스레드가 취소 요청에 반응하는지 여부 및 시점은 해당 스레드의 두 가지 속성에 따라 달라진다: 취소 가능 상태와 유형이다.

  • 취소 가능 상태는 pthread_setcancelstate()에 의해 결정된다.

  • 취소 유형은 pthread_setcanceltype()에 의해 결정된다.

2) 파라미터 설명

  • thread: 취소할 스레드의 ID이다.

3) 반환 값

  • 성공: 0

  • 오류: 비영(非零) 오류 번호

Summary

  1. select() 함수

  • 동기 I/O 다중화

  • 여러 파일 디스크립터의 상태를 동시에 모니터링

    • timeout 설정으로 대기 시간 조절 가능

    • fd_set으로 파일 디스크립터 관리 (FD_ZERO, FD_SET, FD_CLR, FD_ISSET)

  1. 입출력 함수

  • 주요 함수들

    • recv/send: 소켓 통신용 기본 함수

    • readv/writev: 여러 버퍼에 대한 입출력을 한 번에 처리

    • fdopen/fileno: 파일 디스크립터와 스트림 간 변환

  • Half-close 해결책

    • dup()로 파일 디스크립터 복제

    • shutdown()으로 특정 방향만 통신 종료 가능

    • 읽기/쓰기 스트림 독립적 제어 가능

  1. 멀티스레드 프로그래밍

  • 스레드 특징

    • 프로세스보다 가벼운 실행 단위

    • 스택 제외한 메모리 공간 공유

    • 스레드 간 통신이 프로세스 간 통신보다 효율적

  • 주요 Pthread 함수

    • pthread_create: 새 스레드 생성

    • pthread_join: 스레드 종료 대기

    • pthread_detach: 스레드 분리

    • pthread_cancel: 스레드 취소

  1. 중요 포인트:

  • select()는 I/O 다중화의 기본

  • 파일 디스크립터 복제로 half-close 구현

  • 스레드는 자원 공유로 효율적인 병렬 처리 가능


목차