ネットワークライブラリ制作という趣味で得た知見まとめ(epoll/IOCP他)

# おことわり
本記事ではglibcやwindows apiなどの様々な関数の使用方法等について書いていますが私は完全に理解しているわけではありません。
不正確な記述があるかもしれないことをご了承下さい。
(見つけたら訂正リクエストしてください)

# はじめに
ここ1年ほどC++でsocketやssl/tlsなどのラッパーライブラリやhttp/http2/websocket/quic等の実装を何回も1から作り直すということを趣味にしている。
なかなか生産性のない趣味ではあるが意外と楽しいものである。
今回はその中で得た知見についてつらつらと書いていこうと思う。

# 最近のライブラリ
最近(?10年くらい前とかもあるが)の実用レベルのライブラリは
ほぼほぼnon-blocking IOとepollやIOCPなどの仕組みを使い、複数リクエストを
並行処理できるものが大半である(と思われる)。
またGo言語などのようにユーザーから見るとblockingなコードもruntimeレベルで
並行・並列化している例もある。
なので私の作るライブラリもこの例にそって全部non-blockingでepollやIOCPなどを
使う。
だが、あんまりC/C++でのsocketの非同期操作だけをまとめた記事は少ないように感じるのでそこら辺を書いていく。

# Name Resolvement
## getaddrinfo
まず,ソケット通信をするとなると`gethostbyname(非推奨)`や`getaddrinfo`等で
IPアドレスを取得する処理を書くと思う。

```cpp
int getaddrinfo(const char *node, const char *service,
                const struct addrinfo *hints,
                struct addrinfo **res);
```
まあ今から作るなら`getaddrinfo`一択だと思うが
non-blockingなライブラリを作るときここが一番のネックである。
`getaddrinfo`はブロッキングするのである。
(まあ、ここは別にblockingしてもいいやと言うのであればパスしてもいい。)
この問題を解決するためには別の関数を使う必要があるのだがlinux(系?)とwindowsで方法が異なる。

## getaddrinfo_a (linux)
まずlinuxでは`getaddrinfo_a`という関数を使う。
```cpp
struct addrinfo {
    int              ai_flags;
    int              ai_family;
    int              ai_socktype;
    int              ai_protocol;
    socklen_t        ai_addrlen;
    struct sockaddr *ai_addr;
    char            *ai_canonname;
    struct addrinfo *ai_next; 
};

struct gaicb {
    const char            *ar_name;
    const char            *ar_service;
    const struct addrinfo *ar_request;
    struct addrinfo       *ar_result; 
};

int getaddrinfo_a(int mode, struct gaicb *list[],
                int nitems, struct sigevent *sevp);
```
https://linuxjm.osdn.jp/html/LDP_man-pages/man3/getaddrinfo_a.3.html

この関数は`gaicb`構造体に`getaddrinfo`の引数を突っ込んで渡すと、
別スレッドで`getaddrinfo`を実行する。

