Shujima Blog

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

Raspberry PiどうしでUDPソケット送受信(C言語)

過去記事

www.shujima.work

の続きです.

上記の記事を双方向通信にして,送るデータの種類を増やしてみます.

環境

  • Raspberry Pi 3 Model B ×2
  • 同じネットワーク内に置かれたRaspberry Piどうしでの送受信を想定しています.
  • なお,インターネットをまたいだ別のネットワークの場合は別途工夫が必要です.

クライアントとサーバについて

送受信のとき,きっかけを作る方をクライアント側と呼びます.また,クライアントからのパケットに対して返信する側をサーバ側といいます.

双方が相手を気にせずに喋る(両方クライアントのような)方式も可能ですが,今回はクライアントが1秒間隔で送信し,サーバもそれに反応することで,ぴったり同期するようにしました.

プログラム

クライアント側

#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()
#include <sys/ioctl.h> //ioctl()

const int PortNumber = 60000;
const char *IPaddress = "169.254.46.83"; //サーバーIPアドレス

//クライアントからサーバーへ送るデータの構造
typedef struct {
    int32_t a;
    int32_t b;
}client_to_server_t;

//サーバーからクライアントへ送られるデータの構造
typedef struct {
    int32_t add; //4 byte
    double mean; //8 byte
    char comment[200];
}server_to_client_t;

//受信データ処理関数のプロトタイプ宣言
void receivefunc(server_to_client_t *buf);

//
int main(int argc, char** argv)
{
    struct sockaddr_in sendaddr; //送信用ソケットの設定情報
    struct sockaddr_in recvaddr; //受信用ソケットの設定情報
    struct sockaddr_in serveraddr; //サーバー側の情報
    socklen_t serveraddr_size; //serveraddrのサイズ
    int sendsock_df; //送信用ソケットのディスクリプタ
    int recvsock_df; //受信用ソケットのディスクリプタ
    int nonblockflag; //ノンブロッキング設定用フラグ

    sendsock_df = socket(AF_INET, SOCK_DGRAM, 0); //送信用ソケット
    recvsock_df = socket(AF_INET, SOCK_DGRAM, 0); //受信用ソケット

    if(sendsock_df < 0)
    {
        perror("Couldn't make a socket");
        return -1;
    }

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

    // 受信ソケットの通信設定
    recvaddr.sin_family = AF_INET; //IPv4を指定
    recvaddr.sin_port = htons(PortNumber); //ポート番号。ここでは60000を指定
    recvaddr.sin_addr.s_addr = INADDR_ANY;    
    
    //ノンブロッキング設定
    nonblockflag = 1;
    ioctl(recvsock_df , FIONBIO , &nonblockflag);

    // 受信用バインド。指定したポートをこのプログラムに紐付ける。
    int bind_status;
    bind_status = bind(recvsock_df , (struct sockaddr *)&recvaddr, sizeof(recvaddr));
    if (bind_status < 0)
    {
        perror("bind failed");
        return -1;
    }
    printf("bind success\n");

    //送受信データのバッファ
    client_to_server_t sendbuf;
    server_to_client_t recvbuf;

    int a=0;
    int b=1;

    printf("Client Start\n");

    while(1)
    {
        sendbuf.a = a;
        sendbuf.b = b;

        printf("A=%d, B=%d ---->Server\n", a , b);

        //送信
        ssize_t send_status;
        send_status = sendto(sendsock_df, &sendbuf , sizeof(sendbuf) + 1 , 0,
                     (struct sockaddr *)&sendaddr, sizeof(sendaddr) );        

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

        //フィボナッチ数列の計算
        int c = a;
        a = b;
        b = b + c;

        // 1秒wait
        time_t t;
        time(&t);
        struct tm start, *now;
        now = localtime(&t);
        start = *now; // nowの値をコピー

        //1秒間ループ
        while(now->tm_sec == start.tm_sec)
        {
            //時刻の更新
            time(&t);
            localtime(&t);

            int recv_status;
            recv_status = recvfrom(recvsock_df, &recvbuf, sizeof(recvbuf) + 1,
                 0, (struct sockaddr *)&serveraddr, &serveraddr_size);
            //ノンブロッキング設定をしたため、recvfromは受信していなくても処理を終える(recv_status = -1)。
            
            //recvfromでエラーならばスルー
            if ( recv_status < 0)continue;
            // 送信元アドレスがサーバーのものと異なったらスルー
            if ( serveraddr.sin_addr.s_addr != inet_addr(IPaddress) )continue;

            //受信したときの処理をする自作関数。recvbufのアドレスを引数で送る。
            receivefunc(&recvbuf);  
        }

        //1秒ごとに出力される
        printf(".\n");

    }
    close(sendsock_df);
    close(recvsock_df);

    return 0;
}

