背景(疑問)
ソケットプログラムでは、よく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クラス では send
メソッドはありますが、 sendto
メソッドはありません。ではRubyでは sendto
関数相当の処理は行えないのか?
確認したこと概要
Ruby公式ドキュメントを確認すると、 send(mesg, flags, dest_sockaddr = nil)
のように dest_sockaddr
引数がオプションとなっています。これは 「ソケットアドレス構造体を pack した文字列」を指定します。
とあります。
この引数を明示的に指定したときは、C言語のレベルでの sendto
関数が実行されることとなるようですので、少し深堀りしたいと思います。
メモ
RubyのBasicSocket#sendと、C言語(システムコール)レベルのsend/sendto との関係
SEND manページによれば、 send
と sendto
の関数の仕様の違いは、以下の通りです。
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となっています。
/* Structure describing a generic socket address. */
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
ただし、こちらのようなUDPのサンプルプログラム を見てもわかるとおり、実際にデータ格納時に用いるのは同じサイズの別の構造体でIPv4であれば sockaddr_in
となります。
sockaddr 型構造体は、さまざまなプロトコルで使えるサイズを定義しているのみで、実際に利用する際には各プロトコルごとに同じサイズの別の構造体が用意されています。代表的なものとして、IPバージョン4(IPv4)のためのsockaddr_in型構造体[...]があります。
『Linuxネットワークプログラミングバイブル』 https://amzn.to/3sRBljg
戻りまして、Rubyのsendメソッドの第3引数は 「ソケットアドレス構造体を pack した文字列」
を指定とありましたが、この「ソケットアドレス構造体」がsockaddr型構造体に該当し、IPv4であれば、sockaddr_in型構造体の構造に合わせてデータを格納すればいいことになります。
pack は文字列を元にバイナリ化します。sockaddr_in構造体に合わせた形で文字列を用意してpackすればいいこととなります(このあたりは別記事でまとめたい)。
Ruby Coreのソースコードを覗いてみる
このあたりを実際のRuby Coreのソースコードで確認してみます。
BasicSocket#send
の定義場所をRDocで確認すると、 ext/socket/basicsocket.c
の rsock_bsock_send
関数とわかるのでそこを見てみます。コードの一部を抜粋し、コメントを追加します。( //★
は本記事者の追加コメント)
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
-
connect されていなくても、当該ソケットファミリのヘッダ部から指定できる場合は、宛先アドレスの指定もデータ部に入るから send でいいっぽい。例えば、 AF_INET + RAW_SOCKET + IPPROTO_IP とか AF_PACKET + RAW_SOCKET + とか。仕様としての記載が見つからないけど、動作的にはそう見える。 ↩