tio-http-server 源码浅析(二)Http请求的处理HttpRequestHandler

前言

    在上一篇《tio-http-server 源码浅析(一)HttpRequestDecoder的实现》简单分析了HttpRequestDecoder的源码,并且已经得到了HttpRequest对象,那么下一步就是请求的处理了。服务器怎么知道你想要做什么呢?所以,接下来的内容会解析服务器业务上的处理实现。

补充

    这个图对于理解HTTP很有用处。看明白了这幅图,再去看源码就豁然开朗了。

    

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

源码解析

    了解或者使用过t-io的都知道服务端要实现ServerAioHandler接口。在HttpServerAioHandler handler 方法中有这么一段代码:

    @Override
    public void handler(Packet packet, ChannelContext channelContext) throws Exception {
        HttpRequest request = (HttpRequest)packet;
        //调用requestHandler的handler方法处理HttpRequest
        HttpResponse httpResponse = requestHandler.handler(request);
        if (httpResponse != null){
            //向客户端发送响应信息 HttpResponse
            Aio.send(channelContext,httpResponse);
        }else{
            Aio.remove(channelContext,"handler return null");
        }
    }

    那么代码中的requestHandler是何方神圣呢?他就是tio-http-server端的核心处理类:DefaultHttpRequestHandler,它实现了 HttpRequestHandler接口。

public interface HttpRequestHandler {

    /**
     * 处理请求
     * */
    HttpResponse handler(HttpRequest packet) throws Exception;
    /**
     * 响应404
     * */
    HttpResponse resp404(HttpRequest request,RequestLine requestLine);
    /**
     * 响应500
     * */
    HttpResponse resp500(HttpRequest request, RequestLine requestLine,Throwable throwable);
    /**
     * 清除静态资源缓存
     * */
    void clearStaticResCache(HttpRequest request);
}

    下面我们重点去看handler方法。其中有很多细节,比如创建Session,检查域名,黑名单,统计等不在阐述,这里只是解析主要流程。如下图:

173057_1E8n_3669181.png

    总体流程如上图,具体细节我们详细看代码。备注:上图步骤2之后有一个Interceptor的判断,如果实现了Interceptor并且有响应内容,那么直接返回。

    重置path部分代码如下:

        RequestLine requestLine = request.getRequestLine();
        String path = requestLine.getPath();
        if (StringUtils.isNotBlank(contextPath)) {
            if (StringUtils.startsWith(path, contextPath)) {
                path = StringUtils.substring(path, contextPathLength);
            }
        }
        if (StringUtils.isNotBlank(suffix)) {
            if (StringUtils.endsWith(path, suffix)) {
                path = StringUtils.substring(path, 0, path.length() - suffixLength);
            }
        }
        requestLine.setPath(path);

    这一段的作用是什么呢? 例如解析后的 path 为 /tio/test/hello.php,在初始化HttpConfig的时候已经给ContextPathsuffix赋值。例如: httpConfig = new HttpConfig(8080, null, "/tio", ".php");所以,如果请求路径中包含了contextPath,需要去掉,包含了suffix也需要去掉。最终得到的路径是/test/hello,就是路由真正想拿到的路径。所以,对于contextPathsuffix,我的理解就是起到URL装饰作用。比如把后缀改为 php,asp,aspx或者其他任意自定义的都可以。

    重置session部分就是判断客户端有没有传来cookie值,然后创建session或者从之前的sessionStore中根据sessionId拿到session。

 private void processCookieBeforeHandler(HttpRequest request,RequestLine requestLine) throws ExecutionException{
        if (!httpConfig.isUseSession()){
            return;
        }
        Cookie cookie = getSessionCookie(request,httpConfig);
        HttpSession httpSession = null;
        if (cookie == null){
            httpSession = createSession(request);
        }else{
            String sessionId = cookie.getValue();
            httpSession = (HttpSession)httpConfig.getSessionStore().get(sessionId);
            if (httpSession == null){
                httpSession = createSession(request);
            }
        }
        request.setHttpSession(httpSession);
    }

    接下来就是重头戏了,路由部分。请求静态资源大家都知道,解析出物理路径返回该资源即可。路由部分呢,就是让服务器知道客户端的请求路径到底匹配到哪个方法上。例如 /test/hello ,所以这里我们暂停一下,转战到Routes代码中。

/**
  * 构造方法传入要扫描的包
  * */ 
