[原]读Google Data API源代码一:从创建一个日历(Calendar)开始

读大段的源代码是件苦差事,往往读着读着就不厌其烦了,很难把代码一行一行的搞明白,Google Data API这段代码我也读了好几遍了,但是一直是一知半解,没有搞清楚过细节,今天打算边读边把细节写下来,一方面可以强迫自已把细节搞明白,另一方面也可以为准备读这段源代码的朋友们一个提供一个参考,因本人水平有限,对代码的理解肯定存在偏差,以及对GoogleAPI技术的理解和猜想有误差,希望大家能不吝指正,谢谢。

 

Google文档如下描述如何新增一个Google Calendar:

To create a new calendar, first instantiate a CalendarEntry object and set the appropriate values. Then call the CalendarService.Insert method, specifying the owncalendars feed.

要创建一个新的日历,首先实例化一个CalendarEntry对象,设置相应的属性值,然后指定接收数据的接口(Feed)为owncalendars,通过使用CalendarService.Insert()方法,将该日历添加到指定帐户的日历列表中。

方法签名如下:

public TEntry Insert<TEntry>(Uri feedUri,TEntry entry)
    
where TEntry : AtomEntry

关于AtomEntry的细节问题我们先不关心,现在只需要知道它其实就是一段XML数据的.NET表示即可。

现在我们来看Insert方法内部到底做了什么。

在下面的代码中,我会省略掉无关紧要的代码,以突出我们真正关心的重点。

this.versionInfo.ImprintVersion(newEntry);

即使是接口或者契约,也有更新或是升级的时候,为你的接口或者契约加上版本号是一个不错的选择,在某些时候接口或者契约要保持向前兼容,版本号的作用可不容忽视。

接下来的代码很关键

Stream returnStream = EntrySend(feedUri, newEntry, GDataRequestType.Insert, data);

真希望所有的复杂问题都能通过这样一行代码解决,只不过,这句代码实际上什么也没有说,真正的戏在EntrySend方法中

来看一下EntrySend到底是做什么的吧

Inserts an AtomBase entry against a Uri

使用一个Uri插入一个AtomBase类型的记录(Entry)。

方法签名如下:

internal virtual Stream EntrySend(Uri feedUri, AtomBase baseEntry, GDataRequestType type, AsyncSendData data)

这里要说明的是feedUri是你指定的管理Calendar的资源Uri(当前具体值为"http://www.google.com/calendar/feeds/default/owncalendars/full"),baseEntry是你要插入的Calendar,type是你指定现在要做的操作是Insert类型,data此时为null。

接下来的代码为:

IGDataRequest request = this.RequestFactory.CreateRequest(type,feedUri);

看起来是要创建一个Request对象,我们跟进一步看看到是这个CreateRequest做了什么。

一般情况下,读比较成熟的代码时看到有Factory之类的变量,我是不太指望能看出来它是一个什么类型,这次还是没有意外,Service的RequestFactory不出意外的是一个接口:

        public IGDataRequestFactory RequestFactory
        {
            
get { return this.factory; }
            
set { this.factory = value; OnRequestFactoryChanged(); }
        }

让我去看这个接口的实例是怎么来的我可没有太多的耐心,这篇文章也不是研究面向对象的设计模式的,最直接的方法还是调试一下,看看运行时它是一个什么具体类型:

 

这里的RequestFactory的类型为Google.GData.Client.GDataGAuthRequestFactory。

进入这个类型去看看吧。

方法内部比较简单,直接new了一个GDataAuthRequest对象便返回了:

        public override IGDataRequest CreateRequest(GDataRequestType type, Uri uriTarget)
        {
            
return new GDataGAuthRequest(type, uriTarget, this); 
        }

GDataGAuthRequest的构造方法也不复杂,只不过它似乎把主要工作放在了它的基类中了:

        internal GDataGAuthRequest(GDataRequestType type, Uri uriTarget, GDataGAuthRequestFactory factory)  
            : 
base(type, uriTarget, factory as GDataRequestFactory)
        {
            
this.factory = factory; 
        }

 再进入其基类的构造方法进去看看吧:

        internal GDataRequest(GDataRequestType type, Uri uriTarget, GDataRequestFactory factory)
        {
            
this.type = type;
            
this.targetUri = uriTarget;
            
this.factory = factory; 
            
this.useGZip = this.factory.UseGZip; 
        }

还是比较简单,只是初始了一些变量而已。

接下来就是为这个request对象设置身份验证信息(Credentials)

request.Credentials = this.Credentials;

