【C言語】UDP通信を行う

C言語でUDP通信を行う方法の解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、C言語におけるUDP通信の実現方法について解説していきます。

このページを読めば、C言語で UDP 通信を行う方法だけでなく、UDP 通信の特徴や使い所も理解できると思いますし、ソケットの関数である recvrecvfrom の違い、さらには sendsendto の違いも理解できると思います。

UDP 通信に興味のある方や、UDP 通信を行おうと思ったけど上手くいかなかった方、UDP 通信を実装して上手く動作しているけどなぜ上手く動作しているかが理解できていない方など、様々な方に向けて詳しく分かりやすく解説していますので、是非このページを読み進めていただければと思います!

また、このページではソケットを利用して UDP 通信を実現していく方法を解説していきますので、ソケットについて理解されていない方は、事前に下記ページに目を通していただくことをオススメします。下記ページでは、ソケットを利用した TCP 通信のプログラム例も示していますので、これと対比して UDP 通信の説明を読み進めると、より理解が深まると思います!

ソケット通信解説ページのアイキャッチ 【C言語】ソケット通信について解説

UDP 通信とは?

最初に、UDP 通信とは何なのか?という点について解説しておきます。早くC言語での UDP 通信の実現方法を知りたいという方は、UDP 通信の流れ まで解説をスキップしていただければと思います。

まず、UDP は「User Datagram Protocol」の略語で、Datagram とは下記のようなものになります。

データグラム(英: datagram)は、配送成功・到達時間・到達順序がネットワークサービスによって保証されることがないパケット交換網における基本転送単位である。

引用元:Wikipedia

この説明からも分かるように、UDP 通信ではデータが相手に届くことが保証されません。これが UDP 通信の一番の特徴になります。

また、UDP 通信はコネクションレス型の通信となります。事前に接続を確立することなくデータの送信を行うことが可能です。

さらに、この UDP はトランスポート層のプロトコルであり、このトランスポート層のプロトコルには、UDP の他に TCP が存在します。この TCP と比較することで UDP のことがより理解しやすくなると思いますので、ここでは TCP 通信と UDP 通信の違いを説明していきたいと思います。

TCP 通信との違い

TCP 通信は、UDP 通信とは違い、データが相手に届くことが保証される通信プロトコルになります。

もう少し詳しく言うと、TCP 通信ではデータの送信後に相手から「データが受信できた」という返事を待つようになっています。そして、返事が来なかった場合はデータを再送するようになっています。このデータの再送の仕組みがあるため、データを確実に送信することが可能ではあるのですが、この再送の仕組みがあるために通信に時間がかかります。

TCP通信におけるデータの送信

そして、上記のような仕組みを機能させるために、データの送信前には必ず通信相手との接続を確立しておく必要があります。接続を確立した相手と上記のようなやり取りを行いながら確実にデータを送信できるように通信するプロトコルが TCP になります。

TCP通信で接続を確立してからデータの送受信を行う様子

それに対し、UDP 通信では単にデータの送信をするだけになります。つまり、相手がデータを受信したかどうかの確認は行われません。

UDP通信におけるデータの送信

そして、データの送信前には通信相手との接続の確立も不要です。接続を確立することなく、単にデータを送信すればよいだけになります。

UDP通信で接続を確立することなくデータの送受信を開始する様子

上記のような違いがあるため、TCP 通信に比べて UDP 通信は高速です。簡単に言えば、TCP 通信は安全性重視であるのに対し、UDP 通信はスピード重視の通信プロトコルとなります。

スポンサーリンク

UDP 通信の使いどころ

データが受信できない場合があると聞くと使い物にならないような気もしますが、実はそんなこともありません。

例えばビデオ通話などでは、動画の途中の一部のデータが正しく送受信できなかったとしても、一瞬動画にノイズが入ったり音声が途切れたりするくらいでビデオ通話自体は継続することができます。もし聞き取れなければ聞き返せばよいだけですよね。ですが、通信が遅いとラグが発生し、会話自体が成立しなくなる可能性もあります。なので、こういったリアルタイム性を重視するアプリケーションでは UDP 通信が行われることが多いようです。

それに対し、例えばメールなどは途中の文章が抜けていたら意味の分からないメールになってしまう可能性もあります。リアルタイム性も不要ですので、こう言った場合は TCP 通信の方が良いですよね。

UDP通信の使い所の説明図

UDP 通信と TCP 通信の違いのまとめ

ここまで説明してきた UDP 通信と TCP 通信の違いをまとめると下記のようになります。他にも違いや各プロトコルの特徴があったりするのですが、UDP 通信をプログラミングする上では、まずは下記くらいを理解しておくのでよいと思います。

 UDP通信TCP通信
特徴スピード重視安全性重視
接続確立が不要
(コネクションレス型)
確立が必要
(コネクション型)
再送制御なしあり

UDP 通信の流れ

次は、ソケットを利用して UDP 通信を行うときの処理の流れを解説していきます。ここでも、TCP 通信を行うときの流れと比較しながら説明していきたいと思います。

これらの決定的な違いはデータの送信前に接続を確立しておく必要があるか否かという点になります。TCP 通信の場合は接続を確立しておく必要があるため、そのための処理が必要となります。それに対し、UDP 通信の場合は接続を確立することなくデータの送信が可能です。

