수업/Network Programming

[Network Programming] 다양한 IO Functions

hw-ani 2023. 5. 29. 04:30

send & recv

#include <sys/socket.h>

ssize_t send(int sockfd, const viod * buf, size_t nbytes, int flags);
ssize_t recv(int sockfd, viod * buf, size_t nbytes, int flags);

read/write와 달리 send와 recv는 소켓 전용 입출력 함수이다.(TCP 전용! UDP에서 쓰려면 주소 정보관련 인자도 있어야 함, recvfrom()/sendto() 처럼...)

read/write랑 마지막 인자로 int flags가 추가된 것 말곤 다를게 없다.

따라서 flags 인자에 사용할 수 있는 옵션과 그 의미에 대해 알아보자.

 

 

 

bitwise-or(|) 연산자로 여러 옵션을 선택할 수도 있다. OS에 따라 옵션 종류와 지원 여부는 다를 수 있다.

 

 

 

> MSG_OOB

긴급 데이터를 전송할 때 사용한다.

MSG_OOB가 설정된 메시지를 받으면 SIGURG 시그널이 발생한다. 따라서 이를 처리하기 위해 signal handler를 등록해둬야하고, fcntl 함수를 통해 해당 소켓의 소유자를 현재 실행중인 프로세스로 변경해야 한다.(바로 아래에서 간단하게 설명함)

근데 MSG_OOB를 쓴다고 해서 더 빨리 전송되는건 아니다. 소켓은 그냥 하던대로 전송할 뿐이고, 이를 받은 쪽에서 긴급 상황의 발생을 알려서 우리가 응급 조치를 하도록 도울 뿐이다. 실제로 write랑 send(~,~,MSG_OOB)랑 번갈아 호출해보면 받는 쪽에서도 번갈아가며 받는다, 즉 MSG_OOB라고 더 빨리 보내는건 없다.

또 MSG_OOB 옵션을 사용하면 데이터 양이 얼마든 간에 1 byte만 반환된다.

데이터도 끝에 1 byte만 가져올 수 있고, 다른 애들 제끼고 빨리 전송되는 것도 아니고.. SIGURG 발생시키는 것 말곤 딱히 뭐가 없어서 쓸 경우가 많을진 모르겠음.

 

> fcntl 함수 간단하게 설명
MSG_OOB를 받은 소켓은 SIGURG 시그널을 발생시킨다고 했다. 이는 좀 특수한 경우이다. 왜냐하면 하나의 소켓에 대한 file descriptor를 여러 process가 가지고 있을 수 있기 때문이다.(ex. fork하면 부모자식 같은 FD들 가짐)
그런데 만약 이렇게 여러 process가 가지는 같은 socket으로 MSG_OOB가 날아온다면 어떻게 해야할까? 모든 process가 SIGURG handler를 호출해선 안된다, 그럼 난장판이 될 것이다.
따라서 SIGURG를 핸들링 할 때는 fcntl 함수를 이용해 그 시그널을 처리할 process를 딱 지정해줘야 한다.
그런 이유로 예제 코드를 보면 fcntl 함수를 사용하고 있다.

(다른 process면 안쓰는 file descriptor 닫아버리고 하기도하고, 굳이 같은 socket을 공유하는 경우는 잘 없긴 하지 않나 싶다. 공유 자원이 돼버리니까 semaphore 같은 걸로 보호도 해줘야될거고.. 실제로 같은 socket을 공유하는 경우가 얼마나 있겠냐만은 이론적으론 그렇단 거다.)

 

 

 

> MSG_PEEK | MSG_DONTWAIT

while(1) {
    str_len = recv(recv_sock, buf, sizeof(buf)-1, MSG_PEEK | MSG_DONTWAIT);
    if (str_len>0) break;
}

이렇게 MSG_PEEK와 MSG_DONTWAIT 옵션을 동시에 설정하면, 블로킹 되지 않고 데이터 존재 유무를 확인하는 것이 된다. 즉, 위 코드는 recv_sock으로 데이터가 들어오면 반복문을 빠져나오는 코드이다.

원래 데이터를 읽으면 그 데이터는 버퍼에서 없어지지만 MSG_PEEK 옵션이 설정되면 데이터를 읽어도 소멸되지 않는다.

즉, 위 코드에선 데이터가 있든 없든 계속 데이터를 읽다가, 데이터가 있다면(str_len>0) 반복문을 빠져나오게 된다.

 

Q. 굳이 반복문 돌리지 말고 if문에 MSG_PEEK 만 걸어두면 되지 않나?
A. if (recv(recv_sock, buf, sizeof(buf), MSG_PEEK) > 0) break; 아마 이렇게 짜도 위 코드와 동작은 같을 것이다. 그럼에도 while문과 non-bloking 방식을 선호하는 이유는 유연성 때문이다.
예를들어 여러 socket으로부터 데이터가 오는지 검사한다고 해보자. 이때 if 문에 MSG_PEEK만 걸어둬서 blocking이 발생한다면 동시에 여러 곳을 확인하긴 힘들 것이다. 하지만 non-blocking으로 while 문을 계속 돌린다면 잘 처리할 수 있다.
이런 경우에서 볼 수 있듯이 non-blocking을 이용한 방식이 동적이고 유연하므로 다양한 상황에 대응하기 좋다.

 

 

 

 


writev

#include <sys/uio.h>

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

writev는 여러 버퍼에 나뉘어서 저장된 데이터를 모아서 한번에 전송할 수 있다.

