add middleware feature for ghttp.Server

This commit is contained in:
John 2019-08-03 17:14:54 +08:00
parent 0460a53f53
commit 878305fee3
11 changed files with 143 additions and 206 deletions

View File

@ -10,6 +10,7 @@ package glist
import (
"container/list"
"encoding/json"
"github.com/gogf/gf/internal/rwmutex"
)
@ -371,3 +372,8 @@ func (l *List) IteratorDesc(f func(e *Element) bool) {
}
l.mu.RUnlock()
}
// MarshalJSON implements the interface MarshalJSON for json.Marshal.
func (l *List) MarshalJSON() ([]byte, error) {
return json.Marshal(l.FrontAll())
}

View File

@ -21,27 +21,28 @@ import (
// 请求对象
type Request struct {
*http.Request
Id int // 请求ID(当前Server对象唯一)
Server *Server // 请求关联的服务器对象
Cookie *Cookie // 与当前请求绑定的Cookie对象(并发安全)
Session *Session // 与当前请求绑定的Session对象(并发安全)
Response *Response // 对应请求的返回数据操作对象
Router *Router // 匹配到的路由对象
EnterTime int64 // 请求进入时间(微秒)
LeaveTime int64 // 请求完成时间(微秒)
MiddleWare *MiddleWare // 中间件功能调用对象
handlers []*handlerParsedItem // 请求执行服务函数列表(包含中间件、路由函数、钩子函数)
handlerIndex int // 当前执行函数的索引号
parsedGet bool // GET参数是否已经解析
parsedPost bool // POST参数是否已经解析
queryVars map[string][]string // GET参数
routerVars map[string][]string // 路由解析参数
exit bool // 是否退出当前请求流程执行
params map[string]interface{} // 开发者自定义参数(请求流程中有效)
parsedHost string // 解析过后不带端口号的服务器域名名称
clientIp string // 解析过后的客户端IP地址
rawContent []byte // 客户端提交的原始参数
isFileRequest bool // 是否为静态文件请求(非服务请求,当静态文件存在时,优先级会被服务请求高,被识别为文件请求)
Id int // 请求ID(当前Server对象唯一)
Server *Server // 请求关联的服务器对象
Cookie *Cookie // 与当前请求绑定的Cookie对象(并发安全)
Session *Session // 与当前请求绑定的Session对象(并发安全)
Response *Response // 对应请求的返回数据操作对象
Router *Router // 匹配到的路由对象
EnterTime int64 // 请求进入时间(微秒)
LeaveTime int64 // 请求完成时间(微秒)
MiddleWare *MiddleWare // 中间件功能调用对象
handlers []*handlerParsedItem // 请求执行服务函数列表(包含中间件、路由函数、钩子函数)
handlerIndex int // 当前执行函数的索引号
hasHookHandler bool // 是否注册有钩子函数(用于请求时提高钩子函数功能启用判断效率)
parsedGet bool // GET参数是否已经解析
parsedPost bool // POST参数是否已经解析
queryVars map[string][]string // GET参数
routerVars map[string][]string // 路由解析参数
exit bool // 是否退出当前请求流程执行
params map[string]interface{} // 开发者自定义参数(请求流程中有效)
parsedHost string // 解析过后不带端口号的服务器域名名称
clientIp string // 解析过后的客户端IP地址
rawContent []byte // 客户端提交的原始参数
isFileRequest bool // 是否为静态文件请求(非服务请求,当静态文件存在时,优先级会被服务请求高,被识别为文件请求)
}
// 创建一个Request对象

View File

@ -0,0 +1,23 @@
// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package ghttp
// 获得当前请求,指定类型的的钩子函数列表
func (r *Request) getHookHandlers(hook string) []*handlerParsedItem {
if !r.hasHookHandler {
return nil
}
parsedItems := make([]*handlerParsedItem, 0, 4)
for _, v := range r.handlers {
if v.handler.hookName != hook {
continue
}
item := v
parsedItems = append(parsedItems, item)
}
return parsedItems
}

View File

@ -16,12 +16,13 @@ type MiddleWare struct {
// 执行下一个请求流程处理函数
func (m *MiddleWare) Next() {
item := (*handlerParsedItem)(nil)
for ; m.request.handlerIndex < len(m.request.handlers); m.request.handlerIndex++ {
for {
// 是否停止请求执行
if m.request.IsExited() {
if m.request.IsExited() || m.request.handlerIndex >= len(m.request.handlers) {
return
}
item = m.request.handlers[m.request.handlerIndex]
m.request.handlerIndex++
// 通过中间件模式不执行钩子函数
if item.handler.itemType == gHANDLER_TYPE_HOOK {
continue
@ -38,12 +39,12 @@ func (m *MiddleWare) Next() {
niceCallFunc(func() {
c.MethodByName("Init").Call([]reflect.Value{reflect.ValueOf(m.request)})
})
if m.request.IsExited() {
if !m.request.IsExited() {
niceCallFunc(func() {
c.MethodByName(item.handler.ctrlInfo.name).Call(nil)
})
}
if m.request.IsExited() {
if !m.request.IsExited() {
niceCallFunc(func() {
c.MethodByName("Shut").Call(nil)
})
@ -54,12 +55,12 @@ func (m *MiddleWare) Next() {
item.handler.initFunc(m.request)
})
}
if m.request.IsExited() {
if !m.request.IsExited() {
niceCallFunc(func() {
item.handler.itemFunc(m.request)
})
}
if m.request.IsExited() && item.handler.shutFunc != nil {
if !m.request.IsExited() && item.handler.shutFunc != nil {
niceCallFunc(func() {
item.handler.shutFunc(m.request)
})

View File

@ -42,10 +42,7 @@ type (
closeChan chan struct{} // 用以关闭事件通知的通道
servedCount *gtype.Int // 已经服务的请求数(4-8字节不考虑溢出情况)同时作为请求ID
serveTree map[string]interface{} // 所有注册的服务回调函数(路由表,树型结构,哈希表+链表优先级匹配)
hooksTree map[string]interface{} // 所有注册的事件回调函数(路由表,树型结构,哈希表+链表优先级匹配)
middlewareTree map[string]interface{} // 所有注册的中间件(路由表,树型结构,哈希表+链表优先级匹配)
serveCache *gcache.Cache // 服务注册路由内存缓存
hooksCache *gcache.Cache // 事件回调路由内存缓存
routesMap map[string][]registeredRouteItem // 已经注册的路由及对应的注册方法文件地址(用以路由重复注册判断)
statusHandlerMap map[string]HandlerFunc // 不同状态码下的注册处理方法(例如404状态时的处理方法)
sessions *gcache.Cache // Session内存缓存
@ -65,11 +62,12 @@ type (
// 服务函数注册信息
handlerItem struct {
itemName string // 注册的函数名称信息(用于路由信息打印)
itemType int // 注册函数类型(对象/函数/控制器/中间件)
itemType int // 注册函数类型(对象/函数/控制器/中间件/钩子函数)
itemFunc HandlerFunc // 函数内存地址(与以上两个参数二选一)
initFunc HandlerFunc // 初始化请求回调函数(对象注册方式下有效)
shutFunc HandlerFunc // 完成请求回调函数(对象注册方式下有效)
ctrlInfo *handlerController // 控制器服务函数反射信息
hookName string // 钩子类型名称(注册函数类型为钩子函数下有效)
router *Router // 注册时绑定的路由对象
}
@ -208,9 +206,7 @@ func GetServer(name ...interface{}) *Server {
serverCount: gtype.NewInt(),
statusHandlerMap: make(map[string]HandlerFunc),
serveTree: make(map[string]interface{}),
hooksTree: make(map[string]interface{}),
serveCache: gcache.New(),
hooksCache: gcache.New(),
routesMap: make(map[string][]registeredRouteItem),
sessions: gcache.New(),
servedCount: gtype.NewInt(),

View File

@ -97,7 +97,7 @@ func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) {
// 动态服务检索
if !request.isFileRequest || isStaticDir {
request.handlers = s.getHandlersWithCache(request)
request.handlers, request.hasHookHandler = s.getHandlersWithCache(request)
}
// 判断最终对该请求提供的服务方式
@ -114,7 +114,7 @@ func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) {
// 静态服务
s.serveFile(request, staticFile)
} else {
if request.handlers != nil {
if len(request.handlers) > 0 {
// 动态服务
request.MiddleWare.Next()
} else {

View File

@ -7,12 +7,13 @@
package ghttp
import (
"container/list"
"errors"
"fmt"
"runtime"
"strings"
"github.com/gogf/gf/container/glist"
"github.com/gogf/gf/os/glog"
"github.com/gogf/gf/text/gregex"
"github.com/gogf/gf/text/gstr"
@ -101,16 +102,8 @@ func (s *Server) setHandler(pattern string, handler *handlerItem, hook ...string
if _, ok := s.serveTree[domain]; !ok {
s.serveTree[domain] = make(map[string]interface{})
}
// 用于遍历的指针
p := s.serveTree[domain]
if len(hookName) > 0 {
if _, ok := p.(map[string]interface{})[hookName]; !ok {
p.(map[string]interface{})[hookName] = make(map[string]interface{})
}
p = p.(map[string]interface{})[hookName]
}
// 当前节点的规则链表
lists := make([]*list.List, 0)
lists := make([]*glist.List, 0)
array := ([]string)(nil)
if strings.EqualFold("/", uri) {
array = []string{"/"}
@ -118,7 +111,8 @@ func (s *Server) setHandler(pattern string, handler *handlerItem, hook ...string
array = strings.Split(uri[1:], "/")
}
// 键名"*fuzz"代表模糊匹配节点,其下会有一个链表;
// 键名"*list"代表链表,叶子节点和模糊匹配节点都有该属性;
// 键名"*list"代表链表,叶子节点和模糊匹配节点都有该属性,优先级越高越排前;
p := s.serveTree[domain]
for k, v := range array {
if len(v) == 0 {
continue
@ -129,10 +123,10 @@ func (s *Server) setHandler(pattern string, handler *handlerItem, hook ...string
// 由于是模糊规则,因此这里会有一个*list用以将后续的路由规则加进来
// 检索会从叶子节点的链表往根节点按照优先级进行检索
if v, ok := p.(map[string]interface{})["*list"]; !ok {
p.(map[string]interface{})["*list"] = list.New()
lists = append(lists, p.(map[string]interface{})["*list"].(*list.List))
p.(map[string]interface{})["*list"] = glist.New()
lists = append(lists, p.(map[string]interface{})["*list"].(*glist.List))
} else {
lists = append(lists, v.(*list.List))
lists = append(lists, v.(*glist.List))
}
}
// 属性层级数据写入
@ -143,10 +137,10 @@ func (s *Server) setHandler(pattern string, handler *handlerItem, hook ...string
// 到达叶子节点往list中增加匹配规则(条件 v != "*fuzz" 是因为模糊节点的话在前面已经添加了*list链表)
if k == len(array)-1 && v != "*fuzz" {
if v, ok := p.(map[string]interface{})["*list"]; !ok {
p.(map[string]interface{})["*list"] = list.New()
lists = append(lists, p.(map[string]interface{})["*list"].(*list.List))
p.(map[string]interface{})["*list"] = glist.New()
lists = append(lists, p.(map[string]interface{})["*list"].(*glist.List))
} else {
lists = append(lists, v.(*list.List))
lists = append(lists, v.(*glist.List))
}
}
}
@ -160,15 +154,14 @@ func (s *Server) setHandler(pattern string, handler *handlerItem, hook ...string
switch handler.itemType {
// 判断是否已存在相同的路由注册项,如果是普通路由注册则进行替换
case gHANDLER_TYPE_HANDLER, gHANDLER_TYPE_OBJECT, gHANDLER_TYPE_CONTROLLER:
if handler.itemType == gHANDLER_TYPE_HANDLER {
if strings.EqualFold(handler.router.Domain, item.router.Domain) &&
strings.EqualFold(handler.router.Method, item.router.Method) &&
strings.EqualFold(handler.router.Uri, item.router.Uri) {
e.Value = handler
pushed = true
break
}
if strings.EqualFold(handler.router.Domain, item.router.Domain) &&
strings.EqualFold(handler.router.Method, item.router.Method) &&
strings.EqualFold(handler.router.Uri, item.router.Uri) {
e.Value = handler
pushed = true
break
}
fallthrough
// 否则,那么判断优先级,决定插入顺序
default:
@ -183,8 +176,7 @@ func (s *Server) setHandler(pattern string, handler *handlerItem, hook ...string
l.PushBack(handler)
}
}
// gutil.Dump(s.serveTree)
// gutil.Dump(s.hooksTree)
//gutil.Dump(s.serveTree)
if _, ok := s.routesMap[regkey]; !ok {
s.routesMap[regkey] = make([]registeredRouteItem, 0)
}

View File

@ -7,12 +7,8 @@
package ghttp
import (
"container/list"
"reflect"
"runtime"
"strings"
"github.com/gogf/gf/text/gregex"
)
// 绑定指定的hook回调函数, pattern参数同BindHandler支持命名路由hook参数的值由ghttp server设定参数不区分大小写
@ -21,6 +17,7 @@ func (s *Server) BindHookHandler(pattern string, hook string, handler HandlerFun
itemType: gHANDLER_TYPE_HOOK,
itemName: runtime.FuncForPC(reflect.ValueOf(handler).Pointer()).Name(),
itemFunc: handler,
hookName: hook,
}, hook)
}
@ -34,11 +31,7 @@ func (s *Server) BindHookHandlerByMap(pattern string, hookmap map[string]Handler
// 事件回调处理,内部使用了缓存处理.
// 并按照指定hook回调函数的优先级及注册顺序进行调用
func (s *Server) callHookHandler(hook string, r *Request) {
// 如果没有hook注册那么不用执行后续逻辑
if len(s.hooksTree) == 0 {
return
}
hookItems := s.getHookHandlerWithCache(hook, r)
hookItems := r.getHookHandlers(hook)
if len(hookItems) > 0 {
// 备份原有的router变量
oldRouterVars := r.routerVars
@ -86,111 +79,6 @@ func (s *Server) niceCallHookHandler(f HandlerFunc, r *Request) (err interface{}
return
}
// 查询请求处理方法, 带缓存机制按照Host、Method、Path进行缓存.
func (s *Server) getHookHandlerWithCache(hook string, r *Request) []*handlerParsedItem {
cacheItems := ([]*handlerParsedItem)(nil)
cacheKey := s.handlerKey(hook, r.Method, r.URL.Path, r.GetHost())
if v := s.hooksCache.Get(cacheKey); v == nil {
cacheItems = s.searchHookHandler(r.Method, r.URL.Path, r.GetHost(), hook)
if cacheItems != nil {
s.hooksCache.Set(cacheKey, cacheItems, s.config.RouterCacheExpire*1000)
}
} else {
cacheItems = v.([]*handlerParsedItem)
}
return cacheItems
}
// 事件方法检索
func (s *Server) searchHookHandler(method, path, domain, hook string) []*handlerParsedItem {
if len(path) == 0 {
return nil
}
// 遍历检索的域名列表
domains := []string{gDEFAULT_DOMAIN}
if !strings.EqualFold(gDEFAULT_DOMAIN, domain) {
domains = append(domains, domain)
}
// URL.Path层级拆分
array := ([]string)(nil)
if strings.EqualFold("/", path) {
array = []string{"/"}
} else {
array = strings.Split(path[1:], "/")
}
parsedItems := make([]*handlerParsedItem, 0, 8)
for _, domain := range domains {
p, ok := s.hooksTree[domain]
if !ok {
continue
}
p, ok = p.(map[string]interface{})[hook]
if !ok {
continue
}
// 多层链表(每个节点都有一个*list链表)的目的是当叶子节点未有任何规则匹配时,让父级模糊匹配规则继续处理
lists := make([]*list.List, 0)
for k, v := range array {
if _, ok := p.(map[string]interface{})["*list"]; ok {
lists = append(lists, p.(map[string]interface{})["*list"].(*list.List))
}
if _, ok := p.(map[string]interface{})[v]; ok {
p = p.(map[string]interface{})[v]
if k == len(array)-1 {
if _, ok := p.(map[string]interface{})["*list"]; ok {
lists = append(lists, p.(map[string]interface{})["*list"].(*list.List))
break
}
}
} else {
if _, ok := p.(map[string]interface{})["*fuzz"]; ok {
p = p.(map[string]interface{})["*fuzz"]
}
}
// 如果是叶子节点,同时判断当前层级的"*fuzz"键名,解决例如:/user/*action 匹配 /user 的规则
if k == len(array)-1 {
if _, ok := p.(map[string]interface{})["*fuzz"]; ok {
p = p.(map[string]interface{})["*fuzz"]
}
if _, ok := p.(map[string]interface{})["*list"]; ok {
lists = append(lists, p.(map[string]interface{})["*list"].(*list.List))
}
}
}
// 多层链表遍历检索,从数组末尾的链表开始遍历,末尾的深度高优先级也高
for i := len(lists) - 1; i >= 0; i-- {
for e := lists[i].Front(); e != nil; e = e.Next() {
handler := e.Value.(*handlerItem)
// 动态匹配规则带有gDEFAULT_METHOD的情况不会像静态规则那样直接解析为所有的HTTP METHOD存储
if strings.EqualFold(handler.router.Method, gDEFAULT_METHOD) || strings.EqualFold(handler.router.Method, method) {
// 注意当不带任何动态路由规则时len(match) == 1
if match, err := gregex.MatchString(handler.router.RegRule, path); err == nil && len(match) > 0 {
parsedItem := &handlerParsedItem{handler, nil}
// 如果需要query匹配那么需要重新正则解析URL
if len(handler.router.RegNames) > 0 {
if len(match) > len(handler.router.RegNames) {
parsedItem.values = make(map[string][]string)
// 如果存在存在同名路由参数名称,那么执行数组追加
for i, name := range handler.router.RegNames {
if _, ok := parsedItem.values[name]; ok {
parsedItem.values[name] = append(parsedItem.values[name], match[i+1])
} else {
parsedItem.values[name] = []string{match[i+1]}
}
}
}
}
parsedItems = append(parsedItems, parsedItem)
}
}
}
}
return parsedItems
}
return nil
}
// 生成hook key如果是hook key那么使用'%'符号分隔
func (s *Server) handlerKey(hook, method, path, domain string) string {
return hook + "%" + s.serveHandlerKey(method, path, domain)

View File

@ -7,35 +7,40 @@
package ghttp
import (
"container/list"
"encoding/json"
"fmt"
"strings"
"github.com/gogf/gf/container/glist"
"github.com/gogf/gf/text/gregex"
)
// 缓存数据项
type handlerCacheItem struct {
parsedItems []*handlerParsedItem
hasHook bool
}
// 查询请求处理方法.
// 内部带锁机制可以并发读但是不能并发写并且有缓存机制按照Host、Method、Path进行缓存.
func (s *Server) getHandlersWithCache(r *Request) []*handlerParsedItem {
func (s *Server) getHandlersWithCache(r *Request) (parsedItems []*handlerParsedItem, hasHook bool) {
cacheKey := s.serveHandlerKey(r.Method, r.URL.Path, r.GetHost())
cacheItems := ([]*handlerParsedItem)(nil)
if v := s.serveCache.Get(cacheKey); v == nil {
cacheItems = s.searchHandlers(r.Method, r.URL.Path, r.GetHost())
if cacheItems != nil {
s.serveCache.Set(cacheKey, cacheItems, s.config.RouterCacheExpire*1000)
parsedItems, hasHook = s.searchHandlers(r.Method, r.URL.Path, r.GetHost())
if parsedItems != nil {
s.serveCache.Set(cacheKey, &handlerCacheItem{parsedItems, hasHook}, s.config.RouterCacheExpire*1000)
}
} else {
cacheItems = v.([]*handlerParsedItem)
item := v.(*handlerCacheItem)
return item.parsedItems, item.hasHook
}
if len(cacheItems) == 0 {
return nil
}
return cacheItems
return
}
// 路由注册方法检索,返回所有该路由的注册函数,构造成数组返回
func (s *Server) searchHandlers(method, path, domain string) []*handlerParsedItem {
func (s *Server) searchHandlers(method, path, domain string) (parsedItems []*handlerParsedItem, hasHook bool) {
if len(path) == 0 {
return nil
return nil, false
}
// 遍历检索的域名列表,优先遍历默认域名
domains := []string{gDEFAULT_DOMAIN}
@ -49,24 +54,25 @@ func (s *Server) searchHandlers(method, path, domain string) []*handlerParsedIte
} else {
array = strings.Split(path[1:], "/")
}
parsedItems := make([]*handlerParsedItem, 0, 16)
parsedItems = make([]*handlerParsedItem, 0, 16)
isServeHandlerAdded := false
for _, domain := range domains {
p, ok := s.serveTree[domain]
if !ok {
continue
}
//gutil.Dump(p)
// 多层链表(每个节点都有一个*list链表)的目的是当叶子节点未有任何规则匹配时,让父级模糊匹配规则继续处理
lists := make([]*list.List, 0, 16)
lists := make([]*glist.List, 0, 16)
for k, v := range array {
if _, ok := p.(map[string]interface{})["*list"]; ok {
lists = append(lists, p.(map[string]interface{})["*list"].(*list.List))
lists = append(lists, p.(map[string]interface{})["*list"].(*glist.List))
}
if _, ok := p.(map[string]interface{})[v]; ok {
p = p.(map[string]interface{})[v]
if k == len(array)-1 {
if _, ok := p.(map[string]interface{})["*list"]; ok {
lists = append(lists, p.(map[string]interface{})["*list"].(*list.List))
lists = append(lists, p.(map[string]interface{})["*list"].(*glist.List))
break
}
}
@ -81,7 +87,7 @@ func (s *Server) searchHandlers(method, path, domain string) []*handlerParsedIte
p = p.(map[string]interface{})["*fuzz"]
}
if _, ok := p.(map[string]interface{})["*list"]; ok {
lists = append(lists, p.(map[string]interface{})["*list"].(*list.List))
lists = append(lists, p.(map[string]interface{})["*list"].(*glist.List))
}
}
}
@ -124,12 +130,38 @@ func (s *Server) searchHandlers(method, path, domain string) []*handlerParsedIte
isServeHandlerAdded = true
}
}
if item.itemType == gHANDLER_TYPE_HOOK {
hasHook = true
}
}
}
}
}
}
return parsedItems
return
}
// MarshalJSON implements the interface MarshalJSON for json.Marshal.
func (item *handlerItem) MarshalJSON() ([]byte, error) {
if item.hookName != "" {
return json.Marshal(
fmt.Sprintf(
`%s %s:%s (%s)`,
item.router.Uri,
item.router.Domain,
item.router.Method,
item.hookName,
),
)
}
return json.Marshal(
fmt.Sprintf(
`%s %s:%s`,
item.router.Uri,
item.router.Domain,
item.router.Method,
),
)
}
// 生成回调方法查询的Key

View File

@ -23,18 +23,18 @@ func Test_Router_Basic(t *testing.T) {
s.BindHandler("/:name", func(r *ghttp.Request) {
r.Response.Write("/:name")
})
//s.BindHandler("/:name/update", func(r *ghttp.Request) {
// r.Response.Write(r.Get("name"))
//})
//s.BindHandler("/:name/:action", func(r *ghttp.Request) {
// r.Response.Write(r.Get("action"))
//})
s.BindHandler("/:name/update", func(r *ghttp.Request) {
r.Response.Write(r.Get("name"))
})
s.BindHandler("/:name/:action", func(r *ghttp.Request) {
r.Response.Write(r.Get("action"))
})
s.BindHandler("/:name/*any", func(r *ghttp.Request) {
r.Response.Write(r.Get("any"))
})
//s.BindHandler("/user/list/{field}.html", func(r *ghttp.Request) {
// r.Response.Write(r.Get("field"))
//})
s.BindHandler("/user/list/{field}.html", func(r *ghttp.Request) {
r.Response.Write(r.Get("field"))
})
s.SetPort(p)
s.SetDumpRouteMap(false)
s.Start()

View File

@ -24,8 +24,6 @@ func Test_Router_Hook_Basic(t *testing.T) {
"AfterServe": func(r *ghttp.Request) { r.Response.Write("2") },
"BeforeOutput": func(r *ghttp.Request) { r.Response.Write("3") },
"AfterOutput": func(r *ghttp.Request) { r.Response.Write("4") },
"BeforeClose": func(r *ghttp.Request) { r.Response.Write("5") },
"AfterClose": func(r *ghttp.Request) { r.Response.Write("6") },
})
s.BindHandler("/test/test", func(r *ghttp.Request) {
r.Response.Write("test")