2014년 12월 21일 일요일

TCP/IP 소켓 프로그래밍 13강

13-1 send & recv 입출력 함수


지금까지 윈도우 기반 예제에서는 send&recv함수를 사용했지만 마지막 매개변수에 0 이외의 인자를 전달한 적이 없다.
이번 강에서는 입출력 함수의 마지막 매개변수에 넣는 옵션들에 대해 알아볼 것이다.
양이 많은 관계로 소스는 다운 받아서 해보길 바란다 여기는 주요 함수를 적으려고 한다

리눅스에서의 send & recv


#include <sys/socket.h>
ssize_t send(int sockfd, const void* buf, size_f nbytes, int flags);
-> 성공시 전송된 바이트 수 , 실패시 -1 반환

-sockfd : 데이터 전송 대상과의 연결을 의미하는 소켓의 파일 디스크립터 전달
-buf : 전송할 데이터를 저장하고 있는 버퍼의 주소 값 전달
-nbytes : 전송할 바이트 수 전달
-flags : 데이터 전송시 적용할 다양한 옵션 정보 전달

#include <sys/socket.h>
ssize_t recv(int sockfd, void* buf, size_t nbytes, int flags);
-> 성공시 수신한 바이트 수 (단 EOF 전송시 0), 실패시 -1 반환

-sockfd :  데이터 수신 대상과의 연결을 의미하는 소켓의 파일 디스크립터 전달
-buf : 수신된 데이터를 저장할 버퍼의 주소 값 전달
-nbytes : 수신할 수 있는 최대 바이트 수 전달
-flags : 데이터 수신시 적용할 다양한 옵션 정보 전달

send/recv 함수의 마지막 매개변수에는 옵션 정보가 전달되어야 한다.
옵션 정보는 비트 OR 연산자 ( | 연산자)를 이용해서 둘 이상을 전달 할 수 있다.
매개 변수에 전잘할 수 있는 옵션의 종류를 보자

MSG_OOB : 긴급 데이터(Out-of-band data)의 전송을 위한 옵션(send/recv)
MSG_PEEK : 입력버퍼에 수신된 데이터의 존재 유무 확인을 위한 옵션 (recv)
MSG_DONTROUTE : 로컬 네트워크 상에서 목적지를 찾을 때 사용되는 옵션(send)
MSG_DONTWAIT : 넌-블로킹(Non-blocking) IO 요구에 사용되는 옵션(send/recv)
MSG_WAITALL :  요청한 바이트 수에 해당하는 데이터가 전부 수신 될 때까지, 호출된 함수가 반환되는 것을 막기 위한 옵션(recv)

(1)MSG_OOB : 긴급 메시지의 전송


옵션 MSG_OOB는 'Out-of-band data'라 불리는 긴급 메시지의 전송에 사용된다.
긴급으로 무언가 처리하려면 몇 경로가 달라야 한다.
소스 코드를 보자 (일부만 볼 것 이다)
int main(int argc, char *argv[])
{
int sock;
struct sockaddr_in recv_adr;

if(argc!=3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}

sock=socket(PF_INET, SOCK_STREAM, 0);
  memset(&recv_adr, 0, sizeof(recv_adr));
recv_adr.sin_family=AF_INET;
recv_adr.sin_addr.s_addr=inet_addr(argv[1]);
recv_adr.sin_port=htons(atoi(argv[2]));

if(connect(sock, (struct sockaddr*)&recv_adr, sizeof(recv_adr))==-1)
error_handling("connect() error!");

write(sock, "123", strlen("123"));
//긴급 메세지 전송
send(sock, "4", strlen("4"), MSG_OOB);
write(sock, "567", strlen("567"));
//긴급 메세지 전송
send(sock, "890", strlen("890"), MSG_OOB);
close(sock);
return 0;
}

단지 저리 옵션만 지정해주면 긴급 메세지로 인식하고 전송하게 된다.
하지만 긴급 메시지 수신에는 좀더 복잡하다. 소스를 보자(역시 일부를 본다)

