おはようございます。
エンジニアの yamamoto-mo です。
すっかり寒くなりましたね…
毎年秋を感じる期間が少なくなってるのは気のせいでしょうか…
今年なんて「秋だなぁ」と思えたのは、
2、3 日しかなかった様に思えます。
…まぁそんな事はおいておきまして。
最近プロセス間通信のプログラムを試しに作ってます。
仕事で必要な訳ではなく、個人的な勉強という感じです。
開発中の情報収集やデバッグでプロセス間通信を利用したツールの話を聞くので、
単純に興味を持ち、簡単な事でも経験しておこうと思った事が切っ掛けです。
興味持ったならとりあえず試してみる。大事ですよね。
プロセス間通信とは
まずプロセス間通信がそもそもどういった物かですが、
「複数のプロセスの間でデータをやり取りする仕組み」を指します。
読んで字の如くですね。
「それぞれ起動しているプログラム同士でデータを送りあう事が出来る仕組み」
というともう少し分かり易くなるでしょうか。
ここでいう「プログラム同士」は、同じPCで動いている物とは限らず、
ネットワークを介している場合も該当します。
プロセス間通信を実現する方法には、
- 共有メモリ
- メッセージキュー
- パイプ
- ソケット
…等々、色々あるようです。
ここではソケットを利用したプロセス間通信のプログラムを作る事にしました。
プロセス間通信は異なるプログラム同士でのデータやり取りを実現しますが、
各プログラムが担う役割はハッキリさせる必要があります。
役割には「クライアント」「サーバー」の二つがあります。
どんな情報を乗せてもいいのですが、分かり易いので下記の形で進めます。
クライアント :サーバーに文字を送信する
サーバー :クライアントから文字を受け取り表示する
なお、各プログラムは同じ言語で作られている必要もないので、
こちらも勉強がてら違う言語を使って用意しました。
クライアント側
まずはクライアント側です。
こちらは Go 言語で作ってみました。
package main
import (
"fmt"
"net"
)
// -----------------------------------------------------------
// 入力受付
// -----------------------------------------------------------
func inputWorker(con net.Conn, ch chan<- string) {
for {
fmt.Printf("文字を入力してください\n")
// 入力文字列の送信
var str string
fmt.Scanf("%s\n", &str)
// サーバーに文字を送信
con.Write([]byte(str))
// end と入力すると終了
if str == "end" {
// 終了したことをチャネルで通知する
ch <- str
break
}
}
}
// -----------------------------------------------------------
// メイン関数
// -----------------------------------------------------------
func main() {
// サーバーとの接続を試みる
// ※ここでは 8080 ポートを利用していますが、任意のポート番号に変更してください
con, err := net.Dial("tcp", "localhost:8080")
if err != nil {
return
}
// 終了処理の遅延呼び出し
defer con.Close()
// 入力処理はゴルーチンで分離
ch := make(chan string)
go inputWorker(con, ch)
// チャネルの情報を元に終了するまでブロック
if "end" == <-ch {
fmt.Printf("クライアントの処理を終了します\n")
}
}
終了する箇所を始め色々無理やりではありますが、
ローカル環境で立ち上がっているサーバーに接続し、
文字入力と送信をゴルーチンで繰り返させ、
特定の文字を入力するとアプリケーションを終了する形にしています。
サーバー側
次にサーバー側です。
サーバーは Winsock を利用して C++ で作成してみました。
#include <iostream>
#include <winsock2.h>
#include <array>
#include <thread>
#include <mutex>
#include <vector>
#pragma comment(lib, "ws2_32.lib")
// 同期オブジェクト
std::mutex mtx = {};
// 受信に利用する情報
struct Recive {
SOCKET socket = {}; // 接続時に生成されたこの接続用のソケット識別子
std::vector<std::string> data = {}; // 受信データ
};
// -----------------------------------------------------------
// メイン関数
// -----------------------------------------------------------
int main()
{
WSADATA wsaData = {};
// winsock 利用を開始する
if (WSAStartup(MAKEWORD(2, 0), &wsaData) == 0) {
// ソケットを作成する( IPv4 通信 / TCP を利用する )
auto sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) {
std::cout << "ソケットの作成に失敗しました" << std::endl;
return 1;
}
// ソケットを紐付ける
SOCKADDR_IN addrIn = {};
addrIn.sin_family = AF_INET; // IPv4 通信
addrIn.sin_port = htons(8080); // ポート番号(※任意のポート番号に変更してください)
addrIn.sin_addr.S_un.S_addr = INADDR_ANY; // どのアドレスからでも受け入れる
if (bind(sock, (PSOCKADDR)(&addrIn), sizeof(addrIn)) == SOCKET_ERROR) {
std::cout << "ソケットの紐付けに失敗しました" << std::endl;
return 2;
}
// 接続を待つ(接続待機は一つだけ)
if (listen(sock, 1) == SOCKET_ERROR) {
std::cout << "ソケットの接続準備に失敗しました" << std::endl;
return 3;
}
std::cout << "ソケットの接続準備完了" << std::endl;
// クライアントとの接続情報を保持する
Recive recive;
// 接続してきたクライアント情報の格納先を用意する
SOCKADDR_IN clientInfo = {};
auto infoSize = static_cast<int>(sizeof(clientInfo));
// 接続要求を許可する
recive.socket = accept(sock, (PSOCKADDR)(&clientInfo), &infoSize);
if (recive.socket == INVALID_SOCKET) {
std::cout << "接続が許可されませんでした" << std::endl;
return 4;
}
std::cout << "接続完了" << std::endl;
// 一旦接続が切れたらメインループを終了する
bool finish = {};
// 受信ワーカーを別スレッドで処理する
std::thread worker([&]() {
while (true) {
// データの格納先
std::array<char, 256> buf = {};
// 受信するまで待つ
auto res = recv(recive.socket, buf.data(), buf.size(), 0);
// エラーあるいは通常終了の場合、このスレッドを終える
auto error = (res == SOCKET_ERROR || res == 0);
// データ競合を防ぐ
{
std::lock_guard<decltype(mtx)> lock(mtx);
recive.data.emplace_back(buf.data());
finish = error;
}
if (error) { break; }
}
});
// メインループ
while (true) {
bool endLoop = {};
std::vector<std::string> data = {};
// データ競合を防ぐ
{
std::lock_guard<decltype(mtx)> lock(mtx);
data.swap(recive.data);
endLoop = finish;
}
// 文字を表示する
for (const auto& s : data) {
std::cout << s << std::endl;
}
// メインループを抜ける
if (endLoop) { break; }
// 一秒待つ
Sleep(1000);
}
// 受信ワーカーの終了を待つ
worker.join();
// ソケット通信を終了する
closesocket(sock);
}
// Winsockを終了
WSACleanup();
return 0;
}
ソケットの設定を行い、クライアントからの接続を待った後、
接続が完了したら受信用のスレッドを立ち上げ、
メインループで受信された情報を画面に出力するという事をしています。
クライアントが閉じるとアプリケーションは終了します。
これも終了が結構無理やりで、サーバー側が閉じた場合にクライアントに通知していないので、
色々問題あると思います。
本来なら、サーバーからクライアントにシャットダウンの命令を出して、
クライアントが正しくシャットダウン出来たことをサーバーで受け取るとか必要だろうなと思っています。
この辺りは弊社のネットワーク専門のエンジニアに後日ご教授頂こうかと…
先にサーバー側を起動し、その後にクライアント側を起動して、
クライアント側に文字を入力すると、サーバー側に表示されます。
用途
ゲーム本体をサーバーとみなし、設定ツール等をクライアントとして作ると、
- 設定ツールのGUI部分などはゲーム本体に乗る事がない
- 別プロジェクトでもそのまま流用できる
といった特徴からとても便利だと思います。
反対にゲーム本体をクライアントとみなし、ログ集積システム等をサーバーとして作ると、
開発中のプレイ記録を大量に保存し、
- パフォーマンスの推移確認
- プレイ中の情報の解析等
が出来そうですね。
プロセス間通信は上手く開発に取り入れると、
色々と役立つ事が多いと思います。
是非試してみて下さい。
という訳で自分ももっと勉強します…
免責事項
掲載された内容によって生じた損害等に関して、
弊社は一切の責任を負いかねますので、予めご了承ください。