(Chap.2 は こちら )
3.ソケットAPI 入門 🔗
Goal 🔗
- プロセス-カーネル間でやり取りされるソケットアドレス構造体を理解する。
- プロトコル独立なソケットアドレス構造体を操作可能な
sock_関数を実装する。
ソケットアドレス構造体 🔗
- ソケット関数の多くは、ソケットアドレス構造体へのポインタを引数として要求する。
- プロトコル群はそれぞれのソケットアドレス構造体を定義しており、
sockaddr_プレフィックスと、プロトコル特有のサフィックスが付けられることが多い。
IPv4 ソケットアドレス構造体 🔗
- 一般に「インターネットソケットアドレス構造体」と呼ばれる。
sockaddr_inという名前で、<netinet/in.h>ヘッダで定義されている。- 以下に Posix.1g の定義を記載する。
struct in_addr {
/*
32ビット IPv4アドレス
ネットワーク バイトオーダー
*/
in_addr_t s_addr;
}
struct sockaddr_in {
uint8_t sin_len; /* 構造体の大きさ (16バイト) */
sa_family_t sin_family; /* AF_INET */
in_port_t sin_port /* 16ビットのTCPあるいはUDPポート番号 */
struct in_addr sin_addr; /* IPv4アドレス */
char sin_zero[8] /* 未使用 */
}
struct in6_addr {
/*
128ビットのIPv6アドレス
ネットワーク バイトオーダー
*/
uint8_t s6_addr[16];
}
#define SIN6_LEN /* コンパイル時の検査に必要 */
struct sockaddr_in6 {
unit8_t sin6_len; /* この構造体の大きさ (24バイト) */
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* トランスポート番号 */
uint32_t sin6_flowinfo /* 優先度&フローラベル */
struct in6_addr sin6_addr; /* IPv6 アドレス */
}
- Posix.1g では、
sin_family,sin_port,sin_addrの3つのメンバーのみが要求されている。- 大半の実装では、ソケットアドレス構造体の大きさを16バイトに統一するために
sin_zeroが追加されている。
- 大半の実装では、ソケットアドレス構造体の大きさを16バイトに統一するために
- IPv4 アドレスおよびポート番号は、いずれも常にネットワークバイトオーダーで格納されている。
- ソケットアドレス構造体がソケット関数の引数として用いられる場合、常に参照渡しになる。
- サポートされているソケットアドレス構造体全てを受け付ける必要がある。
- ANSI C では、総称ポインタ
void *を使える。 - ソケット関数は ANSI C より以前に存在していたので、
<sys/socket.h>に総称(generic)ソケット構造体を定義している。
struct sockaddr {
uint8_t sa_len;
sa_family_t sa_family;
char sa_data[14];
}
- ソケット関数は、この総称ソケットアドレス構造体へのポインタを受けとるように定義される。
- ANSI C の宣言:
int bind(int, struct sockaddr *, socklen_t) - 呼び出し時にキャストが必要。
- ANSI C の宣言:
struct sockaddr_in serv;
/* ... serv の設定 ... */
bind(sockfd, (struct sockaddr *) &serv, sizeof(serv));
- IPv6 のソケットアドレス構造体が、長さメンバ(
sin6_len)をサポートする場合、SIN6_LEN定数を定義する必要がある。 sin6_flowinfo- 低位 24 bit: フローラベル
- 次の 4 bit: 優先度
- 次の 4 bit: (予約)
値-結果 の受け渡し 🔗
- ソケット関数にソケットアドレス構造体を渡す時は、必ずポインタが渡される(2回目)
bind,connect,sendtoの3つの関数は、プロセスからカーネルへソケットアドレス構造体を渡す。- カーネルはポインタと構造体のサイズを受け取るので、プロセスからカーネル内部へ何バイトのコピーを行えば良いのかを判定できる。
struct sockaddr_in serv;
/* ... */
connect(sockfd, (SA *) &serv, sizeof(serv));
(SA=struct sockaddr)
accept,recvfrom,getsockname,getpeernameの4つの関数は、カーネルからプロセスにソケットアドレスの情報を渡す。- 構造体のサイズは、引数として渡すときには、ソケットアドレス構造体のサイズをプロセスからカーネルに通知している。
- カーネルがソケットアドレス構造体の大きさを超えて書き込まないようにするため。
- 関数の処理が完了した後は、カーネルが実際に書き込んだ大きさをプロセスに通知している。
- IPv4の場合は16バイト、IPv6の場合は24バイトでそれぞれ固定。
- 可変長の場合は、指定したサイズ以下。
- 構造体のサイズは、引数として渡すときには、ソケットアドレス構造体のサイズをプロセスからカーネルに通知している。
struct sockaddr_un cli /* Unixドメインソケット */
socklen_t len;
len = sizeof(cli);
getpeername(unixfd, (SA *) &cli, &len)
バイトオーダー 🔗
-
リトルエンディアン
- 低位バイト(little end)を先頭に書く。
-
ビッグエンディアン
- 高位バイト(big end)を先頭に書く。
-
ローカルメモリに書き込む時のバイトオーダーは、ホストマシンによって決まっている。
- [hands on] ホストバイトオーダーを確認する。
>> aarch64-unknown-linux-gnu: little-endian
-
インターネット用のプロトコルでは、ビッグエンディアンを用いる
- ソケットアドレス構造体のアドレス(32bit)とポインタ(16bit)はインターネットのバイトオーダーで渡す。
<netinet/in.h>に定義されている以下の4つの関数を使う。htons: host to network shorthtonl: host to network longntohs: network to host shortntohl: network to host long
- インタネットプロトコルと同じバイトオーダーのホストマシンでは、null macro になっている。
よく使う関数 🔗
- 文字列ではなく(null終端を意識せず)バイト列をそのまま扱う関数群。
- Berkeley派生
void bzero(void *dest, size_t nbytes): ゼロクリアvoid bcopy(const void *src, void *dest, size_t nbytes): バイトコピーint bcmp(const void *ptr1, const void *prt2, size_t bytes): バイト比較
- ANCI C
void *memset(void *dest, int c, size_t len): バイト列を c で初期化void *memcpy(void *dest, const void *src, size_t nbytes): バイトコピーint memcmp(const void *ptr1, const void *prt2, size_t nbytes): バイト比較- dest=src の代入式の順。
- Berkeley派生
- ASCII文字列で記述されたIPアドレスを、ネットワークバイトオーダーに変換する。
- in_addr 構造体ベース
int inet_aton(const char *strptr, struct in_addr *addrptr)char *inet_ntoa(struct in_addr inaddr)in_addr_t inet_addr(const char *strptr)- inet_addr は deprecated.
- 文字列ベース
int inet_pton(int family, const char *strptr, void *addrptr): strptr の文字列を数値に変換し、addrptr に格納する。const char *inet_ntop(int family, void *addrptr, char *strptr, size_t len)
- in_addr 構造体ベース
-
簡易版を実装
#include <arpa/inet.h>
#include <errno.h>
#include <features.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
int inet_pton(int family, const char *strptr, void *addrptr) {
if(family == AF_INET) {
struct in_addr in_val;
if(inet_aton(strptr, &in_val)) {
// IPv4 で正式なフォーマットの場合
memcpy(addrptr, &in_val, sizeof(struct in_addr));
return (1);
}
// IPv4 で不正なフォーマット
return (0);
}
errno = EAFNOSUPPORT;
return (-1);
}
int main() {
char *src = "127.0.0.1";
struct in_addr dest;
if(inet_pton(AF_INET, src, &dest)) {
printf("%x\n", dest.s_addr);
return 0;
};
printf("EAFNOSUPOPRT");
return 1;
}
inet_atonが見つからない。_BSD_SOURCEを定義する。- ただし、
_BSD_SOURCEは deprecated。
- ただし、
- 動かすと、出力は
100007f- 先頭の
0が抜けていて0100007f - オクテットに分割すると、
0100007f 127.0.0.1は、7f000001- 前述の通り、自分の環境が little endian であることを考えると、妥当な結果。
- 先頭の
inet_ntopはプロトコル依存になる。- これをプロトコル非依存に治す。
#include "unp.h"
char *sock_ntop(const struct sockaddr *sa, socklen_t salen) {
char portstr[7];
static char str[128]; /* 返り値を保持するバッファ */
switch(sa->sa_family) {
case AF_INET: {
struct sockaddr_in *sin = (struct sockaddr_in *)sa;
if(inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL)
return NULL;
if(ntohs(sin->sin_port) != 0) {
snprintf(portstr, sizeof(portstr), ".%d", ntohs(sin->sin_port));
strcat(str, portstr);
}
return (str);
}
#ifdef IPV6
case AF_INET6: {
struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)sa;
str[0] = '[';
if(inet_ntop(AF_INET6, &sin6->sin6_addr, str + 1, sizeof(str) - 1) == NULL)
return NULL;
if(ntohs(sin6->sin6_port) != 0) {
snprintf(portstr, sizeof(portstr), "]:%d", ntohs(sin6->sin6_port));
strcat(str, portstr);
return str;
}
return (str + 1); // str から 1 つだけオフセット.(L20で'['を先に詰めてしまっているため。)
}
}
#endif
return NULL;
}
->(アロー演算子)(*sin).sin_portと同じ。
- ストリームソケットへのI/O
- 要求したバイト数より少ない読み書きしかしないケースがある。
- カーネルのソケットバッファの上限に達する場合。
- write 時は、ブロッキングIOの場合発生しない。
- 要求したバイト数より少ない読み書きしかしないケースがある。
- いくつかのユーティリティを実装する。
readn: ディスクリプタからnバイト読みだす。writen: ディスクリプタへnバイト書き込むreadline: ディスクリプタから1バイトずつ1行分を読みだす。- ただし、「1バイトずつ読みだす」実装だと、バイト数分のシステムコール(ディスクからの読み取り)が発生し、非常に重たい。
- 改良のアイディアとして、1回のシステムコールであらかじめ一定のバイト数(
MAXLINE)を読み込んでおきメモリに格納しておく。1バイトずつのアクセスには、メモリから値を返す。
EINTR- システムコールがシグナルによって割り込まれた。
- ディスクリプタの検査
- ディスクリプタが特定の方であることをチェックする必要があるケースが存在する。
fstat(get file status) 関数を使って、返り値をマクロS_ISXXXと比較する。
#include "unp.h"
ssize_t readn(int fd, void *vptr, size_t n) {
size_t nleft;
ssize_t nread;
char *ptr; // C では void 型ポインタの加算演算(オフセット)がないので、一度 char* に代入。
ptr = vptr;
nleft = n;
while(nleft > 0) {
if((nread = read(fd, ptr, nleft)) < 0) {
if(errno == EINTR)
nread = 0;
else
return -1;
} else if(nread == 0)
break;
nleft -= nread;
ptr += nread;
}
return (n - nleft);
};
ssize_t writen(int fd, const void *vprt, size_t n) {
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vprt;
nleft = n;
while(nleft > 0) {
if(nwritten = write(fd, ptr, nleft) <= 0) {
if(errno == EINTR)
nwritten = 0;
else
return -1;
}
nleft -= nwritten;
ptr += nwritten;
}
return n;
};
横道: リンカとアセンブリ 🔗
gcc -static: バイナリにライブラリが組み込まれてコンパイルされる。- アセンブリ言語にもいくつかある。
- メジャーどころだと GAS(GNU Assembler) と NASM
- 参考
.data
msg:
.ascii "Hello, world\n"
len = . - msg
.text
.global _start
_start:
mov x0, #1
ldr x1, =msg
ldr x2, =len
mov w8, #64
svc #0
mov x0, #0
mov w8, #93
svc #0