- 必要とする知識など:
- UNIX (含む Linux) 上での C 言語と SOCKET によるプログラミング経験および TCP/IP に関する知識。
クライアント側bind()システムコール、一回目です。
前回 まではサーバ側の処理を見てきましたが、今回からクライアント側の処理を見ていきます。
本連載の 二回目 に掲載したclient.cを再度掲載します。
client.c
#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#define NLOOPS 10
#define MAX_VAL 10
#define DATA_LENGTH 16
/**
* Main
*/
int
main(int argc, char *argv[])
{
char *addr;
int port;
int sd;
int result;
struct addrinfo hints;
struct addrinfo *res;
struct sockaddr_in from;
struct sockaddr_in to;
socklen_t addrlen;
ssize_t rbytes;
ssize_t sbytes;
int i;
/* Is not enough argument? */
if (argc != 3) {
fprintf(stderr, "Usage: client host PortNo\n");
exit(1);
}
addr = argv[1];
port = atoi(argv[2]);
/* Get address information */
bzero(&hints, sizeof (hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_DGRAM;
result = getaddrinfo(addr, 0, &hints, &res);
if (result != 0) {
fprintf(stderr, "getaddrinfo: %s: %s\n", addr, gai_strerror(result));
if (result == EAI_SYSTEM) {
fprintf(stderr, "getaddrinfo: %s: %s\n", addr, strerror(errno));
}
exit(1);
}
bzero(&to, sizeof (to));
bcopy(res->ai_addr, &to, res->ai_addrlen);
to.sin_port = htons(port);
addrlen = res->ai_addrlen;
freeaddrinfo(res);
/* Create a socket */
sd = socket(PF_INET, SOCK_DGRAM, 0);
if (sd < 0) {
perror("socket");
exit(1);
}
/* Bind a port number */
bzero(&from, sizeof (from));
#ifdef __FreeBSD__
from.sin_len = sizeof (from);
#endif /* __FreeBSD__ */
from.sin_family = AF_INET;
from.sin_port = 0;
result = bind(sd, (struct sockaddr *)&from, sizeof (from));
if (result < 0) {
perror("bind");
exit(1);
}
/* Bind the destination address and port number */
result = connect(sd, (struct sockaddr *)&to, addrlen);
if (result < 0) {
perror("connect");
exit(1);
}
for (i = 0; i < NLOOPS; i++) {
int j;
char sbuf[DATA_LENGTH];
char rbuf[DATA_LENGTH+1];
/* Inisialize send buffer */
for (j = 0; j < DATA_LENGTH; j++) {
sbuf[j] = '0' + ((i + j) % MAX_VAL);
}
/* Send data */
sbytes = send(sd, sbuf, sizeof (sbuf), 0);
if (sbytes < 0) {
perror("send");
break;
}
if (sbytes != sizeof (sbuf)) {
fprintf(stderr, "Length of the transmitted data does not matched: Tried to send %dbytes, transmitted %dbytes.\n", sizeof (sbuf), sbytes);
}
/* Receive data */
bzero(&rbuf, sizeof (rbuf));
rbytes = recv(sd, rbuf, sbytes, 0);
if (rbytes < 0) {
perror("recv");
break;
}
if (sbytes != rbytes) {
fprintf(stderr, "Length of the received data does not matched: Transmitted %dbytes, received %dbytes\n", sbytes, rbytes);
}
/* Print data */
printf("Data: Transmitted: \"%s\", Received: \"%s\"\n", sbuf, rbuf);
}
/* Success */
return 0;
}
最初に呼び出しているライブラリ関数getaddrinfo()は、第一引数で指定したホスト名をIPアドレスに変換し、第四引数のresに格納して返却します。ホスト名ではなく “192.168.0.1”のようのIPアドレスを指定した場合、これを数値に変換して返却します。
getaddrinfo()で取得したアドレス(res->ai_addr)をstruct sockaddr_in toにコピーした後freeaddrinfo()を使用してresで返却された領域を解放しています。freeaddrinfo()を忘れるとメモリーリークの原因となりますので、扱いには気を付けてください。
getaddrinfo()/freeaddrinf()に関しては詳細は割愛します。必要であればオンラインマニュアルなどを参照してください。
次に実行しているのはsocket()システムコールです。この呼び出しに関しては、本連載の 第三回 と同じなので、詳細は割愛します。
socket()システムコール呼び出し時のデータ構造を以下に示します。なお以下のデータ構造は、本連載の 第十二回 の末尾に掲載したものと同一です。
socket()システムコールの呼び出しで確保した領域を緑、変更した変数を青、変更した値は赤で示しています。
次の処理はbind()システムコールです。bind()は、本連載の 第十三回 で説明しています。server.cでは起動時の引数で指定されたポート番号を指定していますが、client.cではポート番号に0を指定しています。ポート番号に0を指定した場合には、カーネル内で未使用のポート番号を割り当ててくれます。
bind()のカーネル内の処理はserver.cからの呼び出しと途中までは同じです。しかし、第十七回 で解説したポート番号の扱いのあたりから異なります。第十七回 ではポート番号に8000番が指定されたものとして説明していますが、client.cでは以下のように明示的に0番が指定されています。
from.sin_family = AF_INET;
from.sin_port = 0;
result = bind(sd, (struct sockaddr *)&from, sizeof (from));
このため、server.cとは異なる部分から処理を追いかけていきます。具体的には、sys_bind() -> kern_bind() -> sobind() -> udp_bind() -> in_pcbbind() -> in_pcbbind_setup()と処理が進んで、in_pcbbind_setup()の以下の「} else {」
if (nam != NULL && laddr.s_addr != INADDR_ANY)
return (EINVAL);
if ((so->so_options & (SO_REUSEADDR|SO_REUSEPORT)) == 0)
lookupflags = INPLOOKUP_WILDCARD;
if (nam == NULL) {
if ((error = prison_local_ip4(cred, &laddr)) != 0)
return (error);
} else {
の中の、以下の処理からとなります。
laddr = sin->sin_addr;
if (lport) {
struct inpcb *t;
struct tcptw *tw;
上記の二行目のif文
if (lport) {
で参照している変数lportは、in_pcbbind_setup()内で以下のよう定義されています。
int
in_pcbbind_setup(struct inpcb *inp, struct sockaddr *nam, in_addr_t *laddrp,
u_short *lportp, struct ucred *cred)
{
struct socket *so = inp->inp_socket;
struct sockaddr_in *sin;
struct inpcbinfo *pcbinfo = inp->inp_pcbinfo;
struct in_addr laddr;
u_short lport = 0;
ここまでの処理で変数lportは変更されていません。したがって、
if (lport) {
このif文は偽となるので次の処理に進みます。
if (*lportp != 0)
lport = *lportp;
if (lport == 0) {
error = in_pcb_lport(inp, &laddr, &lport, cred, lookupflags);
if (error != 0)
return (error);
}
lportpには、struct inpcbのinp_lportのポインタが渡されます。struct inpcbは 十一回目 で解説しましたが、bzero()にて初期化したのち、まだinp_lportには何も代入していないので、
if (*lportp != 0)
このif文は偽となります。
次のif文ですが、
if (lport == 0) {
前述の通り、lportには0が代入されているので、このif文は真となり次の行の in_pcb_lport()が呼ばれます。
in_pcb_lport()は、/usr/src/sys/netinet/in_pcb.c (http://svnweb.freebsd.org/base/release/9.1.0/sys/netinet/in_pcb.c?view=markup) で以下のように定義されています。
int
in_pcb_lport(struct inpcb *inp, struct in_addr *laddrp, u_short *lportp,
struct ucred *cred, int lookupflags)
{
struct inpcbinfo *pcbinfo;
struct inpcb *tmpinp;
unsigned short *lastport;
int count, dorandom, error;
u_short aux, first, last, lport;
#ifdef INET
struct in_addr laddr;
#endif
pcbinfo = inp->inp_pcbinfo;
/*
* Because no actual state changes occur here, a global write lock on
* the pcbinfo isn't required.
*/
INP_LOCK_ASSERT(inp);
INP_HASH_LOCK_ASSERT(pcbinfo);
if (inp->inp_flags & INP_HIGHPORT) {
first = V_ipport_hifirstauto; /* sysctl */
last = V_ipport_hilastauto;
lastport = &pcbinfo->ipi_lasthi;
} else if (inp->inp_flags & INP_LOWPORT) {
error = priv_check_cred(cred, PRIV_NETINET_RESERVEDPORT, 0);
if (error)
return (error);
first = V_ipport_lowfirstauto; /* 1023 */
last = V_ipport_lowlastauto; /* 600 */
lastport = &pcbinfo->ipi_lastlow;
} else {
first = V_ipport_firstauto; /* sysctl */
last = V_ipport_lastauto;
lastport = &pcbinfo->ipi_lastport;
}
ローカル変数の定義の直後の以下の処理の説明は割愛し、その次のif文から見ていきます。
pcbinfo = inp->inp_pcbinfo;
INP_LOCK_ASSERT(inp);
INP_HASH_LOCK_ASSERT(pcbinfo);
以下のif文で参照しているinp->inp_flagsは、十一回目 で解説したbzero()にて初期化したのちにまだ何も代入していないので、いずれも偽となりelse内の処理が実行されます。
if (inp->inp_flags & INP_HIGHPORT) {
...
} else if (inp->inp_flags & INP_LOWPORT) {
...
} else {
first = V_ipport_firstauto; /* sysctl */
last = V_ipport_lastauto;
lastport = &pcbinfo->ipi_lastport;
}
V_ipport_firstautoとV_ipport_lastautoは、/usr/src/sys/netinet/in_pcb.h (http://svnweb.freebsd.org/base/release/9.1.0/sys/netinet/in_pcb.h?view=markup) で以下のように定義されています。
#define V_ipport_firstauto VNET(ipport_firstauto)
#define V_ipport_lastauto VNET(ipport_lastauto)
VNETに関しての説明は割愛します。
以下の処理のコメントにある通り、
first = V_ipport_firstauto; /* sysctl */
last = V_ipport_lastauto;
V_ipport_firstautoとV_ipport_lastautoはsysctlコマンドで操作可能なカーネル内の変数のようで、以下が該当するものと思われます。
$ sysctl net.inet.ip.portrange.first net.inet.ip.portrange.last
net.inet.ip.portrange.first: 10000
net.inet.ip.portrange.last: 65535
ということで、上記の処理は以下と同等と考えます。
first = 10000;
last = 65535;
この後も行頭に「V _」が付与された変数が出てきますが、いずれもsysctlコマンドで操作可能なカーネル内の変数として扱います。
次の処理を掲載します。
/*
* For UDP, use random port allocation as long as the user
* allows it. For TCP (and as of yet unknown) connections,
* use random port allocation only if the user allows it AND
* ipport_tick() allows it.
*/
if (V_ipport_randomized &&
(!V_ipport_stoprandom || pcbinfo == &V_udbinfo))
dorandom = 1;
else
dorandom = 0;
V_ipport_randomizedはsysctlコマンドで確認すると、以下が該当すると思われます。
$ sysctl net.inet.ip.portrange.randomized
net.inet.ip.portrange.randomized: 1
V_ipport_stoprandomはsysctlコマンドで確認しても該当するものが見当たらないので、0とみなします。
pcbinfo (inp->inp_pcbinfo)は、struct inpcbinfo udbinfoを指しています。V_udbinfoはudbinfoなので、上記のif文は真となり
dorandom = 1;
が実行されます。
続くif文で参照している変数ははいずれも先ほど出てきたばかりです。いずれも偽となります。
/*
* It makes no sense to do random port allocation if
* we have the only port available.
*/
if (first == last)
dorandom = 0;
/* Make sure to not include UDP packets in the count. */
if (pcbinfo != &V_udbinfo)
V_ipport_tcpallocs++;
/*
* Instead of having two loops further down counting up or down
* make sure that first is always <= last and go with only one
* code path implementing all logic.
*/
if (first > last) {
aux = first;
first = last;
last = aux;
}
次に進みます。
#ifdef INET
/* Make the compiler happy. */
laddr.s_addr = 0;
if ((inp->inp_vflag & (INP_IPV4|INP_IPV6)) == INP_IPV4) {
KASSERT(laddrp != NULL, ("%s: laddrp NULL for v4 inp %p",
__func__, inp));
laddr = *laddrp;
}
#endif
INETはコンパイル時に指定されているので、この#ifdef INETで括られた処理は有効です。
最初に初期化しているladdrは、本関数の最初のところで定義されていますが、ここで初めて初期化されます。
次のif文で参照しているinp->inp_vflagはINP_IPV4が代入されているので、このif文は真となります。if文の中の処理に関しての説明は割愛します。
続く処理も、arc4random()を除き詳細は割愛します。
tmpinp = NULL; /* Make compiler happy. */
lport = *lportp;
if (dorandom)
*lastport = first + (arc4random() % (last - first));
count = last - first;
arc4random()は、/usr/src/sys/libkern/arc4random.c (https://svnweb.freebsd.org/base/release/9.1.0/sys/libkern/arc4random.c?view=markup) で以下のように定義されています。
/*
* MPSAFE
*/
void
arc4rand(void *ptr, u_int len, int reseed)
{
u_char *p;
struct timeval tv;
getmicrouptime(&tv);
if (reseed ||
(arc4_numruns > ARC4_RESEED_BYTES) ||
(tv.tv_sec > arc4_t_reseed))
arc4_randomstir();
mtx_lock(&arc4_mtx);
arc4_numruns += len;
p = ptr;
while (len--)
*p++ = arc4_randbyte();
mtx_unlock(&arc4_mtx);
}
uint32_t
arc4random(void)
{
uint32_t ret;
arc4rand(&ret, sizeof ret, 0);
return ret;
}
詳細は割愛しますが、arc4random()はuint32_t型のランダムな数値を返却する関数だと推測し、ここでは1433が返却されたものと見なします。したがって以下の処理は、
if (dorandom)
*lastport = first + (arc4random() % (last - first));
以下と同等となり、
*lastport = 10000 + (1433 % (65535 - 10000));
結局以下の処理となります。
*lastoport = 11433;
次の処理
count = last - first;
は、
count = 65535 - 10000;
なので、
count = 55535;
となります。
次の処理を掲載します。
do {
if (count-- < 0) /* completely used? */
return (EADDRNOTAVAIL);
++*lastport;
if (*lastport < first || *lastport > last)
*lastport = first;
lport = htons(*lastport);
#ifdef INET6
if ((inp->inp_vflag & INP_IPV6) != 0)
tmpinp = in6_pcblookup_local(pcbinfo,
&inp->in6p_laddr, lport, lookupflags, cred);
#endif
#if defined(INET) && defined(INET6)
else
#endif
#ifdef INET
tmpinp = in_pcblookup_local(pcbinfo, laddr,
lport, lookupflags, cred);
#endif
} while (tmpinp != NULL);
INET6とINETはいずれもコンパイル時に指定されているので、#ifdef INET6, #ifdef INETで括られた処理は、いずれも有効です。
doの直後から順番に見ていきます。
if (count-- < 0) /* completely used? */
return (EADDRNOTAVAIL);
++*lastport;
if (*lastport < first || *lastport > last)
*lastport = first;
lport = htons(*lastport);
各処理の詳細は割愛しますが、ここまでで各変数は以下のように変化します。
実行前:
count = 55535;
*lastport = 11433;
lport = 0;
実行後:
count = 55534;
*lastport = 11434; /* == 0x2caa */
lport = 0xaa2c;
htons()は、本連載の 第十七回 でも出てきました。詳細はman htonsなどで確認してください。
続く以下のif文ですが、inp->inp_vflagにはINP_IPV4が代入されているので、
if ((inp->inp_vflag & INP_IPV6) != 0)
このif文は偽となり、elseの処理が実行されます。
else
tmpinp = in_pcblookup_local(pcbinfo, laddr,
lport, lookupflags, cred);
ここで呼び出しているin_pcblookup_local()は既に本連載の 第十七回 でも出てきましたが、第二引数と第三引数で渡しているladdrとlportに該当するstruct inpcbが存在すれば、そのポインタを返却します。存在しなければNULLを返却します。ここではNULLが返却されたものとみなし、次の処理に進みます。
} while (tmpinp != NULL);
先ほどのin_pcblookup_local()がNULLを返却しているので、do~whileループから抜けます。
#ifdef INET
if ((inp->inp_vflag & (INP_IPV4|INP_IPV6)) == INP_IPV4)
laddrp->s_addr = laddr.s_addr;
#endif
*lportp = lport;
return (0);
}
最初のif文は真となります。見たままなので詳細は割愛しますが、最後に0を返却して in_pcbbind_setup()に戻ります。結局、in_pcb_lport()は、未使用のポート番号を割り当てて、そのポート番号を*lportpに返却します。
in_pcb_lport()からin_pbbbind_setup()に戻ったところの処理ですが、in_pcb_lport()の呼び出し部分も含めて掲載します。
error = in_pcb_lport(inp, &laddr, &lport, cred, lookupflags);
if (error != 0)
return (error);
}
*laddrp = laddr.s_addr;
*lportp = lport;
return (0);
}
* laddrpには、in_pcb_lport()で割り当てた0が代入されます。* lportpには、in_pcb_lport()で割り当てたポート番号11434をhtons()で変換した値 0xaa2cが代入されます。
in_pcbbind_setup()からin_pcbbind()に戻ったところの処理ですが、in_pcbbind_setup()の呼び出し部分も含めて掲載します。
error = in_pcbbind_setup(inp, nam, &inp->inp_laddr.s_addr,
&inp->inp_lport, cred);
if (error)
return (error);
if (in_pcbinshash(inp) != 0) {
inp->inp_laddr.s_addr = INADDR_ANY;
inp->inp_lport = 0;
return (EAGAIN);
}
if (anonport)
inp->inp_flags |= INP_ANONPORT;
return (0);
}
in_pcbbind_setup()からin_pcbbind()に戻ったところの処理は、本連載の 十八回目 で説明していますので、詳細は割愛します。
この後、in_pcbbind() -> udp_bind() -> sobind() -> kern_bind() -> sys_bind() に戻り、client.cのbind()の呼び出しまで戻ります。
bind()システムコールで操作したデータ構造を以下に示します。
bind()システムコールの処理で確保した領域を緑、変更した変数を青、変更した値は赤で示しています。