Linuxシステムコール、INETドメイン ソケット

UNIX / Linux システムコール・プログラミング

プロセス間通信(IPC)、ソケット通信/INETドメイン ソケット

Unixドメイン ソケットとは?

Unixドメイン ソケットは、ノード間のネットワーク通信を行うことができ、現代のインターネット環境の基盤技術の1つと言えます。
INETドメイン ソケットを利用することで実際のネットワーク上のプロトコルであるTCP/IPやUDP/IPを意識したプログラミングが可能になります。

※今回のサンプルコードは、TCP/IPです。

基本的に、INETドメイン ソケットもUnixドメイン ソケットと同様の実装となりますが、異なるノード間での通信を実現するため、ネットワークアドレスやポート番号を意識する必要があります。

それでは、サーバー側のコードを以下に示します。

inet-dimain-server.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// Make server socket.
//
int makeServer(int portNo) {

  int                sfd;
  int                ret;
  struct sockaddr_in serv;

  // Create server socket.
  if((sfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
    perror("ERROR:Server Socket()");
    return -1;
  }

  // Bind socket
  memset(&serv, 0, sizeof(struct sockaddr_in));
  serv.sin_family = AF_INET;
  serv.sin_addr.s_addr = htonl(INADDR_ANY);
  serv.sin_port = htons(portNo);
  if ((ret = bind(sfd,
    (const struct sockaddr *)&serv,
    sizeof(struct sockaddr_in))) < 0) {
    perror("ERROR:Server bind()");
    return -1;
  }

  // Listen, prepair for accept()
  if((ret = listen(sfd, 10)) < 0) {
    perror("ERROR:Server listen()");
    return -1;
  }

  return sfd;
}
// Make data socket for client.
//
int acceptClient(int sfd) {

  struct sockaddr_in addr;
  int addrSz = sizeof(addr);
  int dfd;

  if((dfd = accept(sfd, (struct sockaddr*)&addr, &addrSz)) < 0) {
    perror("ERROR:Server accept()");
    return -1;
  }
  printf("Client Cennected : %s\n", inet_ntoa(addr.sin_addr));
  

  return dfd;
}

// This main()
//
int main(int argc, char** argv) {

  int  sfd, dfd;
  int  portNo;
  int  ret;
  char buf[256];

  // Check running argument.
  if(argc < 2) {
    printf("inet-domain-server port-No.\n");
    return -1;
  }
  portNo = atoi(argv[1]);

  if((sfd = makeServer(portNo)) < 0) {
    return -1;
  }

  if((dfd = acceptClient(sfd)) < 0) {
    close(sfd);    // Close server socket.
    return -1;
  }

  // Main loop
  while(1) {
    if((ret = read(dfd, buf, sizeof(buf))) < 0) {
      perror("ERROR:Data read()");
      return -1;
    }

    printf("%s", buf);

    if(strncmp(buf, "Quit", 4) == 0) {
      printf("Received Quit\n");
      break;
    }

    // Send to client "NeXT".
    if((ret = write(dfd, "NeXT", 4)) < 0) {
      perror("ERROR:Data write()");
      return -1;
    }
  }

  // Send "Quit" for client.
  write(dfd, "QUIT", 4);

  // Close data socket.
  close(dfd);

  // Close server socket.
  close(sfd);

  return 0;
}



サーバー側のコードについて説明します。
サーバー側のプログラムは引数でポート番号を指定して起動します。

まず、INETドメイン ソケットをsocket()システムコールを使って生成します。
  socket(AF_INET, SOCK_STREAM, 0)
    AF_INET、生成するソケットはINETドメイン ソケットである
      また、IPv4のアドレスファミリーである。
    SOCK_STREAM、バイトストリーム(TCP/IP)を指定
    0、SOCK_STREAM固有のプロトコルなので0を指定

  socket()は、ソケットへのファイルディスクリプタを返します。
このサーバー側のソケットはsocket()システムコールによってIPv4のアドレスファミリーで、TCP/IPプロトコルで動作するバイトストリームとして生成されます。

次にbind()システムコールを使ってソケットに名前を割り当てます。INETドメインの場合、bind()には、sockaddr_in構造体でアドレスファミリー、受け入れアドレス、受け入れポート番号を指定します。
  serv.sin_family = AF_INET;  ← IPv4ファミリ
  serv.sin_addr.s_addr = htonl(INADDR_ANY);
   → INADDR_ANYの場合ホストアドレスを指定しない
   → 指定した場合、そのアドレスからのみ受け入れる
  serv.sin_port = htons(portNo); ← ポート番号

  bind( sfd, (const struct sockaddr *)&name,
     (const struct sockaddr *)&serv,
     sizeof(struct sockaddr_in))
    sfd、socket()で得られたファイルディスクリプタ
    serv、アドレス構造体ポインタ
    構造体サイズ

このbind()によって、このソケットがネットワークへの受け入れ準備がととのいました。

次に、このソケットがサーバーとして動作するためにlisten()システムコールを使って、受け入れソケットにします。これは、Unixドメインのときと同じです。
  listen(sfd, 10) 
    sfd、ソケットファイルディスクリプタ
    backlog、同時に受け入れ可能なクライアント数

これで、クライアントの受け入れ準備ができました。
今度は、accept()システムコールを使ってクライアントからの要求を待ちます。
ただし、今回は接続してきたクライアントを確認できるようにしました。
  struct sockaddr_in addr;
  int addrSz = sizeof(addr);
  accept(sfd, (struct sockaddr*)&addr, &addrSz)
    sfd、受け入れソケットファイルディスクリプタ
    addr、sockaddr構造体ポインタ
    構造体サイズのポインタ

クライアントからの接続を待ち、接続が完了するとクライアント間ソケットのファイルディスクリプタが帰ります。
そして、sockaddr構造体にはクライアントのIPアドレスを確認することができます。

クライアントの接続ログを採ったり、特定のアドレスからの接続を遮断するなどをプログラミングで実現することが可能になります。

以降、このクライアント間ソケットを利用して双方向の通信が可能になります。
このコードでは、クライアントからのデータを待って表示を行い、クライアントからのデータが"Quit"である場合は終了します。それ以外の場合は"NeXT"をクライアントに送って次のデータを待ちます。
終了時は、クライアントに"QUIT"を送り、クライアント側にも終了を促し、クライアント間のソケット、クライアントの受け入れソケットを閉じて終了します。

Unixドメインのときと同様、このサンプルコードではクライアントとの接続は1つだけしか行っていませんが、複数のクライアントからの要求を処理するようなサーバーとしての動作も可能です。

それでは、今度はクライアント側のコードについて解説します。

inet-domain-client.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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

#define HOST_ADDR "192.168.1.16"

int main(int argc, char** argv) {

  int                dfd;
  int                portNo;
  int                ret;
  struct sockaddr_in serv;
  struct hostent*    servAddr;
  char               buf[256];
  char               rcv[256];

  // Check running argument.
  if(argc < 3) {
    printf("inet-domain-client server-address port-No.\n");
    return -1;
  }
  portNo = atoi(argv[2]);

  // Create socket
  if((dfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
    perror("ERROR:Client socket()");
    return -1;
  }

  // Connect server
  memset(&serv, 0, sizeof(struct sockaddr_in));
  serv.sin_family = AF_INET;
  serv.sin_port = htons(portNo);
  if((servAddr = gethostbyname(argv[1])) == NULL) {
    perror("ERROR:Client gethostbyname()");
    return -1;
  }
  bcopy((char*)servAddr->h_addr,
 (char*)&serv.sin_addr.s_addr,
 servAddr->h_length);

  if((ret = connect(dfd,
      (const struct sockaddr*)&serv,
      sizeof(struct sockaddr))) < 0) {
    perror("ERROR:Client connect()");
    return -1;
  }

  while(1) {
    // User input to send data.
    printf("#");
    memset(buf, 0, sizeof(buf));
    fflush(stdin);
    fgets(buf, 256, stdin);

    // Send to server.
    if((ret = write(dfd, buf, strlen(buf) + 1)) < 0) {
      perror("ERROR:Client read()");
      close(dfd);
      return -1;
    }

    // Reveive data from server.
    memset(rcv, 0, sizeof(rcv));
    if((ret = read(dfd, rcv, sizeof(rcv) - 1)) < 0) {
      perror("ERROR:Client read()");
      close(dfd);
      return -1;
    }
    printf("%s", rcv);

    if(strncmp(rcv, "QUIT", 4) == 0) {
      printf("\nReceived QUIT\n");
      break;
    }
  }

  // Close data socket.
  close(dfd);

  return 0;
}



クライアント側のプログラムは、サーバー側を起動したあとで実行します。
クライアント側のプログラムは起動時の引数で、接続先サーバーのIPアドレスとポート番号を指定します。ポート番号は、サーバーを起動したときに引数で指定したものと同じ番号です。

まず、Unixドメイン ソケットをsocket()システムコールを使って生成します。
  socket(AF_INET, SOCK_STREAM, 0)
    AF_INET、生成するソケットはINETドメイン ソケットである
      また、IPv4のアドレスファミリーである。
    SOCK_STREAM、バイトストリームを指定
    0、SOCK_STREAM固有のプロトコルなので0を指定

  socket()は、ソケットへのファイルディスクリプタを返します。
この部分はサーバー側と同じです。

次にconnect()システムコールを使って、サーバーに接続を行います。
  serv.sin_family = AF_INET;  ← IPv4ファミリ
  serv.sin_port = htons(portNo); ← ポート番号(第2引数)

  servAddr = gethostbyname(argv[1])
  第1引数で指定されたIPアドレスをgethostbyname()を使って変換します。
  変換したアドレスは、serv.sin_addr.s_addrに格納します。

  connect(dfd, (const struct sockaddr*)&serv, sizeof(struct sockaddr))
    dfd、socket()で得られたファイルディスクリプタ
serv、接続するサーバーのアドレス構造体ポインタ
    構造体サイズ

この時点でサーバー側がaccept()で待っている状態であれば、サーバー側で受けれられて通信が可能となります。

このコードではユーザーからの入力をサーバに送り、サーバーからの応答を待ちます。サーバーからの応答が"QUIT"であったとき(ユーザーが"Quit"を入力したとき)はソケットのファイルディスクリプタを閉じて終了します。
それ以外の応答である場合は、ユーザー入力を続けます。


実行サンプル

サーバー側

$ ./inet-domain-server 99999
Client Cennected : 192.168.0.130
HogeHoge
Moshi Moshi
Quit
Received Quit
$
クライアント側

$ ./inet-domain-client 192.168.0.5 99999
#HogeHoge
NeXT#Moshi Moshi
NeXT#Quit
QUIT
Received Quit
$


ソケットについて

ソケットはもともとBSD系のUnixに実装されたものでした。共有メモリなどはSystem V IPCであると伝えましたが、以前UNIXは、System V系とBSD系の2系統に大きく別れ、APIも異なっていた時代がありました。それがSVR4で統合され、現在に至っています。

APIもいいものは生き残る。
でしょうか?



Have a Happy Hucking!!

Lightning Brains

コメント

このブログの人気の投稿

Linuxシステムコール、共有メモリの使い方

Linuxシステムコール、メッセージキューの使い方

Linuxシステムコール、セマフォの使い方