public Routes(String[] scanPackages){
    //其他逻辑
}

    Routes 实在服务器启动的时候初始化的。它的原理就是通过扫描构造方法中传入的包的类和方法,根据类和方法中是否带有 RequestPath 注解来实现路由表的初始化和相应信息的保存。扫描包的工具是io.github.lukehutch.fastclasspathscanner.FastClasspathScanner。比如我们新创建了一个Controller,代码如下:

@RequestPath(value = "/test")
public class TestController {

    @RequestPath(value = "/hello")
    public HttpResponse test(HttpRequest request){
        return Resps.json(request,"hello,this is tio-http-server test");
    }
}

    然后初始化Routes 的时候就会扫描到这个类,然后匹配到@RequestPath。详细代码如下:

    //扫描带有RequestPath注解的类。
            fastClasspathScanner.matchClassesWithAnnotation(RequestPath.class, new ClassAnnotationMatchProcessor() {
                //匹配到之后执行的方法
                @Override
                public void processMatch(Class<?> classWithAnnotation) {
                    //这里classWithAnnotation就是 TestController
                    try {
                        //通过反射新创建一个TestController的实例
                        Object bean = classWithAnnotation.newInstance();
                        //获取到注解对象
                        RequestPath mapping = classWithAnnotation.getAnnotation(RequestPath.class);
                        //拿到 value 值 即:/test
                        String beanPath = mapping.value();
                        //暂时忽略,源代码中注释掉了,返回beanPath,即无处理
                        beanPath = formateBeanPath(beanPath);
                        //判断有没有重复定义的路由
                        Object obj = pathBeanMap.get(beanPath);
                        if (obj != null) {
                            errorStr.append("mapping[" + beanPath + "] already exists in class [" + obj.getClass().getName() + "]\r\n\r\n");
                        } else {
                            //将 /test 和 TestController 对象的实例存储到 TreeMap中
                            pathBeanMap.put(beanPath, bean);
                            //存储 /test class  以便后续使用
                            pathClassMap.put(beanPath, classWithAnnotation);
                            //存储 class /test 以便后续使用
                            classPathMap.put(classWithAnnotation, beanPath);
                        }
                    } catch (Throwable e) {

                    }
                }
            });
            //扫描带有RequestPath注解的方法
            fastClasspathScanner.matchClassesWithMethodAnnotation(RequestPath.class, new MethodAnnotationMatchProcessor() {
                @Override
                public void processMatch(Class<?> matchingClass, Executable matchingMethodOrConstructor) {
                    //匹配到方法之后获取注解
                    RequestPath mapping = matchingMethodOrConstructor.getAnnotation(RequestPath.class);
                    //得到方法名
                    String methodName = matchingMethodOrConstructor.getName();
                    //得到路由path
                    String methodPath = mapping.value();
                    methodPath = formateMethodPath(methodPath);
                    String beanPath = classPathMap.get(matchingClass);

                    if (StringUtils.isBlank(beanPath)) {
                        errorStr.append("方法有注解,但类没注解, method:" + methodName + ", class:" + matchingClass + "\r\n\r\n");
                        return;
                    }

                    Object bean = pathBeanMap.get(beanPath);
                    String completeMethodPath = methodPath;
                    //组合路径  /test + /hello
                    if (beanPath != null) {
                        completeMethodPath = beanPath + methodPath;
                    }

                    //获取方法的参数类型数组
                    Class<?>[] parameterTypes = matchingMethodOrConstructor.getParameterTypes();
                    Method method;
                    try {
                        //这儿有点小看不懂,应该就是获取方法的参数信息
                        method = matchingClass.getMethod(methodName, parameterTypes);
                        Paranamer paranamer = new BytecodeReadingParanamer();
                        String[] parameterNames = paranamer.lookupParameterNames(method, false);
                        Method checkMethod = pathMethodMap.get(completeMethodPath);
                        if (checkMethod != null) {
                            errorStr.append("mapping[" + completeMethodPath + "] already exists in method [" + checkMethod.getDeclaringClass() + "#" + checkMethod.getName() + "]\r\n\r\n");
                            return;
                        }
                        //存储  /test/hello method
                        pathMethodMap.put(completeMethodPath, method);
                        //用于打印
                        pathMethodstrMap.put(completeMethodPath, methodToStr(method, parameterNames));
                        //存储  method  参数
                        methodParamnameMap.put(method, parameterNames);
                        //存储 method  对应 TestController实例
                        methodBeanMap.put(method, bean);
                    } catch (Throwable e) {

                    }
                }
            });

    下图为运行之后Routes 对象的截图:

182059_PlZW_3669181.png

    所以现在我们知道DefaultHttpRequestHandler中的method是如何获取到的了。

 Method method = null;
 if (routes != null) {
    method = routes.getMethodByPath(path, request);
 }

     但是只拿到 method 还不够,我们还要给method的参数赋值。

