UDPでiPhoneとMacでソケット通信【C言語】

 

UDPでiOSとMacでデータ送受信【C言語】
UDPでiOSとMacでデータ送受信【C言語】

この記事では、C言語を使ってUDPでiPhoneとMacでソケット通信を行なっていきます。

UDPとは、User Datagram Protocolの略です。Datagram(データグラム)とは、「配送が成功したかどうかの確認はしないので、届くかどうかは保証されないですよ」という意味です。その分、高速で少ないパケット量での通信が可能です。

確実なデータの送受信が必要な場合は、TCPを使うようにしましょう。TCP通信については、iPhoneからC言語を使ってTCP通信してみようで書きました。

はじめに

PHPからプログラミングを入門した私としては、HTTP以下の低位レイヤーがとても苦手でした。今までなんとなく誤魔化してきましたが、Arduinoで遊びはじめたこときっかけに、低位レイヤーについて詳しく知りたくなりました。そこでこの記事では、iOS端末からMacのパソコンへUDPソケット通信を行なってみました。低位レイヤーの学習が目的ですので、NSStream などは使わず、あえて難しいC言語に挑戦します。一緒に勉強していきましょう。

ところで、Arduinoやラズパイなどのマイコンボードでの無線通信方法は、UDPやTCPはもちろん、WebSocketやHTTP、BLE、ESP-NOWなどの通信方法があります。興味のある方は、これらの記事をご参考になさってみてください。

iOSからMacへUDPソケット送信

iOSからMacへUDPソケット送信
iOSからMacへUDPソケット送信

まずは、iOSからMacへUDPソケットを送信してみましょう。

UDPサーバーの立ち上げ(Mac)

Macのターミナルを開き、netcatコマンドを使ってUDPサーバーを立ち上げます。

たったの一行でUDPサーバーを立ち上げることができます。

$ nc -u -l 8888

ちなみに、TCPサーバーを立ち上げたい場合は、次のようにします。

$ nc -l 8888

ここで、ポートの確認にすこしハマりました。UDPは一方向的な送信なので、サーバーのポートが開いているかどうかのスキャンを行なっても、返事がないため確認できないのです。たとえば、8888ポートが開いているかどうかのコマンドは次のようになります。

$ nc -vz 192.168.100.101 8888

UDPサーバーとTCPサーバーで、それぞれ応答があるかどうか確かめてみると面白いでしょう。

UDPクライアントの作成(iOS)

次に、iPhoneでUDPクライアントを作成します。ここからは、iOSアプリのプログラミングとなりますのでXCodeなどの開発環境が必要となります。

また、プログラミング言語はObjective-Cをベースにしてますが、UDP通信の部分はC言語を使いたいです。その場合は、.mファイルを.mmの拡張子にする必要があるようです。

次のプログラムが、C言語で書いたiOSアプリで動くUDPクライアントのプログラムとなります。

#import "ViewController.h"
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
}

- (IBAction)clickedSendButton:(id)sender {
    int sock;
    struct sockaddr_in addr;
    
    sock = socket(AF_INET, SOCK_DGRAM, 0);
    
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8888);
    addr.sin_addr.s_addr = inet_addr("192.168.100.101");
    
    sendto(sock, "HELLO", 5, 0, (struct sockaddr *)&addr, sizeof(addr));
    
    close(sock);
}
@end

UDPクライアントの解説(C言語)

プログラムをご覧のとおり、UDPクライアントのプログラムはとてもカンタンです。それでも、C言語の関数がありますので、すこし解説します。

まず、sockaddr_in構造体に送信先アドレスとポート番号を指定します。これらは、先ほどのMacで立ち上げたUDPサーバーの情報になります。

つぎにsocketクラスを初期化します。初期化の時にSOCK_DGRAMを指定することでUDPを使うことができます。またAF_INETは、IPv4のインターネットプロトコルを使用する宣言となります。

さて、このプログラムをビルドしてiPhoneなどのiOSアプリとして動かせば、先ほどMac側で立ち上げたUDPサーバーへデータがUDPで送信されます。ボタンをタップするたびに、HELLOというメッセージがMacのターミナル上に表示されるはずです。

UDPのプロトコル仕様

