このページでは、C言語におけるUDP通信の実現方法について解説していきます。
このページを読めば、C言語で UDP 通信を行う方法だけでなく、UDP 通信の特徴や使い所も理解できると思いますし、ソケットの関数である recv
と recvfrom
の違い、さらには send
と sendto
の違いも理解できると思います。
UDP 通信に興味のある方や、UDP 通信を行おうと思ったけど上手くいかなかった方、UDP 通信を実装して上手く動作しているけどなぜ上手く動作しているかが理解できていない方など、様々な方に向けて詳しく分かりやすく解説していますので、是非このページを読み進めていただければと思います!
また、このページではソケットを利用して UDP 通信を実現していく方法を解説していきますので、ソケットについて理解されていない方は、事前に下記ページに目を通していただくことをオススメします。下記ページでは、ソケットを利用した TCP 通信のプログラム例も示していますので、これと対比して UDP 通信の説明を読み進めると、より理解が深まると思います!
【C言語】ソケット通信について解説Contents
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 になります。
それに対し、UDP 通信では単にデータの送信をするだけになります。つまり、相手がデータを受信したかどうかの確認は行われません。
そして、データの送信前には通信相手との接続の確立も不要です。接続を確立することなく、単にデータを送信すればよいだけになります。
上記のような違いがあるため、TCP 通信に比べて UDP 通信は高速です。簡単に言えば、TCP 通信は安全性重視であるのに対し、UDP 通信はスピード重視の通信プロトコルとなります。
スポンサーリンク
UDP 通信の使いどころ
データが受信できない場合があると聞くと使い物にならないような気もしますが、実はそんなこともありません。
例えばビデオ通話などでは、動画の途中の一部のデータが正しく送受信できなかったとしても、一瞬動画にノイズが入ったり音声が途切れたりするくらいでビデオ通話自体は継続することができます。もし聞き取れなければ聞き返せばよいだけですよね。ですが、通信が遅いとラグが発生し、会話自体が成立しなくなる可能性もあります。なので、こういったリアルタイム性を重視するアプリケーションでは UDP 通信が行われることが多いようです。
それに対し、例えばメールなどは途中の文章が抜けていたら意味の分からないメールになってしまう可能性もあります。リアルタイム性も不要ですので、こう言った場合は TCP 通信の方が良いですよね。
UDP 通信と TCP 通信の違いのまとめ
ここまで説明してきた UDP 通信と TCP 通信の違いをまとめると下記のようになります。他にも違いや各プロトコルの特徴があったりするのですが、UDP 通信をプログラミングする上では、まずは下記くらいを理解しておくのでよいと思います。
UDP通信 | TCP通信 | |
特徴 | スピード重視 | 安全性重視 |
接続 | 確立が不要 (コネクションレス型) |
確立が必要 (コネクション型) |
再送制御 | なし | あり |
UDP 通信の流れ
次は、ソケットを利用して UDP 通信を行うときの処理の流れを解説していきます。ここでも、TCP 通信を行うときの流れと比較しながら説明していきたいと思います。
これらの決定的な違いはデータの送信前に接続を確立しておく必要があるか否かという点になります。TCP 通信の場合は接続を確立しておく必要があるため、そのための処理が必要となります。それに対し、UDP 通信の場合は接続を確立することなくデータの送信が可能です。
このため、UDP の方がシンプルな流れで通信を実現することが可能となります。
スポンサーリンク
TCP 通信と UDP 通信の流れの比較
では、TCP 通信と比較しながら UDP 通信を行う際の流れを説明していきます。
下図は、ソケットを利用してクライアントからサーバーにデータを送信し、さらに応答としてサーバーがクライアントにデータを送信する処理の流れのシーケンスを図示したものになります。左側が TCP 通信の場合、右側が UDP 通信の場合のシーケンスとなります。
この図からも分かるように、TCP 通信と UDP 通信の処理の流れに関して言えば、違いはデータの送信前の接続の確立の有無のみとなります。TCP 通信の場合、この接続の確立を行うためにサーバー側はまず接続の受付を行い、クライアント側は接続の受付を行っているサーバーに対して接続要求を送信する必要があります。具体的に言えば、サーバー側は listen
と accept
を実行して接続の受付を行い、クライアント側から 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引数 type
に SOCK_DGRAM
を指定する必要があります。ここが UDP 通信を行う上でのポイントの1つとなります(TCP 通信時には type
に SOCK_STREAM
を指定します)。
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
スポンサーリンク
データの送信 (sendto
)
ソケットを作成すれば、UDP 通信の場合は接続の確立を行うことなくデータの送信を実行することが可能となります(相手がデータの受信待ちになっている必要はあります)。
UDP 通信の場合、このデータの送信には 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);
sendto
と send
の違い
下記ページで示したプログラムのソースコードではデータの送信時に send
関数を利用しています。下記ページで示しているのは TCP 通信によるデータの送受信を行うプログラムのソースコードであり、TCP 通信では send
関数を利用します。
それに対し、UDP 通信では sendto
関数を利用することになります。
これらの使い分けは単純で、send
関数は “接続の確立後” にデータを送信したい場合に使用し、sendto
関数は “接続を確立することなく” データを送信したい場合に使用します。
そもそも、send
関数ではデータの送信先の相手を特定するような情報を引数で指定することが不可です。事前に接続を確立しておくことが前提となっている関数になっており、接続を確立した相手に対してデータの送信を行う関数となっています。
なので、接続を確立せずに send
関数を実行するとエラーになります。ここまで説明してきたように、UDP 通信では接続を確立せずにデータの送受信を行うことになりますので send
関数は利用不可となります。
それに対し、sendto
関数では引数 dest_addr
を指定することで、関数実行時にデータの送信相手を指定することが可能となっています。そのため、sendto
関数では接続を確立せずにデータの送信を行うことが出来ます。
ということで、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 通信の簡単なプログラム例 を参照していただければと思います。
後述で説明するように、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 通信に限った話ではないのですが、データの送信を正常に行うためにはデータの受信待ちを行っているアドレス・ポートに対して送信を行う必要があります。
次は、このデータの受信待ちを行う関数について説明していきます。
データの受信 (recv
・recvfrom
)
データの受信待ちおよびデータの受信を行う関数については2つを紹介していきます。
recv
最初に説明するのは recv
になります。これは、下記ページで紹介している TCP 通信でのデータの受信を行う際にも利用している関数になります。
recv
は下記のように定義された関数となります。第1引数 sockfd
にはソケット(のファイルディスクリプタ)、第2引数 buf
には受信したデータを格納するバッファ、第3引数 len
にはバッファのサイズ、第4引数 flags
には受信の詳細オプションを指定します。
#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 アドレス・ポートに対して受信待ちが行われるようになります。
そして、その IP アドレス&ポート番号に対してデータが送信されてきた際に recv
関数がそのデータを受信することになります。当然ですが、受信待ちしていない IP アドレスやポート番号に対してデータの送信が行われても recv
関数はデータの受信を行いません。そのため、特に sendto
関数を利用する場合は、引数 dest_addr
に指定する変数のメンバーには “受信待ちされている IP アドレスやポート番号” をセットしておく必要があります。
TCP 通信を行う場合は接続の受付(accepct
関数の実行)を行うソケットに対して bind
を行うことになりますが、UDP 通信を行う場合は受信待ちを行うソケットに対して bind
を行い、受信待ちする IP アドレスやポート番号を事前にソケットに設定をしておく必要がある点に注意してください。
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_addr
と addrlen
になります。
src_addr
引数に struct sockaddr
型の変数のアドレスを、addrlen
引数に socklen_t
の変数のアドレスを引数に指定して recvfrom
関数を実行すれば、データを受信したときにデータを送信してきた相手の情報が src_addr
引数に指定したアドレスに、さらにその情報のサイズが addrlen
引数のアドレスにそれぞれ格納されることになります。
socklen_t
の変数には、予め struct sockaddr
の型のサイズを格納しておく必要があります
つまり、recvfrom
関数を利用すれば、データ受信時に、そのデータを送信してきた相手の情報が取得可能です。
これは、データを受信した際に、データの送信元に対して応答となるデータを送信するときに非常に便利です。前述のとおり、sendto
関数では引数に struct sockaddr_in
型の変数を用意し、その変数のメンバーに送信先となる相手のアドレスや送信先の相手が受信待ちしているポートをわざわざ設定する必要があります。
ですが、recvfrom
関数を利用すれば、recvfrom
関数でデータを受信した際にそれらの情報が格納された変数を src_addr
引数から取得することが可能です。さらに、addrlen
引数により、src_addr
引数で指定したアドレスに格納されたデータのサイズも取得可能です。
したがって、recvfrom
関数を利用してデータの受信を行い、そのデータを送信してきた相手に応答を返すのであれば、recvfrom
関数の src_addr
引数と addrlen
引数に指定した変数を sendto
関数の dest_addr
引数と addrlen
引数にそのまま指定してやれば良いことになります。
そして、特にサーバー・クライアント型の通信を行う場合、サーバーはクライアントからリクエストを受け取り、そのレスポンスを返却する形でデータの送受信を行うことになるため、上記のように recvfrom
と sendto
を利用するのが楽だと思います。
逆に、recv
関数を利用する場合、送信元の情報がデータ受信時に取得できないため、予め送信元の情報を知った上でプログラミングする必要があります。例えば、サーバー側はクライアントが受信待ちをしているポートをソースコード上に記述して sendto
の dest_addr
引数に指定する変数のメンバーの設定を行う必要があります。
このあたりの、recvfrom
関数を利用する場合のソースコードと recv
関数を利用する場合のソースコードの具体的な違いに関しては、後述の UDP 通信の簡単なプログラム例 を参照していただければと思います。
ソケットとアドレスを関連付ける (bind
)
ここまでの説明でも登場したように、特にサーバー側で recv
や recvfrom
関数で受信待ちを行うためには、事前に受信待ちを行うアドレス(IP アドレスとポート番号)とソケットとを関連付けておく必要があります。この関連付けを行うのが bind
関数となります。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
クライアント側では、この bind
は必須ではありません。これは、クライアント側では自動的にソケットに対してポート番号が関連付けられるようになっているからになります。おそらく、sendto
関数を実行した際に、第1引数に指定したソケットに対して動的にポート番号が割り当てられて関連付けられることになります。
そのため、sendto
関数の第1引数に指定したものと同じソケットで recv
や recvfrom
関数を実行すれば、その動的に割り当てられたポート番号で受信待ちすることが出来ます。なので、動的に割り当てられたポート番号で受信待ちを行うので良いのであれば、クライアント側に関しては bind
は行う必要はありません。
ただし、サーバーは sendto
関数でデータを送信する前に受信待ちを行うため、事前にポート番号をソケットに bind
で関連付けておく必要があります。どのポート番号を使うかは開発者が決めておく必要があります(クライアントが bind
が不要なのは、最初にデータの送信を sendto
関数で行うからであって、そこでソケットに動的に割り当てられたポート番号が関連付けられます)。
そして、クライアントは、その受信待ちをしているポート番号に対してデータの送信を行う必要があります。送信先のポート番号は、sendto
関数の dest_addr
引数で指定する必要があります。つまり、クライアントはサーバーが受信待ちしているポート番号を事前に知っている必要があります。
それに対し、サーバー側は recvfrom
関数でデータの受信を行うのであれば、クライアント側が受信待ちするポート番号を事前に知っておく必要はありません。というか、動的に割り当てされるので、そのポート番号を事前に知っておくことは不可能です。
ですが、recvfrom (データの受信) で解説したように、データ受信時に recvfrom
関数の src_addr
引数からデータの送信元の情報を取得することができ、このデータにクライアント側が受信待ちするポート番号もセットされているため、src_addr
引数に指定したアドレスをそのまま sendto
関数の dest_addr
引数に指定してやれば、クライアント側で受信待ちしているポート番号を意識することなくサーバーからクライアントへのデータの送信を実現することが可能となります。
ちょっとこの辺りがややこしいのですが、特にサーバー側を開発する際はデータの受信に recvfrom
関数を利用してデータの送信元の情報を src_addr
引数から取得できるようにし、取得した情報を sendto
関数の dest_addr
引数に指定することを心がければ、割とすんなりデータの送受信を実現できるようになると思います。
また、recvfrom
関数ではなく recv
関数をデータの受信に利用する場合は、(おそらく)データ受信時にデータの送信元の情報が取得できません。そのため、この場合はクライアント側でも bind
を行なって特定のポート番号に対して受信待ちを行うようにする必要があります。そして、サーバー側は、クライアント側が受信待ちしているポート番号にデータの送信が行えるよう、sendto
関数の引数を指定して送信を行う必要があります。
クライアント側でも bind
が必要にる例に関しても、後述の UDP 通信の簡単なプログラム例 で紹介したいと思います。
スポンサーリンク
ソケットを閉じる (close
)
UDP 通信では接続の確立を行うことなくデータの送受信を行うことが可能ではあるのですが、TCP 通信同様に UDP 通信においても不要になったソケットはクローズが必要になります。このクローズに関しては、TCP 通信同様に 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
を利用)のソースコードの例となります。
#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_ADDR
・SERVER_PORT
を bind
関数でソケットに関連付けを行なった後に transfer
関数の実行を行っています。transfer
は自作の関数で、データの送受信を実施する関数となります。
そして、transfer
関数では最初に recvfrom
関数を実行しているため、ここで SERVER_ADDR
・SERVER_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
を利用しても問題ありません。
#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
を利用)のソースコードの例となります。
#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_ADDR
・CLIENT_PORT
に対してデータの送信を行なっているわけですから、クライアント側は、CLIENT_ADDR
・CLIENT_PORT
に対して受信待ちを行うようにする必要があります。
クライアントプログラム
続いてクライアント側のプログラムのソースコードを紹介します。下記の client_recv.c
が、UDP 通信を行うクライアントプログラム(データの受信に recv
を利用)のソースコードの例となります。
#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
で受信待ちを行うようにしています(bind
は main
関数で実行しています)。
結局、recvfrom を使用した UDP 通信の例 で示したソースコードとは、クライアント側が受信待ちするポート(これはサーバー側のデータの送信先のポートと一致します)を動的に決めるか静的に決めるのかの違いしかないのですが、静的に決める場合はクライアント側が受信待ちするポート番号を自身で指定する必要があり、さらに、そのポートに対して bind
関数を実行してから recv
関数を実行する必要があって多少面倒です。
さらに、PC 上で複数のクライアントが動作して同時に受信待ちするような場合、クライアントごとに異なるポート番号を割り当てする必要があります。なので、クライアントの数分のポート番号を自身で決める必要があります。
それに対し、サーバー側で recvfrom
関数を使うようにすれば、サーバー側はクライアント側で受信待ちしているポート番号を意識することなく開発することが出来ますし、クライアント側は bind
無しに受信待ちを実現することが出来ます。
ということで、特に通信を確立せずにデータの送受信を行う UDP 通信においては、サーバー側での受信には recvfrom
関数を利用するのが良いと思います。
プログラムの実行手順
先ほど示した2つのソースコードのプログラムの実行手順に関しては プログラムの実行手順 と同様で、ファイル名の recvfrom
を recv
に変更していただければコンパイルや実行が行えるはずです。なので、ここでの説明は省略させていただきます。
まとめ
このページではC言語におけるUDP通信の実現方法について解説しました!
C言語でソケットを利用して UDP 通信を行う場合、ソケットを作成するときに SOCK_DGRAM
を type
引数に指定すること、および接続の確立が不要になる点がポイントになると思います。あとは、データの送受信に sendto
と recvfrom
を使ってやれば特に困ることなく UDP 通信が実現できると思います。クライアント側が受信待ちするポート番号を意識せずにプログラミングできるよう、recv
よりも recvfrom
を使うことをオススメします。
UDP 通信は TCP 通信よりも高速であるメリットがありますので、リアルタイム性を重視するアプリを開発するようなときは UDP 通信を利用することもぜひ検討してみてください!