학교
[컴퓨터 네트워크] Socket programming (Part 2) - (2)
일자 : 24-2 11주차 1차시
[3] select()
#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()
Step One.
Set file descriptors: 모니터링할 파일 디스크립터를 설정하는 것이다. 예를 들어, 읽기, 쓰기, 예외 처리를 위한 파일 디스크립터 세트를 구성하는 작업이다.
Set check range: 확인할 파일 디스크립터의 범위를 설정하는 것이다.
nfds파라미터에 해당하며, 파일 디스크립터의 최대값에 1을 더한 값을 지정한다.Set timeout:
select()함수가 대기할 시간(타임아웃)을 설정하는 것이다. 타임아웃 값에 따라select()함수는 파일 디스크립터의 상태 변화를 기다리는 시간을 결정한다.
Step Two.
Call select(): 설정한 파일 디스크립터 세트와 타임아웃 값을 기반으로
select()함수를 호출하는 것이다.select()함수는 파일 디스크립터가 읽기, 쓰기, 또는 예외 조건에 대해 준비 상태인지 확인한다.
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) |
|
void FD_CLR(int fd, fd_set *set) |
|
int FD_ISSET(int fd, fd_set *set) |
|
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상태를 확인함으로써 알 수 있습니다.
과정 설명
Before calling select()
fd_set내 모든 파일 디스크립터는 준비 상태가 아닌(0) 상태입니다.예를 들어,
fd0,fd1,fd2,fd3의 값이 모두 0으로 설정됩니다.
When fd1 and fd3 change
select()호출 이후, 파일 디스크립터 중fd1과fd3가 준비 상태로 변경됩니다.
After calling select()
fd_set내에서 준비된 파일 디스크립터는 1로 표시됩니다.위의 예시에서
fd1과fd3의 값이 1로 변경된 것을 통해 준비 상태임을 확인할 수 있습니다.
이와 같은 방식으로 select() 함수는 파일 디스크립터의 상태 변화를 감지하며, 각 파일 디스크립터의 준비 여부를 판단할 수 있습니다.
[7] Detecting changes of file descriptor
: 파일 디스크립터의 변화 감지
소켓이 준비 상태가 되는 조건들
조건 | 읽기 가능? | 쓰기 가능? | 예외? |
|---|---|---|---|
읽을 데이터가 있을 때 | O | ||
새로운 연결이 대기 중인 소켓에 준비되었을 때 | O | ||
읽기 반이 닫혔을 때(FIN 수신됨) | O | ||
쓰기를 위한 공간이 있을 때 | O | ||
쓰기 반이 닫혔을 때 | O | ||
TCP의 대역 외(out-of-band) 데이터가 있을 때 | O |
물론입니다! 아래는 요청하신 내용을 한국어로 번역한 것입니다.
2. 다양한 입출력 함수
[1] recv() 및 send()
#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() 시스템 호출의 작동 방식
readv() 함수의 경우

read function: 입력 버퍼(Input buffer)에서 각각의 Array 1, 2, 3으로 개별적인 read() 호출을 통해 데이터를 읽어온다.
readv function: 입력 버퍼에서 단일 readv() 호출로 여러 Array(1, 2, 3)에 동시에 데이터를 읽어올 수 있다.
writev() 함수의 경우

write function: Array 1, 2, 3에서 출력 버퍼(Output buffer)로 각각 개별적인 write() 호출을 통해 데이터를 쓴다.
writev function: 여러 Array(1, 2, 3)에서 출력 버퍼로 단일 writev() 호출로 동시에 데이터를 쓸 수 있다.
주요 차이점:
일반 read/write는 각각의 배열마다 개별적인 시스템 콜이 필요하다.
readv/writev는 한 번의 시스템 콜로 여러 배열의 입출력을 처리할 수 있어 더 효율적이다.
이는 특히 여러 메모리 버퍼를 다룰 때 시스템 콜의 오버헤드를 줄일 수 있다는 장점이 있다.
[2-2] readv() & writev()
#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 {
codevoid *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()
#include <stdio.h>
FILE *fdopen(int fildes, const char *mode);1) 역할
: 파일 디스크립터와 스트림을 연결한다.
파일 디스크립터와 스트림을 연결한다.
2) 파라미터 설명
filedes: 파일 디스크립터
mode:
r: 파일을 읽기 위해 연다.
w: 파일을 쓰기 위해 연다.
a: 파일의 끝에서 쓰기 위해 연다.
r+: 파일을 읽기와 쓰기 위해 연다.
w+: 파일을 읽기와 쓰기 위해 연다.
a+: 파일의 끝에서 읽기와 쓰기 위해 연다.
3) 반환 값
성공: 스트림에 대한 포인터
오류: 널 포인터
[5-2] fileno()
#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):
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):
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) 문제점 설명:
서버가 writefp를 fclose()로 닫으면 소켓 자체가 완전히 닫힌다.
이로 인해 서버의 fgets() 함수는 실행되지 않는다.
소켓이 완전히 닫히면서 File Descriptor와 모든 FILE 포인터들이 함께 닫힌다.
(2) 영향:
이러한 동작은 half-close를 구현하기 어렵게 만든다.
한쪽 방향의 통신만 닫으려고 해도 양방향 통신이 모두 종료된다.
이는 소켓 프로그래밍에서 주의해야 할 중요한 특성이다.
[7-2] dup() 및 dup2()
#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):
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):
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) 주요 변경사항과 특징:
dup()를 사용하여 파일 디스크립터를 복제한다.shutdown()함수를 사용하여 출력 스트림만 종료한다.소켓은 모든 관련 파일 디스크립터가 닫힐 때까지 완전히 종료되지 않는다.
(2) 장점:
half-close 구현이 가능해진다.
읽기와 쓰기 스트림을 독립적으로 제어할 수 있다.
데이터 송수신의 완전성을 보장할 수 있다.
(3) 동작 원리:
파일 디스크립터를 복제하여 독립적인 read/write 스트림을 생성한다.
shutdown() 함수로 특정 스트림만 종료가 가능하다.
모든 관련 파일 디스크립터가 닫혀야 소켓이 완전히 종료된다.
(4) 실행 결과:
서버와 클라이언트 간의 half-close가 성공적으로 구현되어 데이터 송수신이 정상적으로 이루어진다.
fgets() 함수가 정상적으로 실행되어 클라이언트의 응답을 받을 수 있다.
3. 멀티 스레드 프로그래밍
[1] 스레드
경량 프로세스
스레드는 프로세스보다 시스템 자원을 덜 요구한다.
프로세스 내의 모든 스레드는 스택 공간을 제외하고 동일한 주소 공간을 공유한다.
스레드 간 통신은 프로세스 간 통신보다 더 효율적이고 사용하기 쉽다.
[2] 스레드 vs. 프로세스