接下来将request的ETag报头设置为将要创建的CalendarEntry的ETag报头:

            ISupportsEtag eTarget = request as ISupportsEtag;
            ISupportsEtag eSource 
= baseEntry as ISupportsEtag;
            
if (eTarget != null && eSource != null)
            {
                eTarget.Etag 
= eSource.Etag;
            }

再下来又是一句看似平常最崎岖的语句:

Stream outputStream = request.GetRequestStream();

不过跟踪下来发现这次的判断失误了,不过我很乐意这种失误,写程序嘛,能简单就不要复杂,嘿嘿...

        public override Stream GetRequestStream()
        {
            
this.requestCopy = new MemoryStream(); 
            
return this.requestCopy; 
        }

再接下来这句真的简单了:

baseEntry.SaveToXml(outputStream);

进入SaveToXml方法内部:

        public void SaveToXml(Stream stream)
        {
            Tracing.Assert(stream 
!= null"stream should not be null");
            
if (stream == null)
            {
                
throw new ArgumentNullException("stream"); 
            }
            XmlTextWriter writer 
= new XmlTextWriter(stream,System.Text.Encoding.UTF8);
            writer.Formatting 
= Formatting.Indented;
            writer.WriteStartDocument();
            
            SaveToXml(writer);
            writer.Flush();
        }

 再接下来的事情就比较复杂了,以前看了很多次,但每次都没有看仔细,这次打算刨根问底,看个究竟。

request.Execute();
        public override void Execute()
        {
            Execute(
1); 
        }

 这里的的参数1,貌似是指执行Execute方法的次数,执行了指定次数如果没有成功,本次添加工作则告失败。

Execute方法内部第一行比较重要的语句:

CopyRequestData();

CopyRequestData方法内部:

            if (this.requestCopy != null)
            {
                EnsureWebRequest();
                ...
            }

如果你在看这篇文章时没有中断的话,那应该记得起这个requestCopy对象,其实就是上面我们用到的保存CalendarEntry为Xml形式的MemoryStream。

MemoryStream不为空的情况下,进入EnsureWebRequest方法,E文太差,到现在为至,我还没有搞明白这个方法到底什么功能,进入看看吧。

            base.EnsureWebRequest();

