TCP/IP 전송 버퍼 빈 용량 확인하는 방법

2021. 7. 23. 14:32 컴퓨터/프로그래밍

TCP/IP 전송 버퍼 남은 용량 확인 이유

소켓통신 중에 TCP/IP 통신은 반드시 전달해야 하는 데이터를 보내거나 대용량의 데이터를 순서에 맞추어 전송할 때 매우 유용합니다. UDP/IP를 오토바이 택배라고 한다면 TCP/IP는 컨베이어 벨트로 물건을 하나씩 전달하는 것과 비교할 수 있습니다. 오토바이 택배는 배달 사고가 날 수 있지만, 컨베이어 벨트는 좀 더 안전하게 전달할 수 있습니다. 오토바이 택배는 데이터가 많은 경우 여러 명으로 나누어서 보내다 보면 도착하는 순서가 바뀔 수 있지만, 컨베이어 벨트는 물건을 올려놓는 순서대로 상대방이 받습니다.

이렇게 TCP/IP는 전송할 데이터를 write()하면 알아서 수신자가 받아서 편한데요, 이렇게 편한 이유는 중간에 TCP 프로토콜이 열심히 일하기 때문입니다. 혹시 누락된 것이 있으면 다시 요청하고, 상대방이 굼떠서 물건을 받지 못하면 컨베이어 벨트 스위치를 끕니다. 그리고 물건을 꺼내면 다시 컨베이어 벨트를 가동시켜 주지요.

문제는 컨베이어 벨트가 멈추었을 때입니다. 물건을 컨베이어 벨트에 올려놓으려는데 작동이 멈추어서 올려놓을 곳이 없다면 컨베이어 벨트가 다시 돌 때까지 기다려야 하는데, 그게 언제인지 모른다는 것이죠.

이런 경우 TCP/IP 통신에서는 프로그램이 블록 됩니다. write() 함수 호출과 동시에 프로그램이 멈추어 버리는 것이죠. TCP/IP 소켓으로 데이터를 write()하면 전송 버퍼에 쓰기를 하고 복귀해야 하는데, 전송 버퍼의 남은 용량이 부족하면 공간이 생길 때까지 대기 상태가 됩니다.

프로그램이 멈추는 문제는 심각합니다. 그래서 TCP/IP 소켓을 생성할 때 time-out을 지정할 수 있습니다만, 개인적으로 권하고 싶지 않습니다. 개발자마다 생각이 다르겠습니다만, 왠지 TCP/IP의 특성을 헤친다고 생각되어서요. 괜한 대기 시간을 소비하기도 하고요. 이외에도 다른 이유가 있습니다만, time-out보다는 전송 버퍼의 남은 용량을 확인해서 부족하면 여유가 생길 때까지 다른 일을 처리하는 것이 더 좋을 것 같습니다.

TCP/IP 전송 버퍼 정보 구하기

리눅스 시스템에서 전송 버퍼의 남은 용량을 확인하는 방법입니다. /proc/sys/net/core/wmem_max 파일에서 전송 버퍼의 크기를 알수 있습니다. 그리고 ioctl( fd, SIOCOUTQ, &sz_rest)로 전송 버퍼에 남은 데이터 갯수를 구할 수 있습니다. 그러므로 전송 버퍼의 크기에서 잔여 데이터 길이를 빼면 전송 버퍼의 남은 용량을 계산할 수 있습니다.

전송 버퍼의 크기를 반환하는 get_tx_buff_size() 함수입니다.

// tcp/ip의 전송 버퍼를 구합니다.
int get_tx_buff_size( void){
  char   buff[1024];
  int    sz_buff  = 0;
  int    fd;

  if ( 0 < ( fd = open( "/proc/sys/net/core/wmem_max", O_RDONLY))){
    if ( 0 < read( fd, buff, 1024)){
      sz_buff = atoi( buff);
    }
    close( fd);
  }
  return sz_buff;
}

전송 버퍼의 빈 용량을 반환하는 get_tx_free_size()입니다.

// fd의 전송 버퍼에서 빈 용량을 구합니다.
int get_tx_free_size( int fd, int sz_free){

  int sz_rest;
  ioctl( fd, SIOCOUTQ, &sz_rest);

  return sz_free - sz_rest;
}
 

전송 버퍼 남은 용량을 확인하며 통신하는 TCP/IP 예제

예제 코드는 server와 client로 나뉘어 있습니다. 예제 파일은 아래 링크로 구할 수 있습니다.

tcpip_sample.zip
0.00MB

server 소스인 main_tcp_server.c에서 클라이언트로부터 보내 오는 데이터를 계속 read() 하면 client는 멈춤이 발생되지 않습니다.

  while ( 1){
    printf( "receive: %d\n", read ( client_socket, buff, sizeof( buff)));
  }
  close( client_socket);
}

그러나 read() 함수 행을 주석 처리하면 client로부터 오는 데이터를 수신할 수 없게 되고 client 쪽은 전송 버퍼가 계속 쌓여서 공간 부족 문제가 발생될 것입니다. client에서 아무런 확인 없이 write()를 계속 실행한다면 결국 프로그램이 블록되어 버리지만, 예제에서 처럼 빈 용량을 확인한다면 블록되는 문제를 피할 수 있습니다.

]$ ./app-tcp-client
send 0
send 1
전송 버퍼 남은 용량이 부족합니다.
전송 버퍼 남은 용량이 부족합니다.
전송 버퍼 남은 용량이 부족합니다.
전송 버퍼 남은 용량이 부족합니다.
     :

