Shujima Blog

Apple製品,技術系の話をするブログ

Raspberry PiどうしでUDPソケット通信で数値やりとり(C言語)

過去記事 www.shujima.work のC言語バージョンです.

Pythonだと何もかもが遅いので,C言語で書き直すことにします.

筆者の環境

  • Raspberry Pi 3 × 2
  • Raspbian 9.4( stretch )
  • gcc version 6.3.0 20170516 (Raspbian 6.3.0-18+rpi1+deb9u1)

Raspberry Pi 3 Model B V1.2 (日本製) 国内正規代理店品

Raspberry Pi 3 Model B V1.2 (日本製) 国内正規代理店品

前提

  • Raspberry Piが2つある

目次

Cのプログラムが見たい方は下まで飛ばしてください。

1. ネットワーク接続

この内容はPythonと全く同じです。

Raspberry PiどうしをLANケーブルで直接接続します。

f:id:masa_flyu:20180716033616p:plain

または、下図のようにハブを使って接続することもできます。ただし、うまく動かない場合の原因究明が面倒なので、最初は直結のほうがいいでしょう。ストレートケーブルorクロスケーブルはどちらでも大丈夫です。

f:id:masa_flyu:20180716034706p:plain

接続したら、お互いのIPアドレスなどをチェックします。 下記記事を参照してください。

www.shujima.work

ただのチェックなので、IPアドレスなどが既に分かっている場合には飛ばして問題ありません。

2. UDP送受信チェック

次の送信の工程がうまくいくことを確認するのに必要です。 厳密には不要な工程ですが、失敗した時の原因を見極めるのに役立ちます。 面倒ならば飛ばしてください。 この節ではaptパッケージ

  • tcpdump
  • hping3

をインストールします。これらは次節以降のCプログラムの実行には直接関係しません。

tcpdumpをインストール[受信側]

以下のコマンドでtcpdumpをインストールします。

$ sudo apt-get install tcpdump

tcpdumpを受信状態にする[受信側]

以下のコマンドを打ちます

$ sudo tcpdump -A -n udp port 60000

すると2〜3行の

tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes

のような文字列がでます。これ以降、自分のIPアドレス宛に届いた60000番のUDPパケットを受信し、ターミナル上に表示する状態となります。 60000に特に意味はありません。 0〜65535であればなんでもよいです。ただし0〜49151は決まった利用目的が決められているため、この先の工程でインターネットに接続する時には、自由に使える49152〜65535をおすすめします。 この記事では60000に統一しています。

なお、受信を停止するには、Ctrl + Cを押します。

hping3をインストール[送信側]

以下のコマンドでhping3をインストールします。

$ sudo apt-get install hping3

ポートを指定してpingする[送信側]

以下のコマンドで、高度なpingを打ちます。

$ sudo hping3 -2 -p 60000 xxx.xxx.xxx.xxx

すると、tcpdumpを受信状態にしておいた受信側のターミナルに、こんなのが1秒ごとに表示されるはずです。

xx:xx:xx.xxxxxx IP xxx.xxx.xxx.xxx.xxxxx > xxx.xxx.xxx.xxx.60000: UDP, length 0
<...!..'.5....E.......@.`.......@..&.`......................

60000とUDPが確認できれば、あとはどうでもいいです。

f:id:masa_flyu:20181119211325j:plain

3. C言語でUDP送信プログラム

送信側のプログラムを書きます。

文字列送信プログラム[送信側,クライアント側]

以下のページ等を参考にしています

#include <stdio.h> //printf(), perror()
#include <sys/socket.h> //sendto(), socket()
#include <unistd.h> //close()
#include <arpa/inet.h> //htons(), inet_addr()
#include <time.h> //time(), localtime()
#include <string.h> //strlen();

const int PortNumber = 60000;
const char *IPaddress = "169.254.46.83";


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

    struct sockaddr_in addr;
    int sock_df;

    //ソケット作成
    sock_df = socket(AF_INET, SOCK_DGRAM, 0);
    //ソケット作成失敗
    if(sock_df < 0)
    {
        perror("Couldn't make a socket");
        return -1;
    }

    // 通信の設定
    addr.sin_family = AF_INET; //IPv4を指定
    addr.sin_port = htons(PortNumber); //ポート番号。ここでは60000を指定
    addr.sin_addr.s_addr = inet_addr(IPaddress); //サーバー側のアドレス

    ssize_t send_status;
    const char *SendString = "Hello with UDP";

    while(1)
    {
        // 送信     
        send_status = sendto(sock_df, SendString , strlen(SendString)+1 , 0,
                    (struct sockaddr *)&addr, sizeof(addr) );

        // 送信失敗
        if(send_status < 0)
        {
            perror("send error");
            return -1;
        }

        printf("send:%s\n",SendString);

        // 1秒wait
        time_t t;
        time(&t);
        struct tm start, *now;
        now = localtime(&t);
        start = *now; // nowの値をコピー
        while(now->tm_sec == start.tm_sec)
        {
            time(&t);
            localtime(&t);
        }    

    }
    close(sock_df);

    return 0;
}

IPアドレスは受信側のものを入力してください。

以上のCプログラムを"UDPsend.c"のような名前でどこかに保存します。 そして

$ cd (cプログラムのディレクトリ)
$ gcc -o UDPsend UDPsend.c
$ sudo ./UDPsend

で実行します。

gccはcをビルドするコマンドです。拡張子の無い実行可能な形式のUDPsendファイルが生成されます。

