본문 바로가기

OBJECTIVE-C

iPhone Socket

먼저 NSStream 및 bsd socket 프로그래밍과 관련해서 제가 올려놓은 글을 보신 후에 이 글을 보시면 도움이 
되시리라 믿습니다.
CFNetwork 은 BSD Socket 바로 위에 놓여지는 레이어입니다. 그러므로 BSD Socket 를 살짝 추상화한 정도
로 생각하시면 됩니다.



<출처 apple.com>

위 그림에서 CFNetwork 이라는 레이어가 있는데 이 레이어에서 CFSocket 을 사용한다고 보시면 됩니다.
 CFSocket 은 BSD Socket 과 유사합니다. BSD Socket 에서 사용하는 sockaddr_in 을 사용하는 점도 
동일합니다. BSD Socket 보다 편리한 점은 Socket 으로 데이터가 들어올 경우를 별도로 프로그래밍해주지 
않아도 이것을 Callback 으로 처리해 줄 수 있도록 설계되어 있다는 점입니다.

먼저 아래 4개의 헤더를 포함시킵니다.
#import <sys/types.h>
#import <sys/socket.h>
#import <netinet/in.h>
#import <netdb.h>

Socket 의 생성은 다음과 같습니다.
// Network connection
ref = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, 0, 
                     kCFSocketReadCallBack|kCFSocketDataCallBack|kCFSocketConnectCallBack|kCFSocketWriteCallBack,
                     CFSockCallBack, NULL);

첫번째 파라미터는 오브젝트 생성을 위해 메모리할당을 해줄 할당자를 지정하는 곳입니다. 
kCFAllocatorDefault 혹은 0 을 설정하면 디폴트 할당자가 그 일을 맡습니다. PF_INET 은 
인터넷 프로토콜, SOCK_STREAM 은 스트리밍 소켓타입을 의미합니다. 4 번째 항목은 
프로토콜인데 0 을 설정할 경우 3번째 항목이 SOCK_STREAM 이라면 IPROTO_TCP, SOCK_DGRAM 
이라면 IPROTO_UDP 로 자동설정이 됩니다. 물론 IPROTO_TCP 라고 명시적으로 써주셔도 됩니다.
 5번째 항목이 Callback 조건을 지정해주는 것인데 콜백 타입은 아래와 같이 정의되어 있습니다.

enum CFSocketCallBackType {
   kCFSocketNoCallBack = 0,
   kCFSocketReadCallBack = 1,
   kCFSocketAcceptCallBack = 2,
   kCFSocketDataCallBack = 3,
   kCFSocketConnectCallBack = 4,
   kCFSocketWriteCallBack = 8
};

클라이언트 소켓의 경우는 Read, Data, Connect, Write 정도의 콜백을 설정해주면 되겠습니다. 
Accept 는 서버소켓인 경우 설정합니다.
6번째 항목은 Callback 함수를 지정해주는 곳입니다. CFSockCallBack 이라고 이름지어봤습니다. 
마지막 항목은 Socket 컨텍스트 정보를 받기 위한 곳입니다. 안 받으려면 NULL 로 지정합니다.

더 공부하실 분들을 위해, 소켓생성 함수의 원형은 아래와 같습니다.

CFSocketRef CFSocketCreate (
   CFAllocatorRef allocator,
   SInt32 protocolFamily,
   SInt32 socketType,
   SInt32 protocol,
   CFOptionFlags callBackTypes,
   CFSocketCallBack callout,
   const CFSocketContext *context
);


소켓이 생성된 후에는 BSD Socket 과 동일하게 remote address 를 정해주고 연결해야 합니다.

struct sockaddr_in theName;
struct hostent *hp;
    
theName.sin_port = htons(80);
theName.sin_family = AF_INET;
    
hp = gethostbyname("naver.com");
if( hp == NULL ) {
    return;
}
memcpy( &theName.sin_addr.s_addr, hp->h_addr_list[0], hp->h_length );

이렇게 만들어진 sockaddr_in 구조체를 이용해서 접속에 사용될 CFData 레퍼런스를 만듭니다.

