ちょっとできるようになったサーバーエンジニアは是非 C言語でクライアント/サーバー(C/S)プログラムを作ってみるのは大変良い勉強になります。
こんにちは
サーバーエンジニアになって3年くらいすれば OSの設定やらミドルウェアの設定にも慣れて設計に入っていく頃かと思います。
その頃は「そのままでも業務は続けられるし問題はないんだけど、そろそろステップアップしたいな…」と考える頃でしょう。
そういう気持ちになったらオススメなのが、C言語で自作のクライアント/サーバー通信プログラムを作ってみることです。
「クラサバなんて古いシステムでしょう? 今はWEBの時代っスよ? 今から勉強しても意味ないんじゃないスか?」
と思われるかも知れませんが、それは大きな誤りです。
何でかって?
サーバーエンジニアが扱っている通信系のソフトは全部クラサバだからです。
いや、単純にして明快。
クラサバは通信プログラムの超基礎です。
WEBなんて飾りです。偉い人にはそれが分かっとらんのですよ。
サーバーエンジニアで TCPや UDPのポートというものを知ってはいても、TCP/IP通信の本を読んだことがある人がそもそも稀で、ソケットって何なのかという所まで行くと理解しないままの人も多い印象です。
これらを理解しないでもやれはするんですが、クラサバのプログラムの動作原理を知ってると知らないではその後のステップアップの過程で雲泥の差になると思います。
これで奮起して「分かった。じゃやるわ。」となったとして、何で今さら C言語なの?と思いますよね。
それは、C言語だとクラサバプログラムが OSに対して通信作業を依頼することを必要とする事がよく見えるからです。
クラサバプログラムを作ることで、そこから拾える知識と経験に大変なものがありまして、C言語の経験が無くともこれのためだけに C言語を覚えても良いくらいだと思います。
実際に基礎の基礎たるエコーサーバーというのをこのページの最後で作っています。
そして超オススメしたい本がこれ。
私は凄く大事にしていて「まだ買ってない奴は売っているうちに急げ!」と思っています。
本当の事を言っちゃうと、TCP/IPの本を読んでも C言語のクラサバプログラムをコピペで作っても、ソケットの観念がイマイチ腹落ちしなかったんですが、この本を読むのと C言語のクラサバプログラムのソースと動作を見るのをやっていってやっと理解できたのでした。
ソケットの理解のみならず、TCP通信で得られるデータが単に延々と続くデータの羅列でしかないこと、アプリケーションプロトコルの必要性、サーバープログラムがプログラム基礎で悪と習った永久ループをしていること、等々を「身を持って」知ることとなりました。
埋められていなかったジグソーパズルのピースがビシバシと入ってきて、急に目の前が明るくなるような体験でしたね。
上の本と C言語での通信プログラム作成を通してそれを理解してみれば、サーバーで稼働させる全ての通信について、サーバープログラム(プロセス)は何で、クライアントプログラム(プロセス)は何処にいるのだ、という観点を持つようになりました。
理論を知ってるのと、実物に触れるのでは大分違ったというのが素直な感想です。
やはり基本に立ち返って勉強してみると言うのは良いものです。
私は次にこの本を入手しました。
これもまた「まだ買ってない奴は売っているうちに急げ!」と思っています。
こちらの本のプログラムは実際には作りませんでしたが、おかげでイーサーネットから TCPまでの通信レイヤの理解が大変深まりました。
その後、改めて TCP/IPの本を読んでみると「何ということでしょう!あれだけ訳の分からなかった記述の意味が分かるように!」。
ここまでくると WireSharkの本も読み進められるようになり、サーバーにインストールしたミドルウェアの通信パラメータの意味も分かってくるようになりましたね。
もし私がサーバーエンジニア養成のためのお勉強カリキュラムを考えるなら、是非 C言語でのクラサバ通信プログラム作成を入れたいです。
なお、C言語での開発は Windowsよりも Linuxがオススメです。
コンパイラの gccを用意するのは簡単ですので。
複雑なものを作る必要はなく、本当にシンプルなもの、例えばクライアントプログラムから送信したメッセージに対して返答するだけのもので構いません。
自作のプログラムが通信する様子を Wiresharkを使って見てみれば、観念だけだった TCP/IP通信がきっと見えてきますよ。
========== 実際に作る ==========
ここではいわゆる「エコーサーバー」というのを作ります。
クライアントから送られてきたメッセージをそのまま返すだけ。
かつ 1回きりしか動作しません。
つまらない仕様ですけど TCP/IPによる通信の要素が一杯ありますので、このソースに書いてある動作は全て理解するのが望ましいです。
具体的な環境は以下でやりますが、基礎中の基礎のためどの LinuxOSでも同じでしょう。
サーバー:Ubuntu Server 24.04
クライアント:Lubuntu 24.04 (コンパイルもここ)
1.Cコンパイラ(gcc)インストール
クライアントに Cコンパイラの gccをインストールします。
subro@Lubuntu2404:~$ sudo apt -y install gcc
〜〜〜 省略 〜〜〜
何の問題もなくインストールできました。
2.サーバープログラム作成
サーバー側のプログラムソースを Copilotに作ってもらいました。
(なので著作権は私にはありません)
[echo_server.c]
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 12345
#define BUFFER_SIZE 1024
int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
char buffer[BUFFER_SIZE];
socklen_t addr_len = sizeof(client_addr);
// ソケット作成
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("socket error");
exit(EXIT_FAILURE);
}
// サーバーアドレス設定
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
// ソケットにアドレスをバインド
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind error");
close(server_fd);
exit(EXIT_FAILURE);
}
// 接続待ち状態にする
if (listen(server_fd, 5) < 0) {
perror("listen error");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Echoサーバー起動中(ポート %d)...\n", PORT);
// クライアントからの接続を受け付ける
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);
if (client_fd < 0) {
perror("accept error");
close(server_fd);
exit(EXIT_FAILURE);
}
// データの受信と送信
ssize_t n;
while ((n = read(client_fd, buffer, BUFFER_SIZE)) > 0) {
write(client_fd, buffer, n); // 受信データをそのまま返す
}
// 接続終了
close(client_fd);
close(server_fd);
return 0;
}
これをコンパイルします。
-oオプションで [echo_server]ファイルとして実行ファイルを作成します。
subro@Lubuntu2404:~/work/c/echoserver$ gcc -o echo_server echo_server.c
subro@Lubuntu2404:~/work/c/echoserver$ ls -l echo_server
-rwxrwxr-x 1 subro subro 16440 7月 20 08:13 echo_server
できました。
このファイルは scpコマンドで Ubuntu Serverに持っていきました。
3.クライアントプログラム作成
クライアント側のプログラムソースも Copilotに作ってもらいました。
(なので著作権は私にはありません)
こちらもつまらない仕様ですけどやっぱり TCP/IPによる通信の要素が一杯ありますのでこのソースに書いてある動作も全て理解するのが望ましいです。
[echo_client.c]
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define SERVER_IP "192.168.1.103" // サーバーのIPアドレス(ローカルホスト)
#define PORT 12345
#define BUFFER_SIZE 1024
int main() {
int sock_fd;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
// ソケット作成
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd < 0) {
perror("socket error");
exit(EXIT_FAILURE);
}
// サーバーアドレス設定
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
// サーバーへ接続
if (connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("connect error");
close(sock_fd);
exit(EXIT_FAILURE);
}
printf("接続しました。メッセージを入力してください(終了するには Ctrl+D):\n");
// 標準入力からメッセージを読み込み、サーバーに送信→受信して表示
while (fgets(buffer, BUFFER_SIZE, stdin) != NULL) {
write(sock_fd, buffer, strlen(buffer));
ssize_t n = read(sock_fd, buffer, BUFFER_SIZE);
if (n > 0) {
buffer[n] = '\0'; // 文字列終端
printf("Echoされたメッセージ: %s", buffer);
}
}
// ソケットクローズ
close(sock_fd);
return 0;
}
ピンク色の所は接続先サーバー(ここでは Ubuntu Server)の IPアドレスです。
これをコンパイルします。
-oオプションで [echo_client]ファイルとして実行ファイルを作成します。
subro@Lubuntu2404:~/work/c/echoserver$ gcc -o echo_client echo_client.c
subro@Lubuntu2404:~/work/c/echoserver$ ls -l echo_client
-rwxrwxr-x 1 subro subro 16568 7月 23 06:23 echo_client
できました。
4.実行
サーバープログラムは先に起動して待っていてもらわないといけないので、先に Ubuntu Serverで [echo_server] を実行します。
subro@UbuntuServer2404-1:~$ ls -l echo_server
-rwxrwxr-x 1 subro subro 16440 7月 23 06:20 echo_server ←scpコマンドで持ってきたやつ
subro@UbuntuServer2404-1:~$ ./echo_server
Echoサーバー起動中(ポート 12345)...
[tcp/12345]で待受けています。
サーバー側はこれで OK。
クライアントでプログラムを実行して、メッセージを送ってみます。
subro@Lubuntu2404:~/work/c/echoserver$ ./echo_client
接続しました。メッセージを入力してください(終了するには Ctrl+D):
abcde
Echoされたメッセージ: abcde
送ったのと同じメッセージが帰ってきました。
続けてメッセージを送ってみます。
fghij
Echoされたメッセージ: fghij
また送ったのと同じメッセージが帰ってきました。
この後は延々と続くだけなのでCtrl+dで終わらせます。
サーバープログラムの方を見てみると、こちらも終わっていました。
ところで、上でエコーサーバーの動作を説明している中で「1回きりしか動作しない」って書いているのに 「abcde」と「fghij」の 2回のやり取りがあるじゃないのと思いませんか?
TCPでの通信では確立したセッションの中でのデータは全て 1つの流れ(Stream)として扱われていて、サーバープログラムにとっては「abcdefghij」というデータでしか無かったのでした。
サーバーに届いたタイミングが違うのでそれぞれのタイミングで処理はしていますが、サーバープログラムとしてはあくまで 1つのデータ扱いです。
こんな辺りにも TCPという通信プロトコルが見え隠れしていて色々と気づきがあります。
5.どうせだから WireSharkで通信内容を見てみる
プログラムがシンプルゆえに、どういう動作をしているかの理解が深いはずです。
とすると通信内容もまた想定できるというもの。
上の実行時にどんな通信パケットがやりとりされているか、パケットキャプチャーの WireSharkで拾ってみましょう。
以下、フィルタを使ってこれらのプログラムの通信のみ拾っています。
赤枠が TCPセッション確立(3 way handshake)です。
青枠が TCPセッション終了ですね。
TCPセッション確立後の最初のパケットがクライアント→サーバーの向きで「abcde」を送ってるパケットのはず。
パケットの詳細を見てみると想定通りに「abced(とEnter)]が送られていました。
そのあとサーバー→クライアントの向きで「受信しましたよ」という意味のパケット(629番)が送られています。
次がサーバーからクライアントに「abcde」を返す通信が行われるはず。
このパケットの中を見ると想定通り「abced(とEnter)]が送られていました。
以下同文で「fghij」もやりとりされていました。
このように自分で作ったプログラムだからこそ、OSレベルの動作(TCP通信)の辺りまで見えてきます。