memcached完全剖析–1. memcached的基礎

memcached完全剖析–1. memcached的基礎

作者: charlee 來源: idv2.com 發布時間: 2008-09-28 16:47 閱讀: 169850 次 推薦: 48 原文鏈接 [收藏]

系列文章導航:

memcached完全剖析–1. memcached的基礎

memcached全面剖析–2. 理解memcached的內存存儲

memcached全面剖析–3. memcached的刪除機制和發展方向

memcached全面剖析–4. memcached的分布式算法

memcached全面剖析–5. memcached的應用和兼容程序

翻譯一篇技術評論社的文章,是講memcached的連載。fcicq同學說這個東西很有用,希望大家喜歡。

發表日:2008/7/2
作者:長野雅廣(Masahiro Nagano)
原文鏈接:http://gihyo.jp/dev/feature/01/memcached/0001

我是mixi株式會社開發部系統運營組的長野。 日常負責程序的運營。從今天開始,將分幾次針對最近在Web應用的可擴展性領域 的熱門話題memcached,與我公司開發部研究開發組的前坂一起, 說明其內部結構和使用。

memcached是什么?

memcached 是以LiveJournal 旗下Danga Interactive 公司的Brad Fitzpatric 為首開發的一款軟件。現在已成為 mixihatenaFacebookVox、LiveJournal等眾多服務中 提高Web應用擴展性的重要因素。

許多Web應用都將數據保存到RDBMS中,應用服務器從中讀取數據并在瀏覽器中顯示。 但隨著數據量的增大、訪問的集中,就會出現RDBMS的負擔加重、數據庫響應惡化、 網站顯示延遲等重大影響。

這時就該memcached大顯身手了。memcached是高性能的分布式內存緩存服務器。 一般的使用目的是,通過緩存數據庫查詢結果,減少數據庫訪問次數,以提高動態Web應用的速度、 提高可擴展性。

圖1 一般情況下memcached的用途

memcached的特征

memcached作為高速運行的分布式緩存服務器,具有以下的特點。

  • 協議簡單
  • 基于libevent的事件處理
  • 內置內存存儲方式
  • memcached不互相通信的分布式

協議簡單

memcached的服務器客戶端通信并不使用復雜的XML等格式, 而使用簡單的基于文本行的協議。因此,通過telnet 也能在memcached上保存數據、取得數據。下面是例子。

$ telnet localhost 11211
Trying 127.0.0.1...
Connected to localhost.localdomain (127.0.0.1).
Escape character is '^]'.
set foo 0 0 3 (保存命令)
bar (數據)
STORED (結果)
get foo (取得命令)
VALUE foo 0 3 (數據)
bar (數據)

協議文檔位于memcached的源代碼內,也可以參考以下的URL。

基于libevent的事件處理

libevent是個程序庫,它將Linux的epoll、BSD類操作系統的kqueue等事件處理功能 封裝成統一的接口。即使對服務器的連接數增加,也能發揮O(1)的性能。 memcached使用這個libevent庫,因此能在Linux、BSD、Solaris等操作系統上發揮其高性能。 關于事件處理這里就不再詳細介紹,可以參考Dan Kegel的The C10K Problem。

內置內存存儲方式

為了提高性能,memcached中保存的數據都存儲在memcached內置的內存存儲空間中。 由于數據僅存在于內存中,因此重啟memcached、重啟操作系統會導致全部數據消失。 另外,內容容量達到指定值之后,就基于LRU(Least Recently Used)算法自動刪除不使用的緩存。 memcached本身是為緩存而設計的服務器,因此并沒有過多考慮數據的永久性問題。 關于內存存儲的詳細信息,本連載的第二講以后前坂會進行介紹,請屆時參考。

memcached不互相通信的分布式

memcached盡管是“分布式”緩存服務器,但服務器端并沒有分布式功能。 各個memcached不會互相通信以共享信息。那么,怎樣進行分布式呢? 這完全取決于客戶端的實現。本連載也將介紹memcached的分布式。

圖2 memcached的分布式

接下來簡單介紹一下memcached的使用方法。

安裝memcached

memcached的安裝比較簡單,這里稍加說明。

memcached支持許多平臺。

  • Linux
  • FreeBSD
  • Solaris (memcached 1.2.5以上版本)
  • Mac OS X

另外也能安裝在Windows上。這里使用Fedora Core 8進行說明。

memcached的安裝

運行memcached需要本文開頭介紹的libevent庫。Fedora 8中有現成的rpm包, 通過yum命令安裝即可。

$ sudo yum install libevent libevent-devel

memcached的源代碼可以從memcached網站上下載。本文執筆時的最新版本為1.2.5。 Fedora 8雖然也包含了memcached的rpm,但版本比較老。因為源代碼安裝并不困難, 這里就不使用rpm了。

memcached安裝與一般應用程序相同,configure、make、make install就行了。

$ wget http://www.danga.com/memcached/dist/memcached-1.2.5.tar.gz
$ tar zxf memcached-1.2.5.tar.gz
$ cd memcached-1.2.5
$ ./configure
$ make
$ sudo make install

默認情況下memcached安裝到/usr/local/bin下。

memcached的啟動

從終端輸入以下命令,啟動memcached。

$ /usr/local/bin/memcached -p 11211 -m 64m -vv
slab class 1: chunk size 88 perslab 11915
slab class 2: chunk size 112 perslab 9362
slab class 3: chunk size 144 perslab 7281
中間省略
slab class 38: chunk size 391224 perslab 2
slab class 39: chunk size 489032 perslab 2
<23 server listening
<24 send buffer was 110592, now 268435456
<24 server listening (udp)
<24 server listening (udp)
<24 server listening (udp)
<24 server listening (udp)

這里顯示了調試信息。這樣就在前臺啟動了memcached,監聽TCP端口11211 最大內存使用量為64M。調試信息的內容大部分是關于存儲的信息, 下次連載時具體說明。

作為daemon后臺啟動時,只需

$ /usr/local/bin/memcached -p 11211 -m 64m -d

這里使用的memcached啟動選項的內容如下。

選項 說明
-p 使用的TCP端口。默認為11211
-m 最大內存大小。默認為64M
-vv 用very vrebose模式啟動,調試信息和錯誤輸出到控制臺
-d 作為daemon在后臺啟動

上面四個是常用的啟動選項,其他還有很多,通過

$ /usr/local/bin/memcached -h

命令可以顯示。許多選項可以改變memcached的各種行為, 推薦讀一讀。

用客戶端連接

許多語言都實現了連接memcached的客戶端,其中以Perl、PHP為主。 僅僅memcached網站上列出的語言就有

  • Perl
  • PHP
  • Python
  • Ruby
  • C#
  • C/C++
  • Lua

等等。

這里介紹通過mixi正在使用的Perl庫鏈接memcached的方法。

使用Cache::Memcached

Perl的memcached客戶端有

  • Cache::Memcached
  • Cache::Memcached::Fast
  • Cache::Memcached::libmemcached

等幾個CPAN模塊。這里介紹的Cache::Memcached是memcached的作者Brad Fitzpatric的作品, 應該算是memcached的客戶端中應用最為廣泛的模塊了。

使用Cache::Memcached連接memcached

下面的源代碼為通過Cache::Memcached連接剛才啟動的memcached的例子。

#!/usr/bin/perl

use strict;
use warnings;
use Cache::Memcached;

my $key = "foo";
my $value = "bar";
my $expires = 3600; # 1 hour
my $memcached = Cache::Memcached->new({
servers => ["127.0.0.1:11211"],
compress_threshold => 10_000
});

$memcached->add($key, $value, $expires);
my $ret = $memcached->get($key);
print "$ret\n";

在這里,為Cache::Memcached指定了memcached服務器的IP地址和一個選項,以生成實例。 Cache::Memcached常用的選項如下所示。

選項 說明
servers 用數組指定memcached服務器和端口
compress_threshold 數據壓縮時使用的值
namespace 指定添加到鍵的前綴

另外,Cache::Memcached通過Storable模塊可以將Perl的復雜數據序列化之后再保存, 因此散列、數組、對象等都可以直接保存到memcached中。

保存數據

向memcached保存數據的方法有

  • add
  • replace
  • set

它們的使用方法都相同:

my $add = $memcached->add( '鍵', '值', '期限' );
my $replace = $memcached->replace( '鍵', '值', '期限' );
my $set = $memcached->set( '鍵', '值', '期限' );

向memcached保存數據時可以指定期限(秒)。不指定期限時,memcached按照LRU算法保存數據。 這三個方法的區別如下:

選項 說明
add 僅當存儲空間中不存在鍵相同的數據時才保存
replace 僅當存儲空間中存在鍵相同的數據時才保存
set 與add和replace不同,無論何時都保存

獲取數據

獲取數據可以使用get和get_multi方法。

my $val = $memcached->get('鍵');
my $val = $memcached->get_multi('鍵1', '鍵2', '鍵3', '鍵4', '鍵5');

一次取得多條數據時使用get_multi。get_multi可以非同步地同時取得多個鍵值, 其速度要比循環調用get快數十倍。

刪除數據

刪除數據使用delete方法,不過它有個獨特的功能。

$memcached->delete('鍵', '阻塞時間(秒)');

刪除第一個參數指定的鍵的數據。第二個參數指定一個時間值,可以禁止使用同樣的鍵保存新數據。 此功能可以用于防止緩存數據的不完整。但是要注意,set函數忽視該阻塞,照常保存數據

