TCP/IP 통신 함수 사용 순서

TCP/IP 예제 소개

  TCP/IP 예제를 서버와 클라이언트로 나누어서 설명을 드리도록 하겠습니다.

  1. 서버와 클라이언트는 port 4000번을 사용
  2. 클라이언트프로그램에서 서버에 접속하면 실행할 때 입력받은 문자열을 전송
  3. 서버는 클라이언트로부터 자료를 수신하면 문자열 길이와 함께 수신한 문자열을 클라이언트로 전송

서버 프로그램

  서버 프로그램에서 사용해야할 함수와 순서는 아래와 같습니다.

  우선 socket 부터 만들어야 합니다. TCP/IP에서는 SOCK_STREAM을 UDP/IP에서는 SOCK_DGRAM을 사용하는 것을 참고하여 주십시오. socket()에 대한 더 자세한 말씀은 "Unix C Reference의 11장 7절 소켓 열고 닫기"를 참고하십시오.

int     server_socket;

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

  bind() 함수를 이용하여 socket에 server socket 에 필요한 정보를 할당하고 커널에 등록

  1. 만들어진 server_socket 은 단지 socket 디스크립터일 뿐입니다.
  2. 이 socket에 주소를 할당하고 port 번호를 할당해서 커널에 등록해야 합니다.
  3. 커널에 등록해야 다른 시스템과 통신할 수 있는 상태가 됩니다.
  4. 더 정확히 말씀드린다면 커널이 socket 을 이용하여 외부로부터의 자료를 수신할 수 있게 됩니다.
  5. socket에 주소와 port 를 할당하기 위해 sockaddr_in 구조체를 이용합니다.
  6. struct sockaddr_in server_addr;

    memset( &server_addr, 0, sizeof( server_addr);
    server_addr.sin_family      = PF_INET;              // IPv4 인터넷 프로토롤 
    server_addr.sin_port        = htons( 4000);         // 사용할 port 번호는 4000
    server_addr.sin_addr.s_addr = htonl( INADDR_ANY);   // 32bit IPV4 주소

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

  7. htonlINADDR_ANY) 는 주소를 지정해 주는 것으로 inet_addr( "내 시스템의 IP ")로도 지정할 수 있습니다. 그러나 프로그램이 실행되는 시스템 마다 IP 가 다를 것이므로 주소 지정을 고정 IP로 하지 않고 htonlINADDR_ANY) 를 사용하는 것이 편리합니다.

  이제 listen() 함수로 클라이언트 접속 요청을 확인합니다. 

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

  1. listen() 함수를 호출하면 클라이언트의 접속 요청이 올 때 까지 대기 상태가 됩니다. 즉, 블록된 모습이 되죠.
  2. 함수가 리턴이 되었을 때에는 클라이언트의 접속이 요청 되었다든지, 아니면 에러가 발생했을 경우입니다.
  3. 에러 없이 함수가 복귀했다면 클라이언트의 접속 요청입니다.
  4. 접속 요청을 허락합니다. 

  클라이언트 접속 요청에 따라 accept()로 접속을 허락합니다. 

  1. accept()로 접속 요청을 허락하게 되면 클라이언트와 통신을 하기 위해서 커널이 자동으로 소켓을 생성합니다.
  2. 이 소켓을 client socket이라고 하겠습니다.
  3. client socket 정보를 구하기 위해 변수를 선언합니다.  그리고 client 주소 크기를 대입합니다. 
  4. int     client_addr_size;

    client_addr_size = sizeof( client_addr);

  5. accept()를 호출 후에 에러가 없으면 커널이 생성한 client socket 을 반환해 줍니다.
  6. client_socket = accept( server_socket, (struct sockaddr*)&client_addr,
                                                              &client_addr_size);

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

  이제 client socket까지 만들어 졌으므로 read(), write() 함수를 이용하여 자료를 송수신 할 수 있습니다. read() 함수를 이용하여 클라이언트로부터 전송되어 오는 자료를 읽어 들입니다.