int main(int argc, char *argv[])
{
struct sockaddr_in recv_adr, serv_adr;
int str_len, state;
socklen_t serv_adr_sz;
struct sigaction act;
char buf[BUF_SIZE];

if(argc!=2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}

act.sa_handler=urg_handler;
sigemptyset(&act.sa_mask);
act.sa_flags=0;

acpt_sock=socket(PF_INET, SOCK_STREAM, 0);
memset(&recv_adr, 0, sizeof(recv_adr));
recv_adr.sin_family=AF_INET;
recv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
recv_adr.sin_port=htons(atoi(argv[1]));

if(bind(acpt_sock, (struct sockaddr*)&recv_adr, sizeof(recv_adr))==-1)
error_handling("bind() error");
listen(acpt_sock, 5);

serv_adr_sz=sizeof(serv_adr);
recv_sock=accept(acpt_sock, (struct sockaddr*)&serv_adr, &serv_adr_sz);
//내용이 길어 따로 설명한다.
fcntl(recv_sock, F_SETOWN, getpid()); 
//긴급 메세지를 수신하게 되면 운영제체는 SIGURG 시그널을 발생시켜 프로세스가 등록한 시그널 핸들러가 호출되게 한다.
state=sigaction(SIGURG, &act, 0);

while((str_len=recv(recv_sock, buf, sizeof(buf), 0))!= 0)
{
if(str_len==-1)
continue;
buf[str_len]=0;
puts(buf);
}
close(recv_sock);
close(acpt_sock);
return 0;
}

void urg_handler(int signo)
{
int str_len;
char buf[BUF_SIZE];
str_len=recv(recv_sock, buf, sizeof(buf)-1, MSG_OOB);
buf[str_len]=0;
printf("Urgent message: %s \n", buf);
}

파란 부분을 설명한다.
fcntl 함수는 파일 디스크립터의 컨트롤이 사용된다.

파일 디스크립터 recv_sock 이 가리키는 소켓의 소유자 (F_SETOWN)를 getpid 함수가 반환하는 ID의 프로세스로 변경시킨다.

소켓의 소유자는 운영체제다. SIGURG 시그널을 핸들링할 때에는 반드시 시그널을 처리할 프로세스를 지정해줘야 한다. 그리고 getpid는 이 함수를 호출한 프로세스의 ID를 반환한다. 현재 실행중인 프로세스를 SIGURG 시그널의 처리 주체로 지정하는 것이다.
소스를 받아 실행해보면 긴급 메세지를 추가 한다고 더 빨리 데이터가 전송되는 것도 아니고 데이터도 1바이트밖에 되지 않는다.
긴급하게 보내려면 별도의 통신 경로가 확보되어서 고속으로 데이터가 전송되어야 한다
하지만 TCP는 별도의 통신 경로를 제공하지 않고 있다.
다만 TCP에 존재하는 Urgent mode라는 것을 이용해서 데이터를 전송해줄 뿐이다.

Urgent mode의 동작원리

TCP의 긴급 메시지는 병원으로의 빠른 이동은 보장하지 않는다.
대신 수신자에게 빠른 조치를 요구하는 것이다.

URG=1 : 긴급메시지가 존재하는 패킷이다
URG Pointer : Urgent Pointer의 위치가 오프셋의 몇번째 오프셋에 있는지 표시

(2)MSG_PEEK 입력버퍼 검사하기


MSG_PEEK옵션은 MSG_DONTWAIT 옵션과 함께 설정되어 입력버퍼에 수신 된 데이터가 존재하는지 확인하는 용도로 사용된다.
MSG_PEEK 옵션을 주고 recv함수를 호출하면 입력버퍼에 존재하는 데이터가 읽혀지더라도 입력버퍼에서 데이터가 지워지지 않는다.
때문에 MSG_DONTWAIT 옵션과 묶여서 블로킹 되지 않는 데이터의 존재유무를 확인하기 위한 함수의 호출 구성에 사용된다.

소스를 보자. (일부만 볼 것이다.)
int main(int argc, char *argv[])
{
int acpt_sock, recv_sock;
struct sockaddr_in acpt_adr, recv_adr;
int str_len, state;
socklen_t recv_adr_sz;
char buf[BUF_SIZE];
if(argc!=2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}

acpt_sock=socket(PF_INET, SOCK_STREAM, 0);
memset(&acpt_adr, 0, sizeof(acpt_adr));
acpt_adr.sin_family=AF_INET;
acpt_adr.sin_addr.s_addr=htonl(INADDR_ANY);
acpt_adr.sin_port=htons(atoi(argv[1]));

  if(bind(acpt_sock, (struct sockaddr*)&acpt_adr, sizeof(acpt_adr))==-1)
error_handling("bind() error");
listen(acpt_sock, 5);

recv_adr_sz=sizeof(recv_adr);
recv_sock=accept(acpt_sock, (struct sockaddr*)&recv_adr, &recv_adr_sz);

while(1)
{
//recv함수를 호출하면서 MSG_PEEK을 옵션으로 전달하고 있다.
//MSG_DONTWAIT 옵션을 함께 전달하는 이유는 데이터가 존재하지 않아도 블로킹 상태에 두지 않기 위해서이다.
str_len=recv(recv_sock, buf, sizeof(buf)-1, MSG_PEEK | MSG_DONTWAIT);
if(str_len>0)
break;
}

buf[str_len]=0;
printf("Buffering %d bytes: %s \n", str_len, buf);
 //recv 함수를 한번 더 호출하고 있다. 이번에는 아무런 옵션도 설정하지 않았다.
//때문에 이번에 읽어들이 데이터는 입력버퍼에서 지워진다.
str_len=recv(recv_sock, buf, sizeof(buf)-1, 0);
buf[str_len]=0;
printf("Read again: %s \n", buf);
close(acpt_sock);
close(recv_sock);
return 0;
}