增一和減一操作

可以將memcached上特定的鍵值作為計數器使用。

my $ret = $memcached->incr('鍵');
$memcached->add('鍵', 0) unless defined $ret;

增一和減一是原子操作,但未設置初始值時,不會自動賦成0。因此, 應當進行錯誤檢查,必要時加入初始化操作。而且,服務器端也不會對 超過2<sup>32</sup>時的行為進行檢查。

總結

這次簡單介紹了memcached,以及它的安裝方法、Perl客戶端Cache::Memcached的用法。 只要知道,memcached的使用方法十分簡單就足夠了。

下次由前坂來說明memcached的內部結構。了解memcached的內部構造, 就能知道如何使用memcached才能使Web應用的速度更上一層樓。 歡迎繼續閱讀下一章。

48
2

請先登錄 提交中…

標簽:ASP.NET memcached

文章列表

.NET 2.0的Provider模式

.NET 2.0的Provider模式

作者: 極地銀狐.NET 來源: 博客園 發布時間: 2009-03-17 15:25 閱讀: 9493 次 推薦: 0 原文鏈接 [收藏]

摘要:本文全面地介紹了Prodive模式的概念以及應用,對于理解Provider模式的理解起到了很大的幫助
[1] Provider模式統覽
[2] 細節,細節,還是細節
[3] 執行

第一部分: Provider模式統覽

Provider設計模式是在.NET 1.1 framework中被首次介紹到,特別是在ASP.NET快速入門中,以及后來在ASP.NET Whidbey中作為membership management provider API被正式化。它的主要目的在于為一個API進行定義和實現的分離。這樣就通過核心功能的靈活性和易于修改的特點使得API具有靈活性。

在MSDN上能找到關于此模式在.NET 1.1 Framework的更多信息:

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnaspnet/html/asp02182004.asp

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnaspnet/html/asp02182004.asp

Provider設計模式基本上是由GOF的兩個設計模式溶合而成的:strategy和abstract factory。API定義好,其功能通過strategy模式變異而來,是“可插(拔)”的,而功能被加載進內存則是通過大致地一個abstract factory設計模式而實現的。

以下是此部分基本概要,以及它們如何互交。

1. API Class:這是一個通過靜態方法定義和暴露所需功能的類,在API Class中并沒有具體的實現。此類保持一個對Application Provider Base類的引用,這個base類會對API中的功能進行基本的包裝(Wrap)。

本文中的API Class是一個商店(Store)我們可以從中買東西,如Coke(可樂)、Snickers(譯者:不知道這是啥。),還有在吃Snicker和喝Coke時所要的diet pills(減肥藥)。


我們的類包括以下幾部分:

A:我們有基本的API用于存放貨物AddProductToStock,還有如GetProductPrice、GetProductStockCount以及RemoveProductFromStock等核心方法。

B:我們有個叫Initialize()的方法用于從系統配置文件中加載已有的具體商店。

C:我們有所有可用Provider的引用。

D:我們有一個默認Provider的引用,此Provider已被包裝。

一旦實例化,所有對這個API類的請求都會一個一個地轉給默認的Provider。

2. Provider Base Class:這是一個內部抽象類,位于System.Configuration.Provider命名空間,用于定義一個Provider。Initialize()方法用于從配置文件中獲取必要的信息來構建具體的Provider。我們在自己實現這個抽象類的時候要記的重寫Initialize()方法。


3. Application Provider Base:這是一個從ProviderBase類繼承來的一個抽象類,同時也是API類的一個“鏡像”,通過在API中所暴露的方法來為父類定義抽象方法并在父類中實現。

在我們的應用程序中我們把這個類叫做“StoreProvider”,請注意這個Store Provider是如何定義在Store類中已有的方法,同時從ProviderBase類繼承。

4. Concrete ProviderApplication Provider Base中所定義的方法在這個類中實現。Concrete Provider為了從配置文件中讀取信息而會重寫Initialize()方法

這四個類是實現Provider模式所必須的。其它的類則是用來定義一個Provider能提供什么東西(在本文中,是一個Product類)或是在以后為這些對象提供服務的工具類,或是用于管理程序配置文件。

這些東西看起來有點暈,不過當你看到一個請求傳給Store類后再轉交給一個StoreProvider的引用,由具體的CornerStoreProvider去完成時,你就知道這幾個“齒輪”是如何配合的了。

繼續>>下一頁
[第1頁][第2頁][第3頁]

0
0

請先登錄 提交中…

標簽:設計模式 C# Provider模式

文章列表

打造優雅的Linq To SQL動態查詢

打造優雅的Linq To SQL動態查詢

作者: CoolCode 來源: 博客園 發布時間: 2009-09-28 09:51 閱讀: 9465 次 推薦: 9 原文鏈接 [收藏]

  首先我們來看看日常比較典型的一種查詢Form

clip_image002

  這個場景很簡單:就是根據客戶名、訂單日期、負責人來作篩選條件,然后找出符合要求的訂單。

  在那遙遠的時代,可能避免不了要寫這樣的簡單接口:

public interface IOrderService
{
  IList<Order> Search(string customer, DateTime dateFrom, DateTime dateTo, int employeeID);
}

  具體愛怎么實現就怎么實現啦,存儲過程,ORM框架。這里假定是用了孩童時代就開始有的存儲過程吧:

Create Procedure usp_SearchOrder
@Customer nVarchar(20),
@DateFrom DateTime,
@DateTo DateTime,
@EmployeeID Int
AS
/*以下省去幾百行SQL語句*/

  接著寫一個類OrderService實現IOrderService, 調用以上存儲過程,洋洋灑灑的寫上幾句代碼就可以“安枕睡覺”了。但是,噩夢就從此開始了。

  客戶的需求是不斷變化的。過了一段時間,設計這個接口的工程師就先被夸贊一番,然后說客戶提出需要加多“一個”篩選條件。工程師可能也想過這一點,加一個篩選條件“不外乎“給接口加個參數,存儲過程加個參數,where那里加個條件,……苦力活一堆。

  客戶的篩選條件是這樣:既然訂單里有“國家”字段,就想根據國家來篩選條件,并且國家可多選,如下圖:

clip_image008

  工程師看到圖可能就倒下了……

  以上可以當作笑話看看,不過話說回來,沒有一個通用的查詢框架,單靠這樣的接口

public interface IOrderService
{
  IList<Order> Search(string customer, DateTime dateFrom, DateTime dateTo, int employeeID);
}

  是根本不能適應需求變化。

  在沒有Linq 的時代, SQL“ 強人”就試圖通過拼接字符串的方法來結束存儲過程帶來的痛苦,

IList<Order> Search(string sqlQurey);

  結果進入另一個被“SQL注入”的時代(注:我大學時也有一段時間玩過“SQL注入”,不亦樂乎,現在基本上很少找到能夠簡單注入的網站了,有磨難就有前進的動力嘛 )。

  來到Linq To SQL 的時代 (不得不贊嘆Linq把查詢發揮到淋漓盡致), 某些朋友就能輕易地揮灑Linq表達式來解決查詢問題:

IList<Order> Search(Expression<Func<Order, bool>> expression);

  查詢語句:

Expression<Func<Order, bool>> expression = c =>
c.Customer.ContactName.Contains(txtCustomer.Text) &&
c.OrderDate >= DateTime.Parse(txtDateFrom.Text) && c.OrderDate <= DateTime.Parse(txtDateTo.Text) &&
c.EmployeeID == int.Parse(ddlEmployee.SelectedValue);

  然后再一次 “安枕睡覺”。一覺醒來還是不行。

  客戶又來新需求:負責人的下拉框加個“ALL”的選項,如果選了“ALL”就搜索所有的負責人相關的Order。

  工程師刷刷幾下,又加if else,又加 and 來拼裝expression;接著又來新需求,…… 最后expression臃腫無比 (當然這個故事是有點夸張)。

  為什么用上“先進”的工具還是會倒在慘不忍睹的代碼海洋里呢?因為Microsoft提供給我們的只是“魚竿”。這種魚竿不管在小河還是大海都能釣到東西,而且不管你釣的是鯊魚還是鯨魚,也保證魚竿不會斷。但是有些人能釣到大魚,有些則釣到一雙拖鞋。因為關鍵的魚餌沒用上。也就是說,Microsoft給了我們強大的Linq 表達式,可不是叫我們隨便到表現層一放就了事,封裝才是硬道理。

  于是,千呼萬喚始出來,猶抱 QueryBuilder 半遮臉:

var queryBuilder = QueryBuilder.Create<Order>()
.Like(c => c.Customer.ContactName, txtCustomer.Text)
.Between(c => c.OrderDate, DateTime.Parse(txtDateFrom.Text), DateTime.Parse(txtDateTo.Text))
.Equals(c => c.EmployeeID, int.Parse(ddlEmployee.SelectedValue))
.In(c => c.ShipCountry, selectedCountries );

  這樣代碼就清爽很多了,邏輯也特別清晰,即使不懂Linq 表達式的人也能明白這些語句是干什么的,因為它的語義基本上跟SQL一樣:

