UDPでiPhoneと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やRaspberry Piなどのマイコンボードでの無線通信方法は、UDPやTCPだけではありません。WebSocketやHTTP、BLE、ESP-NOWなどの通信方法があります。
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プロトコルではヘッダ部とデータ部に分かれてます。データ部は64ビット目から開始されることがわかります。ポートは16ビットの長さまで使えますので、0番〜65536番(2の16乗)までの範囲を指定できます。
もちろんこれらのデータは、101010000101010のように0と1が連なったシリアルデータで表現されます。
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から送信したメッセージが表示されるはずです。そのようすはこちらの動画でご覧いただけます。
Successfully received a message from the Mac with UDP on iPhone! - YouTube