Bootstrap

Thrift搭建分布式微服务(三)

    第一篇 《连接配置》

    第二篇 《连接池》

第三篇 标准通信

一、TCP的连接是无状态的,怎样知道我的请求被服务端接受并且正确执行了呢?

    我的解决方案是使用自己定义的标准输入输出,Push操作和Delete操作都要返回Json的字符串,也就是说,每一个Thrift接口方法的输入参数和返回参数都是Json字符串。标准返回,Code表示状态码,Desc表示对执行结果的描述,如果Code表示服务端出错,Desc为错误信息。

 1     public class StandResponse<T>
 2     {
 3         /// <summary>
 4         /// 服务端成功调用:0
 5         /// 服务端业务异常:-1
 6         /// </summary>
 7         public string Code { get; set; }
 8 
 9         public string Desc { get; set; }
10 
11         public T Data { get; set; }
12     }

 

二、一般情况,服务端和客户端服务器应在同一个内网,所以可以不用进行接口调用的身份验证,只需保证服务端不被外网访问即可。当然也可以简单的,采用对客户端调用时间戳加密,并把时间戳和密文发到服务端后,用相同的加密算法对时间戳加密,对密文进行比较来验证。

//标准请求,包括客户端服务器的IP及主机名,如果客户端是Web服务器,用户请求的URL和用户请求IP也会一并带到服务器端,用于做验证或请求限制。
    public class StandRequest<T>
    {
        private string SIGN_KEY = ConfigurationManager.AppSettings["ThriftSignKey"] ?? "Thrift_Sign";

        public string HostName { get; set; }

        public string IP { get; set; }

        public string RequestUrl { get; set; }

        public string UserRequestIP { get; set; }

        public string TimeStamp { get; set; }

        public string Sign { get; set; }

        public T Data { get; set; }

        public string SignTimestamp(string timeStamp)
        {
            MD5 md5 = new MD5CryptoServiceProvider();
            byte[] array = md5.ComputeHash(Encoding.UTF8.GetBytes(timeStamp + SIGN_KEY));
            return Convert.ToBase64String(array);
        }

        //验证客户请求是否合法
        public bool IsValid()
        {
            MD5 md5 = new MD5CryptoServiceProvider();
            byte[] array = md5.ComputeHash(Encoding.UTF8.GetBytes(TimeStamp + SIGN_KEY));
            string sign = Convert.ToBase64String(array);
            return Sign.Equals(sign);
        }
    }

可通过在客户端和服务端,配置相同的ThriftSignKey来确保请求来自自己开发的客户端。其实在标准请求里还应该定义一个参数,就是当前请求的时间,这样到服务端时可以监控请求在传输时的时延。

三、使用了标准输入输出来传输数据,但每次让开发者去序列化标准输入输出进行这些重复性劳动也是不能忍的。以及让开发者去处理超时异常和服务端如果抛出未经处理的异常,客户端和服务端会失去连接,客户端抛TTransportException,每请求一次都让开发者来处理这些异常,让人难以接受。我再次想到使用反射,这样我可以在结果返回给开发者之前做一些处理。相当于拦截器,或者理解为面向切面的处理。

 1         private string serviceName;
 2 
 3         private TTransport transport;
 4 
 5         private T instance;        
 6 
 7         private bool disposed;
 8 
 9         public ThriftClient(string serviceName)
10         {
11             disposed = false;
12             this.serviceName = serviceName;
13             transport = ThriftFactory.BorrowInstance(serviceName);
14             TProtocol protocol = new TBinaryProtocol(transport);
15             //使用Invoker创建实例,InvokerEmitter抛异常“找不到公开实例构造方法”,可能是内部类获取的类名有点奇怪导致,例如“HelloWorldService.Client”。
16             //instance = (T)Invoker.CreateInstance<T>(protocol);     
17             instance = (T)Activator.CreateInstance(typeof(T),protocol);
18         }

初始化得到一个服务的Client,也就是instance。

    真正的访问服务端的方法被封装成一个使用标准输入输出的私有化方法,完成服务端和客户端通信的方法就只有Invoker.MethodInvoke一行,其余的都是对异常进行处理。

 1          private StandResponse<P> Invoke<Q, P>(string methodName, StandRequest<Q> request)
 2         {
 3             StandResponse<P> res = null;
 4             StandResponse<string> response = null;             
 5             try
 6             {
 7                 string result = (string)Invoker.MethodInvoke(instance, methodName, SerializeHelper.JsonSerialize2(request));
 8                 response = SerializeHelper.JsonDeserialize2<StandResponse<string>>(result);
 9             }
10             catch (IOException idEx)
11             {
12                 throw new ThriftException(string.Format("请求超时。\r请求服务:{0}\r请求方法:{1}\r请求数据:{2}", serviceName, methodName, SerializeHelper.JsonSerialize2(request)));
13             }
14             catch (TTransportException transEx)
15             {
16                 throw new ThriftException(string.Format("服务端未处理系统异常。\r请求服务:{0}\r请求方法:{1}\r请求数据:{2}", serviceName, methodName, SerializeHelper.JsonSerialize2(request)));
17             }
18             catch(Exception ex)
19             {
20                 throw ex;
21             }
22             if (response != null && response.Code == "-1")
23             {
24                 throw new ThriftException(string.Format("请求数据异常。\r请求服务:{0}\r请求方法:{1}\r请求数据:{2}", serviceName, methodName, SerializeHelper.JsonSerialize2(request)));
25             }
26             if (response != null && response.Code == "-2")
27             {
28                 throw new ThriftException(string.Format("服务端系统异常。\r请求服务:{0}\r请求方法:{1}\r请求数据:{2}", serviceName, methodName, SerializeHelper.JsonSerialize2(request)));
29             }                      
30             if (response != null)
31             {
32                 res = new StandResponse<P>()
33                 {
34                     Code = response.Code,
35                     Desc = response.Desc,
36                     Data = SerializeHelper.JsonDeserialize2<P>(response.Data)
37                 };
38             }
39             return res;
40         }

然后再对这个私有的方法进一步封装,返回开发者看得懂的数据结构。

        /// <summary>
        /// 无返回接口调用
        /// </summary>
        /// <typeparam name="Q"></typeparam>
        /// <typeparam name="P"></typeparam>
        /// <param name="methodName"></param>
        /// <param name="request"></param>
        public void Invoke<Q>(string methodName, Q request)
        {
            Invoke<Q, string>(methodName, BuildRequest<Q>(request));
        }

        /// <summary>
        /// 无参接口调用,服务端一定是有请求参数的,这里的无参是指Service方法无参
        /// </summary>
        /// <typeparam name="P"></typeparam>
        /// <param name="methodName"></param>
        /// <returns></returns>
        public P Invoke<P>(string methodName)
        {
            StandResponse<P> response = Invoke<string, P>(methodName, BuildRequest<string>(string.Empty));
            if (response != null)
            {
                return response.Data;
            }
            return default(P);
        }

        public P Invoke<Q, P>(string methodName, Q request)
        {
            StandResponse<P> response = Invoke<Q, P>(methodName, BuildRequest<Q>(request));
            if (response != null)
            {
                return response.Data;
            }
            return default(P);
        }

关于Tcp连接的归还释放,请自行下载源码来看。

    下一篇会用一个Demo来示范怎样使用Thrift.Utility来快速暴露接口,让客户端和服务端通信。

Thrift微服务代码下载Thrift.Utility

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。