使用WCF來實現一個ShadowSocks客戶端(三)

自定義Session管理及WCF版本的TcpClient和TcpListener

在WCF中Session的管理使用比較複雜,而且還和傳輸及消息格式有關聯,由於傳輸和消息都被自定義了,Session管理我就完全拋棄了WCF的部分,只用一個全局表示來爲ListenerChannel分配的每個連接表示Session,這個全局標識就是該Channel的Socket.Handler,簡單點說就是把socket id作爲session id。

之前介紹過我用InvokerStub來獲得連接創建和斷開通知,而通知的參數就是ISocketChannel

    public interface ISocketChannel : IChannel, IContextChannel
    {
        Socket Socket { get; }
    }

每個Channel都實現這個接口,IContextChannel會有個SessionId屬性,這個屬性在自定義實現裏就返回了Socket.Handler。

在Channel上有了session id作爲標識後,之前提到的Invoke第一個參數就有發揮的餘地了。作爲客戶端是不需要維護多連接的,所以基本不需要爲Session管理再做什麼,客戶端調用服務端的Invoke時連session id都不需要指定。而服務器就不一樣,需要對多客戶端進行管理維護。

之前爲了簡便起見我用了InvokerStub同時作爲服務端的Servie和客戶端Callback

    public delegate void InvokeDelegate(string sessionId, byte[] data);
    public delegate void ConnectionEventDelegate(ISocketChannel session);

    [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, 
        UseSynchronizationContext = false,
        InstanceContextMode = InstanceContextMode.Single)]
    public class InvokerStub : IInvokerService, IInvokerServiceCallback
    {
        public event InvokeDelegate OnInvoke = delegate { };
        public event ConnectionEventDelegate Connect = delegate { };
        public event ConnectionEventDelegate Disconnect = delegate { };

        public virtual void Invoke(string sessionId, byte[] data)
        {
            OnInvoke(sessionId, data);
        }

        internal virtual void OnOpen(ISocketChannel session)
        {
            Connect(session);
        }

        internal virtual void OnClose(ISocketChannel session)
        {
            Disconnect(session);
        }
    }

爲了代碼公用就先抽象出一個基礎類作爲服務端和客戶端的公共部分

    public abstract class WCFTcpBase : IDisposable
    {
        protected InvokerStub _stub;
        protected CustomBinding _customBinding;

        protected WCFTcpBase(IRealEncoder encoder)
        {
            _stub = new InvokerStub();
            _stub.OnInvoke += OnInvoke;
            _stub.Connect += _OnConnect;
            _stub.Disconnect += _OnDisconnect;

            _customBinding = new CustomBinding();
            if (encoder != null)
                _customBinding.Elements.Add(new InnerEncoderBingdingElement(encoder));
            _customBinding.Elements.Add(new CustomEncodingBindingElement());

            _customBinding.Elements.Add(new CustomTcpBindingElement(_stub));
        }

        protected virtual void OnInvoke(string sessionId, byte[] data)
        {
            OnData(sessionId, data);
        }

        protected virtual void _OnDisconnect(ISocketChannel session)
        {
            OnDisconnect(session);
        }

        protected virtual void _OnConnect(ISocketChannel session)
        {
            OnConnect(session);
        }

        protected abstract void OnDisconnect(ISocketChannel session);
        protected abstract void OnConnect(ISocketChannel session);
        protected abstract void OnData(string sessionId, byte[] data);

        public abstract void Open();
        public abstract void Close();

        public virtual void Dispose()
        {
            Close();
        }
    }

服務端在維護客戶端會話時,是在連接創建時把會話加入到hashtable,在斷開時再移除

        private ConcurrentDictionary<string, SessionItem> _sessions;

        protected class SessionItem
        {
            public ISocketChannel Session;
            public IInvokerServiceCallback Callback;
        }
        protected override void _OnConnect(ISocketChannel session)
        {
            _sessions[session.SessionId] = new SessionItem() { Session = session, Callback = null };

            base._OnConnect(session);
        }

        protected override void _OnDisconnect(ISocketChannel session)
        {
            base._OnDisconnect(session);

            SessionItem item;

            _sessions.TryRemove(session.SessionId, out item);
        }

這裏要注意的是,CallbackChannel只有通過OperationContext.Current才能獲得,但是由於這裏的Connect事件是由Stub打入的,這時WCF的OperationContext.Current還沒創建,所以這時沒辦法得到CallbackChannel,只有客戶端成功調用Invoke這個Service後才能從OperationContext.Current獲得CallbackChannel

        protected override void OnInvoke(string sessionId, byte[] data)
        {
            var item = GetSessionItem(sessionId);

            if (item.Callback == null)
                item.Callback = OperationContext.Current.GetCallbackChannel<IInvokerServiceCallback>();
            _currentCallback = item.Callback;

            OnData(sessionId, data);
        }

因此我這個自定義綁定(也許應該說WCF本身)有個嚴重缺陷就是,必須客戶端先發起服務請求後服務端才能創建CallbackChannel,也就是說就算客戶端連接成功在沒發出請求前,服務端是沒辦法通過CallbackChannel往客戶端發送回調消息的,當然還是可以通過實際Channel調用Send來發送數據,但這就等於繞過WCF框架了。

關於這個缺陷還有個比較有趣的現象,要知道WCF自帶的Tcp綁定通道的連接打開和關閉通知只要實現IChannelInitializer,然後把實現追加到ChannelDispatcher上就能準確獲得通知,但實際測試時發現IChannelInitializer返回的代理Channel的Opened消息並不是在TcpChannel連接時發送的,也是在客戶端發送第一個數據消息到服務端時觸發的,然後是如果把IChannelInitializer運用在我這自定義的綁定上,客戶端斷開連接的時候Closed消息始終沒法觸發,就是因爲這點我才放棄使用IChannelInitializer轉而用Stub來獲得連接打開和關閉,雖然這種做法也不太符合WCF框架,但沒找到解決辦法前,這個做法應該算是不錯的選擇了。

現在算是前期準備工作都完成了,接下來就是運用之前所有的東西來實現ShadowSocks客戶端了,當然我只做了Tcp協議,所以socks5的udp部分就暫不支持了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章