3行目./UDPsendで実行されます。sudoは付けなくても動きましたが念のため。

実行すると、tcpdumpを受信状態にしておいた受信側のターミナルに

xx:xx:xx.xxxxxx IP xxx.xxx.xxx.xxx.xxxxx > xxx.xxx.xxx.xxx.60000: UDP, length 13
E..)tf....  ...@...... .`..t.Hello with UDP.....

のように表示されます。関係ない文字列が前後にありますが、「Hello with UDP」の文字列が送られていることがわかります。 この表示が1秒ごとに増えれば成功です.

f:id:masa_flyu:20181121152748j:plain

C言語でUDP数値送信プログラム[送信側,クライアント側]

上記のソースをさらに改変します。

#include <stdio.h> //printf(), perror()
#include <sys/socket.h> //sendto(), socket()
#include <unistd.h> //close()
#include <arpa/inet.h> //htons(), inet_addr()
#include <time.h> //time(), localtime()

const int PortNumber = 60000;
const char *IPaddress = "169.254.46.83";


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

    struct sockaddr_in addr;
    int sock_df;

    sock_df = socket(AF_INET, SOCK_DGRAM, 0);
    if(sock_df < 0)
    {
        perror("Couldn't make a socket");
        return -1;
    }

    // 通信の設定
    addr.sin_family = AF_INET; //IPv4を指定
    addr.sin_port = htons(PortNumber); //ポート番号。ここでは60000を指定
    addr.sin_addr.s_addr = inet_addr(IPaddress); //サーバー側のアドレス

    ssize_t send_status;
    double sendval = 3.141592654;

    while(1)
    {
        // 送信
        send_status = sendto(sock_df, &sendval , sizeof(double) , 0,
                    (struct sockaddr *)&addr, sizeof(addr) );

        // 送信失敗
        if(send_status < 0)
        {
            perror("send error");
            return -1;
        }

        printf("send:%f\n",sendval);

        // 1秒wait
        time_t t;
        time(&t);
        struct tm start, *now;
        now = localtime(&t);
        start = *now; // nowの値をコピー
        while(now->tm_sec == start.tm_sec)
        {
            time(&t);
            localtime(&t);
        }    

    }
    close(sock_df);

    return 0;
}

文字列送信プログラムから数行だけ変えています。

これを実行すると

xx:xx:xx.xxxxxx IP xxx.xxx.xxx.xxx.xxxxx > xxx.xxx.xxx.xxx.60000: UDP, length 8
E..$.<@.@.>7.......S.].`....8./T.!    @..........

受信側に表示されます。先ほどと違い、実行結果が入力と一致しません。 これは、double型のバイト列を数値に戻していないためです。 そのためには受信プログラムが必要です。

4. C言語でUDP数値受信プログラム

受信側のプログラムを書きます。

数値を受信する[受信側,サーバー側]

ソースは以下を参考にしています.

#include <stdio.h> //printf(), perror()
#include <sys/socket.h> //socket()
#include <sys/ioctl.h> //ioctl()
#include <netinet/in.h> //htons(), inet_addr()
#include <string.h> //memset()
#include <unistd.h> //close()
#include <stdlib.h> //atof()

const int PortNumber = 60000;
const int BufLength = 2048;

int main(int argc, char** argv)
{
    struct sockaddr_in addr , from_addr;
    int sock_df;
    socklen_t from_addr_size;    

    //ソケット作成
    sock_df = socket(AF_INET, SOCK_DGRAM, 0);
    //ソケット作成失敗
    if(sock_df < 0)
    {
        perror("Couldn't make a socket");
        return -1;
    }

    //通信の設定
    addr.sin_family = AF_INET; //IPv4を指定
    addr.sin_port = htons(PortNumber); //ポート番号。ここでは60000を指定
    addr.sin_addr.s_addr = INADDR_ANY; //クライアントを指定せず,全て受け付ける

    //addrとソケットの紐付け
    int bind_status;
    bind_status = bind(sock_df, (struct sockaddr *)&addr, sizeof(addr) );
    if( bind_status < 0)
    {
        perror("bind");
        return -1;
    }

    printf("bind success\n");

    //受信バッファ
    char buf[BufLength];
    memset(buf, 0, sizeof(buf)); //初期化

    while(1)
    {
        int recv_status;
        
        //受信
        recv_status = recvfrom(sock_df, buf, BufLength + 1, 0, (struct sockaddr *)&from_addr, &from_addr_size);
        if ( recv_status < 0)continue;

        buf[BufLength - 1]= '\0'; //もしbufに\0が含まれていなかったときでも正常にprintできるように
        printf("receive data:%s\n", buf);

        //バッファbufの先頭8バイトが送られてきた数値である。
        //そこで、bufの先頭のアドレスをdoubleのアドレスとして読むことで、数値として読み出せる。
        double *valp = (double *)&buf;
        printf("value:%f\n", *valp);
        
    }

    //ソケットをクローズ
    close(sock_df);

    return 0;
}

送信側は先ほどのプログラム、受信側はこちらのプログラムを共に実行すると、受信側で

receive data:8�/T�!  @
value:3.141593

が1秒ごとに表示されます。

f:id:masa_flyu:20181121212655j:plain

下段の数値をプログラム内で利用することもできます。

5. お互いに送受信するプログラム

長くなったので別の記事で投稿しました.

www.shujima.work

プログラムが動かないなどの問題があればTwitterやお問い合わせフォームでお知らせいただきますと幸いです(返信できない可能性があります).

当ブログをご利用いただく際には免責事項をお読みください。