2007-04-02

使用 Wininet 以 PUT 方法上传文件

客户端与 HTTP 服务器通常使用 GET 和 POST 方法进行交互,其中, GET 方法是客户端从服务器取得文件, POST 方法是客户端提交数据给服务器。
当我们需要上传文件给服务器的时候,可以使用 POST 和 PUT 方法。 POST 方法通常用来向表单提交数据,此时表单的 enctype 属性被设置为默认值"application/x-www-form-urlencoded" ,当需要向服务器提交大量文本、二进制数据时, enctype 属性将被设置为"multipart/form-data" 。 POST方法的优势在于能够在一次提交过程中传输多条数据和多个文件,但是实现起来比较复杂,如果我们只是需要传输一个文件的时候,使用 PUT 方法就方便多了。
PUT 方法的实现很简单,只需要打开 HTTP 连接,发送文件大小,读取本地文件,将内容以二进制方式写入到 HTTP 消息体中, MFC 代码如下:
  CInternetSession session;
  CHttpConnection * pHttpConn = NULL;
  CHttpFile * pHttpFile = NULL;
  try
  {
    pHttpConn =session.GetHttpConnection(m_serverHost, m_serverPort);

    //接收所有文件类型
    TCHAR accept[] = _T("*/*");
    //参数需要的数组
    const TCHAR *acArray[2] = {accept,NULL};
    LPCTSTR* ppstrAcceptTypes = acArray;

    //打开PUT方法的HTTP连接
    pHttpFile =pHttpConn->OpenRequest("PUT", m_serverObjectName, NULL, 1, ppstrAcceptTypes);

    //发送提交文件的大小,m_pLocalFile为文件对象指针
    DWORD fileLength =m_pLocalFile->GetLength();
    pHttpFile->SendRequestEx(fileLength);

    BYTE buffer[HTTP_BUFFER];
    //写入文件内容
    m_pLocalFile->SeekToBegin();       
    int nRead = m_pLocalFile->Read(buffer,HTTP_BUFFER);
    while (nRead==HTTP_BUFFER)
    {
      pHttpFile->Write(buffer, nRead);
      nRead =m_pLocalFile->Read(buffer, HTTP_BUFFER);
    }
    pHttpFile->Write(buffer, nRead);
    pHttpFile->EndRequest();

    //如果需要读取返回的结果,在这里调用 CHttpFile::Read 函数

    //释放资源
    pHttpFile->Close();
    pHttpConn->Close();
  }
  catch (CInternetException * pEx)
  {
    pEx->Delete();
    if (pHttpFile!=NULL)
    {
      pHttpFile->Close();
    }
    if (pHttpConn!=NULL)
    {
      pHttpConn->Close();
    }
  }
在上面的代码中,我们将本地文件上传到服务器中,由于 PUT 方法的消息体只能传送文件内容,所以,如果需要传输附加的参数,可以调用 CHttpFile::AddRequestHeaders 函数,具体使用方法请参考 MSDN 。
在 WEB 应用程序中,如果服务器端使用 ASP.NET ,可以通过 HttpRequest.InputStream 属性得到 Stream对象,从中读出二进制数据;如果服务器端使用 Java Servlet ,可以通过 HttpServletRequest.getInputStream 方法得到 ServletInputStream 对象,从中读出二进制数据。

2006-11-29

在 WEB 开发中指示文件的格式和显示方式

网络上的 WEB 服务器,往往网络地址的扩展名决定下载内容的格式。比如, http://www.google.com/images/logo_sm.gif 表示一个 gif 图片文件,http://www.ietf.org/rfc/rfc0001.txt 表示一个 txt 文本文件。然后,也有少数不是这样的,比如 Gmail ,链接地址都是看不懂的一串字符,可是这也能在浏览器中显示图片,并且对于附件中的图片,提供了直接在浏览器中查看和下载到本地两个按钮。这些功能都很人性化,是在返回的消息头信息中,告诉浏览器本消息格式这样的方式来实现的。
在返回的头信息中, Content-Type 字段指示了消息的类型,消息的类型由 MIME 决定。MIME ,全名 Multipurpose Internet Mail Extensions (多用途网际邮件扩展),是一个描述消息内容的标准,网络浏览器通过此标准解析收到的内容。
在 RFC( Request for Comments) 文档 rfc2045 中,对消息内容的类型作了以下说明:

content := "Content-Type" ":" type "/" subtype
           *(";" parameter)
           ; Matching of media type and subtype
           ; is ALWAYS case-insensitive.

type := discrete-type / composite-type

discrete-type := "text" / "image" / "audio" / "video" /
                 "application" / extension-token

composite-type := "message" / "multipart" / extension-token

extension-token := ietf-token / x-token

ietf-token := <An extension token defined by a
               standards-track RFC and registered
               with IANA.>

x-token := <The two characters "X-" or "x-" followed, with
            no intervening white space, by any token>

subtype := extension-token / iana-token

iana-token := <A publicly-defined extension token. Tokens
               of this form must be registered with IANA
               as specified in RFC 2048.>

parameter := attribute "=" value

attribute := token
             ; Matching of attributes
             ; is ALWAYS case-insensitive.

value := token / quoted-string

token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
            or tspecials>

