65.9K
CodeProject 正在变化。 阅读更多。
Home

在 WCF REST 服务中使用 HttpWebRequest 维护 HTTP 会话状态

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (14投票s)

2012 年 2 月 2 日

CPOL

10分钟阅读

viewsIcon

113433

downloadIcon

2582

本文演示了当从桌面应用程序使用 HttpWebRequest 对象调用 WCF REST 服务时,如何维护 HTTP 会话状态。

引言

本文演示了当从桌面应用程序使用 HttpWebRequest 对象调用 WCF REST 服务时,如何维护 HTTP 会话状态

背景

REST 近来已成为开发轻量级面向服务应用程序的热门话题。由于“表述性状态转移”理论的起源,将其服务托管在 Web 服务器(如 IIS 服务器)上是自然的。同样,使用 HTTP 会话状态来保持与每个单独客户端的服务状态也是自然的。例如,将用户的登录信息保存在 Web 会话中非常理想,这样服务客户端就不需要在每次调用时都发送用户凭据。总的来说,有两种方式可以调用服务:

  • 可以使用 JavaScript 中的 AJAX 调用从 Web 浏览器调用 REST 服务。jQuery 是最流行的 JavaScript 库之一,支持 AJAX 调用;
  • 也可以使用 HTTP 客户端库从桌面应用程序调用 REST 服务。在 Microsoft 生态系统中,HttpWebRequest 类是一个流行的选择。

要维护服务和客户端之间的 Web 会话,客户端需要向服务发送 Cookie。当以 AJAX 方式调用服务时,向服务发送 Cookie 是 Web 浏览器的责任。但是,当从桌面应用程序调用服务时,大多数客户端库默认不会发送 Cookie。要维护 HTTP 会话状态,我们需要在客户端进行一些额外的努力。

本文的目的是通过简单地扩展 HttpWebRequest 类,展示一个从桌面应用程序调用服务时如何维护 Web 会话的示例。

322436/SolutionExplorer.jpg

附带的 Visual Studio 2010 解决方案包含三个项目:

  • SharedLibraries 项目是一个类库。它定义了一些 REST 服务和客户端之间用于相互发送数据的共享类。该项目还实现了一个简单的 工厂类来创建 HttpWebRequest 对象。如果使用此工厂类创建的 HttpWebRequest 对象,HTTP 会话状态将自动维护。
  • Service 项目是一个 ASP.NET 项目。这里实现了一个简单的 REST 服务。
  • Client 项目是一个 WPF MVVM 应用程序。我将在此项目中展示如何调用 REST 服务。我将进行两次服务调用。一次调用中,我将使用默认的 WebRequest 类工厂创建的 HttpWebRequest 对象。另一次调用中,我将使用 SharedLibraries 项目实现的工厂类创建的 HttpWebRequest 对象。您将看到一次调用会丢失会话,而另一次则会保持会话。

我将首先介绍 SharedLibraries 项目,展示用于创建支持会话的 HttpWebRequest 对象的工厂类,以及服务和客户端之间共享的类。然后,我将介绍“Service”项目和“Client”项目,演示如何使用该工厂类。

共享库

322436/SolutionExplorerSharedLib.jpg

SharedLibraries 项目是一个简单的类库项目。它实现了一个工厂类,用于创建支持会话的 HttpWebRequest 对象。它还实现了服务和客户端之间用于相互发送数据的共享类。让我们先看看 CookiedRequestFactory.cs 文件中实现的类库。

using System;
using System.Net;
using System.Collections.Generic;

namespace SharedLibraries.ClientUtilities
{
    public class CookiedRequestFactory
    {
        // This dictionary keeps all the cookie containers for
        // each domain.
        private static Dictionary<string, CookieContainer> containers
            = new Dictionary<string, CookieContainer>();
    
        public static HttpWebRequest CreateHttpWebRequest(string url)
        {
            // Create a HttpWebRequest object
            var request = (HttpWebRequest)WebRequest.Create(url);
    
            // this gets the dmain part of from the url
            string domain = (new Uri(url)).GetLeftPart(UriPartial.Authority);
    
            // try to get a container from the dictionary, if it is in the
            // dictionary, use it. Otherwise, create a new one and put it
            // into the dictionary and use it.
            CookieContainer container;
            if (!containers.TryGetValue(domain, out container))
            {
                container = new CookieContainer();
                containers[domain] = container;
            }
     
            // Assign the cookie container to the HttpWebRequest object
            request.CookieContainer = container;
    
            return request;
        }
    }
}
  • 静态“工厂方法CreateHttpWebRequest 用于创建支持会话的 HttpWebRequest 对象。
  • 静态 Dictionary containers 为应用程序已发送过 Web 请求的每个域维护一个 CookieContainer。给定一个 URL,例如“http://domainb.foo:8080/image.jpg”,URL 的域由“http://domainb.foo:8080”标识,其中包含 端口号

