おことわり
本記事では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アドレスを取得する処理を書くと思う。
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
という関数を使う。
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);
この関数はgaicb
構造体にgetaddrinfo
の引数を突っ込んで渡すと、
別スレッドでgetaddrinfo
を実行する。
引数のlist
はgaicb**
つまりgaicb
構造体へのポインタの配列になるので、渡したあと完了するまでにgaicb
構造体の寿命が尽きないよう注意する必要がある
// まとめて結果取得
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のソースコード
まあ、ソースなどを見るに結局はマルチスレッドで待機キューを使って実装しているので、
スレッドをすべて自力で管理したいのであれば自力で似たようなコードを書くことになる。
GetAddrInfoExW (windows)
一方,WindowsではGetAddrInfoExW
という関数を使う。
GetAddrInfoExA
やマクロ定義のGetAddrInfoEx
などもあるが基本的に上記関数で良い。
そもそもWindowsの~A
とか~W
というのはUnicode過渡期に昔のコードをそのまま利用できるように考え出された苦肉の策であって、もはやUnicode全盛期の今では~W
(UTF-16対応)関数を使うべきである。
// 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
);
なんかいろいろ引数があって分かりづらいのだが、ただnon-blockingにしたいだけなら、以下のように使う。
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
ハンドルを渡すことで行える。
以上が名前解決をnon-blocking、非同期で行う方法である。
(もう疲れた...でもまだ名前解決しかしてない...)
Socket
WSASocket (windows)
さて次はソケットである。
まずはプラットフォーム共通でsocket
関数を呼び出す....
と思いきや,WindowsでIOCPを利用するためには別途WSASocketW
関数を使わなければならない。
SOCKET WSAAPI WSASocketW(
[in] int af,
[in] int type,
[in] int protocol,
[in] LPWSAPROTOCOL_INFOW lpProtocolInfo,
[in] GROUP g,
[in] DWORD dwFlags
);
第1から第3引数までは通常のソケット関数と同じだが、IOCPに対応させるためにはdwFlags
にWSA_FLAG_OVERLAPPED
フラグを設定する必要がある。(lpProtocolInfo
はnullptr
,g
は0
で良い)
そして、ソケットをnon-blockingにするためには、
ioctl
関数を使う
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の略(多分)
しかし、ここもまたwindowsではioctlsocket
という関数をioctl
関数の代わりに使う。(引数などは同じ)
socket関数群はBSD socketがもとになっており共通部分も多いが、
細かいところを突っ込むと意外とプラットフォームごとに差異が多いものである。
connect
さて、次はclient側の流れを追ってみよう。
client側ではまずconnect
関数を呼び出す。
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
このとき出てくる、sockaddr
構造体があるが、これはgetaddrinfo
構造体(Name Resolvement参照)の.ai_addr
メンバからから取得できる。
addrlenに当たるものは.ai_addrlen
メンバから取得できる
この、sockaddr
構造体というのは例えるならば抽象クラスのようなもので、基礎となるプロトコル(IPv4,IPv6など)に応じてsockaddr_in
やsockaddr_in6
などといった構造体をキャストして渡し、もとの構造体の大きさをaddrlen
として渡す。
getaddrinfo
から取得できるものは予め適切な構造体が確保されてキャストされているためこのことについては考える必要はない。
通常、blockingモードでconnect
関数を呼び出すと成功するまで返らないが、
non-blockingモードでは即時成功すれば0
を返すがブロッキングすると-1
を返し
errno
にEAGAIN
またはEWOULDBLOCK
を返す。
(プラットフォームによってEAGAIN
とEWOULDBLOCK
の値が同じだったり違ったりする(らしい))
windows環境ではWSAGetLastError
関数がWSAEWOULDBLOCK
を返す。
ここからさきはerrno
と書いたらwindows環境ではWSAGetError
関数を呼び出すという意味である。また、エラーコードにE~
とかいたら、windows環境ではWSAE~
となる。
select
そして、connect
関数がblockingしたら、select
、poll
、epoll
関数などを使って完了を待つ必要がある。ここでは、select
関数を使った例を書く。
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);
// 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
関数を使う。
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);
まず、bind
関数だがこれはソケットにローカルのアドレスを割り当てる関数である。
まあ、語弊があるのは承知でいうとどのポート番号で接続を受けるかを設定する関数である。
というのも、大抵(すべて?)のケースではipアドレスはINADDR_ANY
を指定してどこからでも受けるようにするからである。
これもgetaddrinfo
関数のhost名にnullptr
を渡し、hint.ai_flags
にAI_PASSIVE
を指定して呼び出すとbind
関数に適したsockaddr
構造体を取得できる。
getaddrinfo
って意外と万能
setsockopt
ここでいくつか設定しておくべきオプションというものを紹介する。
ソケットにはsetsockopt
という関数でオプションを指定できる。
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
まずその1
u_long yes=1;
setsockopt(sock,SOL_SOCKET, SO_REUSEADDR,&yes,sizeof(yes));
これを指定することで、複数のソケットに同じアドレスをbind
してもエラーが出なくなる。
また、デバッグなどで何回もサーバーを立ち上げたり閉じたりしたときにbind
でエラーが出なくなる。詳しくは下記などを参照。
2022/09/29追記
その後調べてみると、複数のソケットに同じアドレスをbind
してもエラーが出なくなるという挙動はwindowsのみで、linuxではTIME-WAITの状態でbindできるようにする(=これはwindowsのデフォルト)というオプションである。下記記事など参照。
そしてその2
u_long no=0;
setsockopt(sock,IPPROTO_IPV6, IPV6_V6ONLY, &no,sizeof(no));
これはIPv6のソケットでIPv4の接続をハンドルするかどうかを設定する。
0を設定することでIPv6のソケットでもIPv4の接続が受け入れられるようになる。
特にwindowsではデフォルトでは1(not 0)になっているので0に設定するべきである。
その後bind
が成功したらlisten
関数を呼び出して接続の受け入れを開始する。
backlog
引数というのはTCPの状態がestablishedになってまだaccept
されていないものを保持しておくキューのサイズである。一定以上の大きさを指定すると丸められる。
(ごめんなさい私はよく知りません)
accept,epoll (linux)
そしていよいよaccept
関数である。
ついでなのでここでepoll
を使ってaccept
待ちするコードを例示してみる。
#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);
// 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
のときと本質は変わらなくて、
- ソケットを待機集合に登録
- 待機関数で待つ
- 設定したフラグの条件がヒットしたら操作を行う
というような感じである。
あとは、accept
関数で受け取るソケットはnon-blockingではないので
別途ioctl
関数などで設定をする必要がある。
(linux等ではaccept4
という関数で初めからnon-blockingで取得できるオプションが有る(らしい))
TODO(on-keyday):AcceptEx(windows)を使ったコードを書く
send
さて、とりあえずconnect
やaccept
したので
send
関数にいこう...かと思いますがこれは調べればすぐ出るしそこまで
blockingすることもない(大量のデータを扱えばあるかもしれないがそうするくらいならもっと分割して送るべきである)ので割愛する。(まあ、対応したいならrecv系と同じような手段でできるので参考にされたし)
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
recv
そして次がrecv
関数系でこちらはIOCP/epollとともによく使われると思う。
IOCPとepollの違い
世のライブラリで非同期ネットワークライブラリの基盤技術として
windowsではIO Completion Port(IOCP)、linuxではepoll APIが使われる。1
この2つの違いを一言でいうとすると、
完了まで面倒見るか、着信通知だけするか
といったところになると思う。(違ってたらごめんなさい)
まず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バージョンを使う。
epollのlevel triggerとedge trigger
epollには2つの動作モード、levelトリガーとedgeトリガーというのがある。
levelトリガーはフラグで指定したイベントの条件が満たされる限り、
epoll_waitから同じeventが何度も提供される。
edgeトリガーはフラグで指定したイベントの条件が満たされたら一回だけ、
epoll_waitからeventが返される。
EPOLLIN(入力待ち)の例で説明すると、
- levelトリガー 受け取っていない入力がOSのバッファに残っている限りepoll_waitから返される。
- edgeトリガーでは最初に入力を受け取ったときだけepoll_waitから返され、再び入力が受信されるまでたとえOSのバッファに残っていてもepoll_waitから返されない。
と言った違いがある。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// 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の関数群
HANDLE WINAPI CreateIoCompletionPort(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE ExistingCompletionPort,
_In_ ULONG_PTR CompletionKey,
_In_ DWORD NumberOfConcurrentThreads
);
windowsのすごい(皮肉)ところはepoll_create1
とepoll_ctl
のEPOLL_CTL_ADD
指定相当がこれ一つでできることである。
...と言うのは冗談でまずIOCPのハンドル作成は
auto iocp_handle = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 0);
で作成し、ソケットの登録は
// sockはSOCKET型(≒std::uintptr_t)なのでキャストが必要
CreateIoCompletionPort((HANDLE)sock, iocp_handle, 0, 0);
で行う。(CompletionKey
は用途が謎すぎる。NumberOfConcurrentThreads
は0で良い)
でrecv
だがこれもwindows独自関数のWSARecv
という関数を使う。
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
);
で使い方は次の通り。
ここでは、OVERLAPPED構造体の使い方に注目してほしい。
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の方で書く。
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
);
// 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部分と完了を受け取る場所を分けてかけるようになる。
個人的にはこれはちょっと欠陥仕様なんじゃないかなと思う。
以上が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
-
あとはBSDのkqueueといったのなどもあるがあいにくBSDにふれる機会がないのでなんともいえない ↩
-
まあ、もっと高度に最適化したいというのであればいろいろ差異があるとは思うが... ↩
-
epollの中身の説明についてはhttps://moriyoshi.hatenablog.com/entry/20090519/1242764245 など(13年前...)があるのでなんで早いかとかといったことは別で調べてください。 ↩
-
具体的な差分吸収の方法は既存のライブラリを参照。あとはC/C++言語ではないがGo言語のruntimeのコードなどもおすすめ。 ↩