はじめに

TCP keepalive を使ったプログラムを Ruby で書こうとしてはまったので,メモです.

TCP keepalive とは

TCP (ソケット) を使って pub/sub 形式のサービスに接続して,サーバからのイベントを待っていたとします. その際,サーバからのデータがしばらく届かない状態が続くと,単にイベントがないだけなのか, セッションが切れてしまっているのかを知ることができません. そこで,何の用事がなくても一定時間毎に空パケットを相手に送って返信を貰うことで, セッションの維持を確認するのが TCP keepalive です.

ちなみに,Websocket にも,ping/pong という仕組みがあり, 同様の事をより上のレイヤで実現しようとしている例です. Slack や IRC もアプリケーションのレイヤで ping/pong の仕組みを持っています.

また,HTTP にも keepalive と呼ばれる仕組みがありますが,上記の類とは別物で目的も違います.

TCP keepalive を設定するには

TCP keepalive がデフォルト有効かどうかは OS 次第のようですが, 有効であっても,タイムアウトが 2時間といった状態なので, 目的に応じてセッション (ソケット) 毎に時間を設定したほうがいいでしょう.

C言語だと,こんな感じです.Linux の TCP_KEEPIDLE に相当するものは, macOS (Darwin) では, TCP_KEEPALIVE となっているので注意が必要です.これではまりました.

#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>

/* macOS (Darwin) uses TCP_KEEPALIVE, while Linux uses TCP_KEEPIDLE */
#if defined(__APPLE__) && defined(__MACH__)
#  define TCP_KEEPIDLE TCP_KEEPALIVE
#endif

void setsockopt_test(int socket)
{
  /* After 40-sec silence, send 2 keepalives with 10-sec interval */

  int enable  =  1; /* set keepalive on/off */
  int idle    = 40; /* idle time used when SO_KEEPALIVE is enabled */
  int intvl   = 10; /* interval between keepalives */
  int keepcnt =  2; /* number of keepalives before close */

  setsockopt(socket, SOL_SOCKET,  SO_KEEPALIVE,  &enable,  sizeof(enable));
  setsockopt(socket, IPPROTO_TCP, TCP_KEEPIDLE,  &idle,    sizeof(idle));
  setsockopt(socket, IPPROTO_TCP, TCP_KEEPINTVL, &intvl,   sizeof(intvl));
  setsockopt(socket, IPPROTO_TCP, TCP_KEEPCNT,   &keepcnt, sizeof(keepcnt));
}

Ruby だと:

require 'socket'

MY_TCP_KEEPIDLE = if RUBY_PLATFORM =~ /darwin/ then 0x10 else :TCP_KEEPIDLE end

def set_tcp_keepalive(sock)
  # After 40-sec silence, send 2 keepalives with 10-sec interval

  enable  = true # set keepalive on/off
  idle    = 40   # idle time used when SO_KEEPALIVE is enabled
  intvl   = 10   # interval between keepalives
  keepcnt =  2   # number of keepalives before close

  sock.setsockopt(:SOL_SOCKET,  :SO_KEEPALIVE,   enable)
  sock.setsockopt(:IPPROTO_TCP, MY_TCP_KEEPIDLE, idle)
  sock.setsockopt(:IPPROTO_TCP, :TCP_KEEPINTVL,  intvl)
  sock.setsockopt(:IPPROTO_TCP, :TCP_KEEPCNT,    keepcnt)
end

どうやら,macOS 用の TCP_KEEPALIVE をシンボルで受け付けないようです.これではまりました. 行儀が悪いですが,0x10 を直接渡しています.(何かいい方法はないのでしょうか?) ちなみに 0x10 は, /usr/include/netinet/tcp.h から調べました:

// macOS 10.12.6 (Sierra) /usr/include/netinet/tcp.h
#define TCP_KEEPALIVE 0x10  /* idle time used when SO_KEEPALIVE is enabled */
#define TCP_KEEPINTVL 0x101 /* interval between keepalives */
#define TCP_KEEPCNT   0x102 /* number of keepalives before close */