HttpServerChannel.cs

  1. // ==++==
  2. // 
  3. //   
  4. //    Copyright (c) 2002 Microsoft Corporation.  All rights reserved.
  5. //   
  6. //    The use and distribution terms for this software are contained in the file
  7. //    named license.txt, which can be found in the root of this distribution.
  8. //    By using this software in any fashion, you are agreeing to be bound by the
  9. //    terms of this license.
  10. //   
  11. //    You must not remove this notice, or any other, from this software.
  12. //   
  13. // 
  14. // ==--==
  15. //==========================================================================
  16. //  File:       HttpServerChannel.cs
  17. //
  18. //  Summary:    Implements a client channel that transmits method calls over HTTP.
  19. //
  20. //  Classes:    public HttpClientChannel
  21. //              internal HttpClientTransportSink
  22. //
  23. //==========================================================================
  24. using System;
  25. using System.Collections;
  26. using System.IO;
  27. using System.Net;
  28. using System.Net.Sockets;
  29. using System.Reflection;
  30. using System.Runtime.Remoting;
  31. using System.Runtime.Remoting.Channels;
  32. using System.Runtime.Remoting.Messaging;
  33. using System.Runtime.Remoting.Metadata;
  34. using System.Runtime.Remoting.MetadataServices;
  35. using System.Text;
  36. using System.Threading;
  37. using System.Runtime.InteropServices;
  38. namespace System.Runtime.Remoting.Channels.Http
  39. {
  40.     /// <include file='doc/HttpServerChannel.uex' path='docs/doc[@for="HttpServerChannel"]/*' />
  41.     public class HttpServerChannel : BaseChannelWithProperties,
  42.                                      IChannelReceiver, IChannelReceiverHook
  43.     {
  44.         private int               _channelPriority = 1;  // priority of channel (default=1)
  45.         private String            _channelName = "http"// channel name
  46.         private String            _machineName = null;   // machine name
  47.         private int               _port = -1;            // port to listen on
  48.         private ChannelDataStore  _channelData = null;   // channel data
  49.         private String _forcedMachineName = null// an explicitly configured machine name
  50.         private bool _bUseIpAddress = true// by default, we'll use the ip address.
  51.         private IPAddress _bindToAddr = IPAddress.Any; // address to bind to.
  52.         private bool _bSuppressChannelData = false// should we hand out null for our channel data
  53.         
  54.         private IServerChannelSinkProvider _sinkProvider = null;
  55.         private HttpServerTransportSink    _transportSink = null;
  56.         private IServerChannelSink         _sinkChain = null;
  57.         private bool _wantsToListen = true;
  58.         private bool _bHooked = false// has anyone hooked into the channel?       
  59.         
  60.         
  61.         private TcpListener _tcpListener;
  62.         private Thread      _listenerThread;
  63.         private bool        _bListening = false// are we listening at the moment?
  64.         private Exception   _startListeningException = null// if an exception happens on the listener thread when attempting
  65.                                                          //   to start listening, that will get set here.
  66.         private AutoResetEvent  _waitForStartListening = new AutoResetEvent(false);
  67.         /// <include file='doc/HttpServerChannel.uex' path='docs/doc[@for="HttpServerChannel.HttpServerChannel"]/*' />
  68.         public HttpServerChannel() : base()
  69.         {
  70.             SetupMachineName();
  71.             SetupChannel();
  72.         }
  73.         /// <include file='doc/HttpServerChannel.uex' path='docs/doc[@for="HttpServerChannel.HttpServerChannel1"]/*' />
  74.         public HttpServerChannel(int port) : base()
  75.         {
  76.             _port = port;
  77.             SetupMachineName();
  78.             SetupChannel();
  79.         } // HttpServerChannel()
  80.     
  81.         /// <include file='doc/HttpServerChannel.uex' path='docs/doc[@for="HttpServerChannel.HttpServerChannel2"]/*' />
  82.         public HttpServerChannel(String name, int port) : base()
  83.         {
  84.             _channelName = name;
  85.             _port = port;
  86.             SetupMachineName();
  87.             SetupChannel();
  88.         } // HttpServerChannel()
  89.         /// <include file='doc/HttpServerChannel.uex' path='docs/doc[@for="HttpServerChannel.HttpServerChannel3"]/*' />
  90.         public HttpServerChannel(String name, int port, IServerChannelSinkProvider sinkProvider) : base()
  91.         {
  92.             _channelName = name;
  93.             _port = port;
  94.             _sinkProvider = sinkProvider;
  95.             SetupMachineName();
  96.             SetupChannel();
  97.         } // HttpServerChannel()
  98.         /// <include file='doc/HttpServerChannel.uex' path='docs/doc[@for="HttpServerChannel.HttpServerChannel4"]/*' />
  99.         public HttpServerChannel(IDictionary properties, IServerChannelSinkProvider sinkProvider) : base()
  100.         {               
  101.             if (properties != null)
  102.             {
  103.                 foreach (DictionaryEntry entry in properties)
  104.                 {
  105.                     switch ((String)entry.Key)
  106.                     {
  107.                     case "name": _channelName = (String)entry.Value; break
  108.                     case "bindTo": _bindToAddr = IPAddress.Parse((String)entry.Value); break;
  109.                     case "listen": _wantsToListen = Convert.ToBoolean(entry.Value); break;   
  110.                     case "machineName": _forcedMachineName = (String)entry.Value; break;
  111.                     case "port": _port = Convert.ToInt32(entry.Value); break;
  112.                     case "priority": _channelPriority = Convert.ToInt32(entry.Value); break;
  113.                     case "suppressChannelData": _bSuppressChannelData = Convert.ToBoolean(entry.Value); break;
  114.                     case "useIpAddress": _bUseIpAddress = Convert.ToBoolean(entry.Value); break;
  115.                 
  116.                     default
  117.                          throw new ArgumentException(
  118.                             String.Format(
  119.                                 CoreChannel.GetResourceString(
  120.                                     "Remoting_Channels_BadCtorArgs"),
  121.                                 entry.Key));
  122.                     }
  123.                 }
  124.             }
  125.             _sinkProvider = sinkProvider;
  126.             SetupMachineName();
  127.             SetupChannel();
  128.         } // HttpServerChannel
  129.         private void SetupMachineName()
  130.         {
  131.             if (_forcedMachineName != null)
  132.             {
  133.                 // an explicitly configured machine name was used
  134.                 _machineName = CoreChannel.DecodeMachineName(_forcedMachineName);
  135.             }
  136.             else
  137.             {
  138.                 if (!_bUseIpAddress)
  139.                     _machineName = CoreChannel.GetMachineName();
  140.                 else
  141.                 {
  142.                     if (_bindToAddr == IPAddress.Any)
  143.                         _machineName = CoreChannel.GetMachineIp();
  144.                     else
  145.                         _machineName = _bindToAddr.ToString();
  146.                 }
  147.             }
  148.         } // SetupMachineName
  149.         private void SetupChannel()
  150.         {   
  151.             // set channel data
  152.             // (These get changed inside of StartListening(), in the case where the listen
  153.             //   port is 0, because we can't determine the port number until after the
  154.             //   TcpListener starts.)
  155.             _channelData = new ChannelDataStore(null);
  156.             if (_port > 0)
  157.             {
  158.                 String channelUri = GetChannelUri();
  159.                 _channelData.ChannelUris = new String[1];
  160.                 _channelData.ChannelUris[0] = channelUri;
  161.                 _wantsToListen = false;
  162.             }
  163.             // set default provider (soap formatter) if no provider has been set
  164.             if (_sinkProvider == null)
  165.                 _sinkProvider = CreateDefaultServerProviderChain();
  166.             CoreChannel.CollectChannelDataFromServerSinkProviders(_channelData, _sinkProvider);
  167.             // construct sink chain
  168.             _sinkChain = ChannelServices.CreateServerChannelSinkChain(_sinkProvider, this);
  169.             _transportSink = new HttpServerTransportSink(_sinkChain);
  170.             // set sink properties on base class, so that properties will be chained.
  171.             SinksWithProperties = _sinkChain;
  172.             
  173.             if (_port >= 0)
  174.             {
  175.                 // Open a TCP port and create a thread to start listening
  176.                 _tcpListener = new TcpListener(_bindToAddr, _port);
  177.                 ThreadStart t = new ThreadStart(this.Listen);
  178.                 _listenerThread = new Thread(t);
  179.                 _listenerThread.IsBackground = true;
  180.                 // Wait for thread to spin up
  181.                 StartListening(null);
  182.             }
  183.         } // SetupChannel
  184.         private IServerChannelSinkProvider CreateDefaultServerProviderChain()
  185.         {
  186.             IServerChannelSinkProvider chain = new SdlChannelSinkProvider();            
  187.             IServerChannelSinkProvider sink = chain;
  188.             
  189.             sink.Next = new SoapServerFormatterSinkProvider();
  190.             sink = sink.Next;
  191.             sink.Next = new BinaryServerFormatterSinkProvider();
  192.             
  193.             return chain;
  194.         } // CreateDefaultServerProviderChain
  195.         //
  196.         // IChannel implementation
  197.         //
  198.         /// <include file='doc/HttpServerChannel.uex' path='docs/doc[@for="HttpServerChannel.ChannelPriority"]/*' />
  199.         public int ChannelPriority
  200.         {
  201.             get { return _channelPriority; }
  202.         }
  203.         /// <include file='doc/HttpServerChannel.uex' path='docs/doc[@for="HttpServerChannel.ChannelName"]/*' />
  204.         public String ChannelName
  205.         {
  206.             get { return _channelName; }
  207.         }
  208.         // returns channelURI and places object uri into out parameter
  209.         /// <include file='doc/HttpServerChannel.uex' path='docs/doc[@for="HttpServerChannel.Parse"]/*' />
  210.         public String Parse(String url, out String objectURI)
  211.         {            
  212.             return HttpChannelHelper.ParseURL(url, out objectURI);
  213.         } // Parse
  214.         //
  215.         // end of IChannel implementation
  216.         //
  217.         //
  218.         // IChannelReceiver implementation
  219.         //
  220.         /// <include file='doc/HttpServerChannel.uex' path='docs/doc[@for="HttpServerChannel.ChannelData"]/*' />
  221.         public Object ChannelData
  222.         {
  223.             get
  224.             {
  225.                 if (!_bSuppressChannelData && 
  226.                         (_bListening || _bHooked))
  227.                 {
  228.                     return _channelData;
  229.                 }
  230.                 else
  231.                 {
  232.                     return null;
  233.                 }
  234.             }
  235.         } // ChannelData
  236.         /// <include file='doc/HttpServerChannel.uex' path='docs/doc[@for="HttpServerChannel.GetChannelUri"]/*' />
  237.         public String GetChannelUri()
  238.         {
  239.             if ((_channelData != null) && (_channelData.ChannelUris != null))
  240.             {
  241.                 return _channelData.ChannelUris[0];
  242.             }
  243.             else
  244.             {
  245.                 return "http://" + _machineName + ":" + _port;
  246.             }
  247.         } // GetChannelURI
  248.         /// <include file='doc/HttpServerChannel.uex' path='docs/doc[@for="HttpServerChannel.GetUrlsForUri"]/*' />
  249.         public virtual String[] GetUrlsForUri(String objectUri)
  250.         {
  251.             String[] retVal = new String[1];
  252.             if (!objectUri.StartsWith("/"))
  253.                 objectUri = "/" + objectUri;
  254.             retVal[0] = GetChannelUri() + objectUri;
  255.             return retVal;
  256.         } // GetURLsforURI
  257.         /// <include file='doc/HttpServerChannel.uex' path='docs/doc[@for="HttpServerChannel.StartListening"]/*' />
  258.         public void StartListening(Object data)
  259.         {
  260.             InternalRemotingServices.RemotingTrace("HttpChannel.StartListening");
  261.             if (_port >= 0)
  262.             {
  263.                 if (_listenerThread.IsAlive == false)
  264.                 {
  265.                     _listenerThread.Start();
  266.                     _waitForStartListening.WaitOne(); // listener thread will signal this after starting TcpListener
  267.                     if (_startListeningException != null)
  268.                     {
  269.                         // An exception happened when we tried to start listening (such as "socket already in use)
  270.                         Exception e = _startListeningException;
  271.                         _startListeningException = null;
  272.                         throw e;
  273.                     }
  274.                     _bListening = true;
  275.                     // get new port assignment if a port of 0 was used to auto-select a port
  276.                     if (_port == 0)
  277.                     {
  278.                         _port = ((IPEndPoint)_tcpListener.LocalEndpoint).Port;
  279.                         
  280.                         if (_channelData != null)
  281.                         {
  282.                             String channelUri = GetChannelUri();
  283.                             _channelData.ChannelUris = new String[1];
  284.                             _channelData.ChannelUris[0] = channelUri;
  285.                         }
  286.                     }
  287.                 }
  288.             }
  289.         } // StartListening
  290.         /// <include file='doc/HttpServerChannel.uex' path='docs/doc[@for="HttpServerChannel.StopListening"]/*' />
  291.         public void StopListening(Object data)
  292.         {
  293.             InternalRemotingServices.RemotingTrace("HTTPChannel.StopListening");
  294.             if (_port > 0)
  295.             {
  296.                 _bListening = false;
  297.             
  298.                 // Ask the TCP listener to stop listening on the port
  299.                 if(null != _tcpListener)
  300.                 {
  301.                     _tcpListener.Stop();
  302.                 }
  303.             }
  304.         } // StopListening
  305.         //
  306.         // end of IChannelReceiver implementation
  307.         //
  308.         //
  309.         // IChannelReceiverHook implementation
  310.         //
  311.         /// <include file='doc/HttpServerChannel.uex' path='docs/doc[@for="HttpServerChannel.ChannelScheme"]/*' />
  312.         public String ChannelScheme { get { return "http"; } }
  313.         /// <include file='doc/HttpServerChannel.uex' path='docs/doc[@for="HttpServerChannel.WantsToListen"]/*' />
  314.         public bool WantsToListen 
  315.         { 
  316.             get { return _wantsToListen; } 
  317.             set { _wantsToListen = value; }
  318.         } // WantsToListen
  319.         /// <include file='doc/HttpServerChannel.uex' path='docs/doc[@for="HttpServerChannel.ChannelSinkChain"]/*' />
  320.         public IServerChannelSink ChannelSinkChain { get { return _sinkChain; } }
  321.         /// <include file='doc/HttpServerChannel.uex' path='docs/doc[@for="HttpServerChannel.AddHookChannelUri"]/*' />
  322.         public void AddHookChannelUri(String channelUri)
  323.         {
  324.             if (_channelData.ChannelUris != null)
  325.             {
  326.                 throw new RemotingException(
  327.                     CoreChannel.GetResourceString("Remoting_Http_LimitListenerOfOne"));
  328.             }
  329.             else
  330.             {
  331.                 // replace machine name with explicitly configured
  332.                 //   machine name or ip address if necessary
  333.                 if (_forcedMachineName != null)
  334.                 {
  335.                     channelUri = 
  336.                         HttpChannelHelper.ReplaceMachineNameWithThisString(channelUri, _forcedMachineName);
  337.                 }
  338.                 else
  339.                 if (_bUseIpAddress)
  340.                 {
  341.                     channelUri = 
  342.                         HttpChannelHelper.ReplaceMachineNameWithThisString(channelUri, CoreChannel.GetMachineIp());
  343.                 }
  344.             
  345.                 _channelData.ChannelUris = new String[] { channelUri };
  346.                 _wantsToListen = false;
  347.                 _bHooked = true;
  348.             }
  349.         } // AddHookChannelUri
  350.         
  351.         
  352.         //
  353.         // end of IChannelReceiverHook implementation
  354.         //
  355.         //
  356.         // Server helpers
  357.         //
  358.         // Thread for listening
  359.         void Listen()
  360.         {
  361.             bool bOkToListen = false;
  362.         
  363.             try
  364.             {
  365.                 _tcpListener.Start();
  366.                 bOkToListen = true;
  367.             }
  368.             catch (Exception e)
  369.             {
  370.                 _startListeningException = e;
  371.             }                
  372.             _waitForStartListening.Set(); // allow main thread to continue now that we have tried to start the socket                
  373.             InternalRemotingServices.RemotingTrace( "Waiting to Accept the Socket on Port: " + _port);
  374.             //
  375.             // Wait for an incoming socket
  376.             //
  377.             Socket socket;
  378.             
  379.             while (bOkToListen)
  380.             {
  381.                 InternalRemotingServices.RemotingTrace("TCPChannel::Listen - tcpListen.Pending() == true");                
  382.                 try
  383.                 {
  384.                     socket = _tcpListener.AcceptSocket();
  385.                     if (socket == null)
  386.                     {
  387.                         throw new RemotingException(
  388.                             String.Format(
  389.                                 CoreChannel.GetResourceString("Remoting_Socket_Accept"),
  390.                                 Marshal.GetLastWin32Error().ToString()));
  391.                     }
  392.                     else
  393.                     {
  394.                         // disable nagle delay
  395.                         socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, 1);
  396.                         // set linger option
  397.                         LingerOption lingerOption = new LingerOption(true, 3);
  398.                         socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Linger, lingerOption);
  399.                     
  400.                         HttpServerSocketHandler streamManager = new HttpServerSocketHandler(socket, CoreChannel.RequestQueue);
  401.                         streamManager.DataArrivedCallback = new WaitCallback(_transportSink.ServiceRequest);
  402.                         streamManager.BeginReadMessage();               
  403.                     }
  404.                 } 
  405.                 catch (Exception e)
  406.                 {
  407.                     if (!_bListening)
  408.                     {
  409.                         // We called Stop() on the tcp listener, so gracefully exit.
  410.                         bOkToListen = false;                        
  411.                     }
  412.                     else
  413.                     {
  414.                         // we want the exception to show up as unhandled since this
  415.                         //   is an unexpected failure.
  416.                         if (!(e is SocketException))
  417.                         {
  418.                             //throw;                   
  419.                         }
  420.                     }
  421.                 }
  422.             } // while (bOkToListen)
  423.         }
  424.         //
  425.         // Support for properties (through BaseChannelWithProperties)
  426.         //
  427.         /// <include file='doc/HttpServerChannel.uex' path='docs/doc[@for="HttpServerChannel.this"]/*' />
  428.         public override Object this[Object key]
  429.         {
  430.             get { return null; }
  431.         
  432.             set
  433.             {
  434.             }
  435.         } // this[]
  436.     
  437.         /// <include file='doc/HttpServerChannel.uex' path='docs/doc[@for="HttpServerChannel.Keys"]/*' />
  438.         public override ICollection Keys 
  439.         {
  440.             get
  441.             {
  442.                 return new ArrayList(); 
  443.             }
  444.         }
  445.     } // HttpServerChannel
  446.     
  447.     internal class HttpServerTransportSink : IServerChannelSink
  448.     {
  449.         private static String s_serverHeader =
  450.             "MS .NET Remoting, MS .NET CLR " + System.Environment.Version.ToString();
  451.     
  452.         // sink state
  453.         private IServerChannelSink _nextSink;
  454.         
  455.         public HttpServerTransportSink(IServerChannelSink nextSink)
  456.         {
  457.             _nextSink = nextSink;
  458.         } // IServerChannelSink
  459.         
  460.     
  461.         internal void ServiceRequest(Object state)
  462.         {        
  463.             HttpServerSocketHandler streamManager = (HttpServerSocketHandler)state;
  464.             ITransportHeaders headers = streamManager.ReadHeaders();
  465.             Stream requestStream = streamManager.GetRequestStream();
  466.             // process request
  467.             ServerChannelSinkStack sinkStack = new ServerChannelSinkStack();
  468.             sinkStack.Push(this, streamManager);
  469.             IMessage responseMessage;
  470.             ITransportHeaders responseHeaders;
  471.             Stream responseStream;
  472.             ServerProcessing processing = 
  473.                 _nextSink.ProcessMessage(sinkStack, null, headers, requestStream, 
  474.                                          out responseMessage,
  475.                                          out responseHeaders, out responseStream);
  476.             // handle response
  477.             switch (processing)
  478.             {                    
  479.             case ServerProcessing.Complete:
  480.             {
  481.                 // Send the response. Call completed synchronously.
  482.                 sinkStack.Pop(this);
  483.                 streamManager.SendResponse(responseStream, "200""OK", responseHeaders);
  484.                 break;
  485.             } // case ServerProcessing.Complete
  486.             
  487.             case ServerProcessing.OneWay:
  488.             {
  489.                 // Just send back a 200 OK
  490.                 streamManager.SendResponse(null"202""Accepted", responseHeaders);
  491.                 break;
  492.             } // case ServerProcessing.OneWay
  493.             case ServerProcessing.Async:
  494.             {
  495.                 sinkStack.StoreAndDispatch(this, streamManager);
  496.                 break;
  497.             }// case ServerProcessing.Async
  498.             } // switch (processing)
  499.             // async processing will take care if handling this later
  500.             if (processing != ServerProcessing.Async)
  501.             {
  502.                 if (streamManager.CanServiceAnotherRequest())
  503.                     streamManager.BeginReadMessage();
  504.                 else
  505.                     streamManager.Close();
  506.             }
  507.             
  508.         } // ServiceRequest
  509.       
  510.         //
  511.         // IServerChannelSink implementation
  512.         //
  513.         public ServerProcessing ProcessMessage(IServerChannelSinkStack sinkStack,
  514.             IMessage requestMsg,
  515.             ITransportHeaders requestHeaders, Stream requestStream,
  516.             out IMessage responseMsg, out ITransportHeaders responseHeaders,
  517.             out Stream responseStream)
  518.         {
  519.             // NOTE: This doesn't have to be implemented because the server transport
  520.             //   sink is always first.
  521.             throw new NotSupportedException();
  522.         } // ProcessMessage
  523.            
  524.         public void AsyncProcessResponse(IServerResponseChannelSinkStack sinkStack, Object state,
  525.                                          IMessage msg, ITransportHeaders headers, Stream stream)                 
  526.         {
  527.             HttpServerSocketHandler streamManager = null;
  528.             streamManager = (HttpServerSocketHandler)state;
  529.             // send the response
  530.             streamManager.SendResponse(stream, "200""OK", headers);
  531.             if (streamManager.CanServiceAnotherRequest())
  532.                 streamManager.BeginReadMessage();
  533.             else
  534.                 streamManager.Close();            
  535.         } // AsyncProcessResponse
  536.         public Stream GetResponseStream(IServerResponseChannelSinkStack sinkStack, Object state,
  537.                                         IMessage msg, ITransportHeaders headers)
  538.         {
  539.             HttpServerSocketHandler streamManager = (HttpServerSocketHandler)state;
  540.             if (streamManager.AllowChunkedResponse)
  541.                 return streamManager.GetResponseStream("200""OK", headers);
  542.             else
  543.                 return null;
  544.         } // GetResponseStream
  545.         public IServerChannelSink NextChannelSink
  546.         {
  547.             get { return _nextSink; }
  548.         }
  549.         public IDictionary Properties
  550.         {
  551.             get { return null; }
  552.         } // Properties
  553.         
  554.         //
  555.         // end of IServerChannelSink implementation
  556.         //
  557.         internal static String ServerHeader
  558.         {
  559.             get { return s_serverHeader; }
  560.         }
  561.         
  562.         
  563.     } // HttpServerTransportSink
  564.     internal class ErrorMessage: IMethodCallMessage
  565.     {
  566.         // IMessage
  567.         public IDictionary Properties     { getreturn null;} }
  568.         // IMethodMessage
  569.         public String Uri                      { getreturn m_URI; } }
  570.         public String MethodName               { getreturn m_MethodName; }}
  571.         public String TypeName                 { getreturn m_TypeName; } }
  572.         public Object MethodSignature          { get { return m_MethodSignature;} }
  573.         public MethodBase MethodBase           { get { return null; }}
  574.         public int ArgCount                    { get { return m_ArgCount;} }
  575.         public String GetArgName(int index)    { return m_ArgName; }
  576.         public Object GetArg(int argNum)       { return null;}
  577.         public Object[] Args                   { get { return null;} }
  578.         public bool HasVarArgs                 { get { return false;} }
  579.         public LogicalCallContext LogicalCallContext { get { return null; }}
  580.         // IMethodCallMessage
  581.         public int InArgCount                  { get { return m_ArgCount;} }
  582.         public String GetInArgName(int index)   { return null; }
  583.         public Object GetInArg(int argNum)      { return null;}
  584.         public Object[] InArgs                { get { return null; }}
  585.         String m_URI = "Exception";
  586.         String m_MethodName = "Unknown";
  587.         String m_TypeName = "Unknown";
  588.         Object m_MethodSignature = null;
  589.         int m_ArgCount = 0;
  590.         String m_ArgName = "Unknown";
  591.     } // ErrorMessage
  592. // namespace System.Runtime.Remoting.Channels.Http
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值