프로세스는 서로 별도의 메모리 공간을 유지한다.
스레드는 스택을 제외하고 동일한 메모리 공간을 공유한다.
[3] Pthread
Pthread는 1995년에 IEEE POSIX(휴대용 운영 체제 인터페이스)에 의해 규정된 표준화된 스레드 프로그래밍 인터페이스이다.
Pthreads는 pthread.h 헤더/포함 파일과 스레드 라이브러리를 사용하여 구현된 C 언어 프로그래밍 타입과 프로시저 호출의 집합으로 정의된다.
[4] Pthread API
?스레드 관리?
스레드 생성, 종료, 조인 등 스레드에 직접 작용하는 함수들이다.
?뮤텍스?
뮤텍스 생성, 파괴, 잠금, 잠금 해제를 제공한다.
세마포어
세마포어 생성, 파괴, 대기, 포스트를 제공한다.
조건 변수
특정 변수 값에 따라 생성, 파괴, 대기 및 신호를 주는 함수를 포함한다.
[5] 스레드 생성
#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] 스레드 조인
#include <pthread.h>
// 종료된 스레드와 조인하는 함수
int pthread_join(pthread_t thread, void **retval);1) 역할
: 종료된 스레드와 조인한다.
지정된 스레드가 반환될 때까지 호출 스레드를 차단한다.
부모 스레드가 반환되면 모든 자식 스레드는 종료된다.
2) 파라미터 설명
thread: 종료할 지정된 스레드
retval: 종료된 스레드의 반환 값(참조로 반환되는 포인터)
3) 반환 값
성공: 0
오류: 오류 번호
[7] 스레드 분리 (Thread Detach)
#include <pthread.h>
int pthread_detach(pthread_t thread);1) 역할
: 스레드를 분리한다
thread로 식별된 스레드를 분리 상태로 설정한다.분리된 스레드가 종료되면, 해당 스레드의 자원이 자동으로 시스템에 반환된다. 이는 다른 스레드가 종료된 스레드와 조인(join)할 필요가 없다.
비차단(non-blocking) 함수이다.
2) 파라미터 설명
thread: 종료할 스레드를 지정한다.
3) 반환 값
성공: 0
오류: 오류 번호
[8] 스레드 취소 (Thread Cancel)
#include <pthread.h>
int pthread_cancel(pthread_t thread);1) 역할
: 스레드에 취소 요청을 보낸다
이 함수는 스레드에 취소 요청을 보낸다.
타겟 스레드가 취소 요청에 반응하는지 여부 및 시점은 해당 스레드의 두 가지 속성에 따라 달라진다: 취소 가능 상태와 유형이다.
취소 가능 상태는
pthread_setcancelstate()에 의해 결정된다.취소 유형은
pthread_setcanceltype()에 의해 결정된다.
2) 파라미터 설명
thread: 취소할 스레드의 ID이다.
3) 반환 값
성공: 0
오류: 비영(非零) 오류 번호
Summary
select() 함수
동기 I/O 다중화
여러 파일 디스크립터의 상태를 동시에 모니터링
timeout 설정으로 대기 시간 조절 가능
fd_set으로 파일 디스크립터 관리 (FD_ZERO, FD_SET, FD_CLR, FD_ISSET)
입출력 함수
주요 함수들
recv/send: 소켓 통신용 기본 함수
readv/writev: 여러 버퍼에 대한 입출력을 한 번에 처리
fdopen/fileno: 파일 디스크립터와 스트림 간 변환
Half-close 해결책
dup()로 파일 디스크립터 복제
shutdown()으로 특정 방향만 통신 종료 가능
읽기/쓰기 스트림 독립적 제어 가능
멀티스레드 프로그래밍
스레드 특징
프로세스보다 가벼운 실행 단위
스택 제외한 메모리 공간 공유
스레드 간 통신이 프로세스 간 통신보다 효율적
주요 Pthread 함수
pthread_create: 새 스레드 생성
pthread_join: 스레드 종료 대기
pthread_detach: 스레드 분리
pthread_cancel: 스레드 취소
중요 포인트:
select()는 I/O 다중화의 기본
파일 디스크립터 복제로 half-close 구현
스레드는 자원 공유로 효율적인 병렬 처리 가능