このため、UDP の方がシンプルな流れで通信を実現することが可能となります。

スポンサーリンク

TCP 通信と UDP 通信の流れの比較

では、TCP 通信と比較しながら UDP 通信を行う際の流れを説明していきます。

下図は、ソケットを利用してクライアントからサーバーにデータを送信し、さらに応答としてサーバーがクライアントにデータを送信する処理の流れのシーケンスを図示したものになります。左側が TCP 通信の場合、右側が UDP 通信の場合のシーケンスとなります。

TCP通信とUDP通信の処理の流れ

この図からも分かるように、TCP 通信と UDP 通信の処理の流れに関して言えば、違いはデータの送信前の接続の確立の有無のみとなります。TCP 通信の場合、この接続の確立を行うためにサーバー側はまず接続の受付を行い、クライアント側は接続の受付を行っているサーバーに対して接続要求を送信する必要があります。具体的に言えば、サーバー側は listenaccept を実行して接続の受付を行い、クライアント側から connect で接続要求をして接続を確立します。

この一連の流れにより、サーバーとクライアント側とで接続が確立されます。その後、接続を確立した相手同士でデータの送受信を行うことが可能となります。そして、ソケットをクローズすることで確立された接続もクローズされることになります。

それに対し、UDP 通信の場合は接続の確立を行うことなくデータの送信を行うことが可能です。したがって、サーバー側での listen や accept、クライアント側での connect は不要となります。

また、前述でも説明したようなデータの再送制御などはソケット関連の関数内部で自動的に行われることになります。したがって、ソケットを利用したプログラムを開発する場合は、再送制御などを意識することなくプログラミングすることができます。

そのため、シーケンスに注目すれば、TCP 通信と UDP 通信とでは、結局「データの送信前の接続の確立の有無」の違いしかありません。非常にシンプルな話なのですが、この違いをしっかり頭に入れてプログラミングしないと、UDP 通信を行おうとしているのに接続の確立を行ってしまってエラーが発生してしまうようなことが起こりえます。なので、TCP 通信と UDP 通信の違いはしっかり理解した上でプログラミングしていくのが良いと思います

ただし、シーケンスに関して言えば上記のような違いしかないのですが、実際にプログラミングしようと思うと TCP 通信と UDP 通信とでは使用する関数関数への引数指定が異なります。次は、UDP 通信でのソケット通信に利用する関数について説明を行い、このあたりの補足を行っていきたいと思います。

UDP 通信で使用する関数

ということで、次はC言語で UDP 通信を行うにあたって使用するソケット関連の関数の説明をしていきます。

下記では LInux や MacOS のソケット関連の関数の紹介を行なっていきますが、Windows でも同様の関数が用意されているはずですので、Windows 上でソケット通信を行う場合は Windows 用のヘッダーファイルをインクルードして各関数を利用するようにしてください。

ソケットの作成 (socket)

TCP 通信と同様に、ソケット通信で UDP 通信を実現するためにはソケットの作成を最初に行う必要があります。

そして、このソケットは、UDP 通信でも TCP 通信でも socket 関数を実行することで作成することが出来ます。ただし、socket 関数に指定する引数が TCP 通信の場合と UDP 通信の場合とで異なります。この socket 関数は下記のように定義されており、UDP 通信を行うためには、第2引数 typeSOCK_DGRAM を指定する必要があります。ここが UDP 通信を行う上でのポイントの1つとなります(TCP 通信時には typeSOCK_STREAM を指定します)。

socket
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

スポンサーリンク

データの送信 (sendto)

ソケットを作成すれば、UDP 通信の場合は接続の確立を行うことなくデータの送信を実行することが可能となります(相手がデータの受信待ちになっている必要はあります)。

UDP 通信の場合、このデータの送信には sendto 関数を使用します。sendto は下記のように定義された関数となります。

sendto
#include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
    const struct sockaddr *dest_addr, socklen_t addrlen);

sendtosend の違い

下記ページで示したプログラムのソースコードではデータの送信時に send 関数を利用しています。下記ページで示しているのは TCP 通信によるデータの送受信を行うプログラムのソースコードであり、TCP 通信では send 関数を利用します。

ソケット通信解説ページのアイキャッチ 【C言語】ソケット通信について解説

それに対し、UDP 通信では sendto 関数を利用することになります。

これらの使い分けは単純で、send 関数は “接続の確立後” にデータを送信したい場合に使用し、sendto 関数は “接続を確立することなく” データを送信したい場合に使用します。

そもそも、send 関数ではデータの送信先の相手を特定するような情報を引数で指定することが不可です。事前に接続を確立しておくことが前提となっている関数になっており、接続を確立した相手に対してデータの送信を行う関数となっています。

send関数とsendto関数の違い1

なので、接続を確立せずに send 関数を実行するとエラーになります。ここまで説明してきたように、UDP 通信では接続を確立せずにデータの送受信を行うことになりますので send 関数は利用不可となります。

それに対し、sendto 関数では引数 dest_addr を指定することで、関数実行時にデータの送信相手を指定することが可能となっています。そのため、sendto 関数では接続を確立せずにデータの送信を行うことが出来ます。

send関数とsendto関数の違い2

ということで、UDP 通信においては接続を確立せずにデータの送受信を行うため、send 関数ではなく sendto 関数を利用する必要があります。