두번째 인자로 struct iovec type의 일차원 배열을 전달한다. 세번째 인자로는 두번째 인자인 일차원 배열의 길이 정보를 전달한다.

성공 시 전송한 byte 수, 실패 시 -1 을 반환한다.

 

struct iovec {
    void * iov_base;  //버퍼의 주소 정보
    size_t iov_len;   //버퍼의 크기 정보
};

 

struct iovec은 위처럼 생겼다. 보낼 데이터를 만들 때는, 우선 struct iovec ary[5]; 식으로 일차원 배열을 만든 후 배열의 각 원소에 버퍼 주소와 크기를 넣어준다. 그리고 그냥 writev에 넣어주면 된다.

 

 

int main(int argc, char *argv[]) {
    struct iovec vec[2];     //writev에 넣을 struct iovec 1차원 배열
    char buf1[] = "ABCDEFG";
    char buf2[] = "1234567";
    
    //struct iovec 1차원 배열에 버퍼 정보 넣기
    vec[0].iov_base = buf1;  vec[0].iov_len = 3;
    vec[1].iov_base = buf2;  vec[1].iov_len = 4;
    
    int str_len = writev(1, vec, 2);
    puts("");
    printf("Write bytes: %d\n", str_len);
    return 0;
}

//출력 결과
//ABC1234
//Write bytes: 7

 

 

vec[0]의 크기를 3으로 잡고 vec[1]의 크기를 4로 잡아서, 각각 ABC와 1234 까지만 들어가게 된다.

그리고 출력 결과에서 알 수 있듯이 서로 다른 버퍼에 저장된 데이터들이 순서대로 쭉 모아져서 stdout으로 전송된다.

(%s를 쓰는 것도 아니므로 문자열 끝이 null character이고 하는건 딱히 확인할 필요가 없음. 그냥 처음 버퍼에서 ABC 3개, 두번째 버퍼에서 1234 4개가 쭉 전송되는 것)

 

 

 

 

 

readv

#include <sys/uio.h>

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

readv는 writev와 반대로 수신된 하나의 데이터를 여러 버퍼에 나눠서 저장할 수 있다. writev와 약속된 포맷을 사용해 짝을 이뤄 사용하기 좋다.

writev와 다른 점은 두번째 인자에 수신한 데이터를 담을 버퍼 정보(위치/크기)를 일차원 배열로 전달한다는 것이다.

성공 시 수신한 byte 수, 실패 시 -1 을 반환한다.

 

 

struct iovec vec[2];
char buf1[BUF_SIZE] = {0,};
char buf2[BUF_SIZE] = {0,};

vec[0].iov_base = buf1;  vec[0].iov_len = 5;
vec[1].iov_base = buf2;  vec[1].iov_len = BUF_SIZE;

int str_len = readv(0, vec, 2);
printf("Read bytes: %d\n", str_len);
printf("First message: %s\n", buf1);
printf("Second message: %s\n", buf2);

//출력 결과 : console에 "I like TCP/IP socket programming~" 입력
//Read bytes: 34
//First message: I lik
//Second message: e TCP/IP socket programming~

아마 엔터까지 같이 카운트해서 34bytes가 수신되는 것 같다.

출력 결과에서 알 수 있듯이 한 데이터가 vec에 기술한 버퍼 정보대로 나뉘어서 저장됐다.

(여기선 %s를 사용하긴 하는데, 처음에 각 버퍼를 0으로 초기화해뒀으므로 마지막 null character는 신경 안써도 된다. BUF_SIZE가 충분한 듯!)

 

 

 

 

 

> readv & writev 함수의 적절한 사용

writev 함수를 활용하면 단순하게 일단 함수 호출 횟수를 줄일 수 있다.

Negal 알고리즘이 중지된 상황에 특히 writev는 활용 가치가 높다. Negal 알고리즘이 중지됐을 때 3개 버퍼에 담긴 데이터를 전송한다고 해보자. write를 3번 호출하면 그냥 3개의 패킷이 만들어져서 각각 전송될 확률이 높다. 하지만 writev를 사용하면 이들을 출력 버퍼에 한번에 밀어 넣기 때문에 하나의 패킷으로 구성되어 전송될 확률이 높아진다.(따라서 트래픽 좀 낮아질듯)

큰 배열을 만들어서 3개 버퍼의 데이터를 다 옮긴 후 write를 해도 되지만, 이는 번거롭다. 심지어 상대편에서 이를 받을 때도 처음 보낼 때 버퍼에 담겼던 대로 손수 각 데이터를 잘라줘야 할 수도 있다.

그러느니 writev를 쓰면 간편하게 데이터를 묶어서 한번에 상대방에게 보낼 수 있고, 상대방은 이를 받아서 readv를 이용해 간편하게 처음 보낼 때 버퍼에 담겼던 대로 다시 나눌 수 있다.

보통 버퍼 포맷을 약속하고 writev-readv는 짝을 맞춰서 쓴다.

 

 

 

 

 


recvmsg & sendmsg

#include <sys/socket.h>

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr *msg, int flags);

//성공 시 read or written한 bytes 수 반환
//실패 시 -1 반환

두번째 인자로 주는 구조체가 좀 복잡한 편이다.

 

struct msghdr {
    void           *msg_name;       //destination socekt address(UDP)
    socklen_t       msg_namelen;
    struct iovec   *msg_iov;        //scatter, gather array
    int             msg_iovlen;
    void           *msg_control;    //control information
    socklen_t       msg_controllen;
    int             msg_flags;      //flags returned by recvmsg()
};

진짜 세세하게 컨트롤 할 때 사용한다...