소켓 통신을 하다 보면 여러 문제점을 접하게 되는데, 그 중에 하나가 TCP/IP 통신에서 상대방이 제대로 수신하지 못 하면 데이터 전송 중에 블록이 된다는 점입니다. 정확히는 데이터 전송 함수에서 블록됩니다. 저는 소켓 통신뿐만 아니라 시리얼 통신, 미디어, 화면 출력까지 모두 write()함수를 사용하는데, 전송 버퍼에 여유가 없다면, 상대방이 데이터를 가져가서, 그래서 버퍼에 여유가 생기면, 그때서야 복귀됩니다.

write() 함수에서 블록

시리얼 통신과 비교해서 글을 적어 보겠습니다. 시리얼 통신은 상대방이 자료를 수신하든 못하든 계속 블록 없이 write() 함수를 사용할 수 있습니다. 그러나 TCP/IP 통신에서는 상대방의 수신 여부에 영향을 받습니다.

 

이런 이유는 TCP/IP 통신에서 자료를 전송할 때, 시리얼처럼 무책임하게 전송하는 것이 아니라 상대방의 상태를 확인하면서 전송하기 때문에 발생합니다. TCP/IP 프로토콜에는 자료 전송뿐만 아니라 상대방의 상태를 확인하는 부분이 들어 있어서, 상대가 자료를 수신할 수 있는지, 즉, 상대쪽의 수신 버퍼가 자료를 수신할 수 있을 만한 여유가 있다면, 그때 전송합니다. 그렇지 않으면 전송하지 않게 되는데, 버퍼가 가득차게 되면, wirte() 함수는 버퍼가 빌 때까지 대기하게 됩니다.

 

제 욕심 같아서는 블록되지 말고 에러 코드를 반환하고 그냥 복귀되었으면 좋겠는데, 이게 아니라는 것이죠. 프로그램을 작성하다 보면 제일 답답할 때가, 대책없이 블록될 때 입니다.

fdopen() 함수로 해결?

그렇다면 open() 말고 표준 출력함수를 사용하면 어떨까요? fdopen() 함수를 사용하면 소켓 디스크립터에서 FILE * 포인터를 구할 수 있습니다. 그래서 fputs() 와 같은 표준 출력 함수를 사용할 수 있게 되는데, 이 함수를 사용하게 되면, 블록없이 에러값을 반환하고 복귀되지 않을까요?

 

결론은? 표준 출력함수도 블록되기는 마찬가지 입니다.

 

그렇다면 fseek()나 ftell() 함수를 이용하여 미리 출력 버퍼의 남은 용량을 구할 수 있지 않을까요? 해 보시면 아시겠습니다만, 구할 수 없었습니다. 남은 버퍼량이 전송량보다 적으면 write() 함수 호출을 피하면 되는데, 이게 안 되네요. 흠~

poll()의 POLLOUT ?

poll() 함수를 이용하면 어떨까요? 출력이 가능할 때만 데이터를 전송하는 거죠. 그러나 이것도 해결 방법이 못됩니다. 왜냐하면 poll()의 POLLOUT은 버퍼에 1 바이트만 비어도 발생하거든요. 문제는 제가 전송하려는 데이터 크기가 출력 버퍼의 빈 공간보다 1 바이트라도 많으면 블록되고 맙니다. 그렇다고 출력 버퍼의 빈 용량을 구할 방법은 없고, 그래서 결국 이 방법으로도 해결할 수 없었습니다.

send() 함수로 간단히 해결

해결 방법은 의외로 간단합니다. send() 함수를 사용하면 됩니다. send() 함수에는 MSG_DONTWAIT라는 옵션을 사용할 수 있기 때문이죠. send() 함수의 자세한 설명은 "C 라이브러리 함수" 게시판에 자세히 올려져 있습니다.

형태 int send(int s, const void *msg, size_t len, int flags);
인수
int s : 소켓 디스크립터
void *msg : 전송할 데이터
size_t len : 데이터의 바이트 단위 길이
int flags : 아래와 같은 옵션을 사용할 수 있습니다.
flags 옵션 설명
MSG_DONTWAIT 전송할 준비가 전에 대기 상태가 필요하다면 기다리지 않고 -1을 반환하면서 복귀
MSG_NOSIGNAL 상대방과 연결이 끊겼을 때, SIGPIPE 시그널을 받지 않도록 합니다.
반환
-1 이외 : 실제 전송한 바이트 수
-1 : 실패

 

MSG_DONTWAIT를 사용해서 send() 함수를 호출하고, 반환된 값으로 전송 여부를 확인하는 것이죠. UDP/IP 통신뿐만 아니라 TCP/IP에서도 write() 함수 보다는 send() 함수를 애용해야 겠습니다. 물론 저 개인적인 생각입니다.

