最近有鄙人公司的一位客户要求我们实施一个基于web 的应用程序,要求呢就是可让客户使用web 来访问他们的私有数据。其中的一个结构是不要从web层("web tier")来访问客户数据库,基本上,除了通过在中间层上使用Webservice 或者使用.NET Remoting 框架,我没有别的选择。
经过仔细考虑后,我选择了后者。中间层(Remoting tier)放在IIS下,然后,传递数据的话,让中间层向网络上的数据库层(database tier)来发送请求,并负责接发数据,这样就形成了 UI层<->Remoting tier<->Database tier这样的一种数据处理模式,结构示图如下:
虽然表面上这个像是一个有点儿幼稚的方式来强制性地保护用户的敏感数据,不过,它去很好地做到了一样:除非有人发现了这个用来向服务器层(.NET remoging层)发送请求的中间层的内部接口,否则,它绝对无法获取到用户数据。另外,由于,我把remoting框架 架设到IIS下的中间层计算机上,这样就很容易地实施标准的IIS授权与安全验证,需要的话甚至可以是SSL。
这里我所给出的是一个通用的结构:一个Remoting class library,用来接受由connection名称,存储过程与存储过程参数所一个数据组所组成的数据库请求.还有一个自定义的"fire and forget"方法用来记录用户页面查看记录用作用户tracking 目的.
下面的代码,会向你展示如何生成一个IIS下的使用HTTP通道与二进制格式的remoting类,创建一个单独的"Mirror"代理类来处理客户端必要的接口,创建客户端与服务器端都需要的用来生成remoting channel 与URI 配置文件,以及一个用使用这个结构的简单的Web 客户端.如果需要,你可以让所有代码运行在一台机器上,当然,分别运行在两个分离的IIS应用程序上:一个是服务端一个是客户端,又或者,你只需要一个one-line 对客户端配置文件的次要的更改,你就可以无限量地运行在很多单独的机器上。
首先,我们来看下,Remoting server 类,这里我们故且叫它"General .CustomerTracker"
<In VB.NET>
---------------------------------------------------------------------------------------
Option Explicit On
Option Strict On
Imports System
Imports System.Data
Imports System.Data.SqlClient
Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Messaging
Imports Microsoft.ApplicationBlocks.Data
Imports System.IO
Imports System.Collections.Specialized
<Serializable()> _
Public Class CustomerTracker
Inherits MarshalByRefObject
Public DebugMode As Boolean = False
Private Settings As NameValueCollection
Public Sub New()
If DebugMode Then WriteInfoToFile("Started " _
& System.DateTime.Now.ToLongTimeString & vbCrLf, "log.txt")
Settings = System.Configuration.ConfigurationSettings.AppSettings
DebugMode = Convert.ToBoolean(Settings("debugMode"))
End Sub
<OneWay()> _
Public Function InsertPageView(ByVal UserIPAddress As String, _ ByVal UserLoginId As String, ByVal SourceUrl As String, _
ByVal BrowseType As String, ByVal ClickThruId As String) As Integer
Dim strConn As String = Convert.ToString(Settings("sqlConn"))
Dim cmd As New SqlCommand
Dim cn As New SqlConnection(strConn)
cmd.CommandType = CommandType.StoredProcedure
cmd.CommandText = "usp_InsertUserVisitData"
cmd.Connection = cn
cmd.Parameters.Add(New SqlParameter("@OriginIPAddress", UserIPAddress))
cmd.Parameters.Add(New SqlParameter("@UserId", UserLoginId))
cmd.Parameters.Add(New SqlParameter("@SourceUrl", SourceUrl))
cmd.Parameters.Add(New SqlParameter("@BrowserType", BrowseType))
cmd.Parameters.Add(New SqlParameter("@ClickThruID", ClickThruId))
cn.Open()
Dim retval As Integer = cmd.ExecuteNonQuery()
cn.Close()
cmd.Dispose()
If DebugMode Then WriteInfoToFile("Did page insert " _
& System.DateTime.Now.ToLongTimeString & vbCrLf, "log.txt")
Return retval
End Function
Public Function GenericSpNonQuery(ByVal connectionName As String, _ ByVal spName As String, ByVal spParams As Object()) As Integer
Dim strConn As String = Convert.ToString(Settings("sqlConn"))
Dim retval As Integer = SqlHelper.ExecuteNonQuery(strConn, spName, spParams)
If DebugMode Then WriteInfoToFile("Did GenericSpNonQuery insert" _
& System.DateTime.Now.ToLongTimeString & vbCrLf, "log.txt")
Return retval
End Function
Public Function GenericSpReturnDataSet(ByVal connectionName As String, _ ByVal spName As String, ByVal spParams As Object()) As DataSet
Dim strConn As String = Convert.ToString(Settings("sqlConn"))
Dim ds As DataSet = SqlHelper.ExecuteDataset(strConn, spName, spParams)
If DebugMode Then WriteInfoToFile("Did SpReturnDataSet" _
& System.DateTime.Now.ToLongTimeString & vbCrLf, "log.txt")
Return ds
End Function
Public Function GenericSQLReturnDataSet(ByVal connectionName As String, _
ByVal strSQL As String) As DataSet
Dim strConn As String = Convert.ToString(Settings("sqlConn"))
Dim ds As DataSet = SqlHelper.ExecuteDataset(strConn, CommandType.Text, strSQL)
If DebugMode Then WriteInfoToFile("Did SQLReturnDataSet" _
& System.DateTime.Now.ToLongTimeString & vbCrLf, "log.txt")
Return ds
End Function
<OneWay()> _
Public Sub WriteInfoToFile(ByVal strData As String, ByVal strFileName As String)
If strFileName = "" Then strFileName = "Log.txt"
Try
Dim strPath As String = System.AppDomain.CurrentDomain.BaseDirectory & "/" & strFileName
Dim writer As StreamWriter = New StreamWriter(strPath, True) ' true for Append
writer.Write(strData & System.DateTime.Now.ToLongTimeString)
writer.Close()
Catch
Throw
End Try
End Sub
End Class
----------------------------------------------------------------------------------------
<In C# 翻译工作自动转换而成>
------------------------------------------------------------------------------------------
// TODO: Option Strict On ... Warning!!! not translated
using System ;
using System.Data ;
using System.Data.SqlClient ;
using System.Runtime.Remoting ;
using System.Runtime.Remoting.Messaging ;
using Microsoft.ApplicationBlocks.Data ;
using System.IO ;
using System.Collections.Specialized ;
[Serializable()]
public class CustomerTracker : MarshalByRefObject {
public bool DebugMode = false;
private NameValueCollection Settings ;
public CustomerTracker() {
if (DebugMode) {
WriteInfoToFile(( "Started "
+ (System.DateTime.Now.ToLongTimeString + "/r/n" )), "log.txt" ) ;
}
Settings = System.Configuration.ConfigurationSettings.AppSettings ;
DebugMode = Convert.ToBoolean(Settings[ "debugMode" ]) ;
}
[OneWay()]
public int InsertPageView( string UserIPAddress, void _, string UserLoginId, string SourceUrl, string BrowseType, string ClickThruId) {
string strConn = Convert.ToString(Settings[ "sqlConn" ]) ;
SqlCommand cmd = new SqlCommand() ;
SqlConnection cn = new SqlConnection(strConn) ;
cmd.CommandType = CommandType.StoredProcedure ;
cmd.CommandText = "usp_InsertUserVisitData" ;
cmd.Connection = cn ;
cmd.Parameters.Add( new SqlParameter( "@OriginIPAddress" , UserIPAddress)) ;
cmd.Parameters.Add( new SqlParameter( "@UserId" , UserLoginId)) ;
cmd.Parameters.Add( new SqlParameter( "@SourceUrl" , SourceUrl)) ;
cmd.Parameters.Add( new SqlParameter( "@BrowserType" , BrowseType)) ;
cmd.Parameters.Add( new SqlParameter( "@ClickThruID" , ClickThruId)) ;
cn.Open() ;
int retval = cmd.ExecuteNonQuery() ;
cn.Close() ;
cmd.Dispose() ;
if (DebugMode) {
WriteInfoToFile(( "Did page insert "
+ (System.DateTime.Now.ToLongTimeString + "/r/n" )), "log.txt" ) ;
}
return retval ;
}
public int GenericSpNonQuery( string connectionName, void _, string spName, object [] spParams) {
string strConn = Convert.ToString(Settings[ "sqlConn" ]) ;
int retval = SqlHelper.ExecuteNonQuery(strConn, spName, spParams) ;
if (DebugMode) {
WriteInfoToFile(( "Did GenericSpNonQuery insert"
+ (System.DateTime.Now.ToLongTimeString + "/r/n" )), "log.txt" ) ;
}
return retval ;
}
public DataSet GenericSpReturnDataSet( string connectionName, void _, string spName, object [] spParams) {
string strConn = Convert.ToString(Settings[ "sqlConn" ]) ;
DataSet ds = SqlHelper.ExecuteDataset(strConn, spName, spParams) ;
if (DebugMode) {
WriteInfoToFile(( "Did SpReturnDataSet"
+ (System.DateTime.Now.ToLongTimeString + "/r/n" )), "log.txt" ) ;
}
return ds ;
}
public DataSet GenericSQLReturnDataSet( string connectionName, string strSQL) {
string strConn = Convert.ToString(Settings[ "sqlConn" ]) ;
DataSet ds = SqlHelper.ExecuteDataset(strConn, CommandType.Text, strSQL) ;
if (DebugMode) {
WriteInfoToFile(( "Did SQLReturnDataSet"
+ (System.DateTime.Now.ToLongTimeString + "/r/n" )), "log.txt" ) ;
}
return ds ;
}
[OneWay()]
public void WriteInfoToFile( string strData, string strFileName) {
if ((strFileName == "" )) {
strFileName = "Log.txt" ;
}
try {
string strPath = (System.AppDomain.CurrentDomain.BaseDirectory + ( "//" + strFileName)) ;
StreamWriter writer = new StreamWriter(strPath, true ) ;
// true for Append
writer.Write((strData + System.DateTime.Now.ToLongTimeString)) ;
writer.Close() ;
}
catch (System.Exception Throw) {
}
}
}
------------------------------------------------------------------------------------------
注意,首先,这个类要继承自MarshalByRefObj,这是remoting 框架的必需的。你也有看到在两个方法中有OneWay attribute,这可以给不需要返回任何值的"Fire and forget"方法使用到,还有就是避免remoting框架必须创建其它元素来marshal object 到客户端,这与不需要创建代表的异步的远程调用极为相似.
下面是服务器web.conmfig (IIS)下下的web.config文件相关配置项:
------------------------------------------------------------------------------------
<configuration>
<appSettings>
<add key="sqlConn" value="Server=(local);DataBase=Usertracking;User id=sa;Password=;" />
<add key="debugMode" value="True" />
</appSettings>
<system.runtime.remoting>
<application>
<service>
<wellknown mode="Singleton"
type="General.CustomerTracker, General"
objectUri="CustomerTracker.soap" />
</service>
<channels>
<channel ref="http"/>
<serverProviders>
<formatter ref="binary" />
</serverProviders>
</channels>
</application>
</system.runtime.remoting>
</configuration>
-------------------------------------------------------------------------------------
大家都能看到,我添加了一个appSetting 段来设置我的debugMode Boolean(用于控制是否写日志事件到作为测试之用的log 文件中)与一个"sqlConn"段来保存我的连接字符串,客户端将需要传递这个连接字符串名作为调用方法的一个参数,保证我们的应用程序可以获得足够多的数据库连接。
同样要注意,我创建了一个Singleton 服务用的是"CustomerTracker.soap"这样的一个objectUri,它使用HTTP通道,以及二进制格式。当布署到IIS上后,你就可以使用浏览器请求到:
Http://<servername>/<vrootname>/CustomerTracker.soap?WSDL 然后,可以在浏览器里面看到生成的WSDL来确认你的服务器部署正确。
在可下载方案中,你也会看到微软应用程序块"SqlHelper" 也是部署到服务器下面来使数据库访问变得简易。在我的东西里面我着重地使用了这种方案,它会使远程调用(remoting calls/或请求)变得容易得多,因为它的很多方法有使用过载(overload),这样呢,可以接受一个简单的包含用于存储过程传值的的SQL参数值的Object array.它使用SqlCommand类的DeriveParameters 方法 来发现填充具体参数,当然,这涉及到对数据库的一个单独的调用,不过,事实上,我想你会发现,这基本上可以快到你基本感觉不到这个连接操作的存在。
General.dll 与Sqlhelper.dll程序集会被放在一个别名为IIImageHandler的Vroot的应用程序下的bin文件夹下面,web.config放在根录下面,好了,这样就算全部把remoting server在IIS下面部署好了,下面就可以直接运行了。web.config与DLLS并未被IIS锁定,所以部署的话呢,也就可以通过网络直接传递一下就好了。
现在,让我们到客户端看一下,它完全可在部署在一个另外一台计算机上面的(当然,作为测试目的,你也可以放在与remoting server放在同一台计算机上).
事实上,在下面的简单的客户端应用程序上有两个页面, 其一用于处理页面上的一个image这样可使客户的tracking info 写到数据库里面去,另外的一个"main page"事实上用来显示这些"images",有一个DataGrid来逞现客户tracking log 表的内容,还有一个按钮可以让我们来清除表中的log entries,简单其见呢,这里仅仅给出主页面,因为两者在概念上是相同的.
首先,主页面里面的关键代码块如下:
----------------------------------------------------------------------------------------
Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels.Http
Imports System.Runtime.Remoting.Channels
Imports System.Diagnostics
Public Class WebForm1
Inherits System.Web.UI.Page
Protected WithEvents Button1 As System.Web.UI.WebControls.Button
Protected WithEvents lblMessage As System.Web.UI.WebControls.Label
Protected WithEvents DataGrid1 As System.Web.UI.WebControls.DataGrid
#Region " Web Form Designer Generated Code "
'This call is required by the Web Form Designer.
<System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
End Sub
Private Sub Page_Init(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Init
'CODEGEN: This method call is required by the Web Form Designer
'Do not modify it using the code editor.
InitializeComponent()
End Sub
#End Region
Private Sub Page_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load
'Set a fake "user" for the tracking call
Session("loginid") = "TestUser"
' Make the remoting call to get the DataSet of log records
Dim mgr As General.CustomerTracker
mgr = New General.CustomerTracker
Dim ds As DataSet = mgr.GenericSpReturnDataSet("sqlConn", "usp_GetLogRecords", Nothing)
DataGrid1.DataSource = ds.Tables(0)
DataGrid1.DataBind()
End Sub
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
' Do the Delete Button call---
Dim mgr As New General.CustomerTracker
Dim retval As Integer = mgr.GenericSpNonQuery("sqlConn", "usp_UserTrackingDelete", Nothing)
lblMessage.Text = retval.ToString & " items deleted."
End Sub
End Class
----------------------------------------------------------------------------------------
<In C#>
----------------------------------------------------------------------------------------
using System.Runtime.Remoting.Channels.Http ;
using System.Runtime.Remoting.Channels ;
using System.Diagnostics ;
public class WebForm1 : System.Web.UI.Page {
protected System.Web.UI.WebControls.Button Button1 ;
protected System.Web.UI.WebControls.Label lblMessage ;
protected System.Web.UI.WebControls.DataGrid DataGrid1 ;
// This call is required by the Web Form Designer.
[System.Diagnostics.DebuggerStepThrough()]
private void InitializeComponent() {
}
private void Page_Init( object sender, void _, System.EventArgs e) {
// CODEGEN: This method call is required by the Web Form Designer
// Do not modify it using the code editor.
InitializeComponent() ;
}
private void Page_Load( object sender, void _, System.EventArgs e) {
// Set a fake "user" for the tracking call
Session( "loginid" ) = "TestUser" ;
General.CustomerTracker mgr ;
mgr = new General.CustomerTracker() ;
DataSet ds = mgr.GenericSpReturnDataSet( "sqlConn" , "usp_GetLogRecords" , null ) ;
DataGrid1.DataSource = ds.Tables[ 0 ] ;
DataGrid1.DataBind() ;
}
private void Button1_Click( object sender, System.EventArgs e) {
// Do the Delete Button call---
General.CustomerTracker mgr = new General.CustomerTracker() ;
int retval = mgr.GenericSpNonQuery( "sqlConn" , "usp_UserTrackingDelete" , null ) ;
lblMessage.Text = (retval.ToString + " items deleted." ) ;
}
}
----------------------------------------------------------------------------------------
首先,表面上看来,像是与Remoging 没有太多关系。不过,"under the hood"却有几个重要点在这里,第一,在我的Global.asax里面,我有在Application_Start里面做以下调用:
RemotingConfiguration.Configure(HttpContext.Current.Server.MapPath("Client.exe.config"))
client.exe.config文件是一个Remoting "client style" 配置文件,你不能这以下信息放到一个正常的web.confgi文件里面,当然,你这样做的话,它也不会抛出异常,不过,远程框架,却不会被正常配置,所以我们这里建一个独立的"Client.exe.config"文件,如下:
------------------------------------------------------------------------------------------
<system.runtime.remoting>
<application>
<channels>
<channel ref="http" useDefaultCredentials="true" port="0">
<clientProviders>
<formatter
ref="binary"
/>
</clientProviders>
</channel>
</channels>
<client>
<wellknown type="General.CustomerTracker, General"
url="http://localhost/IISImageHandler/CustomerTracker.soap" />
</client>
</application>
</system.runtime.remoting>
</configuration>
------------------------------------------------------------------------------------------
注意,我设置了HTTP通道与二进制格式来与服务器进行匹配,由客户端提供周知的包含名称空间,类名,程序集名称,以及服务器的URL的指令。
有一点需要做的是怎样让客户端发出到远程服务器的请求,客户端需要有metadata来了解server class的的"长相"来做客户端的的代理处理代码,以及发送到服务端的函数调用请求.典型情况下,你可以使用SOAPSuds 实体来生成一个metadata代理类以用于在客户端项目中调用,不过这里,我用BinaryFormatter
的话,它就不能正常运行, 所以我用了另外一个单独的项目(考虑到要防止名字空间的冲突)基本"duplicates"了名字空间,类名,与程序集名称"Gener4al.CustomerTracker"类放在我的Remoting 服务器上面.唯一的区别是它包含了一个签名方法用于"镜像"服务器类,不过如果类由于没有被应用程序没有在本地正确配置被错误调用的话,却没有实现抛出NotSupportedException异常的代码:
-----------------------------------------------------------------------------------------
Public Function InsertPageView(ByVal UserIPAddress As String, ByVal UserLoginId As String, ByVal SourceUrl As String, _
ByVal BrowseType As String, ByVal ClickThruId As String) As Integer
Throw New NotSupportedException("Cannot run method locally")
End Function
-----------------------------------------------------------------------------------------
<In C#>
-----------------------------------------------------------------------------------------
public int InsertPageView(string UserIPAddress, string UserLoginId, string SourceUrl, string BrowseType, string ClickThruId) {
throw new NotSupportedException("Cannot run method locally");
}
-----------------------------------------------------------------------------------------
单独编译这个程序集,然后放在客户端应用程序的bin文件夹下面,如果我们客户商标有了所有的它需要的向远程序服务器发送请求所需要的metadata的话,那么东西基本就算完成了。
在可下载方案中,我已经给出来所有的供引用的类与项目,包括那个分离的需要单独编译的"General Proxy"工程,以及那个需要在Sql server上面生成CustomerTracking 测试用数据库的SQL脚本.如果要在一台机器上运行下面机器的话,你需要遵循以下步骤:
1) 把下载的文件解压到你的IIS下面的wwwroot下面命名为"ImageHandler"
2) 把文件夹下面的WebClient文件夹设为一个IIS应用程序
3) 把Vroot文件夹(这里是服务器)设为IIS应用程序,并设虚拟目录名称为IISImageHandler.
4) sqlserver 里面运行SQL脚本来创建数据库与存储过程
5) 单独编译GeneralProxy 工程序,把General.dll程序集到你的web客户端应用程序的文件夹下面
6) 如要从其它机器运行这个客户端,你只需要修改Client.exe.config配置文件的URL,来指出服务器的地址所在
--原文地址:http://www.eggheadcafe.com/articles/20031124.asp
源文件下载:
http://www.eggheadcafe.com/articles/20031124.zip