目次
- 前置き
- クラス設計
- コードと実行結果
- まとめ
1. 前置き
・前回の続き
・筆者のITスキルレベルは脱初心者くらい
・Linuxの理解度は初心者に毛が生えた程度
2. クラス設計
前回と前々回はベターCとしてC++を使っていたが、ここからはC++っぽくクラスを使った設計をしていくこととする。オブジェクト指向とか色々背景はあるが説明は割愛する。(私も大それたこと言えるレベルじゃないし)
まずクラス設計するのに目標を決めておこう。
【目標】
1.使用する相手はPOSIXメッセージキューを意識しなくてもメッセージキューとして使用可能
2.汎用性や拡張性は考慮しすぎない(あんまり複雑なクラス設計にしない)
1.の目標を達成するためには、ある程度の機能を隠蔽する必要がある。簡単に隠蔽できるものを列挙すると
・Linuxで定義してる構造体や仕様
・openとcloseの処理
・型変換とサイズ
メッセージキューは利用者視点で考えるとデータを送って受け取れればいいので、上記で列挙したことを把握するのは無駄な思考リソースとなる。(POSIXメッセージキューは汎用性を重視してるはずなので、上記で列挙した設定項目が必要になるのは仕方ない)
更に利用者が利用したい機能も列挙しよう。
・キューに送受信
・エラーの確認
さて、このような前提条件の下でまずは足がかりになるクラスを設計すると以下のようになる。ただし最小構成になるため、エラーチェックは未実装。ついでに使う側もコードのサンプルも載せておく。
template <typename MESSAGE_TYPE>
class My_Message_Queue{
private:
const char * queue_name_;
const char * error_log_ = "";
public:
My_Message_Queue(const char * queue_name){
queue_name_ = queue_name;
}
~My_Message_Queue(){
mq_unlink(queue_name_);
}
int Send_q(MESSAGE_TYPE * message){
mqd_t q;
q = mq_open(queue_name_, O_WRONLY | O_CREAT | O_NONBLOCK, S_IRWXU, NULL);
mq_send(q, (const char *)(message) , sizeof(MESSAGE_TYPE) , 0);
mq_close(q);
return 0;
};
int Recv_q(MESSAGE_TYPE * buff){
struct mq_attr attr;
mqd_t q;
q = mq_open(queue_name_, O_RDONLY | O_NONBLOCK);
mq_getattr( q ,&attr );
mq_receive( q, (char*)buff, attr.mq_msgsize,NULL);
mq_close(q);
return 0;
};
const char * Get_error_log();
};
#include <mqueue.h>
#include <string.h>
#include <iostream>
#include "posix_mes_q.h"
int main(){
const char * message = "Hello world";
const char * buff = "";
My_Message_Queue<const char *> MQ("/sample");
MQ.Send_q(&message);
std::cout << "send message = " << message << std::endl;
MQ.Recv_q(&buff);
std::cout << "recv message = " << buff << std::endl;
return 0;
}
prototype_main.cpp
を見てもらうと、使う側は初期設定(宣言の部分)を行った後はメッセージを引数にいれてキューのやり取りのみをするだけで、POSIXメッセージキューの存在を知る必要がない。つまり、POSIXメッセージキューを意識しなくてもメッセージキューとして使用可能という目標を達成している。またクラスも簡素的になっており、クラス数も1つになっている。
以上より、今回の目標の2つを達成できたと言えるだろう。
あとは未実装の部分を実装&追記して完成させたものを次の章で記載する。
3. コードと実行結果
クラスのコードは長くなるのでいつもの場所を参照すること。
クラスの変化点は以下
・エラー処理の追加
・unlinkの追加
unlinkの追加だけ少し解説をすると、デストラクタでunlinkするとプロセスを消すタイミングでキューも消えてしまう。それだと使い勝手悪いので、unlinkのタイミングは実行者側に委ねることにした。
実行する側のコードは短いのでいつもの場所から引用すると、
#include <mqueue.h>
#include <string.h>
#include <iostream>
#include "posix_mes_q.h"
int main(){
int status;
SAMPLE_OBJ message = {"taro yamada", 20, 75};
My_Message_Queue<SAMPLE_OBJ> MQ("/sample");
status = MQ.Send_q(&message);
std::cout << "status = " << status << std::endl;
return 0;
}
#include <mqueue.h>
#include <string.h>
#include <iostream>
#include "posix_mes_q.h"
int main(){
int status;
SAMPLE_OBJ message;
My_Message_Queue<SAMPLE_OBJ> MQ("/sample");
status = MQ.Recv_q(&message);
std::cout << "status = " << status << std::endl;
if(status == 0){
std::cout << "name = " << message.name << std::endl;
std::cout << "age = " << message.age << std::endl;
std::cout << "score = " << message.score << std::endl;
}
else{
std::cout << "error_log = " << MQ.Get_error_log() << std::endl;
}
// error confirmation
status = MQ.Recv_q(&message);
std::cout << "status = " << status << std::endl;
std::cout << "error_log = " << MQ.Get_error_log() << std::endl;
MQ.Unlink_q();
return 0;
}
続いて実行結果を記載する。
/sf_cpp_workspace/POSIX/POSIX_class$ ./qsend
status = 0
/sf_cpp_workspace/POSIX/POSIX_class$ ./qrecv
status = 0
name = taro yamada
age = 20
score = 75
status = -1
error_log = func_name:mq_receive, errno:11
statusが0の場合は処理が正しく実行されているという意味になる。qrecv
の2~4行目でデータの受け取りが正しくできていることが分かる。5~6行目はエラーの再現をするたびに再びmq_receiveを呼び出している。キューの中身は空なので、当然エラーになる。
ちなみにqsend
を2回実行した後に、qrecv
は実行するとキューの中にメッセージが2つあるのでエラーが起きない。
nk@nk-VirtualBox:/media/sf_cpp_workspace/POSIX/POSIX_class$ ./qsend
status = 0
nk@nk-VirtualBox:/media/sf_cpp_workspace/POSIX/POSIX_class$ ./qsend
status = 0
nk@nk-VirtualBox:/media/sf_cpp_workspace/POSIX/POSIX_class$ ./qrecv
status = 0
name = taro yamada
age = 20
score = 75
status = 0
error_log = None
エラーのログも初期値であるNoneが確認できる。
tips集
今回作成したメッセージキューを使うには以下の制約を守る必要がある
・サイズを8192バイト以下にする
・キューの名前と型の関係を一致させること
サイズの問題は可変にすればすぐ解決するので問題はないだろう。問題があるのは「キューの名前と型の関係を一致させること」である。
例を挙げて説明すると、queue_name_ = "/sample"に対し送る側がcharを選んで、受け取る側がintを選ぶと、最終的にcharをintに型変換したものが代入される。つまり、送る側と受け取る側は型の情報を共有する必要がある。
ただPOSIXメッセージキューをわざわざ使うということは、動的に情報を共有することはできないはずである。したがって設計の段階で設計者同士の同意によってキューの名前と型の関係は統一するしかないだろう。
もしこれを解決するには、型の情報をtypeid演算子で取得して、どのキューの名前に送信するか判断する機能を追加する必要がある。ただそういう検索する機能は時間コストがかかるため、キューの機能としてはあまり相応しくないとは思う。
もしくはキューの型がint→char→int→char...のような規則性があるなら、それを利用して設計してもよいかも知れない。この方法でも設計者同士で同意を得る必要があるので、素直に名前と型を一致させた方が分かりやすいとは思うけど。
stringを使ってない理由
特になし。強いて言えば、POSIXメッセージキューの型変換がconst char *だから。
4. まとめ
今回はPOSIXメッセージキューの仕様を隠蔽したクラスを作成した。
POSIXメッセージキューと自作メッセージキューの最大の違いは、使えるまでの学習コストだろう。使いこなせるなら当然POSIXメッセージキューの方が良い。じゃあ作る意味ないのでは?と思うかもしれないが、学習コストが高いと万人に理解されないので、設計者同士の合意を得る必要がある機能だと都合が悪い。(皆POSIXメッセージキューくらいなら知ってるし理解できるって状況なら本当に必要ないけどさ)
その他の違いとしては自作の方は、POSIXメッセージキューのサイズなどの設定値をどの設計者が作っても同じ値になることだろう。つまり設計者同士で合意した値を隠蔽するため、設計時に「これの設定値なんだっけ?」のようなことが起きにくい。
最終的に今回のようにクラスを作成するとクラス単位での責任が明確になり、読み返したときに構造が分かりやすくなる...はず。(クラスが1個しかないので、恩恵が分かりにくいが...)
以上で、POSIXメッセージキューの実装については終了になります。3回に分けて説明しましたが、複雑になればなるほど説明って難しいですね。できる限り1回1回の説明量は少なめで記事を作成していこうと思うので、今後も機会があったら読んでいただけると嬉しいです。