当创建一个 HttpWebRequest 对象时,工厂方法会查找 containersDictionary”,看是否存在相应的 CookieContainer。如果存在,则将 CookieContainer 关联到 HttpWebRequest 对象。否则,将创建一个新的 CookieContainer。然后,该 CookieContainer 将与 HttpWebRequest 对象关联并添加到 Dictionary 中。通过这样做,工厂方法创建的 HttpWebRequest 对象在调用同一域中的服务时始终具有相同的 CookieContainer。此 CookieContainer 将存储从服务器收到的所有 Cookie。当进行 REST 服务调用时,它也会将所有 Cookie 发送到服务。收到 Cookie 后,服务就可以与客户端维护 HTTP 会话状态

在了解如何使用 CookiedRequestFactory 类之前,让我们先看看服务和客户端共享的类。ServiceResult.cs 文件定义了服务发送给客户端的数据格式。

namespace SharedLibraries.ShareTypes
{
    public class ServiceStatus
    {
        // Default to Fail status
        public ServiceStatus()
        {
            Success = false;
            Message = "Service Call failed";
        }
    
        public bool Success { get; set; }
        public string Message { get; set; }
    }
    
    public class ServiceResult<T>
    { 
        public ServiceResult()
        {
            Status = new ServiceStatus();
        }
    
        public ServiceStatus Status { get; set; }
        public T Result { get; set; }
    }
} 
  • 如果服务只向客户端发送成功/失败状态,则使用 ServiceStatus 类的实例。Message 属性是一个自由文本字段。我们可以放入关于服务状态的详细描述。
  • 如果服务需要向客户端发送某些数据,则使用 ServiceResult 类的实例。如果 Status 字段指示服务调用成功,则 Result 字段包含请求的数据。

服务和客户端用于交换数据的类型在 ServiceTypes.cs 文件中定义。

using System;
    
namespace SharedLibraries.ShareTypes
{
    // Client use this class to send the user credential
    // to login to the service
    public class AppUserCredentail
    {
        public string UserName { get; set; }
        public string Password { get; set; }
    }
    
    // Service use this class to send information to the
    // client, if the client is logged in.
    public class Student
    { 
        public int Id { get; set; }
        public string LastName { get; set; }
        public string FirstName { get; set; }
        public DateTime EnrollmentTime { get; set; }
        public int Score { get; set; }
    }
}
  • AppUserCredentail 类用于客户端向服务发送用户的访问凭据。成功登录后,服务将用户的登录状态保存在 Web 会话中。
  • Student 类用于服务器将数据发送给客户端。如果服务在 Web 会话中找到预期的用户登录状态,它将在请求时向客户端发送一个学生列表。

现在我们可以看看 REST 服务是如何创建的。

REST 服务

322436/SolutionExplorerService.jpg

在 Microsoft 生态系统中,有几种方法可以创建 WCF REST 服务。最流行的包括:

  • 我们可以使用 MVC 控制器创建 REST 服务。我喜欢这种方法,并认为它是一个自然的选择。如果您有兴趣,可以阅读这篇文章
  • 我们还可以使用 WCF Web API,您可以在这里这里找到一些好的教程。

但在本文中,我将采用一种不同的方法。我从这里学到了这种方法,并在这里使用过它。我认为这是一种创建 REST 服务的简单方法,并且它附带了一个辅助帮助页面,您稍后会看到。在此项目中,服务实现在“StudentService.cs”文件中。

using System;
using System.Collections.Generic;
using System.Web;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using SharedLibraries.ShareTypes;
    
namespace Service.Services
{
    [ServiceContract]
    [AspNetCompatibilityRequirements(RequirementsMode
        = AspNetCompatibilityRequirementsMode.Allowed)]
    public class StudentService
    {
        private readonly HttpContext context;
        public StudentService()
        {
            context = HttpContext.Current;
        }
    