13-2 : readv & writev 입출력 함수 


데이터의 송수신의 효율성을 향상시키는데 도움이 되는 함수들이다.
readv & writev 함수의 기능은 "데이터를 모아서 전송하고, 모아서 수신하는 기능의 함수"다
먼저 writev 함수를 보자.

#include <sys/uio.h>
ssize_t writev(int filedes, const struct iovec* iov, int iovcnt);
-> 성공시 전동된 바이트 수, 실패시 -1 반환

-fileds : 데이터 전송의 목적지를 나타내는 소켓의 파일 티스크립터 전달
-iov : 구조체 iovec배열의 주소 값 전달, 구조체 iovec의 변수에는 전송할 데이터의 위치 및 크기 정보가 담긴다
-iovcnt : 두 번째 인자로 전달된 주소 값이 가리키는 배열의 길이정보 전달.

그리고 위의 함수 두 번째 인자로 전달되는 배열의 구조체 iovec는 다음과 같이 정의되어 있다.

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

구조체 iovec는 전송할 데이터가 저장되어 있는 버퍼의 주소값과 실제 전송할 데이터의 크기 정보를 담기 위해 정의 되었다.
예제를 보자

#include <stdio.h>
#include <sys/uio.h>

int main(int argc, char *argv[])
{
struct iovec vec[2];
char buf1[]="ABCDEFG";
char buf2[]="1234567";
int str_len;

vec[0].iov_base=buf1; //위치는  buf1
vec[0].iov_len=3; //길이는 3
vec[1].iov_base=buf2; //위치는 buf2
vec[1].iov_len=4; //길이는 4
str_len=writev(1, vec, 2); //1은 콘솔로 출력하겠다는 뜻
puts("");
printf("Write bytes: %d \n", str_len);
return 0;
}

이번에는 readv 함수를 보자. writev 함수와 반대로 생각하면 된다.

#include <sys/uio.h>
ssize_t readv(int fileds, const struct iovec* iov, int iovcnt);
-> 성공 시 수신된 바이트 수, 실패시 -1 반환

-fileds : 데이터를 수신할 파일(혹은 소켓)의 파일 디스크립터 인자로 전달
-iov : 데이터를 저장할 위치와 크기 정보를 담고 있는 iovec 구조체 배열의 주소 값 전달
-iovcnt : 두 번째 인자로 전달된 주소 값이 가리키는 배열의 길이정보 전달

예제를 보자.
#include <stdio.h>
#include <sys/uio.h>
#define BUF_SIZE 100

int main(int argc, char *argv[])
{
struct iovec vec[2];
char buf1[BUF_SIZE]={0,};
char buf2[BUF_SIZE]={0,};
int str_len;
//5 바이트만 저장하겠다.
vec[0].iov_base=buf1;
vec[0].iov_len=5;
//나머지는 이곳에 저장하겠다.
vec[1].iov_base=buf2;
vec[1].iov_len=BUF_SIZE;

//0 이기 때문에 콘솔로 수신한다는 뜻
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);
return 0;
}

readv & writev 함수의 적절한 사용

모든 데이터의 전송을 위해서는 여러 번의 write함수 호출이 요구되는데, 
이를 딱 한번의 writev 함수 호출로 대신할 수 있으니 효율적이다.
마찬가지로 입력 버퍼에 수신된 데이터를 여러 저장소에 나눠서 읽어 들이고 싶은 경우에도 여러번 read 함수를 호출하는 것보다 딱 한번 readv함수를 호출하는 것이 효율적이다.