引数 dest_addr

送信相手を指定する dest_addr 引数について補足しておくと、この引数では送信相手となる情報のデータのアドレスを指定します。これにより、その情報に基づいた送信相手にデータが送信されることになります。

特に手動で dest_addr 引数に指定するデータを用意する場合、struct sockaddr_in という構造体の変数を用意し、dest_addr 引数にはこの変数のアドレスを指定することになります。

struct sockaddr_in 構造体を利用するのは、この構造体に IP アドレスやポート番号をセット可能なメンバーが用意されているからになります。具体的には、sin_addr.s_addr メンバーに IP アドレスが、sin_port にポート番号がセット可能になっています。

この struct sockaddr_in 構造体の変数の sin_addr.s_addr に送信先の IP アドレスを、sin_port に送信先のポート番号をセットした状態で、この変数のアドレスを dest_addr 引数に指定して sendto 関数を実行することで、指定した IP アドレス・ポート番号にデータが送信されることになります。実際の dest_addr の型は struct sockaddr_in ではなく struct sockaddr のアドレスとなるのですが、気にせず struct sockaddr_in の構造体の変数を用意し、その変数のアドレスを引数に指定してやれば良いです(コンパイル時の警告が気になるのであればキャストすれば OK)。dest_addr 引数への具体的な指定例に関しては後述の UDP 通信の簡単なプログラム例 を参照していただければと思います。

sendto関数の引数の説明図

後述で説明するように、dest_addr 引数に指定するデータは recvfrom 関数の src_addr 引数から取得することが可能です。この場合は、IP アドレスやポート番号を開発者がセットする必要がないため、struct sockaddr 型の変数を利用するので問題ありません。

また、sendto 関数の場合、dest_addr 引数に指定したアドレスのデータのサイズを addrlen 引数に指定する必要があります。

その他の引数に関しては send 関数と同じになります。つまり、第1引数 sockfd にはソケット(のファイルディスクリプタ)、第2引数 buf には送信するデータのアドレス、第3引数 len には送信するデータのサイズ、第4引数 flags には送信の詳細オプションを指定します。

ここまで説明してきたように、UDP 通信では send 関数ではなく sendto 関数を使用する必要があります。そして、sendto 関数実行時にはデータの送信先となる IP アドレスとポート番号を適切に指定する必要があるという点について注意してください。

また、これは UDP 通信に限った話ではないのですが、データの送信を正常に行うためにはデータの受信待ちを行っているアドレス・ポートに対して送信を行う必要があります。

次は、このデータの受信待ちを行う関数について説明していきます。

データの受信 (recvrecvfrom)

データの受信待ちおよびデータの受信を行う関数については2つを紹介していきます。

recv

最初に説明するのは recv になります。これは、下記ページで紹介している TCP 通信でのデータの受信を行う際にも利用している関数になります。

ソケット通信解説ページのアイキャッチ 【C言語】ソケット通信について解説

recv は下記のように定義された関数となります。第1引数 sockfd にはソケット(のファイルディスクリプタ)、第2引数 buf には受信したデータを格納するバッファ、第3引数 len にはバッファのサイズ、第4引数 flags には受信の詳細オプションを指定します。 

recv
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

recv は実行すると受信待ち状態となり、データを受信したタイミングで関数を終了する関数となります(受信待ちを行わないように設定することも可能ですが、このページでは受信待ちを行うことを前提に解説していきます)。

ただし、特にサーバー・クライアント型通信におけるサーバー側では recv 関数は単に実行するだけではダメで、事前に第1引数に指定するソケットに対して bind 関数を実行しておく必要があります(bind 関数については後述で説明します)。

そして、この bind 関数は自身の IP アドレスと受信待ちを行うポートを指定して実行する必要があります。これにより、それらの IP アドレスやポートがソケットに関連付けられ、そのソケットを第1引数に指定して recv 関数を実行すると、その IP アドレス・ポートに対して受信待ちが行われるようになります。

bindの意味合いを説明する図

そして、その IP アドレス&ポート番号に対してデータが送信されてきた際に recv 関数がそのデータを受信することになります。当然ですが、受信待ちしていない IP アドレスやポート番号に対してデータの送信が行われても recv 関数はデータの受信を行いません。そのため、特に sendto 関数を利用する場合は、引数 dest_addr に指定する変数のメンバーには “受信待ちされている IP アドレスやポート番号” をセットしておく必要があります。

sendtoでは受信待ちされているIPアドレス・ポート番号に送信を行う必要があることを説明する図

TCP 通信を行う場合は接続の受付(accepct 関数の実行)を行うソケットに対して bind を行うことになりますが、UDP 通信を行う場合は受信待ちを行うソケットに対して bind を行い、受信待ちする IP アドレスやポート番号を事前にソケットに設定をしておく必要がある点に注意してください。

recvfrom (データの受信)

次に紹介するのが recvfrom 関数になります。recvfrom は下記のように定義された関数となります。

recvfrom
#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
    struct sockaddr *src_addr, socklen_t *addrlen);

基本的には recv 関数と使い方や動作は同じになります。が、recvfrom 関数では recv 関数に存在しない引数が2つ存在します。それが src_addraddrlen になります。