        [OperationContract]
        [WebInvoke(Method = "POST", UriTemplate = "StudentService/Login")]
        public ServiceStatus Login(AppUserCredentail credentail)
        {
            // Initiate status as fail to login.
            var status = new ServiceStatus() 
                { Success = false, Message = "Wrong user name and/or password" };
    
            // For simplicity, this example application has only one user.
            if ((credentail.UserName == "user") && (credentail.Password == "password"))
            {
                status.Success = true;
                status.Message = "Login success";
            }
    
            // Keep the login status in the HttpSessionState
            context.Session["USERLOGGEDIN"] = status.Success? "YES": null;
    
            return status;
        }
    
    
        [OperationContract]
        [WebGet(UriTemplate = "StudentService/GetStudents")]
        public ServiceResult<List<Student>> GetStudents()
        {
            var result = new ServiceResult<List<Student>>();
    
            // Check if client is logged in, if fail, return the status
            if ((string) context.Session["USERLOGGEDIN"] != "YES")
            {
                result.Status.Success = false;
                result.Status.Message = "Not logged in or session is over";
                return result;
            }
    
            // Client is logged in, create a random student list and send back
            var students = new List<Student>();
            var rand = new Random();
            for (int i = 1; i <= 20; i++)
            {
                var student = new Student();
                student.Id = i;
                student.LastName = "LName - " + i.ToString();
                student.FirstName = "FName - " + i.ToString();
                student.EnrollmentTime = DateTime.Now.AddYears(-4);
                student.Score = 60 + (int)(rand.NextDouble() * 40);
    
                students.Add(student);
            }
    
            result.Result = students;
            result.Status.Success = true;
            result.Status.Message = "Success";
    
            return result;
        }
    }
}

StudentService 类实现了两个 OperationContracts

  • LoginOperationContract 接收用户访问凭据以授权用户访问服务。为简单起见,我们只有一个用户,其用户名/密码对是“user/password”。成功登录后,登录状态将保存在 Web 会话中,并告知客户端用户是否有权访问服务。
  • GetStudentsOperationContract 检查 Web 会话中的用户登录状态。如果用户已登录,它将向客户端发送一个随机生成的学生列表。如果未找到登录信息,“Status”字段将被标记为失败,以告知客户端用户需要登录才能进行服务调用。

要使此 WCF REST 服务正常工作,我们需要修改 Global.asax.cs 文件和 Web.config 文件。我们需要将以下代码添加到 Global.asax.cs 文件中的 Application_Start 事件:

void Application_Start(object sender, EventArgs e)
{
    RouteTable.Routes.Add(new ServiceRoute("",
        new WebServiceHostFactory(),
        typeof(StudentService)));
}

我们还需要将以下配置添加到“Web.config”文件中:

<system.serviceModel>
  <serviceHostingEnvironment aspNetCompatibilityEnabled="true"
    multipleSiteBindingsEnabled="true" />
  <standardEndpoints>
    <webHttpEndpoint>
      <standardEndpoint name="" helpEnabled="true"
        automaticFormatSelectionEnabled="true" />
    </webHttpEndpoint>
  </standardEndpoints>
</system.serviceModel>

是的,就是这么简单。没有“svc”文件,也没有 Endpoint 配置,WCF REST 服务就完成了并且可以工作。如果您启动此 ASP.NET 应用程序并在 Web 浏览器中输入 URL“https://:2742/help”,您可以看到此 REST 服务的帮助页面。

322436/ServiceHelpPage.jpg

如果您单击帮助页面上的链接,您可以找到有关如何调用此服务的详细说明。最重要的是,在 Debug 模式下访问两个 OperationContracts 的 URL 和方法如下:

  • “Login” - “https://:2742/StudentService/Login”, “POST”
  • “GetStudents” - “https://:2742/StudentService/GetStudents”, “GET”。

我们现在完成了简单的 REST 服务,让我们来看看客户端。

服务客户端

322436/SolutionExplorerClient.jpg

“Client”项目是一个 WPF MVVM 应用程序。我不会深入介绍 MVVM 的实现细节。如果您有兴趣,可以下载附带的解决方案自行查看。我尽力使此应用程序遵循 MVVM 实践来 分离关注点。在此应用程序中,服务调用实现在 StudentServiceProxy.cs 文件中。

