2009/01/14


追記

POSIX では明確にソケットの最大値とはうたってはいないものの、Linux の実装を見ても最大値と扱う方が良い様です。また Winsock では select(2) の第一引数は無視されるようです。

C言語でソケットを使うプログラミングを行う際、ソケットディスクリプタがシグナル状態かを調べる方法としてselect(2)があります。
使い方は int r;
fd_set rfds;

FD_ZERO(&rfds);
FD_SET(sock, &rfds);

r = select(1, &rfds, NULL, NULL, NULL);
といった感じ。ここでselect(2)の第一引数に渡している値は、ディスクリプタ集合rfdsの内、いくつ検証するかを指す値。つまりrfdsに対してFD_ZERO/FD_CLRしてからFD_SETした回数となります。
ちなみに戻り値は、ディスクリプタ集合の内どれだけシグナル状態かの数が返ります。つまり r = select(num_fds, &rfds, &wfds, &efds, NULL);
の場合、rfds/wfds/efdsの内、シグナル状態であるディスクリプタの総数が返ります。

昔のC言語で書かれたソースを見ると、よく int r;
fd_set rfds;

FD_ZERO(&rfds);
FD_SET(sock, &rfds);

r = select(FD_SETSIZE, &rfds, NULL, NULL, NULL);
FD_SETSIZEを使って書かれた記述を見ます。これはrfdsが構造体であり、そのメンバに持つディスクリプタ格納配列fd_arrayFD_SETSIZEでサイズ定義されている事を利用している為です。
これを無駄と考える人がいた為か、こういう記述も未だに良く見かけます。
int r;
fd_set rfds;

FD_ZERO(&rfds);
FD_SET(sock1, &rfds);
FD_SET(sock2, &rfds);
int check_sock = max(sock1, sock2);

r = select(max(sock1, sock2), &rfds, NULL, NULL, NULL);
確かにFD_SETSIZEの理屈から言えば納得が行く話かも知れませんが、FD_SETSIZEの定義とはfd_arrayの個数でありディスクリプタの値が取り得る値の最大値ではありません。ディスクリプタが順列で生成されるなんて仕様もありません。
つまり、間違いです。

にも関わらず、ディスクリプタの最大値はFD_SETSIZEだと思わせる記述が出回ってしまったのだと思います。
Manpage of SELECT_TUT

nfds: 全ての集合に含まれるファイルディスクリプタのうち、値が最大のものに 1 を足した整数である。すなわち、ファイルディスクリプタを集合に加える作業の途中で、全てのファイルディスクリプタを見て最大値を求め、それに 1 を加えて nfds として select に渡さないといけない、ということだ。

http://www.linux.or.jp/JM/html/LDP_man-pages/man2/select_tut.2.html
おそらく初期のディスクリプタの実装が配列の添え字であった為、こういう記述となり残っていったのだと思う。

例えばWindowsで言えば、socket(7)関数で返るディスクリプタの値はFD_SETSIZEに収まらない値で返ります。またMinGW(Minimalist GNU for Windows)のwinsock.hで定義されているFD_SETSIZEは64と中途半端な値になっています。
それはなぜか...
適当だからです。

通常、1プロセスが扱えるディスクリプタの数が設定されるべきですが、それが明確に定義すべきでない環境では意味のない値になったのだと思います。
しかしながら、多数のディスクリプタ(ファイルもソケットも)を扱うプログラムならば64個使い切ってしまう事はあり得りえるでしょうね。
ではどうすれば良いか。FD_SETSIZEfd_arrayの個数を定義するマクロであり、FD_ZERO/FD_CLR/FD_SETでそれを操作する再に用いられる閾値であり、ループ回数なのです。さらにFD_ZERO/FD_CLR/FD_SETFD_SETSIZEと同じくマクロなのです。 #define FD_CLR(fd,set) do { u_int __i;\
for (__i = 0; __i < ((fd_set *)(set))->fd_count ; __i++) {\
       if (((fd_set *)(set))->fd_array[__i] == (fd)) {\
       while (__i < ((fd_set *)(set))->fd_count-1) {\
               ((fd_set*)(set))->fd_array[__i] = ((fd_set*)(set))->fd_array[__i+1];\
               __i++;\
       }\
       ((fd_set*)(set))->fd_count--;\
       break;\
       }\
}\
} while (0)
しかもFD_SETSIZEの定義は#ifdefにより使い手側が変更出来る様になっています。もし通常よりも多くディスクリプタを扱いたいならばコンパイル時にFD_SETSIZEを定義してやれば良いのです。
この事は、MSDNにも書いてあります。
select Function (Windows) http://msdn.microsoft.com/en-us/library/ms740141.aspx

Four macros are defined in the header file Winsock2.h for manipulating and checking the descriptor sets. The variable FD_SETSIZE determines the maximum number of descriptors in a set. (The default value of FD_SETSIZE is 64, which can be modified by defining FD_SETSIZE to another value before including Winsock2.h.)