src_addr 引数に struct sockaddr 型の変数のアドレスを、addrlen 引数に socklen_t の変数のアドレスを引数に指定して recvfrom 関数を実行すれば、データを受信したときにデータを送信してきた相手の情報が src_addr 引数に指定したアドレスに、さらにその情報のサイズが addrlen 引数のアドレスにそれぞれ格納されることになります。

MEMO

socklen_t の変数には、予め struct sockaddr の型のサイズを格納しておく必要があります

つまり、recvfrom 関数を利用すれば、データ受信時に、そのデータを送信してきた相手の情報が取得可能です。

recvfromで送信元の情報が取得できることを説明する図

これは、データを受信した際に、データの送信元に対して応答となるデータを送信するときに非常に便利です。前述のとおり、sendto 関数では引数に struct sockaddr_in 型の変数を用意し、その変数のメンバーに送信先となる相手のアドレスや送信先の相手が受信待ちしているポートをわざわざ設定する必要があります。

ですが、recvfrom 関数を利用すれば、recvfrom 関数でデータを受信した際にそれらの情報が格納された変数を  src_addr 引数から取得することが可能です。さらに、addrlen 引数により、src_addr 引数で指定したアドレスに格納されたデータのサイズも取得可能です。

したがって、recvfrom 関数を利用してデータの受信を行い、そのデータを送信してきた相手に応答を返すのであれば、recvfrom 関数の src_addr 引数と addrlen 引数に指定した変数を sendto 関数の dest_addr 引数と addrlen 引数にそのまま指定してやれば良いことになります。

recvfromとsendtoを利用してデータの送信元に応答を返却する様子

そして、特にサーバー・クライアント型の通信を行う場合、サーバーはクライアントからリクエストを受け取り、そのレスポンスを返却する形でデータの送受信を行うことになるため、上記のように recvfromsendto を利用するのが楽だと思います。

逆に、recv 関数を利用する場合、送信元の情報がデータ受信時に取得できないため、予め送信元の情報を知った上でプログラミングする必要があります。例えば、サーバー側はクライアントが受信待ちをしているポートをソースコード上に記述して sendtodest_addr 引数に指定する変数のメンバーの設定を行う必要があります。

このあたりの、recvfrom 関数を利用する場合のソースコードと recv 関数を利用する場合のソースコードの具体的な違いに関しては、後述の UDP 通信の簡単なプログラム例 を参照していただければと思います。

ソケットとアドレスを関連付ける (bind)

ここまでの説明でも登場したように、特にサーバー側で recvrecvfrom 関数で受信待ちを行うためには、事前に受信待ちを行うアドレス(IP アドレスとポート番号)とソケットとを関連付けておく必要があります。この関連付けを行うのが bind 関数となります。

bind
#include <sys/socket.h>

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

クライアント側では、この bind は必須ではありません。これは、クライアント側では自動的にソケットに対してポート番号が関連付けられるようになっているからになります。おそらく、sendto 関数を実行した際に、第1引数に指定したソケットに対して動的にポート番号が割り当てられて関連付けられることになります。

そのため、sendto 関数の第1引数に指定したものと同じソケットで recvrecvfrom 関数を実行すれば、その動的に割り当てられたポート番号で受信待ちすることが出来ます。なので、動的に割り当てられたポート番号で受信待ちを行うので良いのであれば、クライアント側に関しては bind は行う必要はありません。

クライアント側でbindを実行する必要がない理由を説明する図

ただし、サーバーは sendto 関数でデータを送信する前に受信待ちを行うため、事前にポート番号をソケットに bind で関連付けておく必要があります。どのポート番号を使うかは開発者が決めておく必要があります(クライアントが bind が不要なのは、最初にデータの送信を sendto 関数で行うからであって、そこでソケットに動的に割り当てられたポート番号が関連付けられます)。

そして、クライアントは、その受信待ちをしているポート番号に対してデータの送信を行う必要があります。送信先のポート番号は、sendto 関数の dest_addr 引数で指定する必要があります。つまり、クライアントはサーバーが受信待ちしているポート番号を事前に知っている必要があります。

サーバー側でbindを実行する必要があることを示す図

それに対し、サーバー側は recvfrom 関数でデータの受信を行うのであれば、クライアント側が受信待ちするポート番号を事前に知っておく必要はありません。というか、動的に割り当てされるので、そのポート番号を事前に知っておくことは不可能です。

ですが、recvfrom (データの受信) で解説したように、データ受信時に recvfrom 関数の src_addr 引数からデータの送信元の情報を取得することができ、このデータにクライアント側が受信待ちするポート番号もセットされているため、src_addr 引数に指定したアドレスをそのまま sendto 関数の dest_addr 引数に指定してやれば、クライアント側で受信待ちしているポート番号を意識することなくサーバーからクライアントへのデータの送信を実現することが可能となります。

クライアントのポート番号を知らなくてもサーバーがデータの送信が可能である理由を示す図

ちょっとこの辺りがややこしいのですが、特にサーバー側を開発する際はデータの受信に recvfrom 関数を利用してデータの送信元の情報を src_addr引数から取得できるようにし、取得した情報を sendto 関数の dest_addr 引数に指定することを心がければ、割とすんなりデータの送受信を実現できるようになると思います。