又是一句,再进入看看

        protected virtual void EnsureWebRequest()
        {

            
if (this.webRequest == null && this.targetUri != null)
            {
                
this.webRequest = WebRequest.Create(this.targetUri);

                
this.webResponse = null
                
if (this.webRequest == null)
                {
                    
throw new OutOfMemoryException("Could not create a new Webrequest"); 
                }
                HttpWebRequest web 
= this.webRequest as HttpWebRequest;
                ...
         }

这个方法第一行的webRequest之前没有看到过,猜它应该为空,看了下果然为空,但是targetUri不为空,查了下值为:"http://www.google.com/calendar/feeds/default/owncalendars/full",回想一下,我们似乎在前面使用那个RequestFactory.CreateRequst时使用过Uri为这个值,应该就是那里设置的吧。接着往下看,开始创建webRequest了,这里的WebRequest.Create方法调用的是System.Net.WebRequest。webResponse之前没有见过,应该为null,不为null也让它为null了。再次验证webRequest是否为空,为空了就不能往下进行了。在这种应用场景下,webRequest实例应该是HttpWebRequest类型的。这里值的一题的是在使用Silverlight中的WebRequest时,默认是不支持PUT和DELETE方法的,在建立RESTful的Web Services时难以真正达到RESTful效果,应该使用WebRequestCreator.ClientHttp.Create方法来创建WebRequest,详情请参见拙作《Silverlight3 支持所有HTTP方法 》,再接下来的几行代码稍好懂点:

                if (web != null)
                {
                    
switch (this.type) 
                    {
                        
case GDataRequestType.Delete:
                            web.Method 
= HttpMethods.Delete;
                            
break;
                        
case GDataRequestType.Update:
                            web.Method 
= HttpMethods.Put;
                            
break;
                        
case GDataRequestType.Batch:
                        
case GDataRequestType.Insert:
                            web.Method 
= HttpMethods.Post;
                            
break;
                        
case GDataRequestType.Query:
                            web.Method 
= HttpMethods.Get;
                            
break;
    
                    }
                    
if (this.useGZip == true)
                        web.Headers.Add(
"Accept-Encoding""gzip");

同样的type方法是使用RequestFactory.CreateRequest时指定的,这里web.Method为"POST"。

接下来的内容是设置标签的,我在创建CalenarEntry的实例时只指定的了一个Title,没有设置任何其它内容,所以没有执行到,等到研究Update或是Delete方法时再看详情吧

                    if (this.Etag != null)
                    {
                            web.Headers.Add(GDataRequestFactory.EtagHeader, 
this.Etag);
                            
switch (this.type) 
                            {
                                
case GDataRequestType.Update:
                                
case GDataRequestType.Delete:
                                    
if (Utilities.IsWeakETag(this== false)
                                    {
                                        web.Headers.Add(GDataRequestFactory.IfMatch, 
this.Etag);
                                    }
                                    
break;
                                
case GDataRequestType.Query:
                                    web.Headers.Add(GDataRequestFactory.IfNoneMatch, 
this.Etag);
                                    
break;
                            }
                    }

接下来设置If-Modified-Since Http报头的值,本来以为这是个无所谓的报头,结果查了下《RESTful Web Services》中的资料,发现这个报头的重要程序居然是“非常高”,不管3721,抄到这里来,以后查阅方便吧。

If-Modified-Since请求报头是支持条件HTTP GET的关键。客户端把通过前一次对同一URI的请求得到的Last-Modified响应报头的值,放在本次请求的If-Modified-Since报头里。服务器可以根据比较If-Modified-Since的值与资源的最后更新日期,判断自客户端上次请求以来资源有没有发生过改变。若资源已发生改变,也就是说条件If-Modified-Since成立,那么服务器将把新的实体主体返回给客户端。若资源没有发生改变,也就是说条件If-Modified-Since不成立,那么服务器将返回响应代码304("Not Modified")且不发送实体主体。换句话说,当条件If-Modified-Since不成立时,条件HTTP GET成功。

由于Last-Modified只能精确到1秒,所以有时仅靠If-Modified-Since会出现错误的结果,这就是为什么我们引入ETag与If-None-Match的原因。

原来ETag并不是简单啊,不过到目前为止,我还是不想过多的关注HTTP过于细节的东西,否则这篇文章可能就写不下去了。

接下来是设置HttpWebRequest对象的一些其它属性:

                    web.ContentType = this.ContentType;
                    web.UserAgent 
= this.factory.UserAgent;
                    web.KeepAlive 
= this.factory.KeepAlive;

这里ContentType被设置为:"application/atom+xml; charset=UTF-8",这是Google的默认设置

UserAgent报头一般标识Web浏览器的类型,在使用代码访问服务器端时,这个报头往往被用来标识访问服务器的客户端库或特定的客户端程序,这里的值是:"G-think8848/GDataGAuthRequestFactory-CS-Version=1.4.0.2"。

KeepAlive在MSDN中解释如下:将此属性设置为 true 以发送带有 Keep-alive 值的 Connection HTTP 标头。应用程序使用 KeepAlive 指示持久连接的首选项。当 KeepAlive 属性为 true 时,应用程序与支持它们的服务器建立持久连接。

在Baidu里找到的比较有用的话是:“如果一个访问会对同一个服务器请求多次那(设置Keep-Alive报头)是有用的。不然就是没用的” 。我在这里的猜想为设置这个值是为了操作失败后重试,差点忘了说明,运行时,此处的值为"true"。

再接下来的代码比较直观,就不作解释了

                    if (this.factory.hasCustomHeaders == true)
                    {
                        
foreach (string s in this.factory.CustomHeaders)
                        {
                            
this.Request.Headers.Add(s); 
                        }
                    }
                    
if (this.factory.Timeout != -1)
                    {
                        web.Timeout 
= this.factory.Timeout;
                    }

                    
if (this.Slug != null)
                    {
                        
this.Request.Headers.Add(GDataRequestFactory.SlugHeader + "" + this.Slug);
                    }
                    
if (this.factory.Proxy != null)
                    {
                        
this.Request.Proxy = this.factory.Proxy; 
                    }

最后,设置HttpWebRequest的身份验证信息:

this.webRequest.Credentials = this.Credentials.NetworkCredential; 

this.Credentials我们前面在RequestFactory.CreateRequest时设置过。

/*ok,今天先到这儿吧,背都疼了,每天都想好好的把Google Data API的客户端代码好好的读读,看看人家是怎么调用Google服务的,一方面是学习一下Google的API,另一方面也是想通过客户端代码,揣摩下Google的服务是如何设计的,但是一直好像都读不下去,读了前面的忘后面的,读了后面的忘前面的,这次下定决心,边读边写下来,小样,让我再偷懒!不信治不了我!哎哟,不行,肚子也饿,背也疼,还是上床先。*/

今天接着来。接下来为HttpWebRequest添加自定义报头"GData-Version":

 

http.Headers.Add(GDataGAuthRequestFactory.GDataVersion, v.ProtocolMajor.ToString() + "." + v.ProtocolMinor.ToString());

这里GData-Version实际值是"1.0"。

接下来设置HttpWebRequest.AllowAutoRedirect为false,AllowAutoRedirect作用为设置客户端是否自动跟随服务器重定向。

http.AllowAutoRedirect = false;

再接下来就是如果设置了HTTP方法重载,则添加相应的报头,这里没有使用HTTP方法重载,则这段代码没有执行到。

                if (this.factory.MethodOverride == true && 
                    http.Method 
!= HttpMethods.Get &&
                    http.Method 
!= HttpMethods.Post)
                {
                    
string currentMethod = http.Method;

                    http.Headers.Add(GoogleAuthentication.Override, currentMethod);
                    http.Method 
= HttpMethods.Post;

                    
if (currentMethod == HttpMethods.Delete)
                    {
                        http.ContentLength 
= 0;

                        Stream req 
= http.GetRequestStream(); 
                        req.Close(); 
                    }
                }

接下来为HttpWebRequest设置了ContentLength报头:ContentLength值为之前保存CalendarEntry字节码的MemoryStream的长度。

this.Request.ContentLength = this.requestCopy.Length;

接下来获取将要发送CalendarEntry至服务器端的流:

            this.requestStream = this.webRequest.GetRequestStream();
            
return this.requestStream; 

这里的requestStream类型为"System.Net.ConnectStream"。

接下来的事情比较简单了,将CalendarEntry写入到这个流中去:

                    while ((numBytes = this.requestCopy.Read(bytes, 0, size)) > 0)
                    {
                        req.Write(bytes, 
0, numBytes);
                        ...
                    }

完成之后不要忘记将requestStream关闭:

req.Close();

接上来的事情就是要获取服务器的响应了。出乎意料的是,居然没有得到201响应(201 被创建),而是得到了302响应(有些地方解释为"暂时转移",有些地方解释为"找到",这里我觉得这里理解为"暂时转移"可能比较确切),经过一番重置工作:

        protected virtual void Reset()
        {
            
this.requestStream = null;
            
this.webRequest = null;
            
if (this.webResponse != null)
            {
                
this.webResponse.Close();
            }
            
this.webResponse = null
        }

资源地址被修改为了"http://www.google.com/calendar/feeds/default/owncalendars/full?gsessionid=ry1EUBtcAV-3Wjmm0ipn-w",看这个参数的样子,应该和Session或是权限有关吧,毕竟Google的Calendar是针对用户的。使用这个资源地址,再次提交。

                CopyRequestData();
                
base.Execute();

这下子没有再出什么意外,很快得到了201响应,得到了正确的Response。

简单的总结一下:添加CalendarEntry时,将CalendarEntry序列化成一段XML保存在一个MemoryStream中,然后又将这个MemoryStream中的字节码以及HTTP报头通过一个ConnectStream写入到服务器端。ConnectStream应该是一个被保护的类型,在MSDN找不到关于该类的说明。从这点上可以看出,Google Calendar服务添加日历的方法应该是纯RESTful的,猜想其定义应该如下所示,(可能猜的比较离谱,请大家多指教啊):

[WebInvoke(UriTemplate = "/calendar/feeds/default/owncalendars/full?gsessionid={gsessionid}", Method = "POST", RequestFormat = "WebMessageFormat.Xml")]
CalendarEntry CreateCalednar(CalendarEntry calendar,
string gsessionid);

纯粹猜想:使用gsessionid可能是google的一个策略,前面曾提到,Calendar是针对用户,而不是所有人都添加到一个Calendar库中,如果单纯使用"http://www.google.com/calendar/feeds/default/owncalendars/full"这个资源地址,那到底是提交给哪个用户呢,所以Google给了一个302响应,为你提供了一个gsessionid,使用了这个gsessionid,你就可以为指定的用户创建日历了,在获取日历列表时同样有一个302响应,并给出了另一个资源地址:"http://www.google.com/calendar/feeds/default/owncalendars/full?gsessionid=AAztATr8Orfm4o1T59YOyQ"。根据RESTful Web Services的原则,获取Calendar列表的服务方法的UriTemplate应该和创建Calendar的方法差不多吧,只不过WebInvoke特性应该换为WebGet特性了。

到这里,创建一个日历的任务差不多完成一大半了,剩下的代码应该就是从服务器响应的HttpWebResponse中提取XML数据,然后再通过Atom解析器把这些XML反序列化成CalendarEntry对象了,具体的代码,如果以后有时间再读了,今天先到这儿吧。

原文地址:https://www.cnblogs.com/think8848/p/1618172.html