SuperSocket を用いた Windows サービス基盤のリアルタイムメッセージプッシュシステム実装

背景と課題解決

従来の IIS 上での SuperWebSocket ホスティング環境では、接続断やプロセス停止が頻発し、定期的なプール回収やサーバー再起動が必要でした。これにより、稼働率の低下や運用コストの増大といった問題が生じていました。これらの不安定さを解消するため、インフラストラクチャを変更し、SuperSocket にて Windows サービスとして動作する独立したソケットサーバーを構築しました。この構成により、アプリケーションのライフサイクルから分離され、より堅牢な通信環境を実現します。

アーキテクチャとデータフロー

本ソリューションでは、以下のパイプラインを用いてメッセージを伝達します。まず、Web 側の業務ロジック(IIS)は WCF サービスを介して通信を行います。WCF は内部の Windows サービスにアクセスし、そこにある SuperSocket サーバーに対して転送命令を出します。最終的に、Windows サービス内のソケットサーバーが Android クライアントへ直接データを配信します。逆に、クライアントからのデータ(位置情報など)は SuperSocket で受領され、データベースへ永続化されます。

図 1: システム全体のデータフローイメージ - IIS (Web API) → WCF - WCF → Windows Service (SuperSocket) - SuperSocket ↔ Android Client

カスタムセッションの実装 (AppSession)

SuperSocket の拡張には、AppSessionクラスを継承して独自のセッション管理クラスを作成する必要があります。ここでは、接続の確立、認証情報の保持、および切断時の状態更新を担う AndroidSessionを実装しています。ログイン状態の維持やクッキー処理もこのクラス内で完結させます。

using System;
using System.Collections.Specialized;
using System.Linq;
using SuperSocket.SocketBase;
using SuperSocket.SocketBase.Protocol;

namespace SocketService.Infrastructure
{
    // メッセージタイプ定義
    public enum MessageType 
    {
        Order = 0,
        TextMsg = 1
    }

    /// <summary>
    /// クライアントごとの接続状態を管理するセッションクラス
    /// </summary>
    public class AndroidSession : AppSession<AndroidSession>
    {
        private bool _isSendingLock = false;
        public StringDictionary Cookies { get; private set; }

        // ユーザー固有情報プロパティ
        public int TargetId { get; set; }
        public int UserType { get; set; }
        public string Username { get; set; }
        public string AccessToken { get; set; }

        protected override void OnSessionStarted()
        {
            base.OnSessionStarted();
            // 接続開始時の初期処理
        }

        protected override void OnSessionClosed(CloseReason reason)
        {
            base.OnSessionClosed(reason);
            CleanupUserData();
        }

        /// <summary>
        /// 認証クッキーのパースと保存
        /// </summary>
        public void ParseAuthentication(string cookieString)
        {
            var dict = new StringDictionary();
            if (!string.IsNullOrWhiteSpace(cookieString))
            {
                foreach (var pair in cookieString.Split(';'))
                {
                    var parts = pair.Trim().Split('=');
                    if (parts.Length == 2)
                        dict[parts[0]] = Uri.UnescapeDataString(parts[1]);
                }
            }
            
            this.Cookies = dict;
            ExtractUserProperties(dict);
        }

        private void ExtractUserProperties(StringDictionary dict)
        {
            if (dict.ContainsKey("username")) Username = dict["username"];
            if (dict.ContainsKey("token")) AccessToken = dict["token"];
            if (dict.ContainsKey("type")) UserType = Convert.ToInt32(dict["type"]);
        }

        /// <summary>
        /// セキュアなデータ送信メソッド
        /// </summary>
        public void SendSecureData(MessageType type, string payload)
        {
            lock (this)
            {
                // スレッド安全性を確保した送信処理
                try 
                {
                    string prefix = type == MessageType.Order ? "ORD:" : "MSG:";
                    Send(prefix + payload);
                }
                catch { /* 例外処理またはログ記録 */ }
            }
        }

        private void CleanupUserData()
        {
            // ログアウト処理や DB 更新ロジック
        }
        
        public AndroidSession FindByTarget(int id, int type)
        {
            return GetServer().GetAllSessions()
                .FirstOrDefault(s => s.TargetId == id && s.UserType == type);
        }
    }
}

カスタムサーバーの実装 (AppServer)

AppServer クラスを継承し、サーバーのライフサイクルイベントをフックすることで、新規接続や切断時のグローバル処理を行えます。ここでは、接続増加時のログ出力と、セッション終了時の整合性確保を行います。