また、recvfrom 関数ではなく recv 関数をデータの受信に利用する場合は、(おそらく)データ受信時にデータの送信元の情報が取得できません。そのため、この場合はクライアント側でも bind を行なって特定のポート番号に対して受信待ちを行うようにする必要があります。そして、サーバー側は、クライアント側が受信待ちしているポート番号にデータの送信が行えるよう、sendto 関数の引数を指定して送信を行う必要があります。

サーバー側でrecv関数を利用した場合にクライアント側でもbindが必要になることを示す図

クライアント側でも bind が必要にる例に関しても、後述の UDP 通信の簡単なプログラム例 で紹介したいと思います。

スポンサーリンク

ソケットを閉じる (close)

UDP 通信では接続の確立を行うことなくデータの送受信を行うことが可能ではあるのですが、TCP 通信同様に UDP 通信においても不要になったソケットはクローズが必要になります。このクローズに関しては、TCP 通信同様に close 関数により行うことが出来ます。

close
#include <unistd.h>

int close(int fd);

他にも UDP 通信を行う際に使用可能な関数はあるのですが、まずはここまで説明してきた関数を利用すれば最低限 UDP 通信によるデータの送受信を実現できるようになります。

UDP 通信の簡単なプログラム例

最後に、UDP 通信を行うプログラムのソースコードの簡単な例を示しておきます。

基本は下記ページで紹介しているソースコードを UDP 通信用に変更したソースコードの紹介を行なっていきます。

ソケット通信解説ページのアイキャッチ 【C言語】ソケット通信について解説

ここでは、データの受信に recvfrom 関数を利用する例と recv 関数を利用する例の2種類のプログラムのソースコードを示していきます。さらに、それぞれでサーバー側のプログラムのソースコードとクライアント側のプログラムのソースコードを示していきます。そのため、合計4つのソースコードを示していきます。

簡単に、紹介するソースコードのプログラムの動作について説明しておきます。

まず、サーバー側は起動するとクライアント側からの受信待ちを行います。また、クライアント側は起動すると scanf 関数で文字列の入力受付を行います。クライアントに対して文字列入力が行われると、クライアントは入力された文字列をサーバーに sendto 関数で送信し、その後に受信待ちを行います。サーバーは、その文字列を受信すると、受信した文字列を printf で出力した後にクライアントに 1 を応答として返却し、その後再度受信待ちを行います。

基本的には、このようなサーバーとクライアントの間でのデータの送受信を繰り返し行うのみのプログラムとなるのですが、サーバーは "finish" という文字列を受信した際には応答として 0 を返却するようになっており、クライアントはサーバーから 0 を受信するとプログラムを終了するようになっています。

recvfrom を使用した UDP 通信の例

まず、データの受信に recvfrom 関数を使用するソースコードの例を示していきます。

サーバープログラム

まず、サーバー側のプログラムのソースコードを紹介します。下記の server_recvfrom.c が、UDP 通信を行うサーバープログラム(データの受信に recvfrom を利用)のソースコードの例となります。

server_recvfrom.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#define SERVER_ADDR "127.0.0.1"
#define SERVER_PORT 50001
#define BUF_SIZE 1024

int transfer(int);

int transfer(int sock) {

    int recv_size, send_size;
    char recv_buf[BUF_SIZE], send_buf;
    struct sockaddr addr;
    socklen_t addrlen = sizeof(addr);

    while (1) {

        /* クライアントから文字列を受信 */
        printf("Wait recv...\n");
        recv_size = recvfrom(sock, recv_buf, BUF_SIZE, 0, &addr, &addrlen);
        
        if (recv_size == -1) {
            printf("recv error\n");
            break;
        }
        if (recv_size == 0) {
            /* 受信サイズが0の場合は相手がソケットを閉じていると判断 */
            printf("Transfer ended\n");
            break;
        }

        /* 受信した文字列を表示 */
        printf("%s\n", recv_buf);
        
        /* 文字列が"finish"ならクライアントとの接続終了 */
        if (strcmp(recv_buf, "finish") == 0) {

            /* 接続終了を表す0をaddrに送信 */
            send_buf = 0;
            send_size = sendto(sock, &send_buf, 1, 0, &addr, addrlen);
            if (send_size == -1) {
                printf("send error\n");
                break;
            }
            break;
        } else {
            /* "finish"以外の場合はクライアントとの接続を継続するために1をaddrに送信 */
            send_buf = 1;
            send_size = sendto(sock, &send_buf, 1, 0, &addr, addrlen);
            if (send_size == -1) {
                printf("send error\n");
                break;
            }
        }
    }
    
    return 0;
}

int main(void) {
    int sock;
    struct sockaddr_in addr;

    /* ソケットを作成 */
    sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock == -1) {
        printf("socket error\n");
        return -1;
    }
    
    /* 受信待ちするIPアドレス・ポート番号をsockにbind */
    memset(&addr, 0, sizeof(struct sockaddr_in));
    addr.sin_family = AF_INET;
    addr.sin_port = htons((unsigned short)SERVER_PORT);
    addr.sin_addr.s_addr = inet_addr(SERVER_ADDR);

    if (bind(sock, (const struct sockaddr *)&addr, sizeof(addr)) == -1) {
        printf("bind error\n");
        close(sock);
        return -1;
    }

    while (1) {
        /* ソケットでデータのやり取り */
        transfer(sock);
    }

    /* ソケットをクローズ */
    close(sock);

    return 0;
}