//受信したときに呼ばれる処理
void receivefunc(server_to_client_t *buf)
{
    printf("Server----> Add=%d, Mean=%f, Even or Odd=%s\n", buf->add, buf->mean, buf->comment);
}

サーバ側のCプログラム

#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()
#include <sys/ioctl.h> //ioctl()

const int PortNumber = 60000;
//const char *IPaddress = "169.254.169.5"; //クライアントIPアドレス

//クライアントからサーバーへ送られるデータの構造
typedef struct {
    int32_t a;
    int32_t b;
}client_to_server_t;

//サーバーからクライアントへ送るデータの構造
typedef struct {
    int32_t add;  //4 byte
    double mean;  //8 byte
    char comment[200];
}server_to_client_t;

// 関数のプロトタイプ宣言
void receivefunc(client_to_server_t *recvbuf , server_to_client_t *sendbuf);

int main(int argc, char** argv)
{
    struct sockaddr_in recvaddr; //受信用ソケットの設定情報
    struct sockaddr_in clientaddr; //クライアント側の情報
    socklen_t clientaddr_size; //clientaddrのサイズ
    int sendsock_df; //送信用ソケットのディスクリプタ
    int recvsock_df; //受信用ソケットのディスクリプタ
    char nonblockflag; //ノンブロッキング設定用フラグ

    sendsock_df = socket(AF_INET, SOCK_DGRAM, 0); //送信用ソケット
    recvsock_df = socket(AF_INET, SOCK_DGRAM, 0); //受信用ソケット

    if(sendsock_df < 0)
    {
        perror("Couldn't make a socket");
        return -1;
    }

    // 受信ソケットの通信設定
    recvaddr.sin_family = AF_INET; //IPv4を指定
    recvaddr.sin_port = htons(PortNumber); //ポート番号。ここでは60000を指定   
    recvaddr.sin_addr.s_addr = INADDR_ANY;    
    
    //ノンブロッキング設定
    //nonblockflag = 1;
    //ioctl(recvsock_df , FIONBIO , &nonblockflag);

    // 受信用バインド。指定したポートをこのプログラムに紐付ける。
    int bind_status;
    bind_status = bind(recvsock_df , (struct sockaddr *)&recvaddr, sizeof(recvaddr));
    if (bind_status < 0)
    {
        perror("bind failed");
        return -1;
    }
    printf("bind success\n");

    //送受信データのバッファ
    server_to_client_t sendbuf;
    client_to_server_t recvbuf;

    printf("Server Start\n");

    while(1)
    {
        int recv_status;
        recv_status = recvfrom(recvsock_df, &recvbuf, sizeof(recvbuf) + 1 ,
             0, (struct sockaddr *)&clientaddr, &clientaddr_size);
        //何らかのパケットを受信するまでrecvfromの処理が終わらない(ブロッキング)。ノンブロッキング設定で変更可能。

        // recvfromでエラーが出たらスルー
        if ( recv_status < 0)continue;

        // 送信元アドレスがクライアントのものと異なったらスルー(const char *IPaddress をコメント解除すること)
        //if ( clientaddr.sin_addr.s_addr != inet_addr(IPaddress) )continue;

        printf("%s----> a= %d, b= %d\n" , inet_ntoa(clientaddr.sin_addr) , recvbuf.a , recvbuf.b );

        //受信したときの処理をする自作 関数。recvbufとsendbufのアドレスを引数で送る。
        receivefunc( &recvbuf , &sendbuf );

        printf("add= %d, mean= %f, comment= %s ---->%s\n", sendbuf.add , sendbuf.mean , sendbuf.comment , inet_ntoa(clientaddr.sin_addr) );

        // 送信
        ssize_t send_status;
        send_status = sendto(sendsock_df, &sendbuf , sizeof(sendbuf) , 0,
             (struct sockaddr *)&clientaddr, sizeof(clientaddr) );

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

        printf(".\n");

    }
    close(sendsock_df);
    close(recvsock_df);

    return 0;
}