ここでは、UDPのプロトコル仕様について見ておきましょう。プロトコルとは「約束事」といった意味あいがあり、情報をやり取りする上での、データの形式やパケットの構成などを取り決めた約束事のことです。考えてみればわかりますが、約束事がないやりとりでは、どの部分からデータになるのか分からりませんから、メッセージを受信しても解析が困難です。そもそもプロトコルがなければ、相手先のIPアドレスがどの部分なのか分からず、届きもしないでしょう。

会話だってそうです。あいさつから始まり、世間話や、相づちなど、普段無意識におこなっている振る舞いも約束事のひとつです。そういったことを逸させずに話したらどうなるでしょうか?きっと「この人とは話が通じないな」と思いますよね。

さて、こちらがUDPのプロトコル仕様の図です。

UDPのプロトコル仕様
UDPのプロトコル仕様

図のように、UDPプロトコルではヘッダ部とデータ部に分かれています。データ部は64ビット目から開始されることがわかります。ポートは16ビットの長さまで使えますので、0番〜65536番(2の16乗)までの範囲を指定できます。

もちろんこれらのデータは、101010000101010のように0と1が連なったシリアルデータで表現されます。

MacからiOSへUDPでソケット送信

MacからiOSへUDPでデータ送信
MacからiOSへUDPでデータ送信

UDPについての理解はだいぶ進んだかと思います。今度は、MacからiPhoneへUDPソケットを送信してみたいと思います。

UDPサーバーの作成(iOS)

iPhoneでUDPを受信するために、UDPサーバーを作っていきます。先ほどのUDPクライアントよりはすこし複雑ですが、ていねいに追っていけば何も難しくはありませんのでご安心ください。

//
//  ViewController.mm
//

#import "ViewController.h"
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UITextView *textView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(reciveUdp) object:nil];
    [thread start];
}


-(void)updateTextView:(NSData *)data {
    NSString *str = [[NSString alloc] initWithData:data
                                          encoding:NSUTF8StringEncoding];
    _textView.text = [NSString stringWithFormat:@"%@%@", _textView.text, str];
}

- (void) reciveUdp {
  
    int sock; // ソケットのファイルディスクリプター
    struct sockaddr_in addr;

    
    sock = socket(AF_INET, SOCK_DGRAM, 0); // SOCK_DGRAMはUDP通信
    
    addr.sin_family = AF_INET; // AF_INETはIPv4のインターネットプロトコル
    addr.sin_port = htons(8888); // 監視しするiPhone側のポート番号
    addr.sin_addr.s_addr = INADDR_ANY; // どのアドレスからの接続でも受け入れる
    
    bind(sock, (struct sockaddr *)&addr, sizeof(addr)); // ソケットへ名前をつける?

    while (1) {
        char buf[2048]; // 受信用の配列
        memset(buf, 0, sizeof(buf)); // buf配列の先頭から、最後までを0で埋めている -> ヌル終端文字列
        long size = recv(sock, buf, sizeof(buf), 0); // UDP受信開始
        
        if(size > 0) {
            printf("%s\n", buf);
            NSData *data = [NSData dataWithBytes:buf length:size];
            
            [self performSelectorOnMainThread:@selector(updateTextView:) withObject:data waitUntilDone:YES];

//            break;
        }
    }

//    close(sock);
}

@end

プログラム中のコメントを読んでいただければ、やってることは分かると思います。ただし、注意しておきたいことがいくつかあります。

まず、UDPソケットを受信するために使っているrecv関数をメインスレッドで動かすとUIが固まってしまいます。そのため、NSThreadを使ってスレッド処理で動かしているのでご注意ください。

また、bufとして使っている配列のメモリ解放を考慮してません。実用する場合は、メモリの開放を行うようにしてください。

UDPクライアントの作成(Mac)

UDPサーバーができたところで、Mac側でUDPクライアントを作成しましょう。Macのターミナルでncコマンドを使って、次のように実行します。IPアドレスはiOS端末のものとなります。ポート番号は、先ほどのプログラム中で指定したものになります。

$ nc -u 192.168.100.119 8888

コマンドを実行後、何か送信したいメッセージを入力してreturnキーを押してください。先ほど作ったUDPサーバーアプリの画面に、Macから送信したメッセージが表示されるはずです。そのようすはこちらの動画でご覧いただけます。

記事に関するご質問などがあれば、
@tosisico または お問い合わせ までご連絡ください。
関連記事