WHERE ([t1].[ContactName] LIKE '%A%') AND
(([t0].[OrderDate]) >= '1/1/1990 12:00:00 AM') AND (([t0].[OrderDate]) <= '9/25/2009 11:
59:59 PM') AND
(([t0].[EmployeeID]) = 1) AND
([t0].[ShipCountry] IN ('Finland', 'USA', 'UK'))

  對于使用這個QueryBuilder的人來說,他覺得很爽,因為他明白釣什么魚用什么魚餌了,模糊查詢用Like,范圍用Between,……

  對于編寫這個QueryBuilder的人來說,也覺得很爽,因為他本身熱愛寫通用型的代碼,就像博客園的老趙那樣。

  看到使用方式,聰明人自然就已經想到大概的實現方式。就像廚師吃過別人煮的菜,自然心中也略知是怎么煮的。

  實現方式并不難,這里簡單說明一下:

  QueryBuilder.Create() 返回的是IQueryBuilder 接口,而IQueryBuilder 接口只有一個 Expression 屬性:

///
/// 動態查詢條件創建者
///
///
public interface IQueryBuilder
{
Expression<Funcbool>> Expression { get; set; }
}

  于是 Like, Between, Equals, In 就可以根據這個Expression 來無限擴展了。

  以下是實現Like的擴展方法:

///
/// 建立 Like ( 模糊 ) 查詢條件
///
/// 實體
/// 動態查詢條件創建者
/// 屬性
/// 查詢值
///
public static IQueryBuilder Like(this IQueryBuilder q, Expression<Funcstring>> property, string value)
{
value = value.Trim();
if (!string.IsNullOrEmpty(value))
{
var parameter = property.GetParameters();
var constant = Expression.Constant(“%" + value + “%");
MethodCallExpression methodExp = Expression.Call(null, typeof(SqlMethods).GetMethod(“Like",
new Type[] { typeof(string), typeof(string) }), property.Body, constant);
Expression<Funcbool>> lambda = Expression.Lambda<Funcbool>>(methodExp, parameter);

q.Expression = q.Expression.And(lambda);
}
return q;
}

  每個方法都是對Expression進行修改,然后返回修改后的Expression,以此實現鏈式編程。

  稍微有點意思的就是 In 的擴展方法(這個害我費了不少時間,前前后后可能4個小時):

///
/// 建立 In 查詢條件
///
/// 實體
/// 動態查詢條件創建者
/// 屬性
/// 查詢值
///
public static IQueryBuilder In(this IQueryBuilder q, Expression<Func> property, params P[] values)
{
if (values != null && values.Length > 0)
{
var parameter = property.GetParameters();
var constant = Expression.Constant(values);
Type type = typeof(P);
Expression nonNullProperty = property.Body;
//如果是Nullable類型,則轉化成X類型
if (IsNullableType(type))
{
type = GetNonNullableType(type);
nonNullProperty = Expression.Convert(property.Body, type);
}
Expression<Funcbool>> InExpression = (list, el) => list.Contains(el);
var methodExp = InExpression;
var invoke = Expression.Invoke(methodExp, constant, property.Body);
Expression<Funcbool>> lambda = Expression.Lambda<Funcbool>>(invoke, parameter);
q.Expression = q.Expression.And(lambda);
}
return q;
}

  如果有興趣的朋友可以在文章末下載源代碼,看看其他兩個擴展方法。

  嗯,似乎又是時候退場了。什么?你說只有Like, Between, Equals, In不夠用?哦,可以自己擴展IQueryBuilder,自己動手豐衣足食嘛。

  我后來又為另外一個Project做了一個“奇怪”的擴展:

  譬如,我們知道打印設置里可以直接寫頁數號碼來篩選要打印哪幾頁——1,4,9 或者 1-8 這樣的方式。

clip_image021 clip_image023

  于是引發這樣的需求:

clip_image025clip_image027

  左圖查詢Bruce和Jeffz的訂單;右圖查詢B直到Z的客戶訂單。

  還美其名曰:Fuzzy ,意即:模糊不清的 (點這里 看 Fuzzy詳細意思)

總結:

  其實這篇文章已經醞釀好久了,近期工作收獲很多編程技巧,因為其中一個Project基本上全由我來寫框架。盡管平時晚上也有兩個多小時可以學習和做自己的框架,但總比不上在公司能夠用上八小時來勁。一個Project下來了,又是時候總結一下,希望有空能夠繼續與大家分享。

9
0

請先登錄 提交中…

標簽:Linq To SQL 動態查詢

文章列表

Memcached的stats命令

Memcached的stats命令

作者: eoiioe 來源: 博客園 發布時間: 2009-07-19 11:49 閱讀: 5793 次 推薦: 0 原文鏈接 [收藏]

命令行查看Memcached運行狀態

很多時候需要監控服務器上的Memcached運行情況,比如緩存的查詢次數,命中率之類的。但找到的
那個memcached-tool是linux下用perl寫的,我也沒試過windows能不能用。后來發現個簡單的辦法
可以做到,就是使用Telnet。
首先登錄到服務器,然后在cmd命令行中鍵入
telnet 127.0.0.1 11211
其中127.0.0.1是服務器的地址(這里是本機) ,11211是memcached綁定的端口號。
之后命令行窗口全黑只有光標提示,摸黑輸入stats,即可得到描述Memcached服務器運行情況的參
數。如下圖:

其中,uptime 是memcached運行的秒數,cmd_get是查詢緩存的次數。這兩個數據相除一下就能得到
平均每秒請求緩存的次數——最近niupu的流量很低,所以平均也就一秒請求一次多,這么點大的壓
力,用文件系統緩存一樣沒問題,根本不會體現出使用memcached的優越。
下面的cmd_set 就是設置key=>value的次數。整個memcached是個大hash,用cmd_get沒有找到的內
容,就會調用一下cmd_set寫進緩存里。緊跟著是get_hits,就是緩存命中的次數。緩存命中率 =
get_hits/cmd_get * 100%。
下面的get_misses的數字加上get_hits應該等于cmd_get。而total_itemscurr_items表示現在在緩
存中的鍵值對個數,在圖上total_items == cmd_set == get_misses,不過當可用最大內存用光時
,memcached就會刪掉一些內容,上面的等式就不成立了。
話說回來,memcached要是能有一套完整的監測工具就太好了。memcached的安裝和php相應配置請看
這里。

Memcached的stats命令

telnet到memcached服務器后有很多的命令可以使用,除了大家熟知的add、get、set、incr、decr、replace、delete等賦值命令外,還有一系列的獲取服務器信息的命令,這部分命令都是以stats開頭的。
用PHP的Memcache::getStats($cmd)也可以訪問這些命令

常用的命令

stats
顯示服務器信息、統計數據等

stats reset
清空統計數據

stats malloc
顯示內存分配數據

stats cachedump slab_id limit_num
顯示某個slab中的前limit_num個key列表,顯示格式如下
ITEM key_name [ value_length b; expire_time|access_time s]
其中,memcached 1.2.2及以前版本顯示的是 訪問時間(timestamp)
1.2.4以上版本,包括1.2.4顯示 過期時間(timestamp)
如果是永不過期的key,expire_time會顯示為服務器啟動的時間

stats cachedump 7 2
ITEM copy_test1 [250 b; 1207795754 s]
ITEM copy_test [248 b; 1207793649 s]

stats slabs
顯示各個slab的信息,包括chunk的大小、數目、使用情況等

stats items
顯示各個slab中item的數目和最老item的年齡(最后一次訪問距離現在的秒數)

stats detail [on|off|dump]
設置或者顯示詳細操作記錄

參數為on,打開詳細操作記錄
參數為off,關閉詳細操作記錄
參數為dump,顯示詳細操作記錄(每一個鍵值get、set、hit、del的次數)

stats detail dump
PREFIX copy_test2 get 1 hit 1 set 0 del 0
PREFIX copy_test1 get 1 hit 1 set 0 del 0
PREFIX cpy get 1 hit 0 set 0 del 0

0
0

請先登錄 提交中…

標簽:Memcached

文章列表

詳解ASP.NET MVC數據分頁

詳解ASP.NET MVC數據分頁

作者: 逆時針 來源: 逆時針 發布時間: 2010-03-24 10:20 閱讀: 21870 次 推薦: 12 [收藏]

ASP.NET MVC框架已經進入2.0時代,本文將從ASP.NET MVC數據分頁談起,希望能對大家有所幫助。

在網頁上進行表格資料或其他顯示資料的分頁是一種十分常見的需求,以前我們有 GridView 或 DataPager 可以幫我們自動分頁,雖然到了 ASP.NET MVC 一切全部重頭來過,但我們也不用真的那麼辛苦的自己實做分頁,因為早就有人幫我們寫好程式并開放原始碼分享給這個世界了。

如果你已經體會到在 ASP.NET MVC 中妥善利用強型別(Strong Typed)特性進行開發的優點時,你將會發現搭配 Visual Studio 2008 進行專桉開發的過程有多美妙。以下我先舉一個簡單的例子:

你可以在 Controller 中定義一個 Action 方法,并在裡面先取得所有需顯示在 View 中的資料,如果你用 LINQ to SQL 的話,可以直接傳入 IQueryable 型別的物件到 View 中,當成 View 裡面使用的 Model,這樣可以享受延遲載入(Defered Loading) 的效果。

代碼

public ActionResult Index()
{
IQueryable<Customer> custs =
from cust in db.Customers
where cust.City == "Taiwan"
select cust;

return View(custs);
}
public ActionResult Index()
{
IQueryable<Customer> custs =
from cust in db.Customers
where cust.City == "Taiwan"
select cust;

return View(custs);
}

之后在你的 View 中宣告繼承時可透過泛型指派 IQueryable 進去:

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<IQueryable<Customer>>" %>
<%@ Page Language="C#"
Inherits="System.Web.Mvc.ViewPage<IQueryable<Customer>>" %>

或是轉型成統一個 IEnumable ,這也是比較常見的用法:

<%@ Page Language="C#"
Inherits="System.Web.Mvc.ViewPage<IEnumable<Customer>>" %>
<%@ Page Language="C#"
Inherits="System.Web.Mvc.ViewPage<IEnumable<Customer>>" %>

然后你就可以利用 foreach 取出所有資料并將資料顯示出來了:

代碼

<table>
<% foreach (var item in Model) { %>
<tr>
<td><%= Html.Encode(item.ID) %></td>
<td><%= Html.Encode(item.Name) %></td>
<td><%= Html.Encode(item.Tel) %></td>
</tr>
<% } %>
</table>
<table>
<% foreach (var item in Model) { %>
<tr>
<td><%= Html.Encode(item.ID) %></td>
<td><%= Html.Encode(item.Name) %></td>
<td><%= Html.Encode(item.Tel) %></td>
</tr>
<% } %>
</table>

這就是標準的 ASP.NET MVC 取得資料并顯示在 View 中的 Pattern。

  我們最近在開發 ASP.NET MVC 專桉的過程中,除了自行研究如何有效分頁以外,也上網找了好幾套 ASP.NET MVC 分頁的解決方桉,最后我們總結出一個最好用的就是這個元件 ( Paging with ASP.NET MVC )。

  要利用這個元件進行資料分頁,不外乎有幾件事必須做到:

  需傳入一個 page 參數到 Action 中,用以指定你目前要顯示「第幾頁」的資料。

  準備傳入的資料(Model),可透過 Paging with ASP.NET MVC 元件中提供的 Extension Method 將 IList, IQueryable, 或 IEnumable 型別的資料轉換成 IPagedList 的型別,并傳入 View 中。

透過一個自訂的 Html Helper 在 View 中必須顯示「分頁導覽列」的資訊 (如下圖)

  依據上面三個步驟進行修改,Action 的程式碼會變成這樣:

view plaincopy to clipboardprint?
// 分頁后每頁顯示的筆數
int defaultPageSize = 10;

// 傳入 page 參數 ( 透過 Model Binder 綁進來的 )
public ActionResult Index(int? page)
{
IQueryable<Customer> custs =
from cust in db.Customers
where cust.City == "Taiwan"
select cust;

// 計算出目前要顯示第幾頁的資料 ( 因為 page 為 Nullable<int> 型別 )
int currentPageIndex = page.HasValue ? page.Value - 1 : 0;

// 透過 ToPagedList 這個 Extension Method 將原本的資料轉成 IPagedList<T>
return View(custs.ToPagedList(currentPageIndex, defaultPageSize));
}
// 分頁后每頁顯示的筆數
int defaultPageSize = 10;

// 傳入 page 參數 ( 透過 Model Binder 綁進來的 )
public ActionResult Index(int? page)
{
IQueryable<Customer> custs =
from cust in db.Customers
where cust.City == "Taiwan"
select cust;

// 計算出目前要顯示第幾頁的資料 ( 因為 page 為 Nullable<int> 型別 )
int currentPageIndex = page.HasValue ? page.Value - 1 : 0;

// 透過 ToPagedList 這個 Extension Method 將原本的資料轉成 IPagedList<T>
return View(custs.ToPagedList(currentPageIndex, defaultPageSize));
}

然后在 View 中顯示資料的地方,透過一個自訂的 Html Helper 方法顯示分頁資訊。

首先必須先修改傳入 View 的 Model 型別

<%@ Page Language="C#"
Inherits="System.Web.Mvc.ViewPage<IPagedList<Customer>>" %>
<%@ Page Language="C#"
Inherits="System.Web.Mvc.ViewPage<IPagedList<Customer>>" %>

然后再宣告匯入 MvcPaging 命名空間,好讓 Html.Pager 這個 Html Helper Method 可以使用。

備注: 也可以在 web.config 設定,請參考 ASP.NET 如何預設匯入指定的命名空間(Namespace) 文章!

<%@ Import Namespace="MvcPaging"%>
<%@ Import Namespace="MvcPaging"%>

然后原本顯示資料的程式「完全不用改寫」,只要加上「分頁導覽列」即可:

代碼

view plaincopy to clipboardprint?
<table>
<% foreach (var item in Model) { %>
<tr>
<td><%= Html.Encode(item.ID) %></td>
<td><%= Html.Encode(item.Name) %></td>
<td><%= Html.Encode(item.Tel) %></td>
</tr>
<% } %>
</table>

<div class="pager">
<%= Html.Pager(Model.PageSize, Model.PageNumber, Model.TotalItemCount) %>
</div>
<table>
<% foreach (var item in Model) { %>
<tr>
<td><%= Html.Encode(item.ID) %></td>
<td><%= Html.Encode(item.Name) %></td>
<td><%= Html.Encode(item.Tel) %></td>
</tr>
<% } %>
</table>

<div class="pager">
<%= Html.Pager(Model.PageSize, Model.PageNumber, Model.TotalItemCount) %>
</div>

就這樣簡單幾個步驟即可完成 ASP.NET MVC 內的分頁了,是不是還不錯呢!

12
1

請先登錄 提交中…

標簽:MVC

文章列表

C#代碼動態編譯、動態執行、動態調試

C#代碼動態編譯、動態執行、動態調試

作者: eaglet 來源: 博客園 發布時間: 2008-11-03 11:33 閱讀: 9589 次 推薦: 0 原文鏈接 [收藏]

  前幾天看到一篇關于.net動態編譯的文章 .NET中的動態編譯 ,很受啟發。在此基礎上我做了一些封裝,為使調用更加簡單,并增加了對動態代碼調試的支持,相同代碼只編譯一次的支持,代碼改動自動重新編譯,代碼引用文件的自動加載和手工加載等功能。

如上圖,我封裝的類CSharpProvider很簡單,下面說明一下一些公共成員的用法。

公共屬性

AssemblyFileName:這個屬性指定動態編譯后生成的配件名稱。

CompilerParameters:這個屬性指定編譯的參數

References:這個屬性指定被編譯代碼中的引用。調用者只要調用References.Add(“xxx.dll"),就可以加入自己的引用,對于System命名空間的所有引用,不需要手工加入,該類會自動加載。對于用戶自己的組件,如果不手工指定引用文件,該類會自動根據名字空間名進行猜測。

SourceCodeFileEncoding:如果以文件形式編譯,指定文件的編碼類型。

公共方法

public bool Compile(string code)

輸入代碼字符串,并編譯

public bool CompileFromFile(string sourceCodeFileName)

編譯輸入的代碼文件

public object CreateInstance(string code, string typeFullName)

創建類的實例

如下面代碼,可以輸入 CreateInstance(code, “MyInterface.IHelloWorld"),也可以輸入CreateInstance(code, “HelloWorld"),程序會根據

類型名稱來自動找到符合條件的類并實例化。如果代碼中有多個指定類型的類,將實例化第一個。

using System;
using MyInterface;

[Serializable]
public class HelloWorld : MarshalByRefObject, IHelloWorld
{
public string Say()
{
return “Hi";
}
}

這里需要特別指出的是由于用到了AppDomain的遠程調用,所有的動態加載的代碼必須繼承自MarshallByRefObject

如果僅僅聲明為[Serializable] 雖然也可以執行,但主應用程序域會記錄下子應用程序域的一個引用,這樣導致子應用程序

域卸載后,依然無法完全釋放內存,從而內存泄漏。所以這個很關鍵,一定要注意。

public object CreateInstanceFromFile(string fileName, string typeFullName)

從文件創建動態實例

下面再談談對動態代碼的調試

動態創建的代碼如果不能調試,就像一個黑盒子,對系統的可維護性有較大破壞。未來實現這個功能,我們需要做以下工作,

第一、編譯時要生成調試信息,這個可以通過設置 CompilerParameters.IncludeDebugInformation = true;來實現

第二、我們必須告訴調試器源碼對應的位置,對于從文件編譯的情況,源碼文件位置會被自動寫入調試信息文件 *.pdb中,而對于從內存編譯的情況,我還沒有找到指定的方法,如果哪位朋友知道,還望賜教。所以目前如果要調試動態代碼,必須從文件編譯,也就是調用CompileFromFile,CreateInstanceFromFile

第三、我們需要在代碼中設置一個斷點,這個可以在代碼中加入 System.Diagnostics.Debugger.Break(); 來解決。

如下圖所示,動態代碼現在可以調試了。

應用程序域

為了避免內存泄漏,本程序封裝了對應用程序域的使用,調用者基本不需要關心應用程序域的調用和卸載過程。本程序在

重新編譯或者對象銷毀時會自動卸載應用程序域,從而釋放內存。由于做這個程序是在應用程序域上遇到了很多麻煩,所以

感覺還是有必要簡單講一下應用程序域。

如上圖所示,應用程序與實際上有點像一個單獨的進程,但這個進程是運行在當前進程里面的,當然這個比喻不夠貼切。

對應用程序域的調用有點類似進程間采用 Remoting 方式的對象調用,也就是說默認應用程序域要調用其他應用程序域中的對象,

必須采用遠程調用的方法,而不能直接調用,如果直接調用,默認應用程序域就會記錄這個被調用的應用程序域的一個內存引用,

即使這個應用程序域執行了Unload 方法卸載后,內存依然無法釋放,這也是我一開始操作應用程序域遇到的最大困擾。

另外所有暴露在兩個應用程序域之間的類必須從MarshalByRefObject基礎,這點非常重要,否則將導致內存無法釋放。

本程序的一些缺陷

1、沒有提供編譯多文件的接口,其實要實現這個很簡單,考慮到用于動態執行的代碼腳本往往比較簡單,所以偷懶沒有做。

2、沒有提供對動態代碼中多個對象的枚舉接口,以后再完善吧。

源碼下載位置

源碼下載

0
0

請先登錄 提交中…

標簽:C# 動態編譯 動態執行 動態調試

文章列表

memcached全面剖析–3. memcached的刪除機制和發展方向

memcached全面剖析–3. memcached的刪除機制和發展方向

作者: charlee 來源: idv2.com 發布時間: 2008-09-28 17:03 閱讀: 22028 次 推薦: 3 原文鏈接 [收藏]

系列文章導航:

memcached完全剖析–1. memcached的基礎

memcached全面剖析–2. 理解memcached的內存存儲

memcached全面剖析–3. memcached的刪除機制和發展方向

memcached全面剖析–4. memcached的分布式算法

memcached全面剖析–5. memcached的應用和兼容程序

下面是《memcached全面剖析》的第三部分。

發表日:2008/7/16
作者:前坂徹(Toru Maesaka)
原文鏈接:http://gihyo.jp/dev/feature/01/memcached/0003

memcached是緩存,所以數據不會永久保存在服務器上,這是向系統中引入memcached的前提。 本次介紹memcached的數據刪除機制,以及memcached的最新發展方向——二進制協議(Binary Protocol) 和外部引擎支持。

memcached在數據刪除方面有效利用資源

數據不會真正從memcached中消失

上次介紹過, memcached不會釋放已分配的內存。記錄超時后,客戶端就無法再看見該記錄(invisible,透明), 其存儲空間即可重復使用。

Lazy Expiration

memcached內部不會監視記錄是否過期,而是在get時查看記錄的時間戳,檢查記錄是否過期。 這種技術被稱為lazy(惰性)expiration。因此,memcached不會在過期監視上耗費CPU時間。

LRU:從緩存中有效刪除數據的原理

memcached會優先使用已超時的記錄的空間,但即使如此,也會發生追加新記錄時空間不足的情況, 此時就要使用名為 Least Recently Used(LRU)機制來分配空間。 顧名思義,這是刪除“最近最少使用”的記錄的機制。 因此,當memcached的內存空間不足時(無法從slab class 獲取到新的空間時),就從最近未被使用的記錄中搜索,并將其空間分配給新的記錄。 從緩存的實用角度來看,該模型十分理想。

不過,有些情況下LRU機制反倒會造成麻煩。memcached啟動時通過“-M”參數可以禁止LRU,如下所示:

$ memcached -M -m 1024

啟動時必須注意的是,小寫的“-m”選項是用來指定最大內存大小的。不指定具體數值則使用默認值64MB。

指定“-M”參數啟動后,內存用盡時memcached會返回錯誤。 話說回來,memcached畢竟不是存儲器,而是緩存,所以推薦使用LRU。

memcached的最新發展方向

memcached的roadmap上有兩個大的目標。一個是二進制協議的策劃和實現,另一個是外部引擎的加載功能。

關于二進制協議

使用二進制協議的理由是它不需要文本協議的解析處理,使得原本高速的memcached的性能更上一層樓, 還能減少文本協議的漏洞。目前已大部分實現,開發用的代碼庫中已包含了該功能。 memcached的下載頁面上有代碼庫的鏈接。

二進制協議的格式

協議的包為24字節的幀,其后面是鍵和無結構數據(Unstructured Data)。 實際的格式如下(引自協議文檔):

 Byte/ 0 | 1 | 2 | 3 |
/ | | | |
|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
+

在Eclipse中使用JUnit4進行單元測試(上)

在Eclipse中使用JUnit4進行單元測試(上)

來源: 163博客 發布時間: 2010-10-28 11:12 閱讀: 1599 次 推薦: 0 原文鏈接 [收藏]
摘要:本篇文章講述在Eclipse中使用JUnit4進行單元測試。

  首先,我們來一個傻瓜式速成教程,不要問為什么,Follow Me,先來體驗一下單元測試的快感!

  首先新建一個項目叫JUnit_Test,我們編寫一個Calculator類,這是一個能夠簡單實現加減乘除、平方、開方的計算器類,然后對這些功能進行單元測試。這個類并不是很完美,我們故意保留了一些Bug用于演示,這些Bug在注釋中都有說明。該類代碼如下:

package andycpp;
public class Calculator ...{
private static int result; //靜態變量,用于存儲運行結果
public void add(int n)...{
result = result + n;
}
public void substract(int n)...{
result = result - 1; //Bug: 正確的應該是 result =result-n
}
public void multiply(int n)...{
}//此方法尚未寫好
public void divide(int n)...{
result = result / n;
}
public void square(int n)...{
result = n * n;
}
public void squareRoot(int n)...{
for (; ;) ; //Bug : 死循環
}
public void clear()...{//將結果清零
result = 0;
}
public int getResult()...{
return result;
}
}

  第二步,將JUnit4單元測試包引入這個項目:在該項目上點右鍵,點“屬性”,如圖:

  在彈出的屬性窗口中,首先在左邊選擇“Java Build Path”,然后到右上選擇“Libraries”標簽,之后在最右邊點擊“Add Library…”按鈕,如下圖所示:

  然后在新彈出的對話框中選擇JUnit4并點擊確定,如上圖所示,JUnit4軟件包就被包含進我們這個項目了。

  第三步,生成JUnit測試框架:在Eclipse的Package Explorer中用右鍵點擊該類彈出菜單,選擇“Newà JUnit Test Case”。如下圖所示:

  在彈出的對話框中,進行相應的選擇,如下圖所示:

  點擊“下一步”后,系統會自動列出你這個類中包含的方法,選擇你要進行測試的方法。此例中,我們僅對“加、減、乘、除”四個方法進行測試。如下圖所示:

在Eclipse中使用JUnit4進行單元測試(初級篇)-蛋蛋- Monicas fossa

  之后系統會自動生成一個新類CalculatorTest,里面包含一些空的測試用例。你只需要將這些測試用例稍作修改即可使用。完整的CalculatorTest代碼如下:

package andycpp;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
public class CalculatorTest ...{
private static Calculator calculator = new Calculator();
@Before
public void setUp() throws Exception ...{
calculator.clear();
}
@Test
public void testAdd()...{
calculator.add(2);
calculator.add(3);
assertEquals(5, calculator.getResult());
}
@Test
public void testSubstract()...{
calculator.add(10);
calculator.substract(2);
assertEquals(8, calculator.getResult());
}
@Ignore("Multiply() Not yet implemented")
@Test
public void testMultiply()...{
}
@Test
public void testDivide()...{
calculator.add(8);
calculator.divide(2);
assertEquals(4, calculator.getResult());
}
}

  第四步,運行測試代碼:按照上述代碼修改完畢后,我們在CalculatorTest類上點右鍵,選擇“Run As à JUnit Test”來運行我們的測試運行結果如下:

  進度條是紅顏色表示發現錯誤,具體的測試結果在進度條上面有表示“共進行了4個測試,其中1個測試被忽略,一個測試失敗”。

  至此,我們已經完整體驗了在Eclipse中使用JUnit的方法。

  【相關文章】:在Eclipse中使用JUnit4進行單元測試(中)

          在Eclipse中使用JUnit4進行單元測試(下)

0
0

請先登錄 提交中…

標簽:Eclipse JUnit4 單元測試

文章列表

在Android上實現Junit單元測試的四部曲

在Android上實現Junit單元測試的四部曲

來源: JavaEye 發布時間: 2010-10-17 22:44 閱讀: 2332 次 推薦: 0 原文鏈接 [收藏]
摘要:本文講述在Android上實現Junit單元測試,利用JUnit等單元測試框架進行單元測試對于Java程序員并不陌生,利用這些非常有效的工具,使得代碼的質量得到有效的監控和維護。

  我們曾經和大家探討過全面剖析Java ME單元測試理念,其實在Android上實現JUnit單元測試也不是很困難,主要是在配置文件和測試環境上將花費很長時間,下面從四步簡單講一下在Android上實現Junit單元測試。

  第一步:新建一個TestCase,記得要繼承androidTestCase,才能有getContext()來獲取當前的上下文變量,這在Android測試中很重要的,因為很多的Android api都需要context。

  Java代碼

 public class TestMath extends AndroidTestCase {

private int i1;
private int i2;
static final String LOG_TAG = "MathTest";

@Override
protected void setUp() throws Exception {
i1 = 2;
i2 = 3;
}

public void testAdd() {
assertTrue("testAdd failed", ((i1 + i2) == 5));
}

public void testDec() {
assertTrue("testDec failed", ((i2 - i1) == 1));
}

@Override
protected void tearDown() throws Exception {
super.tearDown();
}

@Override
public void testAndroidTestCaseSetupProperly() {
super.testAndroidTestCaseSetupProperly();
//Log.d( LOG_TAG, "testAndroidTestCaseSetupProperly" ;
}

}

  第二步:新建一個TestSuit,這個就繼承Junit的TestSuite就可以了,注意這里是用的addTestSuite方法,一開始使用addTest方法就是不能成功。

  Java代碼

publicclass ExampleSuite extends TestSuite {

public ExampleSuite() {
addTestSuite(TestMath.class);
}

} 

  第三步:新建一個Activity,用來啟動單元測試,并顯示測試結果。系統的AndroidTestRunner竟然什么連個UI界面也沒有實現,這里只是最簡單的實現了一個

  Java代碼

代碼

 1. public class TestActivity extends Activity {
2.
3. private TextView resultView;
4.
5. private TextView barView;
6.
7. private TextView messageView;
8.
9. private Thread testRunnerThread;
10.
11. private static final int SHOW_RESULT = 0;
12.
13. private static final int ERROR_FIND = 1;
14.
15. @Override
16. protected void onCreate(Bundle savedInstanceState) {
17. super.onCreate(savedInstanceState);
18. setContentView(R.layout.main);
19. resultView = (TextView)findViewById(R.id.ResultView);
20. barView = (TextView)findViewById(R.id.BarView);
21. messageView = (TextView)findViewById(R.id.MessageView);
22. Button lunch = (Button)findViewById(R.id.LunchButton);
23. lunch.setOnClickListener(new View.OnClickListener() {
24. @Override
25. public void onClick(View v) {
26. startTest();
27. }
28. });
29. }
30.
31. private void showMessage(String message) {
32. hander.sendMessage(hander.obtainMessage(ERROR_FIND, message));
33. }
34.
35. private void showResult(String text) {
36. hander.sendMessage(hander.obtainMessage(SHOW_RESULT, text));
37. }
38.
39. private synchronized void startTest() {
40. if (testRunnerThread != null
41. && testRunnerThread.isAlive()) {
42. testRunnerThread = null;
43. }
44. if (testRunnerThread == null) {
45. testRunnerThread = new Thread(new TestRunner(this));
46. testRunnerThread.start();
47. } else {
48. Toast.makeText(this,
49. "Test is still running",
50. Toast.LENGTH_SHORT).show();
51. }
52. }
53.
54. public Handler hander = new Handler() {
55. public void handleMessage(Message msg) {
56. switch (msg.what) {
57. case SHOW_RESULT:
58. resultView.setText(msg.obj.toString());
59. break;
60. case ERROR_FIND:
61. messageView.append(msg.obj.toString());
62. barView.setBackgroundColor(Color.RED);
63. break;
64. default:
65. break;
66. }
67. }
68. };
69.
70. class TestRunner implements Runnable, TestListener {
71.
72. private Activity parentActivity;
73.
74. private int testCount;
75.
76. private int errorCount;
77.
78. private int failureCount;
79.
80. public TestRunner(Activity parentActivity) {
81. this.parentActivity = parentActivity;
82. }
83.
84. @Override
85. public void run() {
86. testCount = 0;
87. errorCount = 0;
88. failureCount = 0;
89.
90. ExampleSuite suite = new ExampleSuite();
91. AndroidTestRunner testRunner = new AndroidTestRunner();
92. testRunner.setTest(suite);
93. testRunner.addTestListener(this);
94. testRunner.setContext(parentActivity);
95. testRunner.runTest();
96. }
97.
98. @Override
99. public void addError(Test test, Throwable t) {
100. errorCount++;
101. showMessage(t.getMessage() + "\n");
102. }
103.
104. @Override
105. public void addFailure(Test test, AssertionFailedError t) {
106. failureCount++;
107. showMessage(t.getMessage() + "\n");
108. }
109.
110. @Override
111. public void endTest(Test test) {
112. showResult(getResult());
113. }
114.
115. @Override
116. public void startTest(Test test) {
117. testCount++;
118. }
119.
120. private String getResult() {
121. int successCount = testCount - failureCount - errorCount;
122. return "Test:" + testCount + " Success:" + successCount + " Failed:" + failureCount + " Error:" + errorCount;
123. }
124.
125. }
126.
127. }

  第四步:修改AndroidManifest.xml,加入,不然會提示找不到AndroidTestRunner,這里需要注意是這句話是放在applications下面的,我一開始也不知道,放錯了地方,浪費了不少時間

  Xml代碼

 xml version="1.0" encoding="utf-8"?>
<manifest xmlns:Android="http://schemas.Android.com/apk/res/Android"
package="com.test.sample"
Android:versionCode="1"
Android:versionName="1.0">
<application Android:icon="@drawable/icon"
Android:label="@string/app_name" Android:debuggable="true">
<activity Android:name=".TestActivity"
Android:label="@string/app_name">
<intent-filter>
<action Android:name="Android.intent.action.MAIN"/>
<category Android:name="Android.intent.category.LAUNCHER"/>
intent-filter>
activity>
<uses-library Android:name="Android.test.runner"/>
application>
<uses-sdk Android:minSdkVersion="4"/>
manifest> 

0
0

請先登錄 提交中…

標簽:Android Junit 單元測試

文章列表

大話Session

大話Session

作者: 晉哥哥 來源: 博客園 發布時間: 2010-02-22 10:19 閱讀: 6042 次 推薦: 0 原文鏈接 [收藏]

引言

在web開發中,session是個非常重要的概念。在許多動態網站的開發者看來,session就是一個變量,而且其表現像個黑洞,他只需要將東西在合適的時機放進這個洞里,等需要的時候再把東西取出來。這是開發者對session最直觀的感受,但是黑洞里的景象或者說session內部到底是怎么工作的呢?當筆者向身邊的一些同事或朋友問及相關的更進一步的細節時,很多人往往要么含糊其辭要么主觀臆斷,所謂知其然而不知其所以然。

筆者由此想到很多開發者,包括我自己,每每都是糾纏于框架甚至二次開發平臺之上,而對于其下的核心和基礎知之甚少,或者有心無力甚至毫不關心,少了逐本溯源的精神,每憶及此,無不慚愧。曾經實現過一個簡單的HttpServer,但當時由于知識儲備和時間的問題,沒有考慮到session這塊,不過近期在工作之余翻看了一些資料,并進行了相關實踐,小有所得,本著分享的精神,我將在本文中盡可能全面地將個人對于session的理解展現給讀者,同時盡我所能地論及一些相關的知識,以期讀者在對session有所了解的同時也能另有所悟,正所謂授人以漁。

Session是什么

Session一般譯作會話,牛津詞典對其的解釋是進行某活動連續的一段時間。從不同的層面看待session,它有著類似但不全然相同的含義。比如,在web應用的用戶看來,他打開瀏覽器訪問一個電子商務網站,登錄、并完成購物直到關閉瀏覽器,這是一個會話。而在web應用的開發者開來,用戶登錄時我需要創建一個數據結構以存儲用戶的登錄信息,這個結構也叫做session。因此在談論session的時候要注意上下文環境。而本文談論的是一種基于HTTP協議的用以增強web應用能力的機制或者說一種方案,它不是單指某種特定的動態頁面技術,而這種能力就是保持狀態,也可以稱作保持會話。

為什么需要session

談及session一般是在web應用的背景之下,我們知道web應用是基于HTTP協議的,而HTTP協議恰恰是一種無狀態協議。也就是說,用戶從A頁面跳轉到B頁面會重新發送一次HTTP請求,而服務端在返回響應的時候是無法獲知該用戶在請求B頁面之前做了什么的。

對于HTTP的無狀態性的原因,相關RFC里并沒有解釋,但聯系到HTTP的歷史以及應用場景,我們可以推測出一些理由:

1. 設計HTTP最初的目的是為了提供一種發布和接收HTML頁面的方法。那個時候沒有動態頁面技術,只有純粹的靜態HTML頁面,因此根本不需要協議能保持狀態;

2. 用戶在收到響應時,往往要花一些時間來閱讀頁面,因此如果保持客戶端和服務端之間的連接,那么這個連接在大多數的時間里都將是空閑的,這是一種資源的無端浪費。所以HTTP原始的設計是默認短連接,即客戶端和服務端完成一次請求和響應之后就斷開TCP連接,服務器因此無法預知客戶端的下一個動作,它甚至都不知道這個用戶會不會再次訪問,因此讓HTTP協議來維護用戶的訪問狀態也全然沒有必要;

3. 將一部分復雜性轉嫁到以HTTP協議為基礎的技術之上可以使得HTTP在協議這個層面上顯得相對簡單,而這種簡單也賦予了HTTP更強的擴展能力。事實上,session技術從本質上來講也是對HTTP協議的一種擴展。

總而言之,HTTP的無狀態是由其歷史使命而決定的。但隨著網絡技術的蓬勃發展,人們再也不滿足于死板乏味的靜態HTML,他們希望web應用能動起來,于是客戶端出現了腳本和DOM技術,HTML里增加了表單,而服務端出現了CGI等等動態技術。

而正是這種web動態化的需求,給HTTP協議提出了一個難題:一個無狀態的協議怎樣才能關聯兩次連續的請求呢?也就是說無狀態的協議怎樣才能滿足有狀態的需求呢?

此時有狀態是必然趨勢而協議的無狀態性也是木已成舟,因此我們需要一些方案來解決這個矛盾,來保持HTTP連接狀態,于是出現了cookie和session。

對于此部分內容,讀者或許會有一些疑問,筆者在此先談兩點:

1. 無狀態性和長連接

可能有人會問,現在被廣泛使用的HTTP1.1默認使用長連接,它還是無狀態的嗎?

連接方式和有無狀態是完全沒有關系的兩回事。因為狀態從某種意義上來講就是數據,而連接方式只是決定了數據的傳輸方式,而不能決定數據。長連接是隨著計算機性能的提高和網絡環境的改善所采取的一種合理的性能上的優化,一般情況下,web服務器會對長連接的數量進行限制,以免資源的過度消耗。

2. 無狀態性和session

Session是有狀態的,而HTTP協議是無狀態的,二者是否矛盾呢?

Session和HTTP協議屬于不同層面的事物,后者屬于ISO七層模型的最高層應用層,前者不屬于后者,前者是具體的動態頁面技術來實現的,但同時它又是基于后者的。在下文中筆者會分析Servlet/Jsp技術中的session機制,這會使你對此有更深刻的理解。

Cookie和Session

上面提到解決HTTP協議自身無狀態的方式有cookie和session。二者都能記錄狀態,前者是將狀態數據保存在客戶端,后者則保存在服務端。

首先看一下cookie的工作原理,這需要有基本的HTTP協議基礎。

cookie是在RFC2109(已廢棄,被RFC2965取代)里初次被描述的,每個客戶端最多保持三百個cookie,每個域名下最多20個Cookie(實際上一般瀏覽器現在都比這個多,如Firefox是50個),而每個cookie的大小為最多4K,不過不同的瀏覽器都有各自的實現。對于cookie的使用,最重要的就是要控制cookie的大小,不要放入無用的信息,也不要放入過多信息。

無論使用何種服務端技術,只要發送回的HTTP響應中包含如下形式的頭,則視為服務器要求設置一個cookie:

Set-cookie:name=name;expires=date;path=path;domain=domain

支持cookie的瀏覽器都會對此作出反應,即創建cookie文件并保存(也可能是內存cookie),用戶以后在每次發出請求時,瀏覽器都要判斷當前所有的cookie中有沒有沒失效(根據expires屬性判斷)并且匹配了path屬性的cookie信息,如果有的話,會以下面的形式加入到請求頭中發回服務端:

Cookie: name="zj"; Path="/linkage"

服務端的動態腳本會對其進行分析,并做出相應的處理,當然也可以選擇直接忽略。

這里牽扯到一個規范(或協議)與實現的問題,簡單來講就是規范規定了做成什么樣子,那么實現就必須依據規范來做,這樣才能互相兼容,但是各個實現所使用的方式卻不受約束,也可以在實現了規范的基礎上超出規范,這就稱之為擴展了。無論哪種瀏覽器,只要想提供cookie的功能,那就必須依照相應的RFC規范來實現。所以這里服務器只管發Set-cookie頭域,這也是HTTP協議無狀態性的一種體現。

需要注意的是,出于安全性的考慮,cookie可以被瀏覽器禁用。

再看一下session的原理:

筆者沒有找到相關的RFC,因為session本就不是協議層面的事物。它的基本原理是服務端為每一個session維護一份會話信息數據,而客戶端和服務端依靠一個全局唯一的標識來訪問會話信息數據。用戶訪問web應用時,服務端程序決定何時創建session,創建session可以概括為三個步驟:

1. 生成全局唯一標識符(sessionid);

2. 開辟數據存儲空間。一般會在內存中創建相應的數據結構,但這種情況下,系統一旦掉電,所有的會話數據就會丟失,如果是電子商務網站,這種事故會造成嚴重的后果。不過也可以寫到文件里甚至存儲在數據庫中,這樣雖然會增加I/O開銷,但session可以實現某種程度的持久化,而且更有利于session的共享;

3. 將session的全局唯一標示符發送給客戶端。

問題的關鍵就在服務端如何發送這個session的唯一標識上。聯系到HTTP協議,數據無非可以放到請求行、頭域或Body里,基于此,一般來說會有兩種常用的方式:cookie和URL重寫。

1. Cookie

讀者應該想到了,對,服務端只要設置Set-cookie頭就可以將session的標識符傳送到客戶端,而客戶端此后的每一次請求都會帶上這個標識符,由于cookie可以設置失效時間,所以一般包含session信息的cookie會設置失效時間為0,即瀏覽器進程有效時間。至于瀏覽器怎么處理這個0,每個瀏覽器都有自己的方案,但差別都不會太大(一般體現在新建瀏覽器窗口的時候);

2. URL重寫

所謂URL重寫,顧名思義就是重寫URL。試想,在返回用戶請求的頁面之前,將頁面內所有的URL后面全部以get參數的方式加上session標識符(或者加在path info部分等等),這樣用戶在收到響應之后,無論點擊哪個鏈接或提交表單,都會在再帶上session的標識符,從而就實現了會話的保持。讀者可能會覺得這種做法比較麻煩,確實是這樣,但是,如果客戶端禁用了cookie的話,URL重寫將會是首選。

到這里,讀者應該明白我前面為什么說session也算作是對HTTP的一種擴展了吧。如下兩幅圖是筆者在Firefox的Firebug插件中的截圖,可以看到,當我第一次訪問index.jsp時,響應頭里包含了Set-cookie頭,而請求頭中沒有。當我再次刷新頁面時,圖二顯示在響應中不在有Set-cookie頭,而在請求頭中卻有了Cookie頭。注意一下Cookie的名字:jsessionid,顧名思義,就是session的標識符,另外可以看到兩幅圖中的jsessionid的值是相同的,原因筆者就不再多解釋了。另外讀者可能在一些網站上見過在最后附加了一段形如jsessionid=xxx的URL,這就是采用URL重寫來實現的session。

(圖一,首次請求index.jsp)

(圖二,再次請求index.jsp)

Cookie和session由于實現手段不同,因此也各有優缺點和各自的應用場景:

1. 應用場景

Cookie的典型應用場景是Remember Me服務,即用戶的賬戶信息通過cookie的形式保存在客戶端,當用戶再次請求匹配的URL的時候,賬戶信息會被傳送到服務端,交由相應的程序完成自動登錄等功能。當然也可以保存一些客戶端信息,比如頁面布局以及搜索歷史等等。

Session的典型應用場景是用戶登錄某網站之后,將其登錄信息放入session,在以后的每次請求中查詢相應的登錄信息以確保該用戶合法。當然還是有購物車等等經典場景;

2. 安全性

cookie將信息保存在客戶端,如果不進行加密的話,無疑會暴露一些隱私信息,安全性很差,一般情況下敏感信息是經過加密后存儲在cookie中,但很容易就會被竊取。而session只會將信息存儲在服務端,如果存儲在文件或數據庫中,也有被竊取的可能,只是可能性比cookie小了太多。

Session安全性方面比較突出的是存在會話劫持的問題,這是一種安全威脅,這在下文會進行更詳細的說明。總體來講,session的安全性要高于cookie;

3. 性能

Cookie存儲在客戶端,消耗的是客戶端的I/O和內存,而session存儲在服務端,消耗的是服務端的資源。但是session對服務器造成的壓力比較集中,而cookie很好地分散了資源消耗,就這點來說,cookie是要優于session的;

4. 時效性

Cookie可以通過設置有效期使其較長時間內存在于客戶端,而session一般只有比較短的有效期(用戶主動銷毀session或關閉瀏覽器后引發超時);

5. 其他

Cookie的處理在開發中沒有session方便。而且cookie在客戶端是有數量和大小的限制的,而session的大小卻只以硬件為限制,能存儲的數據無疑大了太多。

Servlet/JSP中的Session

通過上述的講解,讀者應該對session有了一個大體的認識,但是具體到某種動態頁面技術,又是怎么實現session的呢?下面筆者將結合session的生命周期(lifecycle),從源代碼的層次來具體分析一下在servlet/jsp技術中,session是怎么實現的。代碼部分以tomcat6.0.20作為參考。

創建

在我問過的一些從事java web開發的人中,對于session的創建時機大都這么回答:當我請求某個頁面的時候,session就被創建了。這句話其實很含糊,因為要創建session請求的發送是必不可少的,但是無論何種請求都會創建session嗎?錯。我們來看一個例子。

眾所周知,jsp技術是servlet技術的反轉,在開發階段,我們看到的是jsp頁面,但真正到運行時階段,jsp頁面是會被“翻譯”為servlet類來執行的,例如我們有如下jsp頁面:

<%@ page language=“java" pageEncoding=“ISO-8859-1″ session=“true"%>

DOCTYPE HTML PUBLIC “-//W3C//DTD HTML 4.01 Transitional//EN">

<html>

<head>

<title>index.jsptitle>

head>

<body>

This is index.jsp page.

<br>

body>

html>

在我們初次請求該頁面后,在對應的work目錄可以找到該頁面對應的java類,考慮到篇幅的原因,在此只摘錄比較重要的一部分,有興趣的讀者可以親自試一下:

……

response.setContentType(“text/html;charset=ISO-8859-1″);

pageContext = _jspxFactory.getPageContext(this, request, response,

null, true, 8192, true);

_jspx_page_context = pageContext;

application = pageContext.getServletContext();

config = pageContext.getServletConfig();

session = pageContext.getSession();

out = pageContext.getOut();

_jspx_out = out;

out.write(“\r\n");

out.write(“\r\n");

out.write(“\r\n");

……

可以看到有一句顯式創建session的語句,它是怎么來的呢?我們再看一下對應的jsp頁面,在jsp的page指令中加入了session="true",意思是在該頁面啟用session,其實作為動態技術,這個參數是默認為true的,這很合理,在此顯示寫出來只是做一下強調。很顯然二者有著必然的聯系。筆者在jsp/servlet的翻譯器(org.apache.jasper.compiler)的源碼中找到了相關證據:

……

if (pageInfo.isSession())

out.printil(“session = pageContext.getSession();");

out.printil(“out = pageContext.getOut();");

out.printil(“_jspx_out = out;");

……

上面的代碼片段的意思是如果頁面中定義了session="true",就在生成的servlet源碼中加入session的獲取語句。這只能夠說明session創建的條件,顯然還不能說明session是如何創建的,本著逐本溯源的精神,我們繼續往下探索。

有過servlet開發經驗的應該記得我們是通過HttpServletRequest的getSession方法來獲取當前的session對象的:

public HttpSession getSession(boolean create);

public HttpSession getSession();

二者的區別只是無參的getSession將create默認設置為true而已。即:

public HttpSession getSession() {

return (getSession(true));

}

那么這個參數到底意味著什么呢?通過層層跟蹤,筆者終于理清了其中的脈絡,由于函數之間的關系比較復雜,如果想更詳細地了解內部機制,建議去獨立閱讀tomcat相關部分的源代碼。這里我將其中的大致流程敘述一下:

1. 用戶請求某jsp頁面,該頁面設置了session="true";

2. Servlet/jsp容器將其翻譯為servlet,并加載、執行該servlet;

3. Servlet/jsp容器在封裝HttpServletRequest對象時根據cookie或者url中是否存在jsessionid來決定是綁定當前的session到HttpRequest還是創建新的session對象(在請求解析階段發現并記錄jsessionid,在Request對象創建階段將session綁定);

4. 程序按需操作session,存取數據;

5. 如果是新創建的session,在結果響應時,容器會加入Set-cookie頭,以提醒瀏覽器要保持該會話(或者采用URL重寫方式將新的鏈接呈現給用戶)。

通過上面的敘述讀者應該了解了session是何時創建的,這里再從servlet這個層面總結一下:當用戶請求的servlet調用了getSession方法時,都會獲取session,至于是否創建新的session取決于當前request是否已綁定session。當客戶端在請求中加入了jsessionid標識而servlet容器根據此標識查找到了對應的session對象時,會將此session綁定到此次請求的request對象,客戶端請求中不帶jsessionid或者此jsessionid對應的session已過期失效時,session的綁定無法完成,此時必須創建新的session。同時發送Set-cookie頭通知客戶端開始保持新的會話。

保持

理解了session的創建,就很好理解會話是如何在客戶端和服務端之間保持的了。當首次創建了session后,客戶端會在后續的請求中將session的標識符帶到服務端,服務端程序只要在需要session的時候調用getSession,服務端就可以將對應的session綁定到當前請求,從而實現狀態的保持。當然這需要客戶端的支持,如果禁用了cookie而又不采用url重寫的話,session是無法保持的。

如果幾次請求之間有一個servlet未調用getSession(或者干脆請求一個靜態頁面)會不會使得會話中斷呢?這個不會發生的,因為客戶端只會將合法的cookie值傳送給服務端,至于服務端拿cookie做什么事它是不會關心的,當然也無法關心。Session建立之后,客戶端會一直將session的標識符傳送到服務器,無論請求的頁面是動態的、靜態的,甚至是一副圖片。

銷毀

此處談到的銷毀是指會話的廢棄,至于存儲會話信息的數據結構是回收被重用還是直接釋放內存我們并不關心。Session的銷毀有兩種情況:超時和手動銷毀。

由于HTTP協議的無狀態性,服務端無法得知一個session對象何時將再次被使用,可能用戶開啟了一個session之后再也沒有后續的訪問,而且session的保持是需要消耗一定的服務端開銷的,因此不可能一味地創建session而不去回收無用的session。這里就引入了一個超時機制。Tomcat中的超時在web.xml里做如下配置:

30

上述配置是指session在30分鐘沒有被再次使用就將其銷毀。Tomcat是怎么計算這個30分鐘的呢?原來在getSession之后,都要調用它的access方法,修改lastAccessedTime,在銷毀session的時候就是判斷當前時間和這個lastAccessedTime的差值。

手動銷毀是指直接調用其invalidate方法,此方法實際上是調用expire方法來手動將其設置為超時。

當用戶手動請求了session的銷毀時,客戶端是無法知道服務端的session已經被銷毀的,它依然會發送先前的session標識符到服務端。而此時如果再次請求了某個調用了getSession的servlet,服務端是無法根據先前的session標識符找到相應的session對象的,這是又要重新創建新的session,分配新的標識符,并告知服務端更新session標識符開始保持新的會話。

Session的數據結構

在servlet/jsp中,容器是用何種數據結構來存儲session相關的變量的呢?我們猜測一下,首先它必須被同步操作,因為在多線程環境下session是線程間共享的,而web服務器一般情況下都是多線程的(為了提高性能還會用到池技術);其次,這個數據結構必須容易操作,最好是傳統的鍵值對的存取方式。

那么我們先具體到單個session對象,它除了存儲自身的相關信息,比如id之外,tomcat的session還提供給程序員一個用以存儲其他信息的接口(在類org.apache.catalina.session. StandardSession里):

public void setAttribute(String name, Object value, boolean notify)

在這里可以追蹤到它到底使用了何種數據:

protected Map attributes = new ConcurrentHashMap();

這就很明確了,原來tomcat使用了一個ConcurrentHashMap對象存儲數據,這是java的concurrent包里的一個類。它剛好滿足了我們所猜測的兩點需求:同步與易操作性。

那么tomcat又是用什么數據結構來存儲所有的session對象呢?果然還是ConcurrentHashMap(在管理session的org.apache.catalina.session. ManagerBase類里):

protected Map sessions = new ConcurrentHashMap();

具體原因就不必多說了。至于其他web服務器的具體實現也應該考慮到這兩點。

Session Hijack

Session hijack即會話劫持是一種比較嚴重的安全威脅,也是一種廣泛存在的威脅,在session技術中,客戶端和服務端通過傳送session的標識符來維護會話,但這個標識符很容易就能被嗅探到,從而被其他人利用,這屬于一種中間人攻擊。

本部分通過一個實例來說明何為會話劫持,通過這個實例,讀者其實更能理解session的本質。

首先,我編寫了如下頁面:

<%@ page language=“java" pageEncoding=“ISO-8859-1″ session=“true"%>

DOCTYPE HTML PUBLIC “-//W3C//DTD HTML 4.01 Transitional//EN">

<html>

<head>

<title>index.jsptitle>

head>

<body>

This is index.jsp page.

<br>

<%

Object o = session.getAttribute(“counter");

if (o == null) {

session.setAttribute(“counter", 1);

} else {

Integer i = Integer.parseInt(o.toString());

session.setAttribute(“counter", i + 1);

}

out.println(session.getAttribute(“counter"));

%>

<a href=<%=response.encodeRedirectURL(“index.jsp")%>>indexa>

body>

html>

頁面的功能是在session中放置一個計數器,第一次訪問該頁面,這個計數器的值初始化為1,以后每一次訪問這個頁面計數器都加1。計數器的值會被打印到頁面。另外,為了比較簡單地模擬,筆者禁用了客戶端(采用firefox3.0)的cookie,轉而改用URL重寫方式,因為直接復制鏈接要比偽造cookie方便多了。

下面,打開firefox訪問該頁面,我們看到了計數器的值為1:

(圖三)

然后點擊index鏈接來刷新計數器,注意不要刷新當前頁,因為我們沒用采用cookie的方式,只能在url后面帶上jsessionid,而此時地址欄里的url是無法帶上jsessionid的。如圖四,我把計數器刷新到了20。

(圖四)

下面是最關鍵的,復制firefox地址欄里的地址(筆者看到的是http://localhost:8080/sessio

n/index.jsp;jsessionid=1380D9F60BCE9C30C3A7CBF59454D0A5),然后打開另一個瀏覽器,此處不必將其cookie禁用。這里我打開了蘋果的safari3瀏覽器,然后將地址粘貼到其地址欄里,回車后如下圖:

(圖五)

很奇怪吧,計數器直接到了21。這個例子筆者是在同一臺計算機上做的,不過即使換用兩臺來做,其結果也是一樣的。此時如果交替點擊兩個瀏覽器里的index鏈接你會發現他們其實操縱的是同一個計數器。其實不必驚訝,此處safari盜用了firefox和tomcat之間的維持會話的鑰匙,即jsessionid,這屬于session hijack的一種。在tomcat看來,safari交給了它一個jsessionid,由于HTTP協議的無狀態性,它無法得知這個jsessionid是從firefox那里“劫持”來的,它依然會去查找對應的session,并執行相關計算。而此時firefox也無法得知自己的保持會話已經被“劫持”。

結語

到這里,讀者應該對session有了更多的更深層次的了解,不過由于筆者的水平以及視野有限,文中也不乏表述欠妥之處,通篇更多地描述了在servlet/jsp中的session機制,但其他開發平臺的機制也都萬變不離其宗。只要認真思考,你會發現其實這里林林總總之間,總有一些因果關系存在。在軟件規模日益增大的背景下,我們更多的時候接觸到的是框架、組件,程序員的雙眼被蒙蔽了,在這些框架、組件不斷產生以及版本的不斷更新中,其實有很多相對不變的東西,那就是規范、協議、模式、算法等等,真正令一個人得到提高的還是那些底層的支撐技術。平時多多思考的話,你就能把類似的探索轉化為印證。做技術猶如解牛,知筋知骨方能游刃有余。

轉載請保留出處:shoru.cnblogs.com 晉哥哥的私房錢

0
0

請先登錄 提交中…

標簽:ASP.NET Session

文章列表