using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Net;
using System.Text;
using System.Web.Script.Serialization;
using SharedLibraries.ClientUtilities;
using SharedLibraries.ShareTypes;
    
namespace Client.ClientProxies
{
    public static class StudentServiceProxy
    {
        private static string LoginUrl;
        private static string GetStudentsUrl;
    
        static StudentServiceProxy()
        {
            LoginUrl = ConfigurationManager.AppSettings["LoginUrl"];
            GetStudentsUrl = ConfigurationManager.AppSettings["GetStudentsUrl"];
        }
    
        // login to the service
        public static ServiceStatus Login(AppUserCredentail credentail)
        {
            // Serialize the students to json
            var serializer = new JavaScriptSerializer();
            var jsonRequestString = serializer.Serialize(credentail);
            var bytes = Encoding.UTF8.GetBytes(jsonRequestString);
    
            // Initiate the HttpWebRequest with session support with CookiedFactory
            var request = CookiedRequestFactory.CreateHttpWebRequest(LoginUrl);
            request.Method = "POST";
            request.ContentType = "application/json";
            request.Accept = "application/json";
    
            // Send the json data to the Rest service
            var postStream = request.GetRequestStream();
            postStream.Write(bytes, 0, bytes.Length);
            postStream.Close();
    
            // Get the login status from the service
            var response = (HttpWebResponse) request.GetResponse();
            var reader = new StreamReader(response.GetResponseStream());
            var jsonResponseString = reader.ReadToEnd();
            reader.Close();
            response.Close();
    
            // Deserialize the json and return the result
            return serializer.Deserialize<ServiceStatus>(jsonResponseString);
        }
    
        // Retrieve a list of the students using client created by "CookiedRequestFactory"
        public static ServiceResult<List<Student>> GetStudentsWithCookie()
        {
            var request = CookiedRequestFactory.CreateHttpWebRequest(GetStudentsUrl);
            return GetStudents(request);
        }
    
        // Retrieve a list of the student using client created by "WebRequest"
        public static ServiceResult<List<Student>> GetStudentsWithoutCookie()
        {
            var request = (HttpWebRequest)WebRequest.Create(GetStudentsUrl);
            return GetStudents(request);
        }
    
        // private utility function
        private static ServiceResult<List<Student>> GetStudents(HttpWebRequest request)
        {
            request.Method = "GET";
            request.Accept = "application/json";
    
            var response = (HttpWebResponse)request.GetResponse();
            var reader = new StreamReader(response.GetResponseStream());
            var jsonResponseString = reader.ReadToEnd();
            reader.Close(); 
            response.Close();
    
            var serializer = new JavaScriptSerializer();
            return serializer.Deserialize<ServiceResult<List<Student>>>(jsonResponseString);
        }
    }
}

在此 StudentServiceProxy 类中,我对 REST 服务进行了三次调用。

  • 在“Login”方法中,向“LoginOperationContract 发送了一个 POST 服务调用。在此调用中,HttpWebRequest 对象由 CookiedRequestFactory 类工厂创建。
  • GetStudentsWithCookie 方法中,向“GetStudentsOperationContract 发送了一个 GET 服务调用。在此调用中使用的 HttpWebRequest 对象由 CookiedRequestFactory 类工厂创建。
  • GetStudentsWithoutCookie 方法也向“GetStudentsOperationContract 发送了一个 GET 服务调用。不同之处在于,HttpWebRequest 对象直接由 WebRequest 类创建。

出于简单起见,所有服务调用都以同步方式实现。服务 URL 保存在 App.config 文件中的 appSettings 部分。

<appSettings>
    <add key="LoginUrl" value="https://:2742/StudentService/Login"/>
    <add key="GetStudentsUrl" value="https://:2742/StudentService/GetStudents"/>
</appSettings>

这些方法通过应用程序的视图模型触发。为了节省 CodeProject 的空间,我将跳过视图模型。如果您有兴趣,可以下载附件自行查看。UI 控件在 MainWindow.xaml 文件中的以下 XAML 部分实现。

<Grid Margin="10">
    <Grid.RowDefinitions>
        <RowDefinition Height="30" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    
    <Grid Grid.Row="0">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
    
        <Button Grid.Column="0" Command="{Binding Path=GetStudentWithCookieCommand}">
            Get Students with Cookie</Button>
        <Button Grid.Column="1" Command="{Binding Path=GetStudentsNoCookieCommand}">
            Get Students without Cookie</Button>
    </Grid>
    
    <DataGrid Grid.Row="1" Margin="0, 5, 0, 0"
                              ItemsSource="{Binding Path=Students, Mode=OneWay}">
    </DataGrid>