아래는 서버 예제 main_tcp_server.c입니다. 클라이언트에서 요청이 오면 접속하고 클라이언트로부터 데이터를 수신하면 데이터 길이를 출력합니다.

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

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[1024*100];

   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);
   }
   if( -1 == listen(server_socket, 5)){
      printf( "listen() 실행 실패\n");
      exit( 1);
   }
   while( 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){
        // 아래 행을 주석 처리 여부에 따라 테스트
        printf( "receive: %ld\n", read ( client_socket, buff, sizeof( buff)));
      }
      close( client_socket);
   }
}

그리고 클라이언트 예제 main_tcp_client.c입니다. 서버에 접속 요청하고 테스트를 위해 100k 크기의 데이터를 계속 전송합니다.

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

// tcp/ip의 전송 버퍼를 구합니다.
int get_tx_buff_size( void){

    char   buff[1024];
    int    sz_buff  = 0;
    int    fd;

    if ( 0 < ( fd = open( "/proc/sys/net/core/wmem_max", O_RDONLY))){
       if ( 0 < read( fd, buff, 1024)){
           sz_buff = atoi( buff);
       }
       close( fd);
    }
    return sz_buff;
}

// fd의 전송 버퍼에서 빈 용량을 구합니다.
int get_tx_free_size( int fd, int sz_free){

    int sz_rest;
    ioctl( fd, SIOCOUTQ, &sz_rest);

    return sz_free - sz_rest;
}

int   main( int argc, char **argv)
{
   int   client_socket;
   struct sockaddr_in   server_addr;
   char   buff[1024*100];
   int  sz_tx_buff  = 0;
   int    count = 0;

   sz_tx_buff  = get_tx_buff_size();

   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);
   }
   while( 1){
      int sz_free = get_tx_free_size( client_socket, sz_tx_buff);

      if ( sz_free < sizeof( buff)){
        printf( "전송 버퍼 남은 용량이 부족합니다.\n");
        sleep(1);
      } else {
         write( client_socket, buff, sizeof( buff));
         printf( "send %d\n", count++);
       }
   }
   close( client_socket);
   
   return 0;
}

Makefile입니다.

## 사용자 소스
TARGET_SERVER    = app-tcp-server
SRCS_SERVER      = main_tcp_server.c

TARGET_CLIENT    = app-tcp-client
SRCS_CLIENT      = main_tcp_client.c

OBJS_SERVER      = $(SRCS_SERVER:.c=.o)
OBJS_CLIENT      = $(SRCS_CLIENT:.c=.o)

CFLAGS           = $(INCLUDEDIRS) -o -W -Wall -O2
LDFLAGS          = $(LIVDIRS)
LIBS             = -lm

#---------------------------------------------------------------------
CC               = gcc
CXX              = g++
AR               = ar rc
AR2              = ranlib
RANLIB           = ranlib
LD               = ld
NM               = nm
STRIP            = strip
#--------------------------------------------------------------------
all : $(TARGET_SERVER) $(TARGET_CLIENT)

$(TARGET_SERVER) : $(OBJS_SERVER)
    $(CC) $(LDFLAGS) $(OBJS_SERVER) -o $@ $(LIBS) 
    $(NM) $(TARGET_SERVER) > $(TARGET_SERVER).map

$(TARGET_CLIENT) : $(OBJS_CLIENT)
    $(CC) $(LDFLAGS) $(OBJS_CLIENT) -o $@ $(LIBS) 
    $(NM) $(TARGET_CLIENT) > $(TARGET_CLIENT).map

%.o:%.c
    @echo "Compiling $< ..."
    $(CC) -c $(CFLAGS) -o $@ $<

%.o:%.cc
    @echo "C++ compiling $< ..."
    $(CXX) -c $(CFLAGS) $(CXXFLAGS) -o $@ $<

dep :
    $(CC)    -M    $(INCLUDEDIRS)    $(SRCS) > .depend

clean:
    rm -f *.bak
    rm -f *.map
    rm -f *.o
    rm -f $(OBJS)
    rm -f $(TARGET_SERVER) core
    rm -f $(TARGET_CLIENT) core

distclean: clean
    rm -rf .depend    

ifeq (.depend,$(wildcard .depend))
include .depend
endif

main_tcp_server.c에서 45 행의 read() 호출 행을 주석 여부에 따라 비교해 보세요. 주석이 없다면 전송하면서도 빈 공간을 확인하여 대기했다가 다시 전송하는 모습을 볼 수 있습니다. 만일 주석을 넣어 read() 함수를 실행하지 않으면 위에서 처럼 남은 용량이 부족하다는 메시만 출력됩니다.

send 1685
send 1686
send 1687
send 1688
전송 버퍼 남은 용량이 부족합니다.
send 1689
send 1690
send 1691
send 1692
send 1693
send 1694
전송 버퍼 남은 용량이 부족합니다.
send 1695
send 1696
send 1697
send 1698
send 1699
전송 버퍼 남은 용량이 부족합니다.
send 1700
send 1701
send 1702
send 1703
send 1704
send 1705
전송 버퍼 남은 용량이 부족합니다.
send 1706
send 1707
이 댓글을 비밀 댓글로

티스토리 로그인이 풀리면 여기를 클릭하세요.

error: Content is protected !!