上記のソースコードでは、まず socket 関数でソケットを生成し、SERVER_ADDRSERVER_PORTbind 関数でソケットに関連付けを行なった後に transfer 関数の実行を行っています。transfer は自作の関数で、データの送受信を実施する関数となります。

そして、transfer 関数では最初に recvfrom 関数を実行しているため、ここで SERVER_ADDRSERVER_PORT に対してデータの受信待ちが行われる事になります

また、transfer 関数内部では recvfrom 関数を利用しているため、データを受信した際には src_addr 引数からデータの送信元の情報を取得可能です。さらに、addrlen 引数から、その送信元の情報のサイズが取得可能です。そのため、上記では、データを受信した後に recvfrom 関数の src_addr 引数に指定している addr と、addrlen 引数に指定している addrlen をそれぞれ sendto 関数の dest_addr 引数と addrlen 引数に指定して sendto 関数を実行し、データを送信してきた相手に応答を返却するようにしています。

ここまで何回も説明してきたように、UDP 通信では接続の確立は不要となる点がポイントの1つとなります。このため、TCP 通信を行う際には必要となる listen 関数や accept 関数の実行が UDP 通信の場合は不要になります。

また、クライアント側が受信待ちしているポート番号を意識せずにクライアント側へのデータの送信が行われている点もポイントになると思います。

クライアントプログラム

次にクライアント側のプログラムのソースコードを紹介します。下記の client_recvfrom.c が、UDP 通信を行うサーバープログラム(データの受信に recvfrom を利用)のソースコードの例となります。この例の場合、recvfrom 関数の代わりに recv を利用しても問題ありません。

client_recvfrom.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#define SERVER_ADDR "127.0.0.1"
#define SERVER_PORT 50001
#define BUF_SIZE 1024

int transfer(int);

int transfer(int sock) {
    char send_buf[BUF_SIZE], recv_buf;
    int send_size, recv_size;
    struct sockaddr_in addr;
    socklen_t addrlen = sizeof(addr);

    /* 送信先のIPアドレス・ポート番号を設定 */
    memset(&addr, 0, sizeof(struct sockaddr_in));
    addr.sin_family = AF_INET;
    addr.sin_port = htons((unsigned short)SERVER_PORT);
    addr.sin_addr.s_addr = inet_addr(SERVER_ADDR);

    while (1) {

        /* サーバーに送る文字列を取得 */
        printf("Input Message...\n");
        scanf("%s", send_buf);

        /* 文字列をaddrに送信 */
        send_size = sendto(sock, send_buf, strlen(send_buf) + 1, 0, (struct sockaddr *)&addr, addrlen);
        if (send_size == -1) {
            printf("send error\n");
            break;
        }


        /* サーバーからの応答を受信 */
        recv_size = recvfrom(sock, &recv_buf, 1, 0, (struct sockaddr *)&addr, &addrlen);
        if (recv_size == -1) {
            printf("recv error\n");
            break;
        }

        if (recv_size == 0) {
            /* 受信サイズが0の場合は相手が接続閉じていると判断 */
            printf("Transfer ended\n");
            break;
        }

        
        /* 応答が0の場合はデータ送信終了 */
        if (recv_buf == 0) {
            printf("Finish transfer\n");
            break;
        }
    }

    return 0;
}

int main(void) {
    int sock;
    
    /* ソケットを作成 */
    sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock == -1) {
        printf("socket error\n");
        return -1;
    }

    /* ソケットでデータのやり取り */
    transfer(sock);

    /* クローズ */
    close(sock);

    return 0;
}

クライアント側においても、TCP 通信を行う際には実行が必要であった connect 関数の実行が不要になります。

ただし、データの送信で sendto 関数を実行する際には、サーバー側で受信待ちされている IP アドレスとポート番号をメンバーに設定した変数のアドレスを dest_addr 引数に、さらにその変数のサイズを addrlen 引数に指定してサーバー側に対してデータが送信されるようにする必要があります。

データ送信後は recvfrom 関数でサーバーから送信されてくる応答の受信を行います。recvfrom 関数を利用していますが、データ受信時に設定される addr 変数の情報は利用しないため、recv 関数に置き換えても正常に動作します。

クライアント側のポイントは受信待ちを行なっているにも関わらず bind が不要という点になると思います。bind が不要なのは、クライアント側で動的にポート番号が割り当てられ、その割り当てられたポート番号に対して受信待ちが行えるようになっているから&サーバー側がクライアントが受信待ちしているポート番号を recvfrom 関数の src_addr 引数から取得できるようになっているからになります。

プログラムの実行手順

続いて、上記で紹介した2つのソースコードのプログラムを実行する手順について説明します。

まず、ターミナル等のコマンドが実行可能なアプリを2つ起動し、両方とも上記のソースコードを保存したフォルダに移動します。

続いて、一方のターミナルで下記コマンドを実行してサーバープログラムを生成します。

gcc server_recvfrom.c -o server.exe

続けて下記コマンドを実行してサーバープログラムを起動します。これにより、サーバープログラムが受信待ち状態になります。

./server.exe
Wait recv...

次に、他方側のターミナルで下記コマンドを実行してクライアントプログラムを生成します。

gcc client_recvfrom.c -o client.exe

続けて下記コマンドを実行してクライアントプログラムを起動します。これにより、クライアントプログラムが文字列の入力受付状態になります。