:::note warn
引数の`list`は`gaicb**`つまり`gaicb`構造体へのポインタの配列になるので、渡したあと完了するまでに`gaicb`構造体の寿命が尽きないよう注意する必要がある
:::
```cpp
// まとめて結果取得
int gai_suspend(const struct gaicb * const list[], int nitems,
                const struct timespec *timeout);
// 一個だけ結果取得
int gai_error(struct gaicb *req);

//要求キャンセル
int gai_cancel(struct gaicb *req);

```
そして,上記の`gai_error`関数などを使って結果を取得するというものである。
[getaddrinfo_aのソースコード](https://codebrowser.dev/glibc/glibc/resolv/getaddrinfo_a.c.html)
まあ、ソースなどを見るに結局はマルチスレッドで待機キューを使って実装しているので、
スレッドをすべて自力で管理したいのであれば自力で似たようなコードを書くことになる。

## GetAddrInfoExW (windows)
一方,Windowsでは`GetAddrInfoExW`という関数を使う。

:::note warn
`GetAddrInfoExA`やマクロ定義の`GetAddrInfoEx`などもあるが基本的に上記関数で良い。
そもそもWindowsの`~A`とか`~W`というのはUnicode過渡期に昔のコードをそのまま利用できるように考え出された苦肉の策であって、もはやUnicode全盛期の今では`~W`(UTF-16対応)関数を使うべきである。
:::

```cpp
// Windowsは整数型やポインタ型にいちいち別名がついてたりして紛らわしい
typedef const wchar_t* PCWSTR;
typedef unsigned int DWORD;

typedef struct addrinfoexW {
  int                ai_flags;
  int                ai_family;
  int                ai_socktype;
  int                ai_protocol;
  size_t             ai_addrlen;
  PWSTR              ai_canonname;
  struct sockaddr    *ai_addr;
  void               *ai_blob;
  size_t             ai_bloblen;
  LPGUID             ai_provider;
  struct addrinfoexW *ai_next;
} ADDRINFOEXW, *PADDRINFOEXW, *LPADDRINFOEXW;

INT WSAAPI GetAddrInfoExW(
  [in, optional] PCWSTR                             pName,
  [in, optional] PCWSTR                             pServiceName,
  [in]           DWORD                              dwNameSpace,
  [in, optional] LPGUID                             lpNspId,
  [in, optional] const ADDRINFOEXW                  *hints,
  [out]          PADDRINFOEXW                       *ppResult,
  [in, optional] timeval                            *timeout,
  [in, optional] LPOVERLAPPED                       lpOverlapped,
  [in, optional] LPLOOKUPSERVICE_COMPLETION_ROUTINE lpCompletionRoutine,
                 LPHANDLE                           lpHandle
);

```
https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfoexw

なんかいろいろ引数があって分かりづらいのだが、ただnon-blockingにしたいだけなら、以下のように使う。
```cpp
const wchar_t* hostname; // 解決したいホスト名
const wchar_t* service; // ポート番号("80"や"433"など)や"http"などの文字列
ADDRINFOEXW hint; // AF_INET6やSOCK_STREMなどのヒント 
ADDRINFOEXW* result;// 処理結果
timeval timeout; // タイムアウト
OVERLAPPED ol; // (Windowsの非同期操作でおなじみの) OVERLAPPED構造体
HANDLE cancel; // キャンセル時に使用するハンドル
// 適切に初期化....
// OVERLAPPED構造体のhEventにeventオブジェクトをセット
ol.hEvent = CreateEventW(nullptr, true, false, nullptr);

auto result = GetAddrInfoExW(hostname,service,0,nullptr,&hint,&result,&timeout,&ol,nullptr,&cancel);

// resultが0なら即時終了(=既にresultがセットされてる)
// resultがWSA_IO_PENDINGなら非同期実行中
// それ以外はエラー
```
そして非同期実行中(`result==WSA_IO_PENDING`)ならば`WaitForSingleObject`関数などを使って`ol.hEvent`が発火するのを待ち、発火したら`GetAddrInfoExOverlappedResult`関数を使ってエラーを確認する。なおキャンセルは`GetAddrInfoExCancel`関数に`cancel`ハンドルを渡すことで行える。

https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfoexoverlappedresult


https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfoexcancel

以上が名前解決をnon-blocking、非同期で行う方法である。
(もう疲れた...でもまだ名前解決しかしてない...)

# Socket
## WSASocket (windows)
さて次はソケットである。
まずはプラットフォーム共通で`socket`関数を呼び出す....
と思いきや,WindowsでIOCPを利用するためには別途`WSASocketW`関数を使わなければならない。
```cpp
SOCKET WSAAPI WSASocketW(
  [in] int                 af,
  [in] int                 type,
  [in] int                 protocol,
  [in] LPWSAPROTOCOL_INFOW lpProtocolInfo,
  [in] GROUP               g,
  [in] DWORD               dwFlags
);
```
https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketw

第1から第3引数までは通常のソケット関数と同じだが、IOCPに対応させるためには`dwFlags`に`WSA_FLAG_OVERLAPPED`フラグを設定する必要がある。(`lpProtocolInfo`は`nullptr`,`g`は`0`で良い)

そして、ソケットをnon-blockingにするためには、
`ioctl`関数を使う
```cpp
auto sock=socket(AF_INET,SOCK_STREAM,0);
// u_longはsocket系ヘッダーで定義される数値型
u_long value=1; // trueの意 
ioctl(sock,FIONBIO,&value); // FIONBIO -> Flag IO Non-Blocking IOの略(多分)
```
https://linuxjm.osdn.jp/html/LDP_man-pages/man2/ioctl.2.html

しかし、ここもまたwindowsでは`ioctlsocket`という関数を`ioctl`関数の代わりに使う。(引数などは同じ)

:::note warn
socket関数群はBSD socketがもとになっており共通部分も多いが、
細かいところを突っ込むと意外とプラットフォームごとに差異が多いものである。
:::

## connect

さて、次はclient側の流れを追ってみよう。
client側ではまず`connect`関数を呼び出す。
```cpp
int connect(int sockfd, const struct sockaddr *addr,
            socklen_t addrlen);
```
https://linuxjm.osdn.jp/html/LDP_man-pages/man2/connect.2.html

このとき出てくる、`sockaddr`構造体があるが、これは`getaddrinfo`構造体([Name Resolvement](#name-resolvement)参照)の`.ai_addr`メンバからから取得できる。
addrlenに当たるものは`.ai_addrlen`メンバから取得できる

:::note warn
この、`sockaddr`構造体というのは例えるならば抽象クラスのようなもので、基礎となるプロトコル(IPv4,IPv6など)に応じて`sockaddr_in`や`sockaddr_in6`などといった構造体をキャストして渡し、もとの構造体の大きさを`addrlen`として渡す。
`getaddrinfo`から取得できるものは予め適切な構造体が確保されてキャストされているためこのことについては考える必要はない。
:::

通常、blockingモードで`connect`関数を呼び出すと成功するまで返らないが、
non-blockingモードでは即時成功すれば`0`を返すがブロッキングすると`-1`を返し
`errno`に`EAGAIN`または`EWOULDBLOCK`を返す。
(プラットフォームによって`EAGAIN`と`EWOULDBLOCK`の値が同じだったり違ったりする(らしい))
windows環境では`WSAGetLastError`関数が`WSAEWOULDBLOCK`を返す。

:::note warn
ここからさきは`errno`と書いたらwindows環境では`WSAGetError`関数を呼び出すという意味である。また、エラーコードに`E~`とかいたら、windows環境では`WSAE~`となる。
:::

## select

そして、`connect`関数がblockingしたら、`select`、`poll`、`epoll`関数などを使って完了を待つ必要がある。ここでは、`select`関数を使った例を書く。
```cpp
int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

int  FD_ISSET(int fd, fd_set *set);


void FD_SET(int fd, fd_set *set);


void FD_ZERO(fd_set *set);
```
```cpp
// sockはconnectを呼び出したソケット
timeval timeout{};
val.tv_sec = /*second*/;
val.tv_usec = /*micro second (u means `μ`)*/;
fd_set wset{}, excset{}; 
// 待機集合の初期化
FD_ZERO(&rset);
FD_ZERO(&excset);
// ソケットのセット
FD_SET(sock, &wset);
FD_SET(sock,&excset);

// 登録されたファイルディスクリプタのうち最も大きな番号のやつを第一引数に渡す
auto result = select(sock+1,nullptr,&wset,&excset);

if(result==-1){
    // select error
}
if(FD_ISSET(sock,&excset)) {
   // ソケットが異常closeしたなどエラーを検知
}
if(FD_ISET(sock,&wset)) {
   // 書き込み可能 = connect完了
}
```

このような感じである。


## bind,listen
さて、次はサーバー側はどうだろうか。
サーバー側は`bind`、`listen`、`accept`関数を使う。
```cpp
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

int listen(int sockfd, int backlog);

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
```
https://linuxjm.osdn.jp/html/LDP_man-pages/man2/bind.2.html

https://linuxjm.osdn.jp/html/LDP_man-pages/man2/listen.2.html

https://linuxjm.osdn.jp/html/LDP_man-pages/man2/accept.2.html

まず、`bind`関数だがこれはソケットにローカルのアドレスを割り当てる関数である。
まあ、語弊があるのは承知でいうとどのポート番号で接続を受けるかを設定する関数である。
というのも、大抵(すべて?)のケースではipアドレスは`INADDR_ANY`を指定してどこからでも受けるようにするからである。
これも`getaddrinfo`関数のhost名に`nullptr`を渡し、`hint.ai_flags`に`AI_PASSIVE`を指定して呼び出すと`bind`関数に適した`sockaddr`構造体を取得できる。

:::note info
`getaddrinfo`って意外と万能
:::

## setsockopt
ここでいくつか設定しておくべきオプションというものを紹介する。
ソケットには`setsockopt`という関数でオプションを指定できる。
```cpp
int setsockopt(int sockfd, int level, int optname,
               const void *optval, socklen_t optlen);
```

https://linuxjm.osdn.jp/html/LDP_man-pages/man2/getsockopt.2.html

まずその１
```cpp
u_long yes=1;
setsockopt(sock,SOL_SOCKET, SO_REUSEADDR,&yes,sizeof(yes));
```
これを指定することで、複数のソケットに同じアドレスを`bind`してもエラーが出なくなる。
また、デバッグなどで何回もサーバーを立ち上げたり閉じたりしたときに`bind`でエラーが出なくなる。詳しくは下記などを参照。

https://www.geekpage.jp/programming/winsock/so_reuseaddr.php

2022/09/29追記
その後調べてみると、複数のソケットに同じアドレスを`bind`してもエラーが出なくなるという挙動はwindowsのみで、linuxではTIME-WAITの状態でbindできるようにする(=これはwindowsのデフォルト)というオプションである。下記記事など参照。

https://www.ymnet.org/diary/d/%E6%97%A5%E8%A8%98%E3%81%BE%E3%81%A8%E3%82%81/SO_EXCLUSIVEADDRUSE

そしてその2
```cpp
u_long no=0;
setsockopt(sock,IPPROTO_IPV6, IPV6_V6ONLY, &no,sizeof(no));
```
これはIPv6のソケットでIPv4の接続をハンドルするかどうかを設定する。
0を設定することでIPv6のソケットでもIPv4の接続が受け入れられるようになる。
特にwindowsではデフォルトでは1(not 0)になっているので0に設定するべきである。

https://linuxjm.osdn.jp/html/LDP_man-pages/man7/ipv6.7.html

https://docs.microsoft.com/ja-jp/windows/win32/winsock/ipproto-ipv6-socket-options


その後`bind`が成功したら`listen`関数を呼び出して接続の受け入れを開始する。
`backlog`引数というのはTCPの状態がestablishedになってまだ`accept`されていないものを保持しておくキューのサイズである。一定以上の大きさを指定すると丸められる。
(ごめんなさい私はよく知りません)

https://kazuhira-r.hatenablog.com/entry/2019/07/10/015733

## accept,epoll (linux)
そしていよいよ`accept`関数である。
ついでなのでここで`epoll`を使って`accept`待ちするコードを例示してみる。

```cpp

#define EPOLL_CTL_ADD /*opaque*/
#define EPOLL_CTL_MOD /*opaque*/

int epoll_create(int size); // 非推奨
int epoll_create1(int flags);

typedef union epoll_data {
    void        *ptr;
    int          fd;
    uint32_t     u32;
    uint64_t     u64;
} epoll_data_t;

struct epoll_event {
    uint32_t     events;      /* epoll イベント */
    epoll_data_t data;        /* ユーザーデータ変数 */
};

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);  

int epoll_wait(int epfd, struct epoll_event *events,
               int maxevents, int timeout);
```

https://linuxjm.osdn.jp/html/LDP_man-pages/man7/epoll.7.html

```cpp
// sockはlisten済みソケット
// epollディスクリプターの作成
auto epoll_fd = epoll_create1_(EPOLL_CLOEXEC);
epoll_event event;
event.event = EPOLLIN; // 入力(=接続)を検知するフラグを設定
event.data.fd=sock;// ユーザーデータ(fdやその他構造体へのポインタなど)
// epollディスクリプタへの登録
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock, &event);
while(true) {
    epoll_event ev[64]{};
    auto result=epoll_wait(epoll_fd,ev,64,/*timeout*/);
    if(result==-1){
        // epoll error
    }
    for(auto i=0;i<result;i++) {
        auto fd = ev[i].data.fd;
        if(fd==sock){ 
            // クライアントの情報を格納する
            // sockaddr_storageが最大サイズのものだが、
            // sockaddr_in6を使えば大抵は大丈夫である
            sockaddr_storage st{}; 
            // inputとしてsizeof(st)
            // outputとして実際のsockaddr_*のサイズ
            // なお、socklen_tでおかないでintなどにすると
            // windowsとlinuxで型が違うので注意
            socklen_t len=sizeof(st);
            auto accepted = accept(sock,(sockaddr*)&st,&len);
            if(accepted==-1) {
                // skip?
            }
            // acceptedのソケットはnon-blockingになっていないため
            // ioctlなどでnon-blockingに設定する必要がある
            // 様々な処理...
         }
    }
}
```

まあ、`select`のときと本質は変わらなくて、
1. ソケットを待機集合に登録
2. 待機関数で待つ
3. 設定したフラグの条件がヒットしたら操作を行う
というような感じである。

あとは、`accept`関数で受け取るソケットはnon-blockingではないので
別途`ioctl`関数などで設定をする必要がある。
(linux等では`accept4`という関数で初めからnon-blockingで取得できるオプションが有る(らしい))

*TODO(on-keyday):AcceptEx(windows)を使ったコードを書く*

## send
さて、とりあえず`connect`や`accept`したので
`send`関数にいこう...かと思いますがこれは調べればすぐ出るしそこまで
blockingすることもない(大量のデータを扱えばあるかもしれないがそうするくらいならもっと分割して送るべきである)ので割愛する。(まあ、対応したいならrecv系と同じような手段でできるので参考にされたし)
```cpp
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
```

https://linuxjm.osdn.jp/html/LDP_man-pages/man2/send.2.html


## recv
そして次が`recv`関数系でこちらはIOCP/epollとともによく使われると思う。

:::note 
### IOCPとepollの違い
世のライブラリで非同期ネットワークライブラリの基盤技術として
windowsではIO Completion Port(IOCP)、linuxではepoll APIが使われる。[^1]
この２つの違いを一言でいうとすると、
**完了まで面倒見るか、着信通知だけするか**
といったところになると思う。(違ってたらごめんなさい)
まずIOCPはIO **Completion** Port の名が示す通り
IO処理が一段落**完了**してから通知が行われる。
具体的に言うと事前にバッファを用意しておき、
それを指定してIO処理を開始するとバッファを使ってIO処理の完了までを
OSがやってくれるというわけである。
一方のepollは`poll`、`select`の発展形と言うかたちで
あくまで「入力が来た」「出力が可能だ」といったことが通知されるだけで、実際にIO処理は行わずユーザーが行う必要がある。
しかし、(OSの内部動作はともかく)プログラム的には
ここらへんが違うくらいで

+ IOの監視を設定
+ イベントを受け取ってから続行する

という基本的な流れ自体は共通するものがあり、上記の差異を理解すれば天と地ほど違うというようなことはないと思う。[^2] [^3] [^4]
:::

## recv, epoll (linux)
さてでは、まずはepoll版から。
まず普通にrecvを行い、blockingした場合、epollに登録する。
また、今回はedge triggerバージョンを使う。

:::note
### epollのlevel triggerとedge trigger
epollには２つの動作モード、levelトリガーとedgeトリガーというのがある。
levelトリガーはフラグで指定したイベントの条件が満たされる限り、
epoll_waitから同じeventが何度も提供される。
edgeトリガーはフラグで指定したイベントの条件が満たされたら一回だけ、
epoll_waitからeventが返される。
EPOLLIN(入力待ち)の例で説明すると、
- levelトリガー  受け取っていない入力がOSのバッファに残っている限りepoll_waitから返される。
- edgeトリガーでは最初に入力を受け取ったときだけepoll_waitから返され、再び入力が受信されるまでたとえOSのバッファに残っていてもepoll_waitから返されない。

と言った違いがある。
:::

```cpp
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
```
https://linuxjm.osdn.jp/html/LDP_man-pages/man2/recv.2.html

```cpp
// sockはconnect/accept済みのnon-blockingソケット。
// epoll_fd はepoll_create1で作成されたepollディスクリプター
// epoll_ctlで追加済
std::string buffer; 
while(true){
   char tmpbuf[1024];  
   auto res = recv(sock,tmpbuf,sizeof(tmpbuf),0);
   if(res==-1) {
      if(errno==EWOULDBLOCK||errno==EAGAIN) {
          constexpr auto edge_trigger=
                EPOLLIN |       // 入力待ち
                EPOLLET |      // EPOLLET = EPOLL Edge Triggerの略(多分)
                EPOLLONESHOT |  // 一回epoll_waitから返されたら再度有効化するまでepoll_waitから返されないようにするためのフラグ
                EPOLLEXCLUSIVE; // マルチプロセスサーバーのときに使うやつ(今回はいらない)(そしてよくわかってない)
          epoll_event event;
          event.event = edge_trigger;
          event.data.fd=sock;// ユーザーデータ
          epoll_ctl(epoll_fd,EPOLL_CTL_MOD,sock,&event); // 有効化
          // その後、別スレッドやここなどでepoll_waitを呼び待機
          // 発火したらevent.event&EPOLLINを調べてtrueなら再びrecv
      }
      // recv error
   }
   else if(res==0){
      break;
   } 
   buffer.append(tmpbuf,res);
}
```

サンプルコードが下手なことをお詫び申し上げます。
*TODO(on-keyday):もっと良いものに治す*
(そして、いよいよ疲れてきた。ただまだIOCPの説明が残っている....)


## WSARecv, IO Completion Port (windows)
...続きましてIOCP版です。
まずIOCPの関数群

```cpp
HANDLE WINAPI CreateIoCompletionPort(
  _In_     HANDLE    FileHandle,
  _In_opt_ HANDLE    ExistingCompletionPort,
  _In_     ULONG_PTR CompletionKey,
  _In_     DWORD     NumberOfConcurrentThreads
);
```

https://docs.microsoft.com/ja-jp/windows/win32/fileio/createiocompletionport

windowsのすごい(皮肉)ところは`epoll_create1`と`epoll_ctl`の`EPOLL_CTL_ADD`指定相当がこれ一つでできることである。
...と言うのは冗談でまずIOCPのハンドル作成は
```cpp
auto iocp_handle = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 0);
```
で作成し、ソケットの登録は
```cpp
// sockはSOCKET型(≒std::uintptr_t)なのでキャストが必要
CreateIoCompletionPort((HANDLE)sock, iocp_handle, 0, 0);
```
で行う。(`CompletionKey`は用途が謎すぎる。`NumberOfConcurrentThreads`は0で良い)

で`recv`だがこれもwindows独自関数の`WSARecv`という関数を使う。
```cpp
typedef struct _WSABUF {
  ULONG len;
  CHAR  *buf;
} WSABUF, *LPWSABUF;

// WSAOVERLAPPEDはOVERLAPPED構造体に等しい

int WSAAPI WSARecv(
  [in]      SOCKET                             s,
  [in, out] LPWSABUF                           lpBuffers,
  [in]      DWORD                              dwBufferCount,
  [out]     LPDWORD                            lpNumberOfBytesRecvd,
  [in, out] LPDWORD                            lpFlags,
  [in]      LPWSAOVERLAPPED                    lpOverlapped,
  [in]      LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
```

https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsarecv

で使い方は次の通り。
ここでは、OVERLAPPED構造体の使い方に注目してほしい。

```cpp
struct UserData {
    OVERLAPPED ol;// OVERLAPPED構造体
    SOCKET sock; // ユーザーデータ
}user;
user.sock=sock;
// sockはソケット
// WSARecvの引数用。引数はWSABUFの配列になっていて
// 複数のバッファを設定できる
// が、普通に使うだけなら1つで十分だと思う
WSABUF wsabuf;
char tmpbuf[1024];
wsabuf.buf=tmpbuf;
wsabuf.len=sizeof(tmpbuf);
// 受信バイト数
// だがIOCPを使うときは使わない。
// (設定しないとWSARecvがエラーになるので) TODO(on-keyday):要検証
DWORD transfered_byte;
// フラグ?(よくわかってない)
// ゼロをセットしておけば良い
DWORD flags=0;
auto result = WSARecv(sock,&wsabuf,1/*wsabufの数*/,&transfered_byte,&flags,&user.ol,nullptr);

if(result!=0){ // 0ならば既に受信完了で完了通知が配送されている
    if(WSAGetLastError()!=WSA_IO_PENDING){
        // error
    }
    // WSA_IO_PENDINGなら受信待機が開始され完了したら完了通知が配送される
}

```
そして待機部分では`GetQueuedCompletionStatus`や`~Ex`関数を使う。
サンプルはExの方で書く。

```cpp
BOOL GetQueuedCompletionStatus(
  [in]  HANDLE       CompletionPort,
        LPDWORD      lpNumberOfBytesTransferred,
  [out] PULONG_PTR   lpCompletionKey,
  [out] LPOVERLAPPED *lpOverlapped,
  [in]  DWORD        dwMilliseconds
);

typedef struct _OVERLAPPED_ENTRY {
  ULONG_PTR    lpCompletionKey;
  LPOVERLAPPED lpOverlapped;
  ULONG_PTR    Internal;
  DWORD        dwNumberOfBytesTransferred;
} OVERLAPPED_ENTRY, *LPOVERLAPPED_ENTRY;

BOOL WINAPI GetQueuedCompletionStatusEx(
  _In_  HANDLE             CompletionPort,
  _Out_ LPOVERLAPPED_ENTRY lpCompletionPortEntries,
  _In_  ULONG              ulCount,
  _Out_ PULONG             ulNumEntriesRemoved,
  _In_  DWORD              dwMilliseconds,
  _In_  BOOL               fAlertable
);
```

https://docs.microsoft.com/ja-jp/windows/win32/api/ioapiset/nf-ioapiset-getqueuedcompletionstatus

https://docs.microsoft.com/en-us/windows/win32/fileio/getqueuedcompletionstatusex-func

```cpp
// GetQueuedCompletionStatusから返される値のセット
OVERLAPPED_ENTRY entries[64];
auto res = GetQueuedCompletionStatusEx(iocp_handle, entries, 64, &rem, time, false);
if (!res) {
    // 完了通知が0
}
for(auto i=0;i<res;i++) {
     auto data=reinterpret_cast<UserData*>(entries[i].lpOverlapped);
     auto sock=data->sock;
     // dataやsockの中身を利用してかく
     // 実用的にはWSARecvに渡したバッファなどもUserDataに入れる必要がある
}

```

このIOCPを使うとき特にみそとなるのが`OVERLAPPED`構造体の部分である。
まず、`OVERLAPPED`構造体を先頭に仕込んだユーザー定義構造体をつくり、そこにデータを突っ込んで`WSARecv`に`OVERLAPPED`構造体として渡す。
そして、`GetQueuedCompletionStatus`で受け取った`OVERLAPPED`構造体を再びユーザー定義型にキャストするというふうにすることでデータをやり取りできる。
これによってrecv部分と完了を受け取る場所を分けてかけるようになる。

:::note warn
個人的にはこれはちょっと欠陥仕様なんじゃないかなと思う。
:::

以上がrecvのコード群である。
(なんでこんな長いの....)
*TODO(on-keyday):他の知見も書く*

# SSL/TLS
も、もうむり...zzz....
*TODO(on-keyday): SSL/TLSライブラリについての知見を書く*

# おわりに
文章書くの疲れた....
なれないことはするべきじゃないな...

とまあ文章を書くのになれていないので読みづらい箇所があったと思われること改めてお詫び申し上げます。
そしてさして長くもないけど長い長いとぶつぶつ書いてあることをお詫び申し上げます。(書くのは数時間読むのは一瞬.......)
特にepollとIOCPについてもっとまとまっている記事があればよかったのですがいかんせんもう十年以上前の技術(らしい、10年前はまだプログラミングやってなかったからわからない)でさらにはライブラリ実装の奥深くに隠蔽されていて直に使うことはほぼないと思われるため調べるのはそこそこ大変でした。(いやまあmanとかmsdnがあるから人によっては簡単なんだろうけど)。
まあ、自分用のメモとして使えればいいかなと思います。
拙い文章と技術力ですが役に立てば幸いです。(立たなかったらごめんなさい)

宣伝(?)コーナー
これらの知見を使ってライブラリを作りました。
まあ、実用にはとても耐えられないと思いますが
実装の参考にはなるかもしれません
(epoll/IOCPはそれぞれlib/dnet/epoll.cpp,lib/dnet/winsock.cppで実装)
https://github.com/on-keyday/utils/tree/main/src/include/dnet
https://github.com/on-keyday/utils/tree/main/src/lib/dnet

[^1]: あとはBSDのkqueueといったのなどもあるがあいにくBSDにふれる機会がないのでなんともいえない
[^2]: まあ、もっと高度に最適化したいというのであればいろいろ差異があるとは思うが...
[^3]: epollの中身の説明についてはhttps://moriyoshi.hatenablog.com/entry/20090519/1242764245 など(13年前...)があるのでなんで早いかとかといったことは別で調べてください。
[^4]: 具体的な差分吸収の方法は既存のライブラリを参照。あとはC/C++言語ではないがGo言語のruntimeのコードなどもおすすめ。