예제 프로그램

예제가 필요 없는 글이겠습니다만, 제가 테스트하면서 사용한 코드를 올립니다. 예제는 서버와 클라이언트로 나누었으며, 서버는 소켓을 열어 놓아도 키를 눌러 주어야 데이터를 읽도록 했습니다. 키를 누르기 전까지는 소켓 데이터를 읽지 않기 때문에, 누군가 서버에 접속할 수는 있어도 계속 송신만 한다면 에러가 발생할 것입니다.

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

#define  BUFF_SIZE   1024

int   main( void)
{
    int   server_socket;
    int   client_socket;
    int   client_addr_size;

    struct sockaddr_in   server_addr;
    struct sockaddr_in   client_addr;

    char   buff_rcv[BUFF_SIZE+5];
    char   buff_snd[BUFF_SIZE+5];
    int    ndx;



    server_socket  = socket( PF_INET, SOCK_STREAM, 0);
    if( -1 == server_socket)
    {
        printf( "server socket 생성 실패n");
        exit( 1);
    }

    memset( &server_addr, 0, sizeof( server_addr));
    server_addr.sin_family     = AF_INET;
    server_addr.sin_port       = htons( 4000);
    server_addr.sin_addr.s_addr= htonl( INADDR_ANY);

    if( -1 == bind( server_socket, (struct sockaddr*)&server_addr, sizeof( server_addr) ) )
    {
        printf( "bind() 실행 에러n");
        exit( 1);
    }

    while( 1)
    {
        if  ( -1 == listen(server_socket, 5))
        {
            printf( "대기상태 모드 설정 실패n");
            exit( 1);
        }

        client_addr_size  = sizeof( client_addr);
        client_socket     = accept( server_socket, (struct sockaddr*)&client_addr, &client_addr_size);

        if  ( -1 == client_socket)
        {
            printf( "클라이언트 연결 수락 실패n");
            exit( 1);
        }

        while ( 1)
        {
            getchar();
            for ( ndx = 0; ndx < 1000; ndx++)
                read ( client_socket, buff_rcv, BUFF_SIZE);
            printf( "receive: %sn", buff_rcv);
        }

    }
    close( client_socket);
}

이번에는 클라이언트 쪽 프로그램입니다. 서버에 접속하고 데이터를 전송합니다. 클라이언트 예제에서는 send() 함수를 사용하고 있습니다. 자료를 계속 전송하다가 전송할 수 없는 상태가 되더라도 블록되지 않고 복귀됩니다.

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

#define  BUFF_SIZE   1024

int   main( int argc, char **argv)
{
    char   *str_test = "FF는 FALiNUX의 포럼입니다. ";
    int     client_socket;
    int     ndx;
    int     n_size;
    struct  sockaddr_in   server_addr;
    char    buff[BUFF_SIZE+5];

    client_socket  = socket( PF_INET, SOCK_STREAM, 0);
    if( -1 == client_socket)
    {
        printf( "socket 생성 실패n");
        exit( 1);
    }

    memset( &server_addr, 0, sizeof( server_addr));
    server_addr.sin_family     = AF_INET;
    server_addr.sin_port       = htons( 4000);
    server_addr.sin_addr.s_addr= inet_addr( "127.0.0.1");

    if( -1 == connect( client_socket, (struct sockaddr*)&server_addr, sizeof( server_addr) ) )
    {
        printf( "접속 실패n");
        exit( 1);
    }
    ndx  = 0;
    while ( 1)
    {
        n_size  = send( client_socket, str_test, strlen( str_test), MSG_DONTWAIT);
        if ( 0 < n_size )
            printf( "%d -- sending-size:%d\n", ndx++, n_size);
        else
        {
            printf( "전송을 못했네요. 1초간 대기\n");
            sleep( 1);
         }

    }

    close( client_socket);

    return 0;
}

실행하는 방법은 서버를 실행하고 클라이언트 프로그램을 실행합니다. 서로 다른 터미널에서 실행하면 편리합니다. 서버를 실행한 상태에서 클라이언트를 실행하면, 무조건 클라이언트에서 서버로 자료를 전송하는데, 얼마 안 있어 전송 에러가 발생합니다. 이때 서버에서 키를 눌러 자료를 읽어 들이면, 클라이언트 쪽에서 다시 자료를 전송하는 것을 보실 수 있습니다.

 

그러나 send() 함수를 사용하지 않고 write() 함수를 사용하면, 클라이언트 프로그램이 멈추어 버리는 것을 보실 수 있습니다.