./client.exe
Input Message...

この状態で、クライアントプログラムを起動しているターミナルに適当な文字列を入力してエンターキーを押します。

Input Message...
Hello!

これにより、入力された文字列がサーバー側に送信され、クライアント側は受信待ち状態となります。それと同時に、サーバーがクライアントから送信されてきた文字列を受信し、その受信した文字列がターミナルに出力されます。

Wait recv...
Hello!

そして、サーバーが受信した旨を伝えるメッセージをクライアントに送信し(1 を送信するようになっている)、それを受信したクライアントプログラムは再度文字列の入力受付状態になります。また文字列を入力してエンターキーを押せば、同様の動作を確認することが出来るはずです。

あとは、入力された文字列が "finish" となるまで同じことが繰り返されます。プログラムを終了したい場合は ctrl + c を入力してプログラムを強制終了させてください。

このような動作より、サーバープログラムとクライアントプログラムで文字列の送受信が行われていることが確認できると思います。また、ここまでの説明のとおり、この通信は UDP 通信により行われています。

スポンサーリンク

recv を使用した UDP 通信の例

次は、データの受信に recv 関数を使用するソースコードの例を示していきます。

サーバープログラム

まず、サーバー側のプログラムのソースコードを紹介します。下記の server_recv.c が、UDP 通信を行うサーバープログラム(データの受信に recv を利用)のソースコードの例となります。

server_recv.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#define SERVER_ADDR "127.0.0.1"
#define SERVER_PORT 50001
#define CLIENT_ADDR "127.0.0.1"
#define CLIENT_PORT 50002
#define BUF_SIZE 1024

int transfer(int);

int transfer(int sock) {

    int recv_size, send_size;
    char recv_buf[BUF_SIZE], send_buf;
    struct sockaddr_in addr;
    socklen_t addrlen = sizeof(addr);

    while (1) {

        /* クライアントから文字列を受信 */
        printf("wait recv...\n");
        recv_size = recv(sock, recv_buf, BUF_SIZE, 0);
        if (recv_size == -1) {
            printf("recv error\n");
            break;
        }
        if (recv_size == 0) {
            /* 受信サイズが0の場合は相手が接続閉じていると判断 */
            printf("Trasfer ended\n");
            break;
        }
        
        /* 受信した文字列を表示 */
        printf("%s\n", recv_buf);

        memset(&addr, 0, sizeof(struct sockaddr_in));

        /* サーバーのIPアドレスとポートの情報を設定 */
        addr.sin_family = AF_INET;
        addr.sin_port = htons((unsigned short)CLIENT_PORT);
        addr.sin_addr.s_addr = inet_addr(CLIENT_ADDR);

        /* 文字列が"finish"ならクライアントとの接続終了 */
        if (strcmp(recv_buf, "finish") == 0) {

            /* 接続終了を表す0をaddrに送信 */
            send_buf = 0;
            send_size = sendto(sock, &send_buf, 1, 0, (struct sockaddr *)&addr, addrlen);
            if (send_size == -1) {
                printf("send error\n");
                break;
            }
            break;
        } else {
            /* "finish"以外の場合はクライアントとの接続を継続するため1をaddrに送信 */
            send_buf = 1;
            send_size = sendto(sock, &send_buf, 1, 0, (struct sockaddr *)&addr, addrlen);
            if (send_size == -1) {
                printf("send error\n");
                break;
            }
        }
    }
    
    return 0;
}


int main(void) {
    int sock;
    struct sockaddr_in addr;

    /* ソケットを作成 */
    sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock == -1) {
        printf("socket error\n");
        return -1;
    }

    /* 受信待ちするIPアドレス・ポート番号をsockにbind */
    memset(&addr, 0, sizeof(struct sockaddr_in));
    addr.sin_family = AF_INET;
    addr.sin_port = htons((unsigned short)SERVER_PORT);
    addr.sin_addr.s_addr = inet_addr(SERVER_ADDR);

    if (bind(sock, (const struct sockaddr *)&addr, sizeof(addr)) == -1) {
        printf("bind error\n");
        close(sock);
        return -1;
    }

    while (1) {
        /* ソケットでデータのやり取り */
        transfer(sock);
    }

    /* ソケットをクローズ */
    close(sock);

    return 0;
}

recvfrom を使用した UDP 通信の例 で示したサーバープログラムのソースコードとの違いは sendto 関数の dest_addr 引数に指定するパラメーターを手動で設定しているところになります。より具体的には、変数 addr に送信先の IP アドレス(CLIENT_ADDR)とポート番号(CLIENT_PORT (50002))を手動で設定し、それを関数の引数に指定して sendto 関数の実行を行っています。

なぜ、このような面倒なことが必要になっているかというと、それは recv 関数を利用しているため、データを送信してきた相手の IP アドレスやポート番号が取得できないからになります。前述のとおり、recvfrom 関数を利用した場合は src_addr 引数からデータを送信してきた相手の IP アドレスおよびポート番号が取得可能です。ですが、今回は recv 関数を利用しているため、自身で設定を行う必要があります。

そして、次の節で説明するように、サーバー側は CLIENT_ADDRCLIENT_PORT に対してデータの送信を行なっているわけですから、クライアント側は、CLIENT_ADDRCLIENT_PORT に対して受信待ちを行うようにする必要があります。

