いつまでも残り続けるシリアル通信な電文プロトコル
時代は変化し、いろいろな通信プロトコルやデータハンドリング手段が登場しています。
SOAP やら RESTful やら WebAPI やら WebSocket やら MQTT やら。
.NETフレームワークでも WebAPI やら WCF やら SingleR といった通信フレームワークがあります。
こういったフレームワークが存在しない時代は、Socket通信APIをつかってゴリゴリ実装していたわけです。
Socket通信なんて死ぬほど書いてきたので慣れては居ますが、やっぱり面倒なんです。
非同期にしたり、再接続できるようにしたり、要件によっては暗号や認証を入れ込んだり。
最近はシングルスレッドの非同期実行イベント駆動の方がパフォーマンスいいよねーとか考えて実装してみたり。
でもね。
自分や社内で実装したものほど、どこにバグが潜んでいるか心配なわけで。。。
標準で高機能な通信フレームワークがあると、余計な実装をせずに済みます。するとビジネスロジックの設計や実装に集中できますよね。
しかしながら、TCP/IP通信であっても、通信相手がそんな最新の通信プロコトルを採用していないことなんでザラです。
制御系と連携するシステム開発を受注すると、昔ながらの電文プロコトルの仕様書をありがたく頂けます。
こういうのね。
コマンドID | データ長 | データ | チェックサム | ETX |
---|---|---|---|---|
2 byte | 2 byte | * byte | 2 byte | 1 byte |
※データ長=データ部+チェックサム部+ETX部 |
こういう電文プロコトルは何者かといいますと、RS-232Cなどでシリアル通信していた時の名残なんです。
通信速度も遅く、大量の電文を送受信できないわけで、必要最低限なバイト数で通信できるように考えた訳ですね。
これはもう今時の通信フレームワークとは無縁なので、ごりごり実装するしか無いわけです。非同期、再接続、コマンド解析、コマンド作成、暗号・複合なんかを。
※遡れば、STXとかETXとかENQといった制御文字は紙テープの時代にまで至りますけど。
ちなみに最近みた案件では、送受信方向と通信コマンドによって利用するTCPポートを分けていました。
サーバー→機器のコマンドA:ポート1035
サーバー→機器のコマンドD:ポート1037
機器→サーバーのコマンドB:ポート1039
なんでこんな事してんのかな~と思ったら、機器側がもともと232Cで対応していて、単にTCPに置き換えているせいだ!!と思う。
.NET のソケット通信が楽に実装できるフレームワークは無いのか?
前置きが長くなりましたが、標準で使えるフレームワークはないので TcpListenerクラス や TcpClientクラス で、Listenを開始し、Acceptで接続し、ストリームで送受信するようなコードは Winsockの時代とあまり変わらない「ザ・ソケット通信」ならでは実装です。
もう飽きたので、どこかに適切なフレームワークは無いものかと探しました。
.NET では SuperSocket というOSSが一番利用されているようなので、これでサーバープログラムを作ってみましょう。いつまでメンテナンスされるか心配なので長期間利用するようなプロダクトでは躊躇するなぁ、とは思いつつ。
ちなみにちょっと前はJavaを使っていたので Netty の一択でした。SuperSocket で実装してしまった後で Netty をフォークした DotNetty というのを見つけてしまって、ちょいと失敗。次にソケット通信を作るときには使おうと思ったのでした。
SuperSocket
http://supersocket.net/
DotNetty
https://github.com/Azure/DotNetty
SuperSocket のデフォルト実装
準備はNuGetパッケージでSuperSocketの参照を追加するだけです。
以下は一番ベーシックな実装方法です。通信ポートの確立やクライアントとの接続などソケット通信の基本なところは良しなにしてくれるので、何も実装することはありませんね。
using System;
using System.Text;
using SuperSocket.SocketBase;
using SuperSocket.SocketBase.Config;
using SuperSocket.SocketEngine;
namespace ServerSample
{
class MyServer
{
int port;
AppServer appServer = null;
SessionHandler<AppSession, CloseReason> sessionClosedDelegate;
SessionHandler<AppSession> newSessionConnectedDelegate;
RequestHandler<AppSession, StringRequestInfo> newRequestReceivedDelegate;
public MyServer(int port)
{
this.port = port;
this.sessionClosedDelegate = new SessionHandler<AppSession, CloseReason>(sessionClosed);
this.newSessionConnectedDelegate = new SessionHandler<AppSession>(newSessionConnected);
this.newRequestReceivedDelegate = new RequestHandler<AppSession, StringRequestInfo>(newRequestReceived);
}
public bool open()
{
close();
appServer = new AppServer();
appServer.SessionClosed += this.sessionClosedDelegate;
appServer.NewSessionConnected += this.newSessionConnectedDelegate;
appServer.NewRequestReceived += this.newRequestReceivedDelegate;
if (!appServer.Setup(this.port))
{
return false;
}
if (!appServer.Start())
{
return false;
}
return true;
}
public void close()
{
if (appServer != null)
{
appServer.Stop();
appServer.SessionClosed -= this.sessionClosedDelegate;
appServer.NewSessionConnected -= this.newSessionConnectedDelegate;
appServer.NewRequestReceived -= this.newRequestReceivedDelegate;
appServer = null;
}
}
private void sessionClosed(AppSession session, CloseReason closeReason)
{
Console.WriteLine("session close:" + session.LocalEndPoint.ToString() + " " + closeReason.ToString());
}
private void newSessionConnected(AppSession session)
{
Console.WriteLine("session connect:" + session.LocalEndPoint.ToString());
session.Send("Welcom");
}
private void newRequestReceived(AppSession session, StringRequestInfo requestInfo)
{
session.Send("echo:" + requestInfo.Key + " " + requestInfo.Body);
}
}
}
SuperSocket でバイナリデータを受信する
デフォルトですとなぜかTelnetサーバーなので、受信が StringRequestInfo の Key と Value に分割されて入っています。
これでは通信プロトコルに合わないのでバイナリデータを受け取るように変更します。何を継承するのか分かりにくかったので、理解するまでちょっと大変でしたけど、出来上がりはシンプルです。
using System;
using SuperSocket.Common;
using SuperSocket.Facility.Protocol;
using SuperSocket.SocketBase;
using SuperSocket.SocketBase.Protocol;
namespace ServerSample
{
public class CustomReceiverFilter : FixedHeaderReceiveFilter<BinaryRequestInfo>
{
public CustomReceiverFilter() : base(4)
{
}
protected override int GetBodyLengthFromHeader(byte[] header, int offset, int length)
{
return (int)header[offset + 2] * 256 + (int)header[offset + 3];
}
protected override BinaryRequestInfo ResolveRequestInfo(ArraySegment<byte> header, byte[] bodyBuffer, int offset, int length)
{
byte[] data = new byte[header.Count + length];
Array.Copy(header.Array, 0, data, 0, header.Count);
Array.Copy(bodyBuffer, offset, data, header.Count, length);
return new BinaryRequestInfo(null, data);
}
}
public class CustomSession : AppSession<CustomSession, BinaryRequestInfo>
{
}
public class CustomSocketServer : AppServer<CustomSession, BinaryRequestInfo>
{
public CustomSocketServer()
: base(new DefaultReceiveFilterFactory<CustomReceiverFilter, BinaryRequestInfo>())
{
}
}
class MyServer
{
int port;
CustomSocketServer appServer = null;
SessionHandler<CustomSession, CloseReason> sessionClosedDelegate;
SessionHandler<CustomSession> newSessionConnectedDelegate;
RequestHandler<CustomSession, BinaryRequestInfo> newRequestReceivedDelegate;
public MyServer(int port)
{
this.port = port;
this.sessionClosedDelegate = new SessionHandler<CustomSession, CloseReason>(sessionClosed);
this.newSessionConnectedDelegate = new SessionHandler<CustomSession>(newSessionConnected);
this.newRequestReceivedDelegate = new RequestHandler<CustomSession, BinaryRequestInfo>(newRequestReceived);
}
public bool open()
{
close();
appServer = new CustomSocketServer();
appServer.SessionClosed += this.sessionClosedDelegate;
appServer.NewSessionConnected += this.newSessionConnectedDelegate;
appServer.NewRequestReceived += this.newRequestReceivedDelegate;
if (!appServer.Setup(this.port))
{
return false;
}
if (!appServer.Start())
{
return false;
}
return true;
}
public void close()
{
if (appServer != null)
{
appServer.Stop();
appServer.SessionClosed -= this.sessionClosedDelegate;
appServer.NewSessionConnected -= this.newSessionConnectedDelegate;
appServer.NewRequestReceived -= this.newRequestReceivedDelegate;
appServer = null;
}
}
private void sessionClosed(CustomSession session, CloseReason closeReason)
{
Console.WriteLine("session close:" + session.LocalEndPoint.ToString() + " " + closeReason.ToString());
}
private void newSessionConnected(CustomSession session)
{
Console.WriteLine("session connect:" + session.LocalEndPoint.ToString());
}
private void newRequestReceived(CustomSession session, BinaryRequestInfo requestInfo)
{
//requestInfo.Bodyにバイナリデータが入っている
}
}
}
サイトのDocumentにも書いてありますが、幾つかのテンプレートがあります。要件に合わせて選択しましょう。
フィルター | 方式 |
---|---|
TerminatorReceiveFilter | 固定フッターまで読み込む |
CountSpliterReceiveFilter | 固定長で区切られた配列データを読み込む |
FixedSizeReceiveFilter | 固定長のデータを読み込む |
BeginEndMarkReceiveFilter | 固定ヘッダーから固定フッターまで読み込む |
FixedHeaderReceiveFilter | プロトコル内にサイズが記載してあるデータを読み込む |