ソケットプログラミングでクライアント・サーバモデルのTCP通信を行うプログラムを書いていたら、acceptを呼び出す時にたまにエラーが発生しました。

accept: Invalid argument

エラーが出るときはやり直しても何度でも出るし、出ないときは全然出ない。うーん、謎だ…

環境

  • Ubuntu 20.04.1 LTS (64bit)
  • CPU: Intel Core i5-6500 CPU @ 3.20GHz
  • RAM: 15.5GiB
  • コンパイラ: gcc 9.3.0

プログラム

複数のクライアントからのメッセージをサーバが拾い、サーバ側で受信したメッセージを表示するプログラム。今回は接続に応じて個別の通信用スレッドを生成することでマルチアクセスに対応しています。

サーバ側(tcp_multi_server.c):

#include <stdio.h>
#include <stdlib.h>

#include <string.h>
#include <errno.h>

#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <pthread.h>

#define	PORT	50000
#define BUFSIZE	2048

// 子スレッドの処理(受信&表示)
void *child_process(int *fd_socket) {
  int fd, recv_len;
  char buf[BUFSIZE];
  fd = (int)*fd_socket;

  recv_len = recv(fd, buf, BUFSIZE, 0);
  if (recv_len > 0) {gcc ./tcp_threads_2.c -o tcp_threads_2 -lpthread
    // 標準出力に受信内容を書き込み
    write(1, buf, recv_len);
  }
  close(fd);
}

int main() {
  struct sockaddr_in saddr, caddr;
  int fd1, fd2, len;
  pthread_t pt;

  // サーバーのソケットを生成
  fd1 = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if (fd1 < 0) {
    perror("socket");
    return -1;
  }

  // ソケットの設定
  memset(&saddr, 0, sizeof(saddr));
  saddr.sin_family = AF_INET;
  saddr.sin_port = htons(PORT);
  saddr.sin_addr.s_addr = htonl(INADDR_ANY);

  // 設定をbind
  if (bind(fd1, (struct sockaddr*)&saddr, sizeof(saddr))) {
    perror("bind");
    return -1;
  }

  // 接続待機
  if (listen(fd1, 5)) {
    perror("listen");
    return -1;
  }

  // 接続し次第子スレッドを生成
  // 続きは子スレッドが行う -> child_process
  while (1) {
    fd2 = accept(fd1, (struct sockaddr*)&caddr, &len);
    if (fd2 < 0) {
      perror("accept");
      exit(1);
    }

    if (pthread_create(&pt, NULL, (void*)(child_process), (void*)&fd2) < 0) {
      perror("pthread_create");
      return -1;
    }
    pthread_detach(pt);
  }

  return 0;
}

クライアント側(tcp_client.c):

#include <stdio.h>
#include <stdlib.h>

#include <string.h>
#include <errno.h>

#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 50000

int main(int argc, char **argv) {
  struct sockaddr_in saddr;
  int fd;
  char *buf="Hello!\n";

  // ソケットを生成
  fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if (fd < 0) {
    perror("socket");
    return -1;
  }

  // ソケットの設定
  memset(&saddr, 0, sizeof(saddr));
  saddr.sin_family      = AF_INET;
  saddr.sin_port        = htons(PORT);
  saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
	
  // サーバに接続
  if (connect(fd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0) {
    perror("connect");
    exit(-1);
  }

  // メッセージbufを送信
  send(fd, buf, strlen(buf), 0);

  close(fd);

  return 0;
}

クライアントはメッセージ「Hello!」を送信したら終了します。サーバ側はプロセスを中断させるまでクライアントからの接続を無限に待機し続けます。

gccでのコンパイル用のコマンドは以下の通り。

$ gcc tcp_multi_server.c -o tcp_multi_server -lpthread
$ gcc tcp_client.c -o tcp_client

サーバ側を先に起動し、クライアント側を後に起動するとメッセージ「Hello!」が自動的にクライアントからサーバへ送信されます。

問題点

数回に一度程度ですが、クライアントがサーバに接続されたとき、サーバ側でこんなエラーが出て強制終了してしまいます。

$ ./tcp_multi_server
accept: Invalid argument

acceptに無効な引数が指定されているとのこと。

man pageによると、accept関数の引数

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

のうち、3番目の引数addrlenは入出力両用の引数で、accept関数の呼び出し時には事前にaddrが指し示す構造体のサイズで初期化しなければならないとのこと。

つまり、先に示したtcp_multi_server.cのうちの変数lenに、accept関数を呼び出す前に構造体caddrのサイズを代入して置かなければならなかったということです。

// 問題の箇所
fd2 = accept(fd1, (struct sockaddr*)&caddr, &len);

解決策

accept文を呼び出す直前に、caddrのサイズをlenに代入しておけばOK。sizeof関数でサイズを取得しています。

len = sizeof(caddr);  // 追加
fd2 = accept(fd1, (struct sockaddr*)&caddr, &len);

サーバ側プログラム(tcp_multi_server.c)の修正版:

#include <stdio.h>
#include <stdlib.h>

#include <string.h>
#include <errno.h>

#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <pthread.h>

#define	PORT	50000
#define BUFSIZE	2048

// 子スレッドの処理(受信&表示)
void *child_process(int *fd_socket) {
  int fd, recv_len;
  char buf[BUFSIZE];
  fd = (int)*fd_socket;

  recv_len = recv(fd, buf, BUFSIZE, 0);
  if (recv_len > 0) {gcc ./tcp_threads_2.c -o tcp_threads_2 -lpthread
    // 標準出力に受信内容を書き込み
    write(1, buf, recv_len);
  }
  close(fd);
}

int main() {
  struct sockaddr_in saddr, caddr;
  int fd1, fd2, len;
  pthread_t pt;

  // サーバーのソケットを生成
  fd1 = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if (fd1 < 0) {
    perror("socket");
    return -1;
  }

  // ソケットの設定
  memset(&saddr, 0, sizeof(saddr));
  saddr.sin_family = AF_INET;
  saddr.sin_port = htons(PORT);
  saddr.sin_addr.s_addr = htonl(INADDR_ANY);

  // 設定をbind
  if (bind(fd1, (struct sockaddr*)&saddr, sizeof(saddr))) {
    perror("bind");
    return -1;
  }

  // 接続待機
  if (listen(fd1, 5)) {
    perror("listen");
    return -1;
  }

  // 接続し次第子スレッドを生成
  // 続きは子スレッドが行う -> child_process
  while (1) {
    len = sizeof(caddr);  // 追加
    fd2 = accept(fd1, (struct sockaddr*)&caddr, &len);
    if (fd2 < 0) {
      perror("accept");
      exit(1);
    }

    if (pthread_create(&pt, NULL, (void*)(child_process), (void*)&fd2) < 0) {
      perror("pthread_create");
      return -1;
    }
    pthread_detach(pt);
  }

  return 0;
}

クライアント側は修正する必要はありません。

おわりに

このacceptのエラー、出るときもあれば出ないときもあって最初は無視していたんですが、後になって頻発したので、時と場合によると思われます。
今回はUbuntuで検証しましたが、他の方もCygwinでaccept: Bad addressとエラーが出て、同様の方法で解決したようです。一方で、別の環境(Gentoo Linuxなど)ではエラーの再現ができなかったとか…何故だろう?
ともかく、man pageにaddrlenを初期化するよう明記されているので、環境依存のエラーを回避するためにも、前もってaddrlenを初期化させたほうが良いかと思います。