クライアントプログラム

続いてクライアント側のプログラムのソースコードを紹介します。下記の client_recv.c が、UDP 通信を行うクライアントプログラム(データの受信に recv を利用)のソースコードの例となります。

client_recv.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#define SERVER_ADDR "127.0.0.1"
#define SERVER_PORT 50001
#define CLIENT_ADDR "127.0.0.1"
#define CLIENT_PORT 50002
#define BUF_SIZE 1024

int transfer(int);

int transfer(int sock) {
    char send_buf[BUF_SIZE], recv_buf;
    int send_size, recv_size;
    struct sockaddr_in addr;
    socklen_t addrlen = sizeof(addr);

    /* 構造体を全て0にセット */
    memset(&addr, 0, sizeof(struct sockaddr_in));

    /* サーバーのIPアドレスとポートの情報を設定 */
    addr.sin_family = AF_INET;
    addr.sin_port = htons((unsigned short)SERVER_PORT);
    addr.sin_addr.s_addr = inet_addr(SERVER_ADDR);

    while (1) {

        /* サーバーに送る文字列を取得 */
        printf("Input Message...\n");
        scanf("%s", send_buf);

        /* 文字列をaddrに送信 */
        send_size = sendto(sock, send_buf, strlen(send_buf) + 1, 0, (struct sockaddr *)&addr, addrlen);
        if (send_size == -1) {
            printf("send error\n");
            break;
        }

        struct sockaddr_in in_addr;
        unsigned int len;

        /* サーバーからの応答を受信 */
        recv_size = recv(sock, &recv_buf, 1, 0);
        if (recv_size == -1) {
            printf("recv error\n");
            break;
        }
        
        if (recv_size == 0) {
            /* 受信サイズが0の場合は相手が接続閉じていると判断 */
            printf("Transfer ended\n");
            break;
        }

        
        /* 応答が0の場合はデータ送信終了 */
        if (recv_buf == 0) {
            printf("Finish transfer\n");
            break;
        }
    }
    return 0;
}

int main(void) {
    int sock;
    struct sockaddr_in addr;
    
    

    /* ソケットを作成 */
    sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock == -1) {
        printf("socket error\n");
        return -1;
    }

    /* 受信待ちするIPアドレス・ポート番号をsockにbind */
    memset(&addr, 0x00, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons((unsigned short)CLIENT_PORT);
    addr.sin_addr.s_addr = inet_addr(CLIENT_ADDR);

    if (bind(sock, (const struct sockaddr *)&addr, sizeof(addr)) == -1) {
        printf("bind error\n");
        close(sock);
        return -1;
    }


    /* ソケットでデータのやり取り */
    transfer(sock);

    /* クローズ */
    close(sock);

    return 0;
}

サーバー側のプログラムでは sendto 関数で CLIENT_PORT (50002) に対して送信を行うようになっているため、クライアント側のプログラムでは CLIENT_PORT (50002) で受信待ちをする必要があります。そのため、予めソケットに対して bind を行って CLIENT_PORT と関連付けを行い、sendto 関数で SERVER_PORT に対してデータの送信を行ってから recv 関数で CLIENT_PORT で受信待ちを行うようにしています(bindmain 関数で実行しています)。

結局、recvfrom を使用した UDP 通信の例 で示したソースコードとは、クライアント側が受信待ちするポート(これはサーバー側のデータの送信先のポートと一致します)を動的に決めるか静的に決めるのかの違いしかないのですが、静的に決める場合はクライアント側が受信待ちするポート番号を自身で指定する必要があり、さらに、そのポートに対して bind 関数を実行してから recv 関数を実行する必要があって多少面倒です。

さらに、PC 上で複数のクライアントが動作して同時に受信待ちするような場合、クライアントごとに異なるポート番号を割り当てする必要があります。なので、クライアントの数分のポート番号を自身で決める必要があります。

それに対し、サーバー側で recvfrom 関数を使うようにすれば、サーバー側はクライアント側で受信待ちしているポート番号を意識することなく開発することが出来ますし、クライアント側は bind 無しに受信待ちを実現することが出来ます。

ということで、特に通信を確立せずにデータの送受信を行う UDP 通信においては、サーバー側での受信には recvfrom 関数を利用するのが良いと思います。

プログラムの実行手順

先ほど示した2つのソースコードのプログラムの実行手順に関しては プログラムの実行手順 と同様で、ファイル名の recvfromrecv に変更していただければコンパイルや実行が行えるはずです。なので、ここでの説明は省略させていただきます。

まとめ

このページではC言語におけるUDP通信の実現方法について解説しました!

C言語でソケットを利用して UDP 通信を行う場合、ソケットを作成するときに SOCK_DGRAMtype 引数に指定すること、および接続の確立が不要になる点がポイントになると思います。あとは、データの送受信に sendtorecvfrom を使ってやれば特に困ることなく UDP 通信が実現できると思います。クライアント側が受信待ちするポート番号を意識せずにプログラミングできるよう、recv よりも recvfrom を使うことをオススメします。

UDP 通信は TCP 通信よりも高速であるメリットがありますので、リアルタイム性を重視するアプリを開発するようなときは UDP 通信を利用することもぜひ検討してみてください!

同じカテゴリのページ一覧を表示