tspecials := "(" / ")" / "<" / ">" / "@" /
             "," / ";" / ":" / "\" / <">
             "/" / "[" / "]" / "?" / "="
             ; Must be in quoted-string,
             ; to use within parameter values

上面指出,目前独立的内容类型主要有5种,分别是 text(文本),image(图片),audio(音频),video(视频),application(应用程序识别),当浏览器收到此类型定义时,解析器根据此字段调用不同的组件显示收到的消息。所以,修改 Content-Type ,我们可以指定浏览器以何种方式显示收到的消息。
同时,对于没有指定内容类型的消息, rfc 文档指定了默认值,如下:

Content-type: text/plain; charset=us-ascii

另外,在文档 rfc2183 中,补充了接收展现的信息,定义为 Content-Disposition :

disposition := "Content-Disposition" ":"
                disposition-type
                *(";" disposition-parm)

disposition-type := "inline"
                    / "attachment"
                    / extension-token
                    ; values are not case-sensitive

disposition-parm := filename-parm
                    / creation-date-parm
                    / modification-date-parm
                    / read-date-parm
                    / size-parm
                    / parameter

filename-parm := "filename" "=" value

creation-date-parm := "creation-date" "=" quoted-date-time

modification-date-parm := "modification-date" "=" quoted-date-time

read-date-parm := "read-date" "=" quoted-date-time

size-parm := "size" "=" 1*DIGIT