</Grid>
    
<!-- Login section-->
<Grid  Visibility="{Binding Path=LoginVisibility}">
    <Rectangle Fill="Black" Opacity="0.08" />
    
    <Border BorderBrush="blue" 
                    BorderThickness="1" CornerRadius="10"
                    Background="White"
                    HorizontalAlignment="Center" VerticalAlignment="Center">
        <Grid Margin="10,10,10,25">
            <Grid.RowDefinitions>
                <RowDefinition Height="auto" />
                <RowDefinition Height="auto" />
                <RowDefinition Height="auto" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="70" />
                <ColumnDefinition Width="110" />
                <ColumnDefinition Width="auto" />
            </Grid.ColumnDefinitions>
    
            <TextBlock Grid.Row="0" Margin="0,0,0,10"
                                       FontWeight="Bold" Foreground="Gray"
                                       Grid.ColumnSpan="3">
                                Please login to the application
            </TextBlock>
    
            <TextBlock Grid.Row="1" Grid.Column="0">User Name</TextBlock>
            <TextBox Grid.Row="1" Grid.Column="1" Width="100"
                                     Text="{Binding Path=UserCredentail.UserName, Mode=TwoWay}" />
            <TextBlock Margin="0,5,0,0" Grid.Row="2" Grid.Column="0">Password</TextBlock>
            <PasswordBox Margin="0,5,0,0" Grid.Row="2" Grid.Column="1" Width="100"
                                     ff:PasswordBoxAssistant.BindPassword="true" 
                                     ff:PasswordBoxAssistant.BoundPassword
                ="{Binding Path=UserCredentail.Password, Mode=TwoWay,
                                UpdateSourceTrigger=PropertyChanged}"/>
    
            <Button Margin="5,5,0,0" Content="Login" Grid.Row="2" Grid.Column="2" Width="80"
                                    Command="{Binding Path=LoginCommand}"/>
        </Grid>
    </Border>
</Grid>
  • “Login Section”中的 XAML 代码包含用于用户名和密码的文本输入框。它还有一个按钮用于触发“Login”服务调用。
  • 代码中还有两个额外的按钮。它们都将触发对“GetStudentsOperationContract 的调用。其中一个使用 CookiedRequestFactory 类创建“HttpWebRequest 对象,另一个使用 WebRequest 类。

现在我们完成了演示应用程序,可以在 Visual Studio 中以 Debug 模式运行它。

运行示例应用程序

如果将“Client”项目设置为启动项目,则可以调试运行应用程序。当客户端应用程序首次启动时,您会看到一个弹出窗口,要求您登录服务。

322436/RunAppStart.jpg

如果您输入用户名/密码“user/password”并单击“Login”按钮,您就可以登录服务。成功登录后,您可以单击“Get Students with Cookie”按钮,您会看到成功从服务接收到学生列表。

322436/RunAppCallWithCookie.jpg

如果单击“Get Students without Cookie”,您将看到我们无法收到请求的学生列表,并且一个消息弹出窗口显示我们未登录。

322436/RunAppCallWithoutCookie.jpg

这个例子表明,如果我们想与 REST 服务维护 Web 会话,我们需要使用 CreateHttpWebRequest 工厂类来创建 HttpWebRequest 对象。

关注点

  • 本文演示了当从桌面应用程序使用 HttpWebRequest 对象调用 WCF REST 服务时,如何维护“Http Session State”。
  • 尽管我只在一种实现 REST 服务的方式上测试了 CookiedRequestFactory,但我认为它应该适用于所有可能的 REST 实现,因为 HttpWebRequest 对象是一个通用的 HTTP 客户端库。如果您在其他类型的 REST 实现中遇到任何问题,请告知我。我认为我们可以用对 CookiedRequestFactory 类的最小修改来满足您的需求。
  • 我没想到会写这么长的文章,希望您没有感到厌烦。如果您感到厌烦,可以看看 CookiedRequestFactory 类并尝试自己使用它。它应该非常简单易用。
  • 我希望您喜欢我的文章,希望本文能以某种方式帮助您。

历史

  • 初稿 - 2012 年 2 月 2 日。
© . All rights reserved.