ディスクリプタ集合をを操作/チェックするためにヘッダーファイルWinsock2.hに4つのマクロが定義されています。FD_SETSIZEはディスクリプタ集合の最大個数を記述子の最大数を決定します。 (FD_SETSIZEの初期値は64です。これはwinsock2.hをインクルードする前に別の値で変更する事が出来ます。)


但し、このFD_SETSIZEが少ない値のままコンパイルされたライブラリと、多く設定した値のライブラリを併用すると場合によっては誤動作する可能性があるので注意が必要です。まぁこれはUNIXでも同じ話ですね。

だらだら書きましたが、何を言いたいかというと

UNIXで開発していて、将来的にWindowsにも移植するかもしれないソフトウェアならば、ディスクリプタの値がディスクリプタ集合の最大個数である...といった様なコーディングは辞めましょう。

という事です。
Posted at by



2008/10/29


追記 最新版はgithubで作ってます。
mattn's nicodown at master — GitHub
http://github.com/mattn/nicodown/tree/master


適当だけど書いてみた。
タイトル取って来る所はlibxml使うの面倒臭かったのでXMLパーサ使わずベタで(実態参照文字あると変になるので気を付けて)。Windowsの場合だけWin32 APIでシフトJISにファイル名を変換しています。
荒いコードなので色々直し所がありますが、サンプルって事で。
//#define CURL_STATICLIB
#include <curl/curl.h>

#define HEX_DIGITS "0123456789ABCDEF"
#define IS_QUOTED(x) (*x == '%' && strchr(HEX_DIGITS, *(x+1)) && strchr(HEX_DIGITS, *(x+2)))

static char* response_data = NULL;  /* response data from server. */
static size_t response_size = 0;    /* response size of data */

static void
curl_handle_init() {
    response_data = NULL;
    response_size = 0;
}

static void
curl_handle_term() {
    if (response_data) free(response_data);
}

static size_t
curl_handle_returned_data(char* ptr, size_t size, size_t nmemb, void* stream) {
    if (!response_data)
        response_data = (char*)malloc(size*nmemb);
    else
        response_data = (char*)realloc(response_data, response_size+size*nmemb);
    if (response_data) {
        memcpy(response_data+response_size, ptr, size*nmemb);
        response_size += size*nmemb;
    }
    return size*nmemb;
}

int
main(int argc, char* argv[]) {
    CURLcode res;
    CURL* curl;
    char error[256];
    char name[256];
    char data[1024];
    char* buf = NULL;
    char* ptr = NULL;
    char* tmp = NULL;
    FILE* fp = NULL;
    int status = 0;

    // usage
    if (argc != 4) {
        fputs("usage: nicodown [usermail] [password] [video_id]", stderr);
        goto leave;
    }

    // default filename
    sprintf(name, "%s.flv", argv[3]);

    curl = curl_easy_init();
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
    curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, &error);
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_handle_returned_data);
    curl_easy_setopt(curl, CURLOPT_COOKIEJAR, "cookies.jar");
    curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "cookies.txt");

    // login
    sprintf(data, "mail=%s&password=%s&next_url=/watch/%s", argv[1], argv[2], argv[3]);
    curl_handle_init();
    curl_easy_setopt(curl, CURLOPT_URL, "https://secure.nicovideo.jp/secure/login?site=niconico");
    curl_easy_setopt(curl, CURLOPT_POST, 1);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data);
    res = curl_easy_perform(curl);
    if (res != CURLE_OK) {
        fprintf(stderr, error);
        goto leave;
    }
    buf = malloc(response_size + 1);
    strcpy(buf, response_data);
    if (strstr(buf, "id=\"login_bar\"")) {
        printf("%s\n", buf);
        free(buf);
        fprintf(stderr, "failed to login\n");
        goto leave;
    }
    free(buf);
    curl_handle_term();

    // get video url, and get filename
    sprintf(data, "http://www.nicovideo.jp/api/getthumbinfo?v=%s", argv[3]);
    curl_handle_init();
    curl_easy_setopt(curl, CURLOPT_URL, data);
    curl_easy_setopt(curl, CURLOPT_POST, 0);
    res = curl_easy_perform(curl);
    if (res != CURLE_OK) {
        fprintf(stderr, error);
        goto leave;
    }
    buf = malloc(response_size + 1);
    strcpy(buf, response_data);
    ptr = strstr(buf, "<title>");
    if (ptr) {
        ptr += 7;
        tmp = strstr(ptr, "</title>");
        if (*tmp) {
            *tmp = 0;
            strcpy(name, ptr);
        }
#ifdef _WIN32
        {
            UINT codePage;
            size_t wcssize;
            wchar_t* wcsstr;
            size_t mbssize;
            char* mbsstr;

            codePage = CP_UTF8;
            wcssize = MultiByteToWideChar(codePage, 0, name, -1,  NULL, 0);
            wcsstr = (wchar_t*)malloc(sizeof(wchar_t) * (wcssize + 1));
            wcssize = MultiByteToWideChar(codePage, 0, ptr, -1, wcsstr, wcssize + 1);
            wcsstr[wcssize] = 0;
            codePage = GetACP();
            mbssize = WideCharToMultiByte(codePage, 0, (LPCWSTR)wcsstr,-1,NULL,0,NULL,NULL);
            mbsstr = (char*)malloc(mbssize+1);
            mbssize = WideCharToMultiByte(codePage, 0, (LPCWSTR)wcsstr, -1, mbsstr, mbssize, NULL, NULL);
            mbsstr[mbssize] = 0;
            sprintf(name, "%s.flv", mbsstr);
            free(mbsstr);
            free(wcsstr);
        }
#endif
    }
    free(buf);
    printf("downloading %s\n", name);
    curl_handle_term();

    // get video url
    sprintf(data, "http://www.nicovideo.jp/api/getflv?v=%s", argv[3]);
    curl_handle_init();
    curl_easy_setopt(curl, CURLOPT_URL, data);
    curl_easy_setopt(curl, CURLOPT_POST, 0);
    res = curl_easy_perform(curl);
    if (res != CURLE_OK) {
        fprintf(stderr, error);
        goto leave;
    }
    buf = malloc(response_size + 1);
    strcpy(buf, response_data);
    ptr = strstr(response_data, "url=");
    if (!ptr) {
        free(buf);
        fprintf(stderr, "failed to get video info\n");
        goto leave;
    }
    tmp = strstr(ptr, "&");
    if (tmp) *tmp = 0;
    tmp = ptr;
    while(*tmp) {
        if (IS_QUOTED(tmp)) {
            char num = 0;
            sscanf(tmp+1, "%02x", &num);
            *tmp = num;
            strcpy(tmp + 1, tmp + 3);
        }
        tmp++;
    }
    strcpy(data, ptr + 4);
    printf("URL: %s\n", data);
    free(buf);
    curl_handle_term();

    // download video
    fp = fopen(name, "wb");
    if (!fp) {
        fprintf(stderr, "failed to open file\n");
        goto leave;
    }
    curl_handle_init();
    curl_easy_setopt(curl, CURLOPT_URL, data);
    curl_easy_setopt(curl, CURLOPT_POST, 0);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
    res = curl_easy_perform(curl);
    fclose(fp);
    if (res != CURLE_OK) {
        fprintf(stderr, error);
        goto leave;
    }