quoted-date-time := quoted-string
                    ; contents MUST be an RFC 822 `date-time'
                    ; numeric timezones (+HHMM or -HHMM) MUST be used

上面指定了展现时主要有两种类型: inline(直接打开)和 attachment(作为附件打开)。通过对 Content-Disposition 的修改,我们可以让浏览器直接显示或弹出保存文件的对话框。常用的附加参数还有文件名,创建日期,修改日期,文件大小,最后读取日期等,浏览器将遵循这些字段保存文件。
可以看到,浏览器通过对 Content-Type 和 Content-Disposition 的读取决定显示消息的类型和方式,与网络地址的扩展名毫无关系。下面分别使用 ASP.NET 和 Servlet 列出在客户端弹出图片下载窗口的方法。
使用 C# 在 ASP.NET 中的实现:

...
Response.ContentType = "image/gif";//指示客户端传输的内容为gif图片
Response.AddHeader("Content-Disposition",
     "attachment;filename=cs.gif");//默认的文件名是cs.gif
//在这里使用 Response.BinaryWrite 方法往客户端写二进制数据
Response.Close();
...

使用 Java 在 Servlet 中的实现:

...
resp.setContentType("image/gif");//指示客户端传输的内容为gif图片
resp.addHeader("Content-Disposition",
      "attachment;filename=java.gif");//默认的文件名是java.gif
//使用 resp.setContentLength 方法设定发送的长度
ServletOutputStream stream = resp.getOutputStream();
//在这里使用 stream.write 方法往客户端写二进制数据
stream.close();
...

2006-11-23

在 .Net 中使用低级键盘钩子屏蔽 Win 键

在微软的文档《HOW TO:在 Visual C# .NET 中设置窗口挂钩 》中,对 .NET 框架中的钩子有如下描述:

在 .NET 框架中不支持全局挂钩
您无法在 Microsoft .NET 框架中实现全局挂钩。若要安装全局挂钩,挂钩必须有一个本机动态链接库 (DLL) 导出以便将其本身插入到另一个需要调入一个有效而且一致的函数的进程中。这需要一个 DLL 导出,而 .NET 框架不支持这一点。托管代码没有让函数指针具有统一的值这一概念,因为这些函数是动态构建的代理。

在 .NET 2.0 中,已经可以开发程序使用全局挂钩了,下面给出使用 C# 开发低级键盘钩子屏蔽 Win 键的代码:
使用钩子需要用到 3 个 Win32 API 函数: SetWindowsHookEx,CallNextHookEx和UnhookWindowsHookEx,在 C# 中需要声明一下:

class Win32API
{
   [DllImport("user32.dll", CallingConvention = CallingConvention.StdCall)]
   public static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);

   [DllImport("user32.dll", CallingConvention = CallingConvention.StdCall)]
   public static extern int CallNextHookEx(IntPtr hhk, int nCode, uint wParam, int lParam);

   [DllImport("user32.dll", CallingConvention = CallingConvention.StdCall)]
   public static extern int UnhookWindowsHookEx(IntPtr hhk);
}

低级键盘钩子用到了 KBDLLHOOKSTRUCT 结构,在 C# 中定义对应的结构:

struct KeyBoardHookStruct
{
   public UInt32 vkCode;
   public UInt32 scanCode;
   public UInt32 flags;
   public UInt32 time;
   public UInt32 dwExtraInfo;
}

对于低级键盘钩子,Win32 提供了 LowLevelKeyboardProc 格式的回调函数,对应的,在 C# 中使用代理:

delegate int HookProc(int nCode, uint wParam, int lParam);

最后封装一个类屏蔽左右 Win 键:

class MaskKey
{
   private const int WH_KEYBOARD_LL = 13;
   private const int HC_ACTION = 0;
   private const int VK_LWIN = 0x5B;
   private const int VK_RWIN = 0x5C;
   private IntPtr m_hook;

   public bool StartMaskKey()
   {
     if (m_hook != IntPtr.Zero) return false;

     IntPtr pInstance = Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().ManifestModule);
     m_hook = Win32API.SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, pInstance, 0);
     if (m_hook == IntPtr.Zero) return false;

     return true;
   }

   public bool StopMaskKey()
   {
     if (m_hook != IntPtr.Zero)
     {
       int succeed = Win32API.UnhookWindowsHookEx(m_hook);
       if (succeed == 0) return false;

       m_hook = IntPtr.Zero;
     }

     return true;
   }

   private int LowLevelKeyboardProc(int nCode, uint wParam, int lParam)
   {
     if (nCode == HC_ACTION)
     {
       KeyBoardHookStruct khs = (KeyBoardHookStruct)Marshal.PtrToStructure(
          new IntPtr(lParam), typeof(KeyBoardHookStruct));
       if (khs.vkCode == VK_LWIN || khs.vkCode == VK_RWIN) return 1;
     }
     return Win32API.CallNextHookEx(m_hook, nCode, wParam, lParam);
   }
}

上面的代码使用了低级键盘钩子,其中 ManifestModule 为 .Net 2.0 新增的属性,作用是获取包含当前程序集清单的模块。
编译成功后会发现程序仍然无法正确运行,这是因为需要修改 VS2005的编译选项,右键选择项目属性,在 Debug 选项卡中取消 Enable the Visual Studio hosting process 前面的选中标记, 重新编译运行,键盘的左右 Win 键已经不能使用了。

2006-10-24

Windows 钩子(Hook)简介

钩子(Hook)是 Windows 消息处理机制的重要组成部分,通过使用钩子,应用程序可以安装一个子程序监视系统消息,并且在它们抵达目标进程之前处理这些消息。由于对系统消息增加了处理步骤,所以钩子往往会降低系统性能。因此,你应该只在必要的时候使用钩子,并尽可能早的卸载它们。
系统支持很多种不同类型的钩子,每种类型的钩子提供了访问消息处理机制的不同方面的能力。对于每种钩子,系统都维持了一个钩子链(Hook Chain)。钩子链是一个由指向钩子程序(Hook Procedure)的指针构成的链表,钩子程序是特殊的,应用程序定义的回调函数。消息在产生的时候,被关联到钩子类型的一种,系统使消息一个接一个的通过整条钩子链,让它们处理。钩子程序能采取的动作取决于钩子的类型,有些钩子程序只能监视消息,另一些则可以修改消息,阻止消息抵达钩子链中的下一个钩子或消息的目标进程。
为了利用钩子,开发人员提供了钩子程序,并使用 SetWindowsHookEx 将钩子程序安装到钩子链中,钩子程序是一个声明如下的函数:

LRESULT CALLBACK HookProc(
int nCode,
WPARAM wParam,
LPARAM lParam
);

参数
HookProc,应用程序定义的名字。
nCode,钩子代码,用来决定钩子的动作,每个值具体的作用跟钩子的类型有关。
wParam和lParam,参数,通常用来保存对应消息的有关信息。

SetWindowsHookEx 函数总是将钩子程序安装到钩子链的最前端,事件在发生的时候,被一个特殊的钩子监听到,系统调用有关钩子链的第一个钩子程序,钩子链中的每一个钩子程序都能决定是否将这个事件传递到下一个钩子,钩子程序可以通过调用 CallNextHookEx 来完成传递操作。
需要说明的是有些类型的钩子程序只能监视消息,这时不论是否调用 CallNextHookEx 函数,系统都将消息传递到钩子链中的每一个钩子程序。
全局钩子(Global Hook)能监视到同一个桌面下所有线程的消息,线程特定的钩子(Thread-Specific Hook)只能监视指定线程的消息。全局钩子程序能在任何进程的上下文(Context)中作为调用线程被调用,所以程序通过独立的动态连接库(DLL) 模块来实现。线程特定的钩子程序只在关联线程的上下文中被调用,所以,如果一个进程安装钩子程序处理自己的线程,钩子程序可以在这个程序的代码或动态连接库中实现;如果一个进程安装钩子程序处理其它进程的线程,钩子程序必须在动态连接库中实现。

每种类型的钩子使应用程序能监视系统消息处理机制的不同方面,下面列出了可用的钩子:

WH_CALLWNDPROC 和 WH_CALLWNDPROCRET
这两种钩子使你能监视发送到 windows 程序的消息。其中,系统将在消息发送到目标程序之前调用 WH_CALLWNDPROC 钩子程序,而在目标程序处理完成消息后调用 WH_CALLWNDPROCRET 钩子程序。
更多的信息,可以参考 CallWndProc 和 CallWndRetProc 函数。

WH_CBT
系统将会调用 WH_CBT 钩子程序在以下情况出现时:
激活,创建,销毁,最小化,最大化,移动窗口,改变窗口大小之前;完成系统命令之前;从系统消息队列中删除鼠标或键盘消息之前;设置输入焦点之前;以及同步系统消息队列之前。 WM_CBT 钩子程序主要是被 CBT (Computer-Based Training)程序使用。
更多的信息,可以参考 CBTProc 函数。

WH_DEBUG
系统将在其它任何钩子程序关联时调用 WH_DEBUG 钩子程序。你可以用这个钩子决定是否允许系统调用其它的钩子程序。
更多的信息,可以参考 DebugProc 函数。

WH_FOREGROUNDIDLE
系统将在背景线程空闲的时候调用 WH_FOREGROUNDIDLE 钩子程序,所以你可以用它来执行那些低优先级的任务。
更多的信息,可以参考 ForegroundIdleProc 函数。

WH_GETMESSAGE
WH_GETMESSAGE 钩子使应用程序能够监视 GetMessage 和 PeekMessage 函数的返回。你可以用这个钩子监视鼠标和键盘输入,以及其他添加到消息队列的消息。
更多的信息,可以参考 GetMsgProc 函数。

WH_JOURNALPLAYBACK
WH_JOURNALPLAYBACK 钩子使应用程序能够将消息插入到消息队列中。你可以用这个钩子回放一系列由 WH_JOURNALRECORD 钩子记录的鼠标和键盘事件。 一旦 WH_JOURNALPLAYBACK 钩子被安装,正常的鼠标和键盘输入就是无效的。 WH_JOURNALPLAYBACK 钩子是全局钩子,不能用作线程特定的钩子。
更多的信息,可以参考 JournalPlaybackProc 函数。  

WH_JOURNALRECORD
WH_JOURNALRECORD 钩子使你能够监视和记录输入事件。你可以用这个钩子记录一个鼠标和键盘输入事件的序列,稍后通过 WH_JOURNALPLAYBACK 回放出来。 WH_JOURNALRECORD 钩子是全局钩子,不能用作线程特定的钩子。
更多的信息,可以参考 JournalRecordProc 函数。

WH_KEYBOARD_LL
WH_KEYBOARD_LL 钩子使你能够记录添加到线程输入队列中的键盘输入事件。
更多的信息,可以参考 LowLevelKeyboardProc 函数。

WH_KEYBOARD
WH_KEYBOARD 钩子使应用程序可以监视 WM_KEYDOWN 和 WM_KEYUP消息, 这些消息通过 GetMessage 或 PeekMessage 函数返回。可以使用这个钩子来监视输入到消息队列中的键盘输入。
更多的信息,可以参考 KeyboardProc 函数。

WH_MOUSE_LL
WH_MOUSE_LL 钩子使你能够记录添加到线程输入队列中的鼠标输入事件。
更多的信息,可以参考 LowLevelMouseProc 函数。

WH_MOUSE
WH_MOUSE 钩子使你能够监视通过 GetMessage 或 PeekMessage 函数返回的鼠标消息。可以使用这个钩子来监视输入到消息队列中的鼠标输入。
更多的信息,可以参考 MouseProc 函数。

WH_MSGFILTER 和 WH_SYSMSGFILTER
这两种钩子使你能够监视菜单,滚动条,消息框,对话框的处理,并且发现用户使用 ALT+TAB 或 ALT+ESC 快捷键切换窗口。 WH_MSGFILTER 钩子只能监视传递到菜单,滚动条,消息框的消息,以及通过安装了钩子程序的应用程序建立的对话框的消息。WH_SYSMSGFILTER 钩子监视所有应用程序消息。
更多的信息,可以参考 MessageProc 和 SysMsgProc 函数。

WH_SHELL
外壳应用程序可以使用WH_SHELL Hook去接收重要的通知。当外壳应用程序是激活的并且当顶层窗口建立或者销毁时,系统调用 WH_SHELL 钩子程序。
更多的信息,可以参考 ShellProc 函数。

2006-10-23

在 Java 中防止 SQL 注入攻击(SQL Injection)的方法

SQL 注入(SQL Injection)是最常见的数据库攻击方式,和其它开发环境一样, Java 也提供了防止 SQL 注入攻击的方法。由于 JDBC 都是基于接口的设计,所以对于不同的数据库,代码基本一样,下面给出一个查询范例:

...
Connection conn = DriverManager.getConnection(url, user, password);
String query = "select * from table_user where user_name=?";
PreparedStatement preState = conn.prepareStatement(query);
preState.setString(1, "aaa");
ResultSet rs = preState.executeQuery();
...

在上面的这一段代码中,我们查询了表 table_user 中字段 user_name 为 "aaa" 的数据,由于采用了参数化查询的方式, JDBC 底层已经防止了 SQL 注入攻击。在 Java 中,对于所有的数据库,参数化查询的方式都和上面类似,区别在于数据库连接字符串。
连接 MySql 数据库,代码如下:

Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://127.0.0.1/myDatabase";
String user = "user";
String password = "password";
Connection conn = DriverManager.getConnection(url, user, password);

连接 Oracle 数据库,代码如下:

Class.forName("oracle.jdbc.driver.OracleDriver");
String url = "jdbc:oracle:thin:@127.0.0.1:1521:myOracleSID";
String user = "user";
String password = "password";
Connection conn = DriverManager.getConnection(url, user, password);

连接 MS SQL Server 数据库,代码如下:

Class.forName("com.microsoft.jdbc.sqlserver.SQLServerDriver");
String url = "jdbc:microsoft:sqlserver://127.0.0.1:1433;DatabaseName=myDbName";
String user = "user";
String password = "password";
Connection conn = DriverManager.getConnection(url, user, password);

在上面的代码中,我们分别连接了 MySql, Oracle, MS SQL Server 三种主流数据库。可以看到,在 JDBC 中,对于不同数据库的连接和查询功能的代码基本上相同,对于开发人员非常的友好。

2006-10-19

在 .NET 中防止 SQL 注入攻击(SQL Injection)的方法

SQL 注入(SQL Injection)是最常见的数据库攻击方式,其原理为构造巧妙的参数,传入数据库执行时改变原有的含义。
举个例子,如果我们要在数据库用户表 table_user 中查询 group_id 为 3 的用户,代码是这样的:

SELECT * FROM table_user WHERE group_id = 3

其中“3”为参数,通常采用的做法是将前一部分字符串与 3 进行拼接,合并后的 SQL 语句传入数据库直接执行。如果我们更改参数,改为“3 OR 1 = 1”,那么代码就变成这样了:

SELECT * FROM table_user WHERE group_id = 3 OR 1 = 1

上面的代码已经完全绕过了 WHERE 部分的条件限制,与我们的本意不相符合,这种情况在安全方面是非常危险的。
SQL 注入的攻击方式都是通过构造参数来实现的,程序中必须要有拼接字符串产生执行语句的部分才有这方面的问题,所以,最安全的防范 SQL 注入攻击的方式就是把所有访问数据库的部分全部使用存储过程(Store Procedure)来实现,在存储过程内部也不拼接语句。这种方式也是最简单的,对于各种开发语言都有效,然而,如果需要在 .NET 程序设计中避免 SQL 注入,又不想使用存储过程,还有另外一种方式,这就是参数化查询。
ADO.NET 对于参数化查询提供了良好的封装,然而,在 ADO.NET 中,每种数据库的参数化查询语句都不一样,下面就是访问 SQL Server 数据库查询表单的一个语句:

...
string sqlText = "SELECT * FROM table_user WHERE group_id = @group_id";
SqlCommand cmd = new SqlCommand(sqlText, connection);
cmd.Parameters.Add("@group_id", SqlDbType.Int);
cmd.Parameters["@group_id"].Value = 3;
...

在上面的代码中,我们使用参数化查询的方式构造了一个 SqlCommand 对象,指定了参数的类型和值。在这种情况下, ADO.NET 底层将会处理 SQL 注入的问题,保证查询过程是安全的。
下面给出访问 MySQL 数据库查询同样数据的例子:

...
string sqlText = "SELECT * FROM table_user WHERE group_id = ?group_id";
MySqlCommand cmd = new MySqlCommand(sqlText, connection);
cmd.Parameters.Add("?group_id", SqlDbType.Int);
cmd.Parameters["?group_id"].Value = 3;
...

以下是访问 Oracle 数据库查询数据的例子:

...
string sqlText = "SELECT * FROM table_user WHERE group_id = :group_id";
OracleCommand cmd = new OracleCommand(sqlText, connection);
cmd.Parameters.Add("group_id", SqlDbType.Int);
cmd.Parameters["group_id"].Value = 3;
...

上面列出了在 3 种主流数据库中使用 .NET 参数化查询的代码,它们中的大部分都相同,只有一些细微的差别,如果需要开发面向多种数据库的程序,这点很麻烦,相对而言, JAVA 在这方面做的非常好。

2006-10-18

SQL Server 中排名函数(Ranking Functions)的使用方法

SQL Server 提供了一组排名函数(Ranking Functions),为结果集分区中的每一行返回一个排名值。根据所用到的函数和选项,某些行的排名值可能相同。排名函数包括RANK, NTILE, DENSE_RANK, ROW_NUMBER 四种,这四种函数使用方法很相似,只是功能稍微有所不同,我们用一些例子来说明用法。

group_id (组编号) user_id(学号) score(成绩)
1 1001 83
1 1002 83
1 1003 78
2
2001 90
2
2002 78
2
2003 73

上面是一张成绩表,表名为 table_ranking,包含 class_id, student_id, score 3个字段,数据都列在表中。使用不同的函数,我们可以取得不同的排名值,我们用排名函数分别做查询,可以得到不同的结果。

1. RANK 函数。RANK 函数返回结果集分区内每行的排名,从1开始,排名值为前一行的排名值加一。如果存在多个行与一个排名关联,则这些关联行将得到相同的排名值,后续行的排名值会与前面关联行的排名值隔开,发生不连续的情况。
语法

RANK ( ) OVER ( [ partition_by_clause ] order_by_clause )

参数
partition_by_clause. 分区字段,为 PARTITION BY column_name... 这样的格式。不同分区排名值的计算是互相独立的。
order_by_clause. 排序字段,为 ORDER BY column_name... 这样的格式。在同一分区内,依据此字段排序,计算排名值。
对1,2组分别按成绩由高往低排名的 sql 查询语句如下:

SELECT group_id, user_id, RANK () OVER ( PARTITION BY group_id ORDER BY score DESC ) AS rank FROM table_ranking

查询结果

group_id user_id rank
1 1001 1
1 1002 1
1 1003 3
2
2001 1
2
2002 2
2
2003 3

对于1,2组一起按成绩由高往低排名的 sql 查询语句如下:

SELECT group_id, user_id, RANK () OVER ( PARTITION BY group_id ORDER BY score DESC ) AS rank FROM table_ranking

查询结果

group_id user_id rank
2
2001 1
1 1001 2
1 1002 2
1
1003 4
2
2002 4
2
2003 6

2. NTILE 函数。NTILE 函数将有序分区中的行分配到指定数目的组中。每个组有编号,从1开始,对于每一行,NTILE 返回对应的组号。组号越小的组取得的记录行越靠近查询结果前列,组号越大的组取得的记录行越靠近查询结果后列。需要说明的是,如果分区的行数不能被组数整除,那么排在序号较小的组将获得更多的行,同时每组包含的行数量将会尽量保持相同,任意两组间包含的行数量差别不会大于一。
语法

NTILE ( integer_expression ) OVER ( [ partition_by_clause ] order_by_clause )

参数
integer_expression. 正整数,为 int 或 bigint 类型,表示每个分区分成组的数量。
partition_by_clause. 分区字段,为 PARTITION BY column_name... 这样的格式。不同分区排名值的计算是互相独立的。
order_by_clause. 排序字段,为 ORDER BY column_name... 这样的格式。在同一分区内,依据此字段排序,计算排名值。
对1,2组分别按成绩由高往低分成两组的 sql 查询语句如下:

SELECT group_id, user_id, NTILE ( 2 ) OVER ( PARTITION BY group_id ORDER BY score DESC ) AS ntile FROM table_ranking

查询结果

group_id user_id ntile
1 1001 1
1 1002 1
1 1003 2
2
2001 1
2
2002 1
2
2003 2

对于1,2组一起按成绩由高往低分成两组的 sql 查询语句如下:

SELECT group_id, user_id, NTILE ( 2 ) OVER ( ORDER BY score DESC ) AS ntile FROM table_ranking

查询结果

group_id user_id ntile
2
2001 1
1 1001 1
1 1002 1
1
1003 2
2
2002 2
2
2003 2

3. DENSE_RANK 函数。DENSE_RANK 函数和 RANK 函数功能一样,返回结果集分区内每行的排名,唯一的区别是 DENSE_RANK 在具有相同排名值的情况下,排名值也保持连续,不会间断。
语法

DENSE_RANK ( ) OVER ( [ partition_by_clause ] order_by_clause )

参数
partition_by_clause. 分区字段,为 PARTITION BY column_name... 这样的格式。不同分区排名值的计算是互相独立的。
order_by_clause. 排序字段,为 ORDER BY column_name... 这样的格式。在同一分区内,依据此字段排序,计算排名值。
对1,2组分别按成绩由高往低计算排名的 sql 查询语句如下:

SELECT group_id, user_id, DENSE_RANK ( ) OVER ( PARTITION BY group_id ORDER BY score DESC ) AS dense_rank FROM table_ranking

查询结果

group_id user_id dense_rank
1 1001 1
1 1002 1
1 1003 2
2
2001 1
2
2002 2
2
2003 3

对于1,2组一起按成绩由高往低计算排名的 sql 查询语句如下:

SELECT group_id, user_id, DENSE_RANK ( ) OVER ( ORDER BY score DESC ) AS dense_rank FROM table_ranking

查询结果

group_id user_id dense_rank
2
2001 1
1 1001 2
1 1002 2
1
1003 3
2
2002 3
2
2003 4

4. ROW_NUMBER 函数。ROW_NUMBER 函数返回结果集内每行的行号,行号从1开始,在结果集分区内唯一,并保持连续。
语法

ROW_NUMBER ( ) OVER ( [ partition_by_clause ] order_by_clause )

参数
partition_by_clause. 分区字段,为 PARTITION BY column_name... 这样的格式。不同分区排名值的计算是互相独立的。
order_by_clause. 排序字段,为 ORDER BY column_name... 这样的格式。在同一分区内,依据此字段排序,计算排名值。
对1,2组分别按成绩由高往低计算行号的 sql 查询语句如下:

SELECT group_id, user_id, ROW_NUMBER ( ) OVER ( PARTITION BY group_id ORDER BY score DESC ) AS row_number FROM table_ranking

查询结果

group_id user_id row_number
1 1001 1
1 1002 2
1 1003 3
2
2001 1
2
2002 2
2
2003 3

对于1,2组一起按成绩由高往低计算行号的 sql 查询语句如下:

SELECT group_id, user_id, ROW_NUMBER ( ) OVER ( ORDER BY score DESC ) AS row_number FROM table_ranking

查询结果

group_id user_id row_number
2
2001 1
1 1001 2
1 1002 3
1
1003 4
2
2002 5
2
2003 6

上面说明了 SQL Server 中排名函数的使用方法,排名函数是 SQL Server 2005 新增的函数,这些函数大大提升了 SQL Server 数据库在统计方面的功能。

2006-09-29

在 MS SQL Server 中限制返回记录数量的 3 种方法

在使用海量数据的时候,限制返回记录的数量是必须的,将所有记录返回很容易使数据库陷入死状态。

对于 Microsoft SQL Server 来说,提供了3种方法来完成这个功能:

1. 使用 TOP 选项。TOP 选项提供了最简单的方式限制返回记录数量,语法如下:

SELECT [ TOP (expression) [ PERCENT ] [ WITH TIES ]] select_list [ other_select_command ]

参数

1. expression. 指定返回行数量的数值,可以是常量或者变量。如果指定了 PERCENT ,则 expression 将转换为 float 类型;如果没有指定 PERCENT ,则 expression 将转换为 bigint 类型。如果查询中包含 ORDER BY 子句,则返回的记录集为排序后的前项记录;如果没有包含 ORDER BY 子句,则返回行的顺序是随意的。

2. PERCENT. 指示限定记录数量的类型。如果指定了 PERCENT ,则按照总数量的百分比计算返回数量;如果没有指定 PERCENT , 则按照返回记录集的行数量来计算。

3. WITH TIES. 指示返回额外的行,只能与 ORDER BY 一起使用。使用此选项时,排序后返回指定数量的记录行,与这些记录集最后一行排序字段相同的行也会返回。所以,返回记录的数量可能会比指定数量要大。

示例代码

SELECT TOP (10) * FROM table1 ORDER BY column1

2. 使用 SET ROWCOUNT 选项。 SET ROWCOUNT 改变了当前环境下返回记录数量的值,语法如下:

SET ROWCOUNT expression

参数

1. expression. 在停止特定查询之前要处理的行数,可以是常量或变量,类型为整形。

执行此命令后, SQL Server 将在执行 SELECT 语句时,返回指定的行数后停止查询。如果需要关闭此选项,执行 SET ROWCOUNT 0 即可。

示例代码

SET ROWCOUNT 10
SELECT * FROM table1 ORDER BY column1
SET ROWCOUNT 0

3. 使用 ROW_NUMBER 函数。 ROW_NUMBER 函数返回当前行对应的行号,每个分区的第一行从 1 开始,语法如下:

ROW_NUMBER ( ) OVER ( [ PARTITION BY partition_name [ , partition_name ] ] ORDER BY order_name [ , order_name ] )

参数

1. partition_name. 根据此列名确认 ROW_NUMBER 函数结果集分区范围。如果此字段未填,则所有计算行号;如果此字段填写,则根据相同结果集分区的记录计算行号,结果集分区不同的记录重新从1开始计算行号。

2. order_name. 根据此列名确定返回行的顺序,计算出行号。

示例代码

SELECT * FROM ( SELECT *, ROW_NUMBER ( ) OVER ( ORDER BY column1 ) AS rowNumber FROM table1 ) AS T WHERE rowNumber < 10

上面列出了 SQL Server 限制返回记录数量的 3 种方法,这 3 种方法均支持常量和变量。 TOP 的使用方法最简单,如果仅仅是限制返回数量,建议使用这种方式;如果要进行分页和结果集分区计算,那么就要使用 ROW_NUMBER 函数了。

2006-09-25

使用 HttpWebRequest 向网站提交数据

HttpWebRequest 是 .net 基类库中的一个类,在命名空间 System.Net 下面,用来使用户通过 HTTP 协议和服务器交互。

HttpWebRequest 对 HTTP 协议进行了完整的封装,对 HTTP 协议中的 Header, Content, Cookie 都做了属性和方法的支持,很容易就能编写出一个模拟浏览器自动登录的程序。

程序使用 HTTP 协议和服务器交互主要是进行数据的提交,通常数据的提交是通过 GET 和 POST 两种方式来完成,下面对这两种方式进行一下说明:

1. GET 方式。 GET 方式通过在网络地址附加参数来完成数据的提交,比如在地址 http://www.google.com/webhp?hl=zh-CN 中,前面部分 http://www.google.com/webhp 表示数据提交的网址,后面部分 hl=zh-CN 表示附加的参数,其中 hl 表示一个键(key), zh-CN 表示这个键对应的值(value)。程序代码如下:

HttpWebRequest req = (HttpWebRequest) HttpWebRequest.Create( "http://www.google.com/webhp?hl=zh-CN" );
req.Method = "GET";
using (WebResponse wr = req.GetResponse())
{
   //在这里对接收到的页面内容进行处理
}

2. POST 方式。 POST 方式通过在页面内容中填写参数的方法来完成数据的提交,参数的格式和 GET 方式一样,是类似于 hl=zh-CN&newwindow=1 这样的结构。程序代码如下:

string param = "hl=zh-CN&newwindow=1";
byte[] bs = Encoding.ASCII.GetBytes(param);

HttpWebRequest req = (HttpWebRequest) HttpWebRequest.Create( "http://www.google.com/intl/zh-CN/" );
req.Method = "POST";
req.ContentType = "application/x-www-form-urlencoded";
req.ContentLength = bs.Length;

using (Stream reqStream = req.GetRequestStream())
{
   reqStream.Write(bs, 0, bs.Length);
}
using (WebResponse wr = req.GetResponse())
{
   //在这里对接收到的页面内容进行处理
}

在上面的代码中,我们访问了 www.google.com 的网址,分别以 GET 和 POST 方式提交了数据,并接收了返回的页面内容。然而,如果提交的参数中含有中文,那么这样的处理是不够的,需要对其进行编码,让对方网站能够识别。

3. 使用 GET 方式提交中文数据。 GET 方式通过在网络地址中附加参数来完成数据提交,对于中文的编码,常用的有 gb2312 和 utf8 两种,用 gb2312 方式编码访问的程序代码如下:

Encoding myEncoding = Encoding.GetEncoding("gb2312");
string address = "http://www.baidu.com/s?" + HttpUtility.UrlEncode("参数一", myEncoding) + "=" + HttpUtility.UrlEncode("值一", myEncoding);
HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(address);
req.Method = "GET";
using (WebResponse wr = req.GetResponse())
{
   //在这里对接收到的页面内容进行处理
}

在上面的程序代码中,我们以 GET 方式访问了网址 http://www.baidu.com/s ,传递了参数“参数一=值一”,由于无法告知对方提交数据的编码类型,所以编码方式要以对方的网站为标准。常见的网站中, www.baidu.com (百度)的编码方式是 gb2312, www.google.com (谷歌)的编码方式是 utf8。

4. 使用 POST 方式提交中文数据。 POST 方式通过在页面内容中填写参数的方法来完成数据的提交,由于提交的参数中可以说明使用的编码方式,所以理论上能获得更大的兼容性。用 gb2312 方式编码访问的程序代码如下:

Encoding myEncoding = Encoding.GetEncoding("gb2312");
string param = HttpUtility.UrlEncode("参数一", myEncoding) + "=" + HttpUtility.UrlEncode("值一", myEncoding) + "&" + HttpUtility.UrlEncode("参数二", myEncoding) + "=" + HttpUtility.UrlEncode("值二", myEncoding);

byte[] postBytes = Encoding.ASCII.GetBytes(param);

HttpWebRequest req = (HttpWebRequest) HttpWebRequest.Create( "http://www.baidu.com/s" );
req.Method = "POST";
req.ContentType = "application/x-www-form-urlencoded;charset=gb2312";
req.ContentLength = postBytes.Length;

using (Stream reqStream = req.GetRequestStream())
{
   reqStream.Write(bs, 0, bs.Length);
}
using (WebResponse wr = req.GetResponse())
{
   //在这里对接收到的页面内容进行处理
}

从上面的代码可以看出, POST 中文数据的时候,先使用 UrlEncode 方法将中文字符转换为编码后的 ASCII 码,然后提交到服务器,提交的时候可以说明编码的方式,用来使对方服务器能够正确的解析。

以上列出了客户端程序使用 HTTP 协议与服务器交互的情况,常用的是 GET 和 POST 方式。现在流行的 WebService 也是通过 HTTP 协议来交互的,使用的是 POST 方法。与以上稍有所不同的是, WebService 提交的数据内容和接收到的数据内容都是使用了 XML 方式编码。所以, HttpWebRequest 也可以使用在调用 WebService 的情况下。

2006-09-21

ASP.NET 1.1 基于Forms的身份验证

ASP.NET 1.1 中已包含 Windows, Forms, Passport 三种身份验证功能,其中 Windows 是基于 Windows 操作系统的用户验证,Passport 是基于 Microsoft Passport 的用户验证,这两种的适用范围都太窄,而 Forms 验证则可以手动控制用户登录过程,比较适合使用。
构建基于 Forms 的身份验证过程如下:
1. 设置 IIS 为可匿名访问,在 asp.net 配置文件 web.config 中设置验证模式为 Forms 验证,并设置登录页面。
<configuration>
  <system.web>
    <authentication mode="Forms">
      <forms name="name" loginUrl="/login.aspx" protection="None" timeout="30" path="/" requireSSL="false" slidingExpiration="false" />
    </authentication>
  </system.web>
</configuration>
2. 处理用户登录逻辑,查询出用户所属于的角色。
3. 构造 FormsAuthenticationTicket 对象,将其加密后保存到 cookie 中。
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
     1,
     userName,
     DateTime.Now,
     DateTime.Now.AddMinutes(30),
     isPersistent,
     "roleA,roleB,roleC",  //角色列表,用逗号区分,可以自定义
     FormsAuthentication.FormsCookiePath);

string encTicket = FormsAuthentication.Encrypt(ticket);
Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, encTicket));
4. 在 AuthenticateRequest 事件中( Global.asax 中包含一个对应的默认函数 Application_AuthenticateRequest ),恢复出 FormsAuthenticationTicket 对象,创建对应的 IPrincipal 对象并保存在 HttpContext.User 中。
HttpCookie authCookie = Context.Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie!=null)
{
    FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);
    string[] roles = authTicket.UserData.Split(new char[]{','});
    Context.User = new GenericPrincipal(Context.User.Identity, roles);
}
5. 对指定页面设定角色控制。
1. 在子目录中添加 web.config 配置文件,修改对整个子目录的访问权限。
<configuration>
  <system.web>
    <authorization>
      <allow roles="RoleName" />
      <deny users="*" />
    </authorization>
  </system.web>
</configuration>
2. 修改当前目录的 web.config 配置文件,为单独的页面修改访问权限。
<configuration>
  <system.web>
    <location path="EditPost.aspx">
      <system.web>
        <authorization>
          <allow roles="RoleName" />
          <deny users="*" />
        </authorization>
      </system.web>
    </location>
  </system.web>
</configuration>
3. 直接在页面中判断用户对应的角色。
if (User.IsInRole("RoleA"))
{
    //用户属于角色 RoleA ,处理...
}
4. 以上步骤均可以达到需要的效果,优先级为1>2>3。