read ( client_socketbuff_rcv, BUFF_SIZE);

  1. read() 를 이용하여 클라이언트로부터 전송된 자료를 읽어 들입니다.
  2. 만일 클라이언트로부터 전송된 자료가 없다면 송신할 때 까지 대기하게 됩니다. 즉, 블록된 모습이 됩니다.
  이번에는 wirte() 함수를 이용하여 클라이언트도 데이터를 전송합니다. 

  1. 수신된 데이터의 길이를 구하여 전송 데이터를 준비합니다.
  2. sprintf( buff_snd, "%d : %s", strlen( buff_rcv), buff_rcv);

  3. write() 를 이용하여 클라이언트로 자료를 송신합니다.

    write( client_socketbuff_snd, strlen( buff_snd)+1); // +1: NULL까지 포함해서 전송

  작업이 완료되면 close() 를 이용하여 client socket 을 소멸 시켜 데이터 통신을 종료합니다.

closeclient_socket);

클라이언트 프로그램

  클라이언트 프로그램은 서버에 비해 간단합니다. 바로 설명 들어갑니다.

  socket() 을 이용하여 소켓을 먼저 생성합니다.

int     client_socket;

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

  connect()를 이용하여 서버로 접속을 시도합니다. 

  1. 주소 정보에 서버의 주소와 포트번호를 지정하고
  2. 서버와의 연결을 시도합니다.
  3. 예제에서는 시스템 자기를 가르키는 IP, 127.0.0.1 을 사용했습니다.
  4. struct sockaddr_in    server_addr;

    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);
    }

  1. 접속에 성공하면 데이터를 전송합니다.
  2. writeclient_socket, argv[1], strlen( argv[1])+1); // +1: NULL까지 포함해서 전송

  3. 자료를 수신하고 화면에 출력합니다.
  4. read ( client_socket, buff, BUFF_SIZE);
    printf( "%sn", buff);

  5. socket 을 소멸하여 통신 작업을 완료합니다.
  6. closeclient_socket);

서버 프로그램 소스

#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];



   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);
      }

      read ( client_socket, buff_rcv, BUFF_SIZE);
      printf( "receive: %sn", buff_rcv);
      
      sprintf( buff_snd, "%d : %s", strlen( buff_rcv), buff_rcv);
      write( client_socket, buff_snd, strlen( buff_snd)+1);          // +1: NULL까지 포함해서 전송
      close( client_socket);
   }
}

클라이언트 프로그램 소스

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

#include "sample.h"

#define  BUFF_SIZE   1024

int   main( int argc, char **argv)
{
   int   client_socket;

   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);
   }
   write( client_socket, argv[1], strlen( argv[1])+1);      // +1: NULL까지 포함해서 전송
   read ( client_socket, buff, BUFF_SIZE);
   printf( "%sn", buff);
   close( client_socket);
   
   return 0;
}

'Tips & Tech > Network' 카테고리의 다른 글

UDP/IP 프로그래밍  (0) 2012.02.27
블록되지 않고 TCP 통신하려면...  (0) 2012.02.27

 소켓 통신을 하다 보면 여러 문제점을 접하게 되는데, 그 중에 하나가 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() 함수를 사용하면, 클라이언트 프로그램이 멈추어 버리는 것을 보실 수 있습니다.


자료출처 : http://forum.falinux.com/zbxe/?mid=network_programming&document_srl=520963

'Tips & Tech > Network' 카테고리의 다른 글

UDP/IP 프로그래밍  (0) 2012.02.27
TCP/IP 프로그래밍 방법  (0) 2012.02.27

1. 명명규칙(naming convention)

- 대소문자가 구분되며 길이에 제한이 없다.

- 예약어를 사용해서는 안 된다.

- 숫자로 시작해서는 안 된다.

- 특수문자는 '_'와 '$'만을 허용한다.

- 클래스 이름의 첫 글자는 항상 대문자로 한다.

- 여러 단어로 이루어진 이름은 단어의 첫 글자를 대문자로 한다.

- 상수의 이름은 모두 대문자로 한다. 여러 단어로 이루어진 경우 '_'로 구분한다.


2. 변수의 타입

- 기본형 : 계산을 위한 실제 값을 저장한다.(boolean, char, byte, short, int, long, float, double)

- 참조형 : 객체의 주소를 저장한다. (4Bytes)

- 문자열 : 문자열 + any type -> 문자열 + 문자열 -> 문자열

'Tips & Tech > JAVA' 카테고리의 다른 글

숫자 중간에 ',(콤마)'찍는 방법  (0) 2012.05.03

+ Recent posts