using System;
using SuperSocket.SocketBase;
using SuperSocket.SocketBase.Config;

namespace SocketService.Infrastructure
{
    /// <summary>
    /// メッセージ配信を扱うサーバー本体
    /// </summary>
    public class MessagingServer : AppServer<AndroidSession>
    {
        protected override bool Setup(IRootConfig rootConfig, IServerConfig config)
        {
            return base.Setup(rootConfig, config);
        }

        protected override void OnStarted()
        {
            base.OnStarted();
            // サーバー起動処理
        }

        protected override void OnStopped()
        {
            base.OnStopped();
            // シャットダウン処理
        }

        protected override void OnNewSessionConnected(AndroidSession session)
        {
            base.OnNewSessionConnected(session);
            // 接続成功時のログ記録
        }

        protected override void OnSessionClosed(AndroidSession session, CloseReason reason)
        {
            base.OnSessionClosed(session, reason);
            
            // セッション切断時の整番処理
            if (session.TargetId != 0)
            {
                UpdateLoginStatus(session.SessionID, false);
            }
        }
    }
}

メッセージハンドリングロジック

サーバー受信ルーチンでは、NewRequestReceivedメソッドをオーバーライドして入力を解析します。ここでは主要なコマンドである認証(Cookie)と GPS 位置情報の送信をサポートしています。

/// <summary>
/// 着信メッセージの処理
/// </summary>
public void ProcessIncoming(AndroidSession session, StringRequestInfo requestInfo)
{
    switch (requestInfo.Key)
    {
        case "Auth":
            HandleLogin(session, requestInfo.Body);
            break;
        case "LocationUpdate":
            HandleGpsUpload(session, requestInfo.Body);
            break;
        default:
            // 未知のリクエスト対策
            break;
    }
}

private void HandleLogin(AndroidSession session, string body)
{
    session.ParseAuthentication(body);
    
    // 非同期でユーザー検証
    Task.Run(() => ValidateAndLogin(session));
}

private async void ValidateAndLogin(AndroidSession session)
{
    bool isValid = await CheckUserCredentialsAsync(session.Username, session.AccessToken);
    
    if (isValid)
    {
        session.TargetId = AssignUserId(session);
        var responseJson = BuildLoginResponse(session.TargetId, true);
        session.Send(responseJson);
    }
    else
    {
        session.Close(CloseReason.NormalClose);
    }
}

IIS と Windows Service の連携 (WCF)

外部の Web サイトからメッセージを送信するために、WCF を使用します。Windows サービスは Singleton モードの WCF サーバーとして動作し、Web アプリからの要求を受けます。

using System.ServiceModel;
using System.Runtime.Serialization;

// WCF サービス契約
[ServiceContract]
public interface INotificationHub
{
    [OperationContract]
    void BroadcastNotification(int userId, int userType, int msgType, string jsonData);
}

// WCF サービス実装
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public class HubService : INotificationHub
{
    public void BroadcastNotification(int userId, int userType, int msgType, string jsonData)
    {
        var noticeModel = new NoticePayload
        {
            UserId = userId,
            UserGroup = userType,
            Type = msgType,
            Content = jsonData
        };
        
        // メッセージキューに登録し、内部サービスに伝達
        InternalMessageHub.Enqueue(noticeModel);
    }
}

イベント駆動型による即時配信

WCF の呼び出しを即座に SuperSocket サーバーへ通知させるため、デlegates とシングルトンパターンを採用しています。これにより、非同期処理を制御しつつ、データの転送遅延を最小限に抑えています。

namespace Service.Core.Messaging
{
    /// <summary>
    /// メッセージ配信の中継マネージャー
    /// </summary>
    public sealed class MessageDispatcher
    {
        public static readonly MessageDispatcher Instance = new MessageDispatcher();
        public event Action<NoticePayload> NotificationReceived;

        private MessageDispatcher() { }

        public void RegisterForDelivery(Action<NoticePayload> handler)
        {
            NotificationReceived += handler;
        }

        public void Deliver(NoticePayload model)
        {
            if (NotificationReceived != null)
            {
                // サーバーのスレッドプールに任せることで即応性を確保
                Task.Run(() => NotificationReceived(model));
            }
        }
    }
}

Windows サービスのメインループでは、上記の Dispatcher に対するイベント登録を行い、WCF 経由で届いたデータを受信可能な状態にセットアップします。サービスのインストールと自動起動設定を行うことで、システム全体としての継続性を担保します。

タグ: SuperSocket C# WCF Windows Services Socket Programming

7月7日 19:27 投稿