最近工作中遇到一个Remoting的回调的问题,即Client取得Server注册后对象后调用其函数,并传递一个Delegate参数,以让服务器Callback。这个功能在局域网内测试通过,如果配成公网地址在局域网内也是成功的,但当放到真正的公网环境中就失败了。其中Server和Client都是在各自的局域网内,通过服务器连接公网,服务器都有固定的公网IP地址,并将Server端公网地址的7788端口映射到Server所在机器,将Client端公网地址的7789端口映射到Client所在机器。经过大量资料的查询和调试,终于成功了,现将方法公布出来与大家一起分享。
一.公用部分:Common工程
1.定义Server端要发布的类的Interface:ICoordinator.cs
using System;
namespace Common
{
//定义Server端要发布的类的Interface
public interface ICoordinator
{
// Register a new Server
int RegisterServer(ServerInfo serverInfo);
}
}
2.定义发布类中方法的参数类和委托:ServerInfo.cs
using System;
namespace Common
{
//定义Callback的Delegate
public delegate void ReportDelegate(string s);
//定义发布类中方法的参数类
[Serializable]
public class ServerInfo
{
public ServerInfo(ReportDelegate onReport)
{
this.OnReport = onReport;
}
public readonly ReportDelegate OnReport;
}
}
3.Channel注册和反注册的通用代码:ChannelCom.cs
using System;
using System.Collections;
using System.Net;
using System.Reflection;
using System.Runtime.Serialization.Formatters;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using System.Runtime.Remoting.Channels.Http;
namespace Common
{
public enum ChannelGroup
{
TCP,
HTTP
}
public class ChannelCom
{
public static void StartChannel(ChannelGroup channelGroup, string channelName, string ip, int port)
{
BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider();
BinaryClientFormatterSinkProvider clientProvider = new BinaryClientFormatterSinkProvider();
serverProvider.TypeFilterLevel = TypeFilterLevel.Full;
IDictionary props = new Hashtable();
if (channelName != null && channelName != "")
props["name"] = channelName;
props["port"] = port;
if(channelGroup == ChannelGroup.TCP)
{
TcpChannel channel = (TcpChannel)GetRegisteredChannel(channelName);
if (channel == null)
{
channel = new TcpChannel(props,clientProvider,serverProvider);
ChannelDataStore channelData = (ChannelDataStore)channel.ChannelData;
SetChannelUris(channelData, ip);
}
ChannelServices.RegisterChannel(channel);
}
else
{
HttpChannel channel = (HttpChannel)GetRegisteredChannel(channelName);
if (channel == null)
{
channel = new HttpChannel(props,clientProvider,serverProvider);
ChannelDataStore channelData = (ChannelDataStore)channel.ChannelData;
SetChannelUris(channelData, ip);
}
ChannelServices.RegisterChannel(channel);
}
}
public static void StopChannel(string channelName)
{
IChannel channel = GetRegisteredChannel(channelName);
if (channel != null)
{
ChannelServices.UnregisterChannel(channel);
}
}
private static void SetChannelUris(ChannelDataStore channelData, string newIpAddress)
{
//检查是否进行调整,"(HOLD)"是个人定义的
if (newIpAddress != "(HOLD)")
{
if (newIpAddress == null || newIpAddress.Trim() == "")
{
//获得本机IP地址(最后一个) 即:如果有外网地址使用外网地址,否则使用内网地址
System.Net.IPAddress[] ipList = System.Net.Dns.GetHostByName(System.Net.Dns.GetHostName()).AddressList;
string localIPAddress;
if (ipList.Length > 1)
localIPAddress = ipList[1].ToString();
else
localIPAddress = ipList[0].ToString();
newIpAddress = localIPAddress;
}
string localPoint = channelData.ChannelUris[0];
//取得协议
int i = localPoint.IndexOf(":");
string confer = localPoint.Substring(0,i + 3);
//取得信道的端口号
i = localPoint.LastIndexOf(":");
string localPort = localPoint.Substring(i,localPoint.Length - i);
//重设信道IP地址
string[] IpAndPort = {confer + newIpAddress + localPort};
channelData.ChannelUris = IpAndPort;
}
}
private static IChannel GetRegisteredChannel(string channelName)
{
foreach(IChannel channel in ChannelServices.RegisteredChannels)
{
if (channel.ChannelName == channelName)
{
return channel;
}
}
return null;
}
}
}
二.Server代码:Server工程
1.实现Server端要发布的类:CenterCoordinator.cs
using System;
using Common;
namespace Server
{
//实现Server端要发布的类
public sealed class CenterCoordinator : MarshalByRefObject, ICoordinator
{
public override object InitializeLifetimeService()
{
return null;
}
public int RegisterServer(ServerInfo serverInfo)
{
string s = "Hello World!";
//进行Callback
if (serverInfo != null && serverInfo.OnReport != null)
{
serverInfo.OnReport("OK");
return 1;
}
else
return 0;
}
}
}
2.开启服务器的窗体:FmServer.cs
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.Runtime.Remoting;
using Common;
namespace Server
{
/// <summary>
/// Summary description for FmServer.
/// </summary>
public class FmServer : System.Windows.Forms.Form
{
private CenterCoordinator _coordinator = null;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.TextBox txtIP;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.TextBox txtPort;
private System.Windows.Forms.Button btnStart;
private System.Windows.Forms.RadioButton rbTCP;
private System.Windows.Forms.RadioButton rbHttp;
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.Container components = null;
public FmServer()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();
//
// TODO: Add any constructor code after InitializeComponent call
//
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.label1 = new System.Windows.Forms.Label();
this.txtIP = new System.Windows.Forms.TextBox();
this.txtPort = new System.Windows.Forms.TextBox();
this.label2 = new System.Windows.Forms.Label();
this.btnStart = new System.Windows.Forms.Button();
this.rbTCP = new System.Windows.Forms.RadioButton();
this.rbHttp = new System.Windows.Forms.RadioButton();
this.SuspendLayout();
//
// label1
//
this.label1.Location = new System.Drawing.Point(16, 16);
this.label1.Name = "label1";
this.label1.TabIndex = 0;
this.label1.Text = "本地公网IP:";
//
// txtIP
//
this.txtIP.Location = new System.Drawing.Point(88, 12);
this.txtIP.Name = "txtIP";
this.txtIP.Size = new System.Drawing.Size(112, 21);
this.txtIP.TabIndex = 1;
this.txtIP.Text = "";
//
// txtPort
//
this.txtPort.Location = new System.Drawing.Point(88, 40);
this.txtPort.Name = "txtPort";
this.txtPort.Size = new System.Drawing.Size(112, 21);
this.txtPort.TabIndex = 3;
this.txtPort.Text = "7788";
//
// label2
//
this.label2.Location = new System.Drawing.Point(16, 42);
this.label2.Name = "label2";
this.label2.TabIndex = 2;
this.label2.Text = "开放端口号:";
//
// btnStart
//
this.btnStart.Location = new System.Drawing.Point(16, 72);
this.btnStart.Name = "btnStart";
this.btnStart.Size = new System.Drawing.Size(240, 23);
this.btnStart.TabIndex = 4;
this.btnStart.Text = "启动";
this.btnStart.Click += new System.EventHandler(this.btnStart_Click);
//
// rbTCP
//
this.rbTCP.Checked = true;
this.rbTCP.Location = new System.Drawing.Point(208, 8);
this.rbTCP.Name = "rbTCP";
this.rbTCP.Size = new System.Drawing.Size(48, 24);
this.rbTCP.TabIndex = 5;
this.rbTCP.TabStop = true;
this.rbTCP.Text = "TCP";
//
// rbHttp
//
this.rbHttp.Location = new System.Drawing.Point(208, 40);
this.rbHttp.Name = "rbHttp";
this.rbHttp.Size = new System.Drawing.Size(48, 24);
this.rbHttp.TabIndex = 6;
this.rbHttp.Text = "HTTP";
//
// FmServer
//
this.AutoScaleBaseSize = new System.Drawing.Size(6, 14);
this.ClientSize = new System.Drawing.Size(264, 102);
this.Controls.Add(this.rbHttp);
this.Controls.Add(this.rbTCP);
this.Controls.Add(this.btnStart);
this.Controls.Add(this.txtPort);
this.Controls.Add(this.label2);
this.Controls.Add(this.txtIP);
this.Controls.Add(this.label1);
this.Name = "FmServer";
this.Text = "Server";
this.Load += new System.EventHandler(this.FmServer_Load);
this.ResumeLayout(false);
}
#endregion
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.Run(new FmServer());
}
private void btnStart_Click(object sender, System.EventArgs e)
{
if (btnStart.Text == "启动")
{
//Server端开启Channel
ChannelGroup channelGroup;
if (rbTCP.Checked)
channelGroup = ChannelGroup.TCP;
else
channelGroup = ChannelGroup.HTTP;
string channelName = "MyChannel";
string ip = txtIP.Text; //这是本机在公网上的地址
int port = int.Parse(txtPort.Text); //这是开放的端口
ChannelCom.StartChannel(channelGroup, channelName, ip, port);
//发布对象
_coordinator = new CenterCoordinator();
string servant = "MyCoordinator"; //这是发布对象的访问名
ObjRef objRef = RemotingServices.Marshal(_coordinator, servant);
btnStart.Text = "停止";
txtIP.Enabled = false;
txtPort.Enabled = false;
rbTCP.Enabled = false;
rbHttp.Enabled = false;
}
else
{
RemotingServices.Disconnect(_coordinator);
ChannelCom.StopChannel("MyChannel");
btnStart.Text = "启动";
txtIP.Enabled = true;
txtPort.Enabled = true;
rbTCP.Enabled = true;
rbHttp.Enabled = true;
}
}
private void FmServer_Load(object sender, System.EventArgs e)
{
txtIP.Text = System.Net.Dns.Resolve(System.Net.Dns.GetHostName()).AddressList[0].ToString();
}
}
}
三.Client代码:Client工程
1.定义包含Callback函数的类:CallbackClass.cs
using System;
using Common;
namespace Client
{
//包含Callback函数的类,必须保证该回调函数所在类继承自MarshalByRefObject
[Serializable]
public class CallbackClass : MarshalByRefObject
{
public override object InitializeLifetimeService()
{
return null;
}
public void OnReport(string s)
{
System.Windows.Forms.MessageBox.Show(s, "回调成功");
}
}
}
2.连接服务器并调用的客户端窗体:FmClient.cs
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using Common;
namespace Client
{
public class FmClient : System.Windows.Forms.Form
{
ICoordinator _coordinator = null;
private System.Windows.Forms.RadioButton rbHttp;
private System.Windows.Forms.RadioButton rbTCP;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label label3;
private System.Windows.Forms.Label label4;
private System.Windows.Forms.Label label5;
private System.Windows.Forms.TextBox txtSvrPort;
private System.Windows.Forms.TextBox txtSvrIP;
private System.Windows.Forms.TextBox txtCallbackPort;
private System.Windows.Forms.TextBox txtLocalIP;
private System.Windows.Forms.Button btnLink;
private System.Windows.Forms.Button btnAccess;
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.Container components = null;
public FmClient()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();
//
// TODO: Add any constructor code after InitializeComponent call
//
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.rbHttp = new System.Windows.Forms.RadioButton();
this.rbTCP = new System.Windows.Forms.RadioButton();
this.txtSvrPort = new System.Windows.Forms.TextBox();
this.label2 = new System.Windows.Forms.Label();
this.txtSvrIP = new System.Windows.Forms.TextBox();
this.label1 = new System.Windows.Forms.Label();
this.label3 = new System.Windows.Forms.Label();
this.txtCallbackPort = new System.Windows.Forms.TextBox();
this.label4 = new System.Windows.Forms.Label();
this.txtLocalIP = new System.Windows.Forms.TextBox();
this.label5 = new System.Windows.Forms.Label();
this.btnLink = new System.Windows.Forms.Button();
this.btnAccess = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// rbHttp
//
this.rbHttp.Location = new System.Drawing.Point(200, 40);
this.rbHttp.Name = "rbHttp";
this.rbHttp.Size = new System.Drawing.Size(48, 24);
this.rbHttp.TabIndex = 12;
this.rbHttp.Text = "HTTP";
//
// rbTCP
//
this.rbTCP.Checked = true;
this.rbTCP.Location = new System.Drawing.Point(200, 16);
this.rbTCP.Name = "rbTCP";
this.rbTCP.Size = new System.Drawing.Size(48, 24);
this.rbTCP.TabIndex = 11;
this.rbTCP.TabStop = true;
this.rbTCP.Text = "TCP";
//
// txtSvrPort
//
this.txtSvrPort.Location = new System.Drawing.Point(80, 40);
this.txtSvrPort.Name = "txtSvrPort";
this.txtSvrPort.Size = new System.Drawing.Size(112, 21);
this.txtSvrPort.TabIndex = 10;
this.txtSvrPort.Text = "7788";
//
// label2
//
this.label2.Location = new System.Drawing.Point(8, 43);
this.label2.Name = "label2";
this.label2.TabIndex = 9;
this.label2.Text = "服务端口号:";
//
// txtSvrIP
//
this.txtSvrIP.Location = new System.Drawing.Point(80, 16);
this.txtSvrIP.Name = "txtSvrIP";
this.txtSvrIP.Size = new System.Drawing.Size(112, 21);
this.txtSvrIP.TabIndex = 8;
this.txtSvrIP.Text = "";
//
// label1
//
this.label1.Location = new System.Drawing.Point(8, 19);
this.label1.Name = "label1";
this.label1.TabIndex = 7;
this.label1.Text = "服务公网IP:";
//
// label3
//
this.label3.ForeColor = System.Drawing.Color.Red;
this.label3.Location = new System.Drawing.Point(8, 80);
this.label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(184, 23);
this.label3.TabIndex = 13;
this.label3.Text = "下面两个仅用于有回调的情况";
//
// txtCallbackPort
//
this.txtCallbackPort.Location = new System.Drawing.Point(80, 128);
this.txtCallbackPort.Name = "txtCallbackPort";
this.txtCallbackPort.Size = new System.Drawing.Size(112, 21);
this.txtCallbackPort.TabIndex = 17;
this.txtCallbackPort.Text = "0";
//
// label4
//
this.label4.Location = new System.Drawing.Point(8, 131);
this.label4.Name = "label4";
this.label4.TabIndex = 16;
this.label4.Text = "回调端口号:";
//
// txtLocalIP
//
this.txtLocalIP.Location = new System.Drawing.Point(80, 104);
this.txtLocalIP.Name = "txtLocalIP";
this.txtLocalIP.Size = new System.Drawing.Size(112, 21);
this.txtLocalIP.TabIndex = 15;
this.txtLocalIP.Text = "";
//
// label5
//
this.label5.Location = new System.Drawing.Point(8, 107);
this.label5.Name = "label5";
this.label5.TabIndex = 14;
this.label5.Text = "本地公网IP:";
//
// btnLink
//
this.btnLink.Location = new System.Drawing.Point(8, 160);
this.btnLink.Name = "btnLink";
this.btnLink.Size = new System.Drawing.Size(112, 32);
this.btnLink.TabIndex = 18;
this.btnLink.Text = "连接";
this.btnLink.Click += new System.EventHandler(this.btnLink_Click);
//
// btnAccess
//
this.btnAccess.Location = new System.Drawing.Point(128, 160);
this.btnAccess.Name = "btnAccess";
this.btnAccess.Size = new System.Drawing.Size(112, 32);
this.btnAccess.TabIndex = 19;
this.btnAccess.Text = "访问";
this.btnAccess.Click += new System.EventHandler(this.btnAccess_Click);
//
// FmClient
//
this.AutoScaleBaseSize = new System.Drawing.Size(6, 14);
this.ClientSize = new System.Drawing.Size(256, 198);
this.Controls.Add(this.btnAccess);
this.Controls.Add(this.btnLink);
this.Controls.Add(this.txtCallbackPort);
this.Controls.Add(this.txtLocalIP);
this.Controls.Add(this.txtSvrPort);
this.Controls.Add(this.txtSvrIP);
this.Controls.Add(this.label4);
this.Controls.Add(this.label5);
this.Controls.Add(this.label3);
this.Controls.Add(this.rbHttp);
this.Controls.Add(this.rbTCP);
this.Controls.Add(this.label2);
this.Controls.Add(this.label1);
this.Name = "FmClient";
this.Text = "Client";
this.Load += new System.EventHandler(this.FmClient_Load);
this.ResumeLayout(false);
}
#endregion
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.Run(new FmClient());
}
private void btnLink_Click(object sender, System.EventArgs e)
{
if (btnLink.Text == "连接")
{
//获取远程对象
ChannelGroup channelGroup;
if (rbTCP.Checked)
channelGroup = ChannelGroup.TCP;
else
channelGroup = ChannelGroup.HTTP;
string protocol = "tcp";
if (channelGroup == ChannelGroup.HTTP) protocol = "http";
string serverIp = txtSvrIP.Text; //Server所在公网地址
int serverPort = int.Parse(txtSvrPort.Text); //Server开放的端口
string servant = "MyCoordinator"; //Server端发布对象的访问名
string uri = string.Format("{0}://{1}:{2}/{3}" , protocol , serverIp , serverPort , servant);
_coordinator = Activator.GetObject(typeof(ICoordinator) ,uri) as ICoordinator;
//开启Callback通道的代码
string channelName = "MyChannel"; //注意这里的名字跟Server端开启的该通道名字一致,这不一定,但建议
string localip = txtLocalIP.Text; //这是本机在公网上的地址
int loaclport = int.Parse(txtCallbackPort.Text); //这是Callback的端口,如果你的机器直接连接公网则可以设置为0,否则应设置具体的数字,因为要做端口映射
ChannelCom.StartChannel(channelGroup, channelName, localip, loaclport);
btnLink.Text = "断开";
txtSvrIP.Enabled = false;
txtSvrPort.Enabled = false;
txtLocalIP.Enabled = false;
txtCallbackPort.Enabled = false;
rbTCP.Enabled = false;
rbHttp.Enabled = false;
btnAccess.Enabled = true;
}
else
{
_coordinator = null;
ChannelCom.StopChannel("MyChannel");
btnLink.Text = "连接";
txtSvrIP.Enabled = true;
txtSvrPort.Enabled = true;
txtLocalIP.Enabled = true;
txtCallbackPort.Enabled = true;
rbTCP.Enabled = true;
rbHttp.Enabled = true;
btnAccess.Enabled = false;
}
}
private void FmClient_Load(object sender, System.EventArgs e)
{
txtLocalIP.Text = System.Net.Dns.Resolve(System.Net.Dns.GetHostName()).AddressList[0].ToString();
}
private void btnAccess_Click(object sender, System.EventArgs e)
{
if (_coordinator != null)
{
CallbackClass lCall = new CallbackClass();
ServerInfo lServerInfo = new ServerInfo(new ReportDelegate(lCall.OnReport));
int i = _coordinator.RegisterServer(lServerInfo);
}
}
}
}
总结一下:
1.关键点是端口的映射(客户端回调通道的端口要在其服务器上以公网地址映射到该电脑上).和通道地址的修改。
2.客户端启动的回调通道的地址要是公网的地址。但想QQ的客户端怎么知道我所在的公网地址呢?自己想办法哦,比如客户端先请求服务端返回当前连接的地址(我猜这总该是客户端公网的地址),然后再用该地址启动回调通道。
3.我在做此测试的时候出现"Cannot find assembly Client"的序列化错误,经过研究发现是Server工程和Client工程都要用到的Common工程生成的Common.dll文件版本不一致的原因。为什么不一致?因为两个工程编译后生成的文件在不同的目录,因为引用关系所以Common工程被编译了两遍,而每编译一遍它的版本号是不同的。解决办法是将Server工程和Client工程的输出目录设为同一个目录,或者在运行时是两边的Common.dll为同一个版本的。