13-3 : 윈도우 기반으로 구현하기



윈도우에는 리눅스에서 보인 형태의 시그널 핸들링이 존재하지 않는다. 그렇게 때문에 다른 방법을 고민해야 한다. 그 방법이 select 함수 사용하는 것이다.
select 함수의 세 가지 관찰 항목은 다음과 같다.
-수신한 데이터를 지니고 있는 소켓이 존재하는가?
-블로킹 되지 않고 데이터의 전송이 가능한 소켓은 무엇인가?
-예외 상황이 발생한 소켓은 무엇인가?

이중 마지막 "예외 상황이 발생한 소켓은 무엇인가?"가 핵심이다.
긴급 메세지를 예외 상황으로 취급하면 MSG_OOB 옵션을 사용 가능하다.
예제를 보자. 다음은 긴급 메세지를 전송하는 코드의 일부다.

int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hSocket;
SOCKADDR_IN sendAdr;
if(argc!=3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error!");

hSocket=socket(PF_INET, SOCK_STREAM, 0);
memset(&sendAdr, 0, sizeof(sendAdr));
sendAdr.sin_family=AF_INET;
sendAdr.sin_addr.s_addr=inet_addr(argv[1]);
sendAdr.sin_port=htons(atoi(argv[2]));


if(connect(hSocket,
               (SOCKADDR*)&sendAdr,sizeof(sendAdr))==SOCKET_ERROR)
          ErrorHandling("connect() error!");

send(hSocket, "123", 3, 0);
//긴급 메세지 설정
send(hSocket, "4", 1, MSG_OOB);
send(hSocket, "567", 3, 0);
//긴급 메세지 설정
send(hSocket, "890", 3, MSG_OOB);
closesocket(hSocket);
WSACleanup();
return 0;
}

이번에는 긴급 메시지를 수신하는 코드다. select 함수를 사용한다.

int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hAcptSock, hRecvSock;
SOCKADDR_IN recvAdr;
SOCKADDR_IN sendAdr;  
int sendAdrSize, strLen;
char buf[BUF_SIZE];
int result;
fd_set read, except, readCopy, exceptCopy;
struct timeval timeout;
if(argc!=2) {
printf("Usage : %s <port>\n", argv[0]);  
exit(1);
}
if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)
ErrorHandling("WSAStartup() error!");
hAcptSock=socket(PF_INET, SOCK_STREAM, 0);
memset(&recvAdr, 0, sizeof(recvAdr));
recvAdr.sin_family=AF_INET;
recvAdr.sin_addr.s_addr=htonl(INADDR_ANY);
recvAdr.sin_port=htons(atoi(argv[1]));

if(bind(hAcptSock, (SOCKADDR*)&recvAdr, sizeof(recvAdr))==SOCKET_ERROR)
ErrorHandling("bind() error");
if(listen(hAcptSock, 5)==SOCKET_ERROR)
ErrorHandling("listen() error");
sendAdrSize=sizeof(sendAdr);
hRecvSock=accept(hAcptSock, (SOCKADDR*)&sendAdr, &sendAdrSize);
FD_ZERO(&read);
FD_ZERO(&except);
FD_SET(hRecvSock, &read);
FD_SET(hRecvSock, &except);

while(1)
{  
readCopy=read;
exceptCopy=except;
timeout.tv_sec=5;
timeout.tv_usec=0; 
//select 함수를 사용해서 긴급 메시지를 선별해서 우선 처리한다.
result=select(0, &readCopy, 0, &exceptCopy, &timeout);

if(result>0)
{
if(FD_ISSET(hRecvSock, &exceptCopy))
{
strLen=recv(hRecvSock, buf, BUF_SIZE-1, MSG_OOB);
buf[strLen]=0;
printf("Urgent message: %s \n", buf);
}

if(FD_ISSET(hRecvSock, &readCopy))
{
strLen=recv(hRecvSock, buf, BUF_SIZE-1, 0);
if(strLen==0)
{
break;
closesocket(hRecvSock);
}
else 
{   
buf[strLen]=0;
puts(buf); 
}
}
}
}
closesocket(hAcptSock);
WSACleanup();
return 0; 
}

최대한 줄여서 쓰다보니 영 설명이 부실하다 ㅡㅡ;;;
역시 소스코드를 다운 받아 직접 실행해가며 해보면 어렵지는 않을 것이다.

다음 강은 멀티캐스트 & 브로드캐스트에 관해서 알아볼 것이다.

댓글 없음:

댓글 쓰기