CFDataRef addressData = CFDataCreate( NULL, &theName, sizeof( struct sockaddr_in ) );

소켓과 어드레스데이터가 만들어졌으므로 이제 connect 하면 되겠죠?

CFSocketConnectToAddress(ref, addressData, 30);

30은 timeout 이며 초단위지정입니다. 만약 음수값이 주어지면 CFSocketConnectToAddress 는 즉각 
반환되며, 백그라운드에서 접속을 시도합니다. 접속이 이루어지면 Socket 생성때 사용했던 CallBack 
함수가 호출됩니다.


CFRunLoopSourceRef FrameRunLoopSource = CFSocketCreateRunLoopSource(NULL, ref , 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), FrameRunLoopSource, kCFRunLoopCommonModes); 

이 두줄은 소켓에 대한 LoopSource 를 만들고 이 LoopSource 를 실행하는 명령입니다. 이 두줄이 아주 
편리한 부분인데, 이로서 특별히 Callback 에 대한 프로그래밍(Thread 나 기타 프로세스 제어)이 없이도 
Callback 을 사용할 수 있도록 해줍니다.


이제 Callback 부분을 보겠습니다.

void CFSockCallBack (
                     CFSocketRef s,
                     CFSocketCallBackType callbackType,
                     CFDataRef address,
                     const void *data,
                     void *info
) {

    NSLog(@"callback!");
    if(callbackType == kCFSocketDataCallBack) {
        NSLog(@"has data");
        // 데이터 수신시 여기에 프로그래밍하세요
        UInt8 * d = CFDataGetBytePtr((CFDataRef)data);
        int len = CFDataGetLength((CFDataRef)data);
        for(int i=0; i < len; i++) {
            NSLog(@"%c",*(d+i));
        }
    }
    if(callbackType == kCFSocketReadCallBack) {
        NSLog(@"to read");
        // 소켓에서 읽을 수 있습니다.
        char buf[100] = {0};
        int sock = CFSocketGetNative(s);
        NSLog(@"to read");
        NSLog(@"read:%d",recv(sock, &buf, 100, 0));
        NSLog(@"%s",buf);
    }
    if(callbackType == kCFSocketWriteCallBack) {
        NSLog(@"to write");
        // 데이터 송신이 가능해졌습니다.
        char sendbuf[100]={0};
        strcpy(sendbuf,"GET / HTTP/1.0\r\n\r\n");
        CFDataRef dt = CFDataCreate(NULL, sendbuf, 100);
        CFSocketSendData(s, NULL, dt, strlen(sendbuf));
    }
    if(callbackType == kCFSocketConnectCallBack) {
        NSLog(@"connected");
        // 연결이 이루어졌습니다.
    }
}

대략 이런 형태를 가집니다. 물론 각각의 callbackType 에 대해 프로그래밍을 해주어야겠죠.
주의 사항은, CallbackType 에 kCFSocketDataCallBack 을 포함시켰을 경우 kCFSocketReadCallBack 은 
호출되지 않습니다. kCFSocketDataCallBack 을 설정하면 Socket 으로 들어오는 데이터는 자동으로 읽혀지며
 읽혀진 데이터는 CFData 형식으로 data 포인터로 전달되어집니다. kCFSocketDataCallBack 이 
Callback type 에 지정되지 않았을 경우는 kCFSocketReadCallBack 이 수행되어야 하며 이내는 소켓으로부터
 직접 데이터를 읽어주어야 합니다. 읽을 데이터가 남아있을 때까지 콜백은 계속 호출됩니다.
위의 경우는 Socket Creation 시 kCFSocketDataCallBack 을 지정해주었기 때문에 데이터가 자동으로 
읽혀졌습니다. 만약 소켓생성시에 kCFSocketDataCallBack 을 콜백타입으로 지정해주지 않았다면 
kCFSocketReadCallBack 을 처리해주어야 하며 예제에서처럼 recv 함수들을 통해 직접 데이터를 읽어야 합니다.

이상 간략하게 CFNetwok 시설에 대한 견학을 마치도록 하겠습니다. 사실 이 이상은 저도 사용해 본 적이 없는지라 더 쓸 것도 없네요 ^^;