//受信したときに呼ばれる処理
void receivefunc(client_to_server_t *recvbuf , server_to_client_t *sendbuf)
{
    //計算し、結果をsendbufのアドレスに格納
    sendbuf->add = recvbuf->a + recvbuf->b;
    sendbuf->mean = (double)sendbuf->add / 2.0;
    strcpy(sendbuf->comment , ((sendbuf->add) % 2 == 0) ? "Even" : "Odd" ) ; //偶数ならEven, 奇数ならOddを返す
}

実行方法

クライアント側

  • CファイルをRaspberry Piのどこかに保存します(ファイル名の例:UDPclient.c).
  • ターミナルで以下のように操作します.
cd (保存したcファイルのあるディレクトリ)
gcc -o UDPclient UDPclient.c
sudo ./UDPclient

なお,クライアント側は単体で実行しても1秒ごとに表示し続けます.

サーバ側

  • CファイルをRaspberry Piのどこかに保存します(ファイル名の例:UDPserver.c).
  • ターミナルで以下のように操作します.
cd (保存したcファイルのあるディレクトリ)
gcc -o UDPserver UDPserver.c
sudo ./UDPserver

なお,サーバ側はクライアント側と通信できないと,表示が止まって動いているのか分からない状態になります. クライアント側と通信できると通信するたびに数行ずつ表示されていきます.

実行結果

プログラムを実行すると,クライアント側から1秒ごとにUDPのパケットで2つの数値を送信します.

サーバ側はそれを受信すると,直ちにその計算結果(合計,平均,偶数か奇数か)をクライアントに送り返します.

f:id:masa_flyu:20181126183102j:plain

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

参考

可変長データを含むUDPソケット通信のやり方 - sternhellerの日記

解説

クライアント側

構造体定義

//クライアントからサーバーへ送るデータの構造
typedef struct {
    int32_t a;
    int32_t b;
}client_to_server_t;

//サーバーからクライアントへ送られるデータの構造
typedef struct {
    int32_t add; //4 byte
    double mean; //8 byte
    char comment[200];
}server_to_client_t;

クライアント側からサーバに送られる2つの数値とサーバからクライアント側に送られる3つのデータをそれぞれ構造体にしています.

Cの構造体は定義した順番にメモリに隙間なく配置されます.

よって構造体の先頭アドレスから構造体の長さ分だけのメモリを送信すると,複数の数値であってもいっぺんに送れます.

d.hatena.ne.jp

ソケット通信設定

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

送受信それぞれのソケットについて設定を行います.

recvaddr.sin_addr.s_addr

は送信側はサーバに設定し,受信側はINADDR_ANYに設定しました.これは全てのアドレスからの受信を許すものです.本当は受信側もアドレスを指定しようとしたのですが,バインド時にエラーが出てしまいました.

動作結果

およそ100[Hz]でデータのやりとりを行えました(同一ネットワーク内).

Pythonでは10[Hz]でデータのやりとりを行えましたので,およそ10倍の速度になりました.

インターネットを経由する場合

上記記事は同じネットワーク内(たとえば,家庭内など)のみで可能な方法です.

インターネットを経由する場合にはなにかしらの工夫が必要です.

例えばこちらなどを参照してください.

www.shujima.work

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