//拿到method的参数 :request
String[] paramnames = routes.methodParamnameMap.get(method);
//拿到参数类型 :httpRequest
Class<?>[] parameterTypes = method.getParameterTypes();

Object bean = routes.methodBeanMap.get(method);
Object obj = null;
//从HttpRequest对象中拿到客户端传过来的参数
Map<String, Object[]> params = request.getParams();
if (parameterTypes == null || parameterTypes.length == 0) {
   //如果方法没有参数,直接执行 method 方法
   obj = method.invoke(bean);

    如果method有参数:就需要判断参数类型,然后给参数赋值了。

 Object[] paramValues = new Object[parameterTypes.length];
    int i = 0;
    for (Class<?> paramType : parameterTypes) {
    //判断参数的类型,给参数赋值
    if (paramType.isAssignableFrom(HttpRequest.class)) {
      paramValues[i] = request;
    } else if (paramType == HttpSession.class) {
      paramValues[i] = httpSession;
    } else if (paramType.isAssignableFrom(HttpConfig.class)) {
      paramValues[i] = httpConfig;
    } else if (paramType.isAssignableFrom(ChannelContext.class)) {
       paramValues[i] = request.getChannelContext();

    上面一段比较简单,也就是参数中最简单粗暴的方法,把httpRequest放到方法参数中去,然后由方法内部自己去处理。另外就是比如方法我们直接写具体的参数名。

 @RequestPath(value = "/post")
    public HttpResponse testPost(String name,int age){
        return Resps.json(null,"test post");
    }

    如上述代码中的  name age,那么从 request对象中拿到相应的值在赋值给 nameage即可。

 Object[] value = params.get(paramnames[i]);
 if (value != null && value.length > 0) {
   if (paramType.isArray()) {
       paramValues[i] = Convert.convert(paramType, value);
     } else {
        paramValues[i] = Convert.convert(paramType, value[0]);
      }
   }

    另外比较复杂的一种就是参数中有我们自己定义的model类型或者其他类型的处理。由于我也没彻底整明白,不在阐述。

    最后执行   obj = method.invoke(bean, paramValues);  这个 obj 有可能是 HttpResponse或者其他对象,都没有问题。处理一下返回即可。具体的处理代码参考 util.Resps 类。其实主要是返回正确的头部信息。例如 json格式的Content-Type 为 application/json;charset=utf-8

    静态资源的处理不在阐述。源码中使用了 Freemarker。

    最后,运行一个示例,没啥问题。

    184344_iaq7_3669181.png

学以致用

    下面做一个小例子,帮助大家理解一下。很简单,实现允许方法的控制。

    第一步,在RequestPath注解中增加 allow属性。

public @interface RequestPath {
    String allow() default "GET,POST";
    String value() default "";
}

    第二步,修改post方法的注解。

   @RequestPath(value = "/post",allow = "POST")
    public HttpResponse testPost(String name,int age){
        return Resps.json(null,"test post");
    }

    第三步,扫描包的时候将path和对应allow保存

  //存放是否允许的方法
  pathAllowMethodMap.put(completeMethodPath,allow);

   第四步,增加请求判断是否允许方法

 /**
     * 检查请求方法是否允许
     * */
    public  boolean checkAllowMethod(String path,HttpRequest request) {
        String allows = pathAllowMethodMap.get(path);
        common.Method requestMethod = request.getRequestLine().getMethod();
        String[] allowMethods = StringUtils.split(allows, ",");
        return ArrayUtil.contains(allowMethods,requestMethod.name());
    }

    第五步,如果不允许,返回405

  if (method != null) {
    if (!routes.checkAllowMethod(path,request)){
         return Resps.resp405(request);
    }
  }

     第六步,测试。

193655_JNOj_3669181.png

    第七步,大功告成。上述小实战看起来功能很小,但是代码已经贯穿了从服务器启动到请求的处理和返回的过程,就酱紫,你看懂了吗?

总结

    很多细节没有讲到,例如路由中 path 带变量的处理。 /test/hello/{userid} ,HttpResponse 的构建等。正如开篇中的那个HTTP 报文格式图,看得懂图就对代码的理解会更加快速一些。还有一些拦截器的使用等等功能很多。麻雀虽小,五脏俱全,如果你厌烦了庞大的web服务器,不妨试试自己写的小型http服务,多有成就感啊,哈哈。今天就到这里啦,拜拜~~

 

转载于:https://my.oschina.net/panzi1/blog/1612574

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值