leave:
    curl_handle_term();
    curl_easy_cleanup(curl);

    return 0;
}
確認はWindowsでしかしてませんが、きっとUN*Xでも動くはず。perlやpythonやruby使わない派の方は雛形として持ってって下さい。

Posted at by



2008/09/19


手順だけ。
cd C:\temp\
wget http://search.cpan.org/CPAN/authors/id/T/TI/TIMB/Memcached-libmemcached-0.2101.tar.gz
zcat Memcached-libmemcached-0.2101.tar.gz | tar xv
cd Memcached-libmemcached-0.2101
mkdir src_inst\include\libmemcached
mkdir src_inst\lib
svn co http://svn.coderepos.org/share/lang/c/libmemcached-win32/libmemcached-latest
cd libmemcached-latest\libmemcached
mingw32-make -f makefile.w32
cd ..\..
copy libmemcached-latest\libmemcached\*.h src_inst\include\libmemcached\.
copy libmemcached-latest\libmemcached\memcached.a src_inst\lib\.
copy src_inst\include\libmemcached\*.h src\libmemcached\libmemcached\.
perl Makefile.PL
set OPT="INC=-IC:\temp\Memcached-libmemcached-0.2101\src_inst\include" "LMCD_BUILT_LIB=C:\temp\Memcached-libmemcached-0.2101\src_inst\lib\memcached.a" "LDFROM=$(OBJECT) C:\temp\Memcached-libmemcached-0.2101\src_inst\lib\memcached.a"
nmake %OPT%
あとは libmemcached-latest\libmemcached\memcached.dll
をパスの通る場所に置いておけばWindowsで use strict;
use Perl6::Say;
use Memcached::libmemcached qw(
    memcached_create
    memcached_server_add
    memcached_set
    memcached_get
);

my $key = "foo";
my $value = "bar";
my $memc = memcached_create();
memcached_server_add($memc, '127.0.0.1');
memcached_set($memc, $key, $value);
say memcached_get($memc, $key);
こんなソースが通る様になります。Cache::Memcached::libmemcachedも動きます。
さらに言うなら、rubyのmemcachedも動きました。 require 'memcached'
$cache = Memcached.new('127.0.0.1:11211')
$cache.set('foo', 'bar')
print $cache.get('foo')
こちらはCOMPATIBILITYファイルを弄ったり、最新バージョンで無くなったWHEELな処理をカットしないといけませんが...
さらにさらにpython-libmemcachedも動きます。
import cmemcached

c = cmemcached.Client(['127.0.0.1:11211'])
c.set("foo", "bar")
print c.get("hoge")
こちらもcmemcached.pyxでWHEELな処理をカットしないといけませんが...
とりあえず、動くって事です。
ちなみにオフィシャルの反応はまだなし...
Posted at by