Unix Network Programming: Chap3

Oct 9, 2022 15:06 · 788 words · 4 minute read

(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 が追加されている。
  • 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)
    • 呼び出し時にキャストが必要。
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 short
      • htonl: host to network long
      • ntohs: network to host short
      • ntohl: 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 の代入式の順。
    • 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)
  • 簡易版を実装

#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
    • オクテットに分割すると、01 00 00 7f
    • 127.0.0.1は、7f 00 00 01
    • 前述の通り、自分の環境が 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;
};

横道: リンカとアセンブリ 🔗

.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