【Ruby】BasicSocket#send の引数に関するメモ

# 背景（疑問）

ソケットプログラムでは、よく`send()` や `sendto()` 関数を使ってデータの送信が行われます。違いの概要を参考文献から引用します。 [^1]

> UDPを利用したソケットプログラミングでは、データを送信する方法としてsendto() や send() システムコールを利用することが一般的です。[...]両者の違いは、send()システムコールがソケットが接続状態（すでにconnect()された状態）であることを要求する点です
『Linuxネットワークプログラミング』 https://amzn.to/3miyddM

.

> TCP/IPの場合はsend(), recv()で送受信を行いましたが、UDP/IPの場合はコネクションレスプロトコルなので、ソケットが特定の通信相手と結びついていません。
そのため、送信、受信のたびに宛先や送信元の情報が必要になるため、引数に宛先や送信元の情報を指定できる、 sendto()、recvfrom()を使用します。
『Linuxネットワークプログラミングバイブル』 https://amzn.to/3sRBljg

ところが、 [RubyのSocketクラス](https://docs.ruby-lang.org/ja/latest/method/BasicSocket/i/send.html) では `send` メソッドはありますが、 `sendto` メソッドはありません。ではRubyでは `sendto` 関数相当の処理は行えないのか？

# 確認したこと概要

Ruby公式ドキュメントを確認すると、 `send(mesg, flags, dest_sockaddr = nil)` のように `dest_sockaddr` 引数がオプションとなっています。これは `「ソケットアドレス構造体を pack した文字列」を指定します。` とあります。
この引数を明示的に指定したときは、C言語のレベルでの `sendto` 関数が実行されることとなるようですので、少し深堀りしたいと思います。

# メモ

## RubyのBasicSocket#sendと、C言語（システムコール）レベルのsend/sendto との関係

[SEND](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/send.2.html) manページによれば、 `send` と `sendto` の関数の仕様の違いは、以下の通りです。

```C
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
```

つまり `sendto` の第4引数、第5引数である

-  const struct sockaddr *dest_addr
-  socklen_t addrlen

です。ここに上述引用部の通り、 `send` では指定していなかった宛先の情報を入力します。具体的なアドレス情報となる第4引数 `*dest_addr` の型 `struct sockaddr` の定義は以下の通りです。（ `4.14.225-169.362.amzn2.aarch64` で確認）
サイズは16Byteとなっています。

```C:/usr/include/bits/socket.h
/* Structure describing a generic socket address.  */
struct sockaddr
  {
    __SOCKADDR_COMMON (sa_);    /* Common data: address family and length.  */
    char sa_data[14];           /* Address data.  */
  };
```

ただし、[こちらのようなUDPのサンプルプログラム](https://www.geekpage.jp/programming/linux-network/udp.php) を見てもわかるとおり、実際にデータ格納時に用いるのは同じサイズの別の構造体でIPv4であれば `sockaddr_in` となります。

> sockaddr 型構造体は、さまざまなプロトコルで使えるサイズを定義しているのみで、実際に利用する際には各プロトコルごとに同じサイズの別の構造体が用意されています。代表的なものとして、IPバージョン4(IPv4)のためのsockaddr_in型構造体[...]があります。
『Linuxネットワークプログラミングバイブル』 https://amzn.to/3sRBljg

戻りまして、Rubyのsendメソッドの第3引数は  `「ソケットアドレス構造体を pack した文字列」` を指定とありましたが、この「ソケットアドレス構造体」がsockaddr型構造体に該当し、IPv4であれば、sockaddr_in型構造体の構造に合わせてデータを格納すればいいことになります。
[pack](https://docs.ruby-lang.org/ja/latest/method/Array/i/pack.html) は文字列を元にバイナリ化します。sockaddr_in構造体に合わせた形で文字列を用意してpackすればいいこととなります（このあたりは別記事でまとめたい）。

## Ruby Coreのソースコードを覗いてみる

このあたりを実際のRuby Coreのソースコードで確認してみます。
`BasicSocket#send ` の定義場所を[RDoc](https://docs.ruby-lang.org/en/3.0.0/BasicSocket.html#method-i-send)で確認すると、 `ext/socket/basicsocket.c` の `rsock_bsock_send` 関数とわかるのでそこを見てみます。コードの一部を抜粋し、コメントを追加します。（ `//★` は本記事者の追加コメント）

https://github.com/ruby/ruby/blob/3df37259d81d9fc71f8b4f0b8d45dc9d0af81ab4/ext/socket/basicsocket.c

```C:basicsocket.c
VALUE
rsock_bsock_send(int argc, VALUE *argv, VALUE sock)
{
    struct rsock_send_arg arg;
    VALUE flags, to;
    rb_io_t *fptr;
    ssize_t n;
    rb_blocking_function_t *func;
    const char *funcname;

    rb_scan_args(argc, argv, "21", &arg.mesg, &flags, &to); // ★3つ目の引数があれば to に格納される

    StringValue(arg.mesg);
    if (!NIL_P(to)) { // ★ to があれば sendto, 無ければ send
        SockAddrStringValue(to);
        to = rb_str_new4(to);
        arg.to = (struct sockaddr *)RSTRING_PTR(to); // ★中身を sockaddr にキャスト
        arg.tolen = RSTRING_SOCKLEN(to);
        func = rsock_sendto_blocking;
        funcname = "sendto(2)";
    }
    else {
        func = rsock_send_blocking;
        funcname = "send(2)";
    }
```

★でコメントしたとおり、3つ目の引数があれば、 sendto を実行し、その中身は文字列を sockaddr 型構造体にキャストしているとわかります。

# 参考
RubyでTCP_FASTOPENを使う - Glass_saga https://glass-saga.hatenadiary.org/entry/20130911/1378907193

第2章 オブジェクト https://i.loveruby.net/ja/rhg/book/object.html

BasicSocket#send (Ruby 3.0.0 リファレンスマニュアル) https://docs.ruby-lang.org/ja/latest/method/BasicSocket/i/send.html

Man page of SEND https://linuxjm.osdn.jp/html/LDP_man-pages/man2/send.2.html

『Linuxネットワークプログラミング』 https://amzn.to/3miyddM

『Linuxネットワークプログラミングバイブル』 https://amzn.to/3sRBljg

[^1]: connect されていなくても、当該ソケットファミリのヘッダ部から指定できる場合は、宛先アドレスの指定もデータ部に入るから send でいいっぽい。例えば、 AF_INET + RAW_SOCKET + IPPROTO_IP とか AF_PACKET + RAW_SOCKET + とか。仕様としての記載が見つからないけど、動作的にはそう見える。