Unixソケットプログラミング/通信の全体像
通信の全体像[編集]
サーバー側 | クライアント側 | |
---|---|---|
ソケットの起動と初期化 | socket() | socket() |
↓ | ↓ | |
ソケットにアドレス登録 | bind | |
↓ | ↓ | |
クライアントからの通信待ち | listen | |
↓ | ↓ | |
クライアントがサーバに接続 | ↓ ------------------------- | connect |
受付可能の状態でサーバ待機 | accept ------------------------- | |
↓ | ↓ | |
データ送受信 | write() / read() -------------- | write() / read() |
↓ | ↓ | |
データ送受信停止と通信切断 | close() | close() |
通信の切断は単に close() だけでいい。
WindowsのWinSockではデータ送受信が send() / receive() ですが、Linux では送受信が write() / read() になることに気をつけましょう。
骨格となるコード[編集]
- 最低限のプログラム (Fedora32 で動作確認. 2020年3月30日)
下記のコードは標準C言語である。よってコンパイルは、gccで可能である。
#include <stdio.h>
#include <sys/socket.h>
int main()
{
int nanika;
nanika = socket(AF_INET, SOCK_STREAM, 0);
if (nanika < 0) {
printf("failed\n");
return 1;
}
printf("maybe success\n");
return 0;
}
- 解説
socket() 関数でソケット作成できるのは、WinSockとほぼ同じだが、
linuxなどの場合、socket()関数の返り値の保管先として、int 型の変数でそのエラー保管用変数を宣言する必要がある。
なお、socket()関数の書式は
socket(アドレスファミリの指定, SOCK_STREAM, 0)
である。
AF_INET は IPアドレスにはIPv4 を使うことを意味している。
SOCK_STREAM とは TCP/IP であることを意味している。(なお UDP を使う場合は SOCK_DGRAM になる)
とりあえず
int nanika; // なにか
nanika = socket(AF_INET, SOCK_STREAM, 0);
とでも宣言しとけばいい。
Linuxソケットプログラミングの場合、socket()などソケット関連の関数は失敗すると負の数(-1 など)を返します。(WinSockとは違います。WinSockの場合、通常はエラーだと0以外を返す。)
Linuxソケットプログラミングでは、成功時には、scoket()などソケット関連の関数は返り値として、負でない整数を返します。
よくある典型的な入門的プログラム例[編集]
- クライアント側
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main() {
struct sockaddr_in srcAdoresu;
int sock;
char buf[32];
int data1;
/* ソケットの作成 */
sock = socket(AF_INET, SOCK_STREAM, 0);
/* 接続先指定用構造体の準備 */
srcAdoresu.sin_family = AF_INET;
srcAdoresu.sin_port = htons(11111); // 12345 とかでも良い
srcAdoresu.sin_addr.s_addr = inet_addr("127.0.0.1");
/* サーバに接続 */
connect(sock, (struct sockaddr *)&srcAdoresu, sizeof(srcAdoresu));
/* サーバからデータを受信 */
memset(buf, 0, sizeof(buf));
data1 = read(sock, buf, sizeof(buf));
printf("%d,2123452222222222222 %s\n", data1, buf);
/* socketの終了 */
close(sock);
return 0;
}
- 参考文献
- 『TCPを使う:Geekなぺーじ』 2020年4月10日に閲覧
- 小高知宏『pythonによるTCP/IPソケットプログラミング』、オーム社、2019年2月20日 初版 第1刷発行、46ページ・50ページなど
など、ソケットプログラミング解説でよくある、典型的なアルゴリズム例である。
- 実行結果
3,2123452222222222222 tes
- 解説
サーバー側のプログラムにあるwrite(sockAc, "test", 3);
のうち、testの最初3文字を受け取ってるので、最後に「tes」とだけ、testの冒頭3文字が表示されている。
なお、pythonなどC言語以外の他のプログラム言語だと、
srcAdoresu.sin_addr.s_addr = inet_addr("127.0.0.1");
の "127.0.0.1" を "localhost" に変えても自動的に実行してくれるが、しかしC言語にはそういう機能は無いので、つまりC言語では具体的に「"127.0.0.1"」と入力するしかない。
どうしても「localhost」的な変数を使いたい場合、include文のあとの行などで
#define LOCALHOST "127.0.0.1"
のようにdefineでマクロを定義してから、
srcAdoresu.sin_addr.s_addr = inet_addr(LOCALHOST); // 127.0.0.1
で呼び出す。
なお、なぜか
LOCALHOST 127.0.0.1 srcAdoresu.sin_addr.s_addr = inet_addr("LOCALHOST"); // 127.0.0.1
としても(二重引用符がinet_addr側)、エラーになる。
- サーバ側
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>
int main(){
int sokku; // ソケット・ディスクリプタ
struct sockaddr_in srcAdoresu;
struct sockaddr_in cliAdoresu;
/* ソケットの作成 */
sokku= socket(AF_INET, SOCK_STREAM, 0);
/* ソケット構造体の設定 */
srcAdoresu.sin_family = AF_INET;
srcAdoresu.sin_port = htons(11111); // 12345
srcAdoresu.sin_addr.s_addr = INADDR_ANY;
printf("ソケット構造体を設定しました\n");
/* ソケットのバインド */
bind(sokku, (struct sockaddr *)&srcAdoresu, sizeof(srcAdoresu));
printf("バインドしました\n");
/* TCPクライアントからの接続要求を許可 */
listen(sokku, 5);
printf("リッスンしました\n");
/* TCPクライアントからの接続要求の待機 */
int medi; // 単なる中間変数。中間変数を使わずにacceptすると何故かエラーになるので。
medi = sizeof(cliAdoresu); // accept()第3引数で使用。中間変数を媒介せずに一続きにすると何故かコンパイルエラー
printf("アクセプトの直前\n");
int sockAc;
sockAc = accept(sokku, (struct sockaddr *)&cliAdoresu, &medi);
printf("送信の直前\n");
/* 送信 */
write(sockAc, "test", 3);
printf("送信した\n");
/* ソケット通信の終了 */
close(sockAc);
/* ソケットの終了 */
close(sokku);
return 0;
}
- 実行結果
(※ クライアント起動前まで)
ソケット構造体を設定しました バインドしました リッスンしました アクセプトの直前
※ 以上、クライアント起動前まで。
(※ クライアント起動後)
ソケット構造体を設定しました バインドしました リッスンしました アクセプトの直前 送信の直前 送信した
※ 以上、クライアント起動後。
- 解説
accept() を使った時、クライアントからの接続要求が届くまで、サーバ側のプログラムの進行が止まり、acceptのコードのあるその場所で自動的に待機する仕組みである[1]。
なので、いちいちwhile文などで待機命令を記述する必要は無い。
- 余談
webブラウザでアドレス欄に「127.0.0.1:11111」と入力すると、もし上記のサーバプログラムが稼働中なら、webブラウザに返事が帰り、ブラウザに「tes」と表示される(Fedora 32 上の firefox 79.0 で確認)。
これは単に、webブラウザが送られてきたメッセージを表示しているだけである。
ただし、ポート番号は「80」番以外にしないといけない(上記コード例ではポート番号を11111番に設定している)。80番に設定しても、Firefoxでは、なぜか、うまくアクセスできない。(おそらく、80番にした場合だけ、Apacheなど既存のwebサーバに最適化した通信をブラウザ側でするように細工してあるのだろう。)
ただし、Google Chrome では、このようなアクセスはできない。
- 「このサイトにアクセスできません
- 127.0.0.1 で接続が拒否されました。」
- 「ERR_CONNECTION_REFUSED」
などとChrome の画面に表示される。
もちろん真相は、本ページに掲載してあるサーバプログラムは一切、接続を拒否していない。Chrome 側が勝手に接続拒否しているだけなのが実態である。
ネットに転がっているコードの解説[編集]
コードの内容[編集]
ネットに転がっている、Unix系ソケットプログラミングのプログラムは、あれは何をしているのか?
webサイトにもよるが、たいてい、ローカルPC内でのサーバ側プログラムとクライアント側プログラムとの通信をしている。
これらの通信を実践するための手順として、
- まず、サーバ側もクライアント側も両方ともコンパイルして、それぞれ実行ファイルを作成する必要がある。
- サーバー側のプログラムをコマンド端末で起動。
- つづいて、コマンド端末を別途立ち上げ(つまり合計2個以上のコマンド端末ウィンドウが結果的にPC画面上にある状態になる)、クライアント側のプログラムをコマンド端末で起動。(起動方法は各自の好きなやり方に任せる)
- クライアント側プログラムをコマンド端末で起動すると、サーバ側との接続が行われ、サーバー側からの返事が帰ってくるので、クライアント側コマンド画面にて返事の内容を確認する。
- サーバ側コマンド画面では、クライアントに返事を送った後にサーバが終了するはずなので、終了していることを確認する。
このような、ローカルPC内の通信プログラムが、ネット上によくあるので、実行してみて、参考にしよう。
ネットのコードの要・更新の事項[編集]
ネットにあるコードのままでは、近年のLinuxではインクルードファイルが不足しているので、そのままではコンパイルエラーになる場合も多い。
サーバー、クライアントとも下記のようにインクルード宣言を冒頭に補うと、コンパイルが成功する。(Fedora32で確認. 2020年3月30日.)
- サーバ側
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>
- クライアント側
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h> > // memset() に必要
#include <arpa/inet.h>
- まとめ
古いサイトだと、
#include <netinet/in.h> //
#include <arpa/inet.h> // sockaddr_in() , inet_addr() に必要
#include <netdb.h> //
が欠けているコードも多い。
ソケット構造体[編集]
サーバとクライアントの共通知識[編集]
ソケットプログラミングではIPv4通信用に sockaddr_in 構造体というのが事前に定義されている。(なお、このほか IPv6通信に sockaddr_in6 構造体というのが別途ある。)
説明の単純化のため、しばらくIPv4通信に限定して、解説していく。
この sockaddr_in 構造体の構造体変数を宣言するので、
struct sockaddr_in 構造体変数;
のようなコードに そして
構造体変数.sin_port = htons(ポート番号)
で使用するポートを、サーバーもクライアントも宣言する。
ポート番号は、サーバ側とクライアント側とで共通の番号にする必要がある。
もしポート番号の片方を別の数字に変更すると、以前は通信できたサーバ-クライアント間が、通信できなってしまう。
だが、ポート番号を変更しても、両方とも同じ数字に変更してあれば、通信できる。
ただしシステムによっては 1024 より小さいポート番号を使えない、または特別な権限が必要になる場合もある。
なので、なるべくそれよりも大きいポート番号を使うのが良い。
さて、
構造体変数.sin_family = AF_INET;
というのは通信方法の指定(アドレスファミリというものの指定)で、IPv4で通信することを指定している。気にせず、このままコピーしてもらいたい。
127.0.0.1 とは、特別な意味をもったIPアドレスであり、自分自身のパソコンを表すアドレスである。
なので、自分のパソコン内だけで試験的に通信をしたい場合、このアドレスを使う。
IPアドレス 127.0.0.1 のことをローカル・ループバック・アドレスという。
ソケット構造体の使い方[編集]
- クライアント側
クライアント側には
struct sockaddr_in 構造体変数;
構造体変数.sin_family = AF_INET;
構造体変数.sin_port = htons(ポート番号);
構造体変数.sin_addr.s_addr = inet_addr("127.0.0.1");
という感じのコードがあるハズ。
構造体変数.sin_addr.s_addr = inet_addr("127.0.0.1");
というのは、相手先のアドレスで、たとえば電話をかける時に電話番号を入力するのと同じ。
なお、このIPアドレスはローカル・ループバック・アドレスというものである(後述)。
いっぽう、クライアントのIPアドレスについては(サーバ側は)知らなくもいい。
- サーバ側
サーバ側には
struct sockaddr_in 構造体変数;
構造体変数.sin_family = AF_INET;
構造体変数.sin_port = htons(ポート番号);
構造体変数.sin_addr.s_addr = INADDR_ANY;
という感じのコードがあるハズ。
構造体変数.sin_addr.s_addr = INADDR_ANY;
の INADDR_ANY; というのは、接続要求されてきたら何相手でも接続するという感じの意味。
サーバ側なので、IPアドレスを指定することはなく、受け身である。
さて、 bind() では sockaddr_in で設定した情報をサーバに登録しているようである。
なので必然的にコードの順番では、
先に sockaddr_in の設定をしてから、
あとから
bind()
を宣言することになる。
- ^ Michael J.Donahoo/Kenneth L.Calvert 共著、小高知宏 訳『TCP/IPソケットプログラミング C言語編』、オーム社、平成29年1月25日 第1版 第16刷発行、40ページ