47929


ソケット通信を行う際に高水準なファイル入出力関数（fgets/fprintf等のFILE構造体を渡す関数）を利用する サンプル用 C
プログラムを書きました。

<https://gist.github.com/kosh04/3772420/d2192618765898413f15c9c08107ece3172711c8>

上記リンクのソース一式を `git clone` で落としてきて `make` コマンドを叩けば Echoサーバ(`echo-server`) と
HTTPサーバ(`http-server`) という2つのプログラムが作成されます。Ubuntu/macOS/Win32(mingw-w64)
など複数環境で動作確認済。

ここからが問題で、[server.c
148-150行目](https://gist.github.com/kosh04/3772420/d2192618765898413f15c9c08107ece3172711c8#file-
server-c-L148-L150) では Winsock2 のソケット記述子をファイル記述子と関連付けする処理を行っています。

[code]

    int h = _open_osfhandle(accept_socket, _O_RDONLY|_O_BINARY);
    r = _fdopen(h, "rb");
    w = _fdopen(h, "wb");
    
[/code]

このように読み込み用の変数 `r` と書き込み用の変数 `w`
を分けた場合は後続のサーバ処理は期待どおりに動作するのですが、この部分をコメントアウトしてある 151
行目のように読み書き用の変数を一つにまとめた書き方にすると入出力の挙動がおかしくなってしまいました。

[code]

    r = w = _fdopen(_open_osfhandle(accept_socket, _O_RDWR|_O_BINARY), "r+b");
    
[/code]

具体的には以下のような動作になります。クライアント側の出力がぐちゃぐちゃで、サーバ側でも書き込みエラーが発生していることがわかります。（シェルは
MSYS2-mingw64 を使用）

[code]

    (サーバ側) $ make echo-server
    (サーバ側) $ ./echo-server.exe
    listening :::1234.
    accept ::1:7103.
    write 2 byte, but expect 3 byte: '12\n'
    write 2 byte, but expect 3 byte: '23\n'
    write 6 byte, but expect 7 byte: '123456\n'
    write 6 byte, but expect 7 byte: '234567\n'
    
    (クライアント側) $ cat testdata.txt (改行は'\n'のみ)
    1
    12
    123
    1234
    12345
    123456
    1234567
    12345678
    123456789
    (クライアント側) $ ncat localhost 1234 < testdata.txt
    1
    1223234
    12345
    1234562345672345678
    123456789
    
[/code]

Linux (unix) 環境では server.c 153 行目 `r = w = fdopen(accept_socket, "r+b");`
のように「ストリームの読み書きを行うフラグ `r+b`」を指定すれば問題なく動作している（ように見えるだけ？）こともあって、Windows
環境でも多分動作するだろうと考えていたのですが、結果は上記の通りダメでした。

### 質問

タイトルの通り、Windows/Winsock2 においてソケットを高水準ファイル入出力関数から扱いたい場合に、読み書き用の `FILE *`
変数を一つだけにすることは可能でしょうか？それとも素直に読み書きの変数を分けて書くべき？


CC BY-SA 4.0
2018-08-29T22:25:35.750
47923
2018-08-30T12:23:51.023
2018-08-30T04:46:45.690
4236
2391
question
c socket
Winsockを高水準入出力関数から扱う場合に入出力用の変数を一つにまとめることは可能？


[Winsockのドキュメント](https://docs.microsoft.com/en-
us/windows/desktop/winsock/socket-handles-2)には

> A socket handle can optionally be a file handle in Windows Sockets 2.

とあるように`HANDLE`（ファイルハンドル）として使用可能であり、`_open_osfhandle()`を使用して`int`（ファイルディスクリプタ）および`_fdopen()`を使うことで`FILE`として使うことはできます。

> 読み書き用の変数を一つにまとめた書き方にすると入出力の挙動がおかしくなってしまいました。

原因は別のところにあります。ソケットに対する`recv()`、`send()`は指定されたデータをすべて送受信可能とは保証されておらず、読み込み出来た範囲、もしくは書き込み出来た範囲でしか扱いません。  
今回のコードを引用すると

[code]

    void execute_echo(FILE *r, FILE *w) {
      char line[RCVBUFSIZE+1] = {0};
      while (fgets(line, sizeof(line), r) != NULL) {
        //fprintf(w, "%s", upcase(line));
        size_t len = strlen(line);
        size_t n = fwrite(upcase(line), 1, len, w);
        if (n < len) {
          debug("write %"PRIdSIZE" byte, but expect %"PRIdSIZE" byte: '%s\\n'\n", n, len, chomp(line));
        }
      }
    }
    
[/code]

となっていますが、`fgets()`は途中までしか読み込めないこともありますし、`fwrite()`も途中までしか書き込めないことがあります。特に`fwrite()`は途中までしか書き込めなかった場合に`FILE`をエラー状態に設定します。`ferror()`で確認、`clearerr()`で解除できますが、これらが行われていません。もちろん、途中までしか書き込まないまま次のループに回っているため、データが欠けます。  
その上で、読み書き用で`FILE`が共通化しているため、`fwrite()`による書き込みエラー状態から次の`fgets()`が実行されることになり、更に被害が拡大しています。

Linux環境で問題が顕在化しなかったのは`localhost`を使用したからこその単なる偶然で、外部のネットワークを経由していればいつでも発生する問題です。

この現象を見る限り、Winsockに限らずソケットに対して高水準なファイル入出力関数を行うのはあまりお勧めできない気がします。

* * *

metropolisさんのコメント

> `socket` の file descriptor を `dup(2)` で複製します。

ですが、先のWinsockのドキュメントには

> A socket handle should not be used with the DuplicateHandle function.

とあり、`dup`や`dup2`は内部で`DuplicateHandle()`を使用して複製するため、特にWinsockも視野に含められている場合は実行してはいけません。


CC BY-SA 4.0
2018-08-30T04:43:52.400
47929
2018-08-30T12:23:51.023
2018-08-30T12:23:51.023
4236
4236
47923
answer