LoginSignup
10
5

More than 3 years have passed since last update.

C言語でUDPサーバーを立てる

Last updated at Posted at 2019-08-27

C言語でサーバーを立てること

c言語でサーバーを立てることは難しい。
本記事は新人や、サーバーを書いたことのない人でも、とりあえず動かせて、ちょっと質問されてもある程度答えられるようになることを目的にしたものです。
あなたがサーバー構築をしなければいけなくて、今仕事中であるならば、下記のコードをコピペして、業務仕様に合わせて書き換えていけば時間短縮になると思います。

今回はUDPの中でも非常にシンプルな作りになります。
実際の要件ではこれでは済まない場合があると思います。
しかしまったく手も足も出ない状態よりは、一応つながっている状態の方が先に進みやすいので
ここから色々いじって要件に合ったコードにしてください。

この記事ではUDPとは何か、といった説明はありません。インターネットに腐るほど解説がありますのでそちらでお願いします。
書籍ならコンピュータネットワーク5版とか良いんですけど、値段と厚さがアレなんで…

環境

一応試した環境をのせるけど、比較的最近のlinuxなら動くと思う。

Ubuntu 18.04.2 LTS
gcc 7.4.0

簡単なUDPサーバー

UDPは実装から見るとシンプルなTCPと考えることができる。

簡単なサーバーとして

  • データはlocalhost 9002から受け取る
  • 受け取ったデータを表示する

とりあえず、コピペすれば動くコードはこんな感じ。

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

#include <stdio.h>
#include <string.h>

int main ()
{
  //1. 受信するアドレスを構築する
  struct addrinfo hints;
  memset (&hints, 0, sizeof (hints));
  hints.ai_socktype = SOCK_DGRAM;

  struct addrinfo *bind_address;
  getaddrinfo ("localhost", "9002", &hints, &bind_address);


  //2. ソケットを作成する
  int  socket_listen;
  socket_listen = socket (bind_address->ai_family,
              bind_address->ai_socktype,
              bind_address->ai_protocol);
  if (socket_listen < 0)
  {
      fprintf (stderr, "socket() failed. (%d)\n", errno);
      return 1;
  }


  //3. ローカルアドレスをソケットにバインドする
  if (bind (socket_listen, bind_address->ai_addr, bind_address->ai_addrlen))
  {
      fprintf (stderr, "bind() failed. (%d)\n", errno);
      return 1;
  }
  freeaddrinfo (bind_address);


  //4. クライアントの接続を待つ
  while (1)
  {
      struct sockaddr_storage client_address;
      socklen_t client_len = sizeof (client_address);

      char read[4096];
      memset(read, 0, sizeof(read));
      int bytes_received = recvfrom (socket_listen, read, 4096, 0,
                     (struct sockaddr *) &client_address,
                     &client_len);
      if (bytes_received < 1)
      {
        fprintf (stderr, "connection closed. (%d)\n", errno);
        return 1;
      }
      printf("receiving: %s", read);
  }

  //5. ソケットを閉じる
  close(socket_listen);
  printf ("Finished.\n");

  return 0;
}

ちょっと解説的なもの

1.受信するアドレスを構築する

接続用の構造体を構築する方法はいくつかる。

  • 手作業でメンバーを一つ一つ設定する方法
  • gethostbyname関数を使う方法
  • getaddrinfo関数を使う方法

今回は3つ目の方法で構築した。

getaddrinfo関数を使うには3つの作業がいる。

  1. 受信するアドレスを決める
  2. 受信するポート番号を決める
  3. ヒントになるaddrinfo構造体を定義する

3のaddrinfo構造体は普通、ip4かip6、TCPかUDPかを設定する。
ipは0であればip4になる。
SOCK_DGRAMはデータグラムのことで、設定するとUDPになる。

今回は使ってないが、getaddrinfo関数の戻り値はerrnoなので、ちゃんとやるなら受け取った方が良い。

2.ソケットを作成する

これは誰がやってもこうなる完全手癖で書くやつ。
ソケットとは(プログラム上の)通信の端子のことで、OSはソケットを通じて受送信する。
socket関数はソケットディスクリプターを作成する。
ソケットディスクリプターはファイルディスクリプターの一種である。

ファイルディスクリプタとはファイルをOSが扱えるようにするための一意の番号である。
linuxは様々なものをファイルとして扱う。
テキストファイル、デバイス(USBメモリとか)など。
ネットワークに対してもファイルと同じように読み書きができるようにするのが
ソケットディスクリプターの役割である。

socket関数の戻り値がソケットディスクリプターである。

3.ローカルアドレスをソケットにバインドする

これも手癖。
bind関数で受信するアドレスをソケットに関連付ける。
これで受信の準備が整う。

4.クライアントの接続を待つ

もっとも重要な処理。
本チャンではここで色々やるんだろうね。
普通サーバーは落とさないので無限ループする。
受信するにはrecvfrom関数を使う。
recvfrom関数は送信元の情報を受け取ります。
sockaddr_storage構造体はip4でもip6でも表現できる構造体。

5.ソケットを閉じる

ソケットもメモリーやスレッドと同じようにリークを起こすので、開放する必要がある。
close関数はファイルを閉じる時に使う。
ソケットもファイルとして扱えるので、ファイルと同じclose関数で開放することができる。

一応クライアントも

いらない気もするけどね。
UDPなのにconnect関数読んでます。
実はUDPでもconnect関数使えます。
サーバーでもconnect関数を使った書き方ができます。
UDPでconnect関数を使った場合、TCPで使った時と違う動きをします。
ここの説明は……うーん。

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

#include <stdio.h>
#include <string.h>

int main(void)
{
    struct addrinfo hints;
    memset(&hints, 0, sizeof(hints));
    hints.ai_socktype = SOCK_DGRAM;
    struct addrinfo *peer_addr;

    if(getaddrinfo("localhost","9002",&hints, &peer_addr))
    {
        fprintf(stderr, "getaddrinfo() failed. (%d)\n", GETSOCKETERRNO());
        return 1;
    }

    char addrbuf[100];
    char servbuf[100];
    getnameinfo(peer_addr->ai_addr, peer_addr->ai_addrlen, addrbuf, sizeof(addrbuf), servbuf, sizeof(servbuf), NI_NUMERICHOST);
    printf("%s %s\n", addrbuf, servbuf);

    puts("Creating socket");
    int socket_peer;
    socket_peer = socket(peer_addr->ai_family, peer_addr->ai_socktype, peer_addr->ai_protocol);
    if(!ISVALIDSOCKET(socket_peer))
    {
        fprintf(stderr, "socket() failed. (%d)\n", GETSOCKETERRNO());
        return 1;
    }

    if(connect(socket_peer,peer_addr->ai_addr, peer_addr->ai_addrlen))
    {
        fprintf(stderr,"connect() failed. (%d).\n", GETSOCKETERRNO());
        return 1;
    }

    freeaddrinfo(peer_addr);

    puts("Connected");

    while(1)
    {

        {
            char read[4096];
            if(!fgets(read, 4096, stdin)) break;

            int bytes = send(socket_peer, read, strlen(read), 0);
            printf("send %d bytes\n", bytes);
        }

    }
    puts("Closing socket");
    CLOSESOCKET(socket_peer);
    return 0;
}
10
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
5