微服务的低风险演变

继续来深入探讨!在之前的文章(第一部分)中,我们为本篇文章建立了一个上下文环境(以便于讨论)。一个基本原则是,当微服务被引入到现有架构中时,不能也不应该破坏当前的请求流程(request flows)。“单体应用(monolish)”程序依然能带来很多商业价值(因此仍将在新的时代被使用,编者注),我们只能在迭代和扩展时,尽可能地减少其负面影响,这过程中就有一个经常被忽略的事实:当我们开始探索如何从单体应用过渡到微服务时,会遇到一些我们不愿意碰到的难题,但显然我们不能视而不见。如果你还没读过这段内容,我建议你再回去看看第一部分。同时也可以参考什么时候不要做微服务[0]。

关注推特上的(@christianposta)或访问http://blog.christianposta.com,以获取最新更新和讨论。

在此前的第一部分,想解决的问题有:

  • 如何可以有效可靠地生成微服务。以及如何建立一个持续交付的系统。
  • 如何能够对服务和单体应用等对象进行测试。
  • 如何在新的微服务中能安全地引入任何变更,包含灰度上线、金丝雀测试等等
  • 如何将流量路由到新的服务中去,以保证启用/终止任何新的特性或更改都不会出现问题
  • 如何面对许多棘手的数据集成挑战

一、技术层面

以下这些技术在我们的实践过程中将具备一定的指导作用:

• 开发人员服务框架(Spring Boot [1],WildFly [2],WildFly Swarm [3])

• API设计(APICur.io [4])

• 数据框架(Spring Boot Teiid [5],Debezium.io [6])

• 集成工具(Apache Camel [7])

• Service Mesh(Istio Service Mesh [8])

• 数据库迁移工具(Liquibase [9])

• 灰度上线/特性标记框架(FF4J [10])

• 部署/CI-CD平台(Kubernetes [11]/OpenShift [12])

• Kubernetes开发工具(Fabric8.io [13])

• 测试工具(Arquillian [14],Pact [15]/Arquillian Algeron [16],Hoverfly [17],Spring-Boot Test [18],RestAssured [19],Arquillian Cube [20])

我使用的是http://developers.redhat.com上的TicketMonster教程,显示从单体应用到微服务的演变,如果感兴趣的话可以关注,你还可以在github上找到相关的代码和文档(文档还在编写中):https://github.com/ticket-monster-msa/monolith

让我们一步步地读完第一部分 [21],具体来看看每一步应该怎么实施。中间还会引入上一部分中出现的一些注意事项,并在当前背景下再讨论一遍。

二、了解单体式应用

回顾下注意事项:

  • 单体式应用(代码和数据库模型)很难变更
  • 变更需要整体重新部署和团队间高度的协调
  • 需要进行大量测试来做回归分析
  • 需要一个全自动的部署方式

可以的话,尽可能为单体应用安排大量的测试,哪怕不是一直有效。随着演变的开始,无论是添加新功能还是替换现有功能,我们都需要清楚了解任何更改可能产生的影响。Michael Feathers 在他《重构遗留代码》[22]的书中,将“遗留代码(legacy code)”定义为没有被测试所覆盖的代码。像JUnit和Arquillian这样的工具就很能帮到大忙。使用Arquillian,可以任意选择远程方法调用的接口的颗粒大小(fine grain or coarse grain),然后打包应用程序,不过仍需要用适当的模拟等方式,来运行打算被测试的一部分程序。例如,在单体应用(TicketMonster)中,我们可以定义一个微部署(micro-deployment),用来将原有的数据库替换为内存数据库,并预加载一些样例数据。Arquillian适用于Spring Boot应用、Java EE等。在本例中,我们将测试一个Java EE的单体架构:

public static WebArchive deployment() {
  return ShrinkWrap
    .create(WebArchive.class, "test.war")
    .addPackage(Resources.class.getPackage())
    .addAsResource("META-INF/test-persistence.xml", "META-INF/persistence.xml")
    .addAsResource("import.sql")
    .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml")
    // Deploy our test datasource
    .addAsWebInfResource("test-ds.xml");
}

更有意思的是,嵌入在运行环境中的测试可以用来验证内部工作的所有组件。例如,在上面的一个测试中,我们可以将BookingService注入到测试中,并直接运行:

@RunWith(Arquillian.class)
public class BookingServiceTest {

    @Deployment
    public static WebArchive deployment() {
        return RESTDeployment.deployment();
    }

    @Inject
    private BookingService bookingService;

    @Inject
    private ShowService showService;

    @Test
    @InSequence(1)
    public void testCreateBookings() {
        BookingRequest br = createBookingRequest(1l, 0, new int[]{4, 1}, new int[]{1,1}, new int[]{3,1});
        bookingService.createBooking(br);

        BookingRequest br2 = createBookingRequest(2l, 1, new int[]{6,1}, new int[]{8,2}, new int[]{10,2});
        bookingService.createBooking(br2);

        BookingRequest br3 = createBookingRequest(3l, 0, new int[]{4,1}, new int[]{2,1});
        bookingService.createBooking(br3);
    }

完整的示例请参阅TicketMonster单体应用模块[23]中的BookingServiceTest。

测试的问题解决了,那么部署呢?

Kubernetes已成为容器化服务或应用程序的实际部署平台。Kubernetes处理诸如健康度检查、扩展、重启、负载平衡等事项。对于Java开发人员来说,像fabric8-maven-plugin[24]这样的工具甚至都可以用来自动构建容器或docker镜像,并生成任意部署资源文件。OpenShift[25]是Red Hat的Kubernetes的产品化版本,其中增加了开发人员的功能,包括CI/CD pipelines等。

无论是微服务、单体应用还是其他平台(比如能够处理持续的工作负载,即数据库等),Kubernetes/OpenShift都是一个适用于应用程序/服务的部署平台。通过Arquillian,容器和OpenShift pipelines,可以持续地将变更引入生产环境。顺便来看一下openshift.io[26],它将开发经验与自动CI/CD pipelines、SCM集成、Eclipse Che[27]开发人员工作区、库扫描等结合在一起。

目前,生产负载指向单体应用。如果我们翻到它的主页,我们会看到这样的内容:

接下来,让我们开始做一些改变…

三、提取用户界面UI

回顾下注意事项:

  • 一开始,先不要变更单体式应用;只需将UI复制粘贴到单独的组件即可
  • 在UI和单体式应用间需要有一个合适的远程API—但并非所有情况下都需要
  • 增加一个安全层
  • 需要用某种方法以受控的方式将流量路由或分离到新的UI或单体式应用,以支持灰度上线(dark launch)/金丝雀测试(canary)/滚动发布(rolling release)[28]

如果我们看下TicketMonster UI v1 [29]代码,就会发现它非常简单。静态HTML/JS/CSS组件已经被移到它自己的Web服务器,还被打包到一个容器中。通过这种方式,我们可以在单体应用之外对它进行单独部署,并独立更改或更新版本。这个UI项目仍然需要与单体应用对话来执行它的功能,所以应该是公开一个REST接口,让UI可以与之交互。对于一些单体应用来说,这说起来容易做起来难。如果你想从遗留代码中打包出来一个不错的REST API,又遇到了挑战,我强烈推荐你看看Apache Camel,尤其是它的REST DSL。

比较有意思的是,实际上单体应用并没有被改变。它的代码没有变动,同时新UI也部署完成。如果查看Kubernetes,我们会看到两个单独的部署对象和两个单独的pod:一个用于单体架构,另一个用于UI。

即使tm-ui-v1用户界面部署完了,也没有任何流量进入这个新的TicketMonster UI组件。为了简单起见,即使这个部署并没有承载生产流量,而是ticket-monster这个单体应用在承担所有流量,我们仍然可以把它当作一个简单的灰度上线。相关的UI端口仍旧可以访问:

接下来,用kubectl cli 工具从本地端口转发到特定的pod(端口80上的tm-ui-v1-3105082891-gh31x),并将其映射到本地端口8080。现在,如果导航到http://localhost:8080,应该得到一个新版本UI(注意突出显示的文本部分,表明这是一个不同的UI,但它直接指向单体应用)

如果我们这个新版本还算满意,就可以开始将流量引入进来。为此,我们将使用Istio service mesh [30]。Istio是用于管理由入口点和服务代理组成的网格控制层(control plane)。我已经写了一些关于像Envoy这样的数据层[31]以及service mesh[32]的文章。我个人强烈建议看看Istio的全部功能。接下来的几段内容,我们会围绕整个项目的全过程来依次展开讨论Istio的各项功能。如果控制层和数据层之间的区分让你困惑,请查看Matt Klein[33]撰写的博客。

我们将从使用Istio Ingress Controller[34]开始。该组件允许使用Kubernetes Ingress规范来控制流量进入Kubernetes集群。一旦安装了Istio,我们可以这样创建一个入口资源,将流量指向Ticket Monster UI的Kubernetes服务,tm-ui:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: tm-gateway
  annotations:
    kubernetes.io/ingress.class: "istio"
spec:
  backend:
    serviceName: tm-ui
    servicePort: 80

一旦有了入口,就可以开始应用Istio路由规则[35]。例如,有一个规则,“任何时候有人试图与在Kubernetes中运行的tm-ui服务对话,将它们指向服务的第一版本v1”:

apiVersion: config.istio.io/v1alpha2
kind: RouteRule
metadata:
  name: tm-ui-default
spec:
  destination:
    name: tm-ui
  precedence: 1
  route:
  - labels:
      version: v1

如此,我们能够更好地控制进入集群甚至深入集群内部的流量。在这个步骤的最后,我们会将所有的流量都转到tm-ui-v1部署。

四、从单体架构移除UI

回顾下注意事项

  • 从单体式应用中移除UI组件
  • 需要对单体式应用进行最小的变更(弃用/删除/禁用UI)
  • 不停机的前提下,再次使用受控的路由/整流方法来引入这种变更

这一步相当直接,通过删除静态UI组件来更新单体应用(删除的部分已经转移到了tm-ui-v1部署)。既然应用程序已经被释放成为一个单体应用的服务,以供UI,API或者其他一些程序调用,那么也可以对这个部署进行一些API层级的更改。而如果想对API进行一些更改,就需要部署一个新版本的UI。此处我们部署了backend-v1服务以及一个新的UI tm-ui-v2,可以利用后端服务中的这个新API。

来看看在Kubernetes集群中的部署情况:

此时,ticket-monster和tm-ui-v1正接收实时流量。backend-v1和指向它的UI–tm-ui-v2则没有流量负载。需要注意的一点是,backend-v1部署与ticket-monster部署共享数据库,但各自有略微不同的外向API(outward facing API)。

现在,新的backend-v1和tm-ui-v2组件已经部署到生产环境中。现在是时候把注意力放在一个简单而又重要的事实上:生产环境部署发生了改变,但是它们还没有发布。在turblabs.io [36]一些优秀的博客更详细地阐述了这一点[37]。现在,我们有机会部署一个非正式的灰度发布。也许我们希望这个部署慢慢来,首先面向内部用户,或者先对某个特定区域内,特定设备的部分用户进行部署等等。

既然已经有了Istio,接下来看看它能做些什么。我们只想为内部用户做一个灰度发布。我们可以用各种方式来识别内部用户,诸如headers、IP等等,在本例中,如果HTTP header带有 x-dark-launch: v2 这样的文本内容,则该请求将会被路由到新的backend-v1和tm -ui-v2服务中。以下是istio路由规则的样子:

apiVersion: config.istio.io/v1alpha2
kind: RouteRule
metadata:
  name: tm-ui-v2-dark-launch
spec:
  destination:
    name: tm-ui
  precedence: 10
  match:
    request:
      headers:
        x-dark-launch:
          exact: "v2"
  route:
  - labels:
      version: v2

任意用户身份登录主页时,应该可以看到当前的部署(即指向ticket-monster单体应用的tm-ui-v1):

现在,如果改变浏览器中的消息头(例如使用Firefox的修改消息头工具或其他类似工具),我们应该被路由到已灰度上线的服务(指向backend-v1的tm-ui-v2):

然后点击“开始”开始修改消息头并刷新页面:

现在,我们已经被重定向到服务的灰度发布版本。由此,可以通过做一个金丝雀发布(这里也许引1%的实时流量到新部署),来向客户群发布,同时,如果没有负面效果的话,那么就缓慢增加流量负载(5%、10%、50%等)。以下是Istio路由规则的一个例子,其将v2流量以1%进行金丝雀发布:

 apiVersion: config.istio.io/v1alpha2
kind: RouteRule
metadata:
  name: tm-ui-v2-1pct-canary
spec:
  destination:
    name: tm-ui
  precedence: 20
  route:
  - labels:
      version: v1
    weight: 99
  - labels:
      version: v2
    weight: 1

能“看到”或“观察”这个版本的影响是至关重要的,稍后我们会进一步讨论。另外请注意,这种金丝雀发布方式目前正在架构外围完成,但是也可以通过istio控制内部服务间通讯/交互时采用金丝雀的方式。在接下来的几个步骤中,我们将开始看到。

五、引入新服务

回顾下注意事项

  • 我们要关注被抽取的服务的API设计或边界
  • 可能需要重写单体式应用中的某些内容
  • 在确定API后,将为该服务实施一个简单的脚手架或者place holder
  • 新的Orders服务将拥有自己的数据库
  • 新Orders服务目前不会承担任何流量

在这一步中,我们开始设计我们所设想的新订单服务的API,在做一些领域驱动设计练习时,我们常常需要确定一些边界(boundaries),新的API应该更多的与这种边界相一致。这里可以使用API建模工具来设计API,部署一个虚拟化的实施,并且随服务消费者的需求变化 一起迭代,而不是一开始花费大量的精力去构建,最后又发现需要不断修改。

在TicketMonster重构时,需要在单体应用中保留一个上文所说的API,以便在最初的服务拆分时尽可能轻松并且降低风险。无论是哪种情况,有两个给力的工具可以帮到我们:一个是网页式的API设计器,apicur.io[38],一个是测试/ API虚拟化工具,Hoverfly[39]。Hoverlfy是模拟API或捕获现有API流量的好工具,可以用来模拟mock端点。

如果我们正在构建一个新的API,或在使用领域驱动设计方法后,想看看API什么样,可以使用apicur.io工具建立一个Swagger/Open API的规范。

在TicketMonster这个例子中,我们通过在代理模式下启动hoverfly,并使用hoverfly捕获从应用程序到后端服务的流量。我们可以在浏览器设置中设置HTTP代理,从而通过hoverfly发送所有流量。这将把每个请求/响应对(request/response pair)的仿真存储在JSON文件中。这样我们就可以在Mock里使用这些请求/响应对,或者更进一步,用它们开始编写测试,以规范具体的实现代码中的一些行为。

对于所关注的请求或响应对(response pairs),我们可以生成一个JSON架构并用于测试中,参见https://jsonschema.net/#/editor

例如,结合使用Rest Assured和Hoverfly,可以调用hoverfly模拟,并确定该响应符合我们预期的JSON架构:

@Test
public void testRestEventsSimulation(){
    get("/rest/events").then().assertThat().body(matchesJsonSchemaInClasspath("json-schema/rest-events.json"));
}

在新的订单服务中,可以查看HoverflyTest.java [40]测试。有关测试Java微服务的更多信息,请查阅Manning这本给力的书,《测试Java微服务》[41],我的一些同事Alex Soto Bueno[42]、Jason Porter[43]和Andy Gumbrecht[44]也参与了这本书的撰写。

由于这篇博文已经很长了,我决定将最后的部分单独写成本主题的第三部分,其中将涉及在单体应用和微服务之间管理数据、服务消费的契约测试(consumer contract testing), 功能发布控制( feature flagging),甚至更复杂的istio路由等内容。本系列的第四部分将展示一个包含上述内容的实操Demo,使用负载仿真测试(load simulation tests)和故障注入(fault injections)。欢迎访问我的网站 [45]和关注我的Twitter [46]。

原文链接:http://blog.christianposta.com/microservices/low-risk-monolith-to-microservice-evolution-part-ii/

参考地址:

[0] http://blog.christianposta.com/microservices/when-not-to-do-microservices/

[1] https://projects.spring.io/spring-boot/

[2] http://wildfly.org/

[3] http://wildfly-swarm.io/

[4] http://www.apicur.io/

[5] https://github.com/teiid/teiid-spring-boot

[6] http://debezium.io/

[7] http://camel.apache.org/

[8] https://istio.io/

[9] http://www.liquibase.org/

[10] https://ff4j.org/

[11] https://kubernetes.io/

[12] https://www.openshift.org/

[13] https://fabric8.io/

[14] http://arquillian.org/

[15] https://github.com/pact-foundation/pact-specification

[16] http://arquillian.org/arquillian-algeron/

[17] https://hoverfly.io/

[18] https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html

[19] http://rest-assured.io/

[20] http://arquillian.org/arquillian-cube/

[21] http://blog.christianposta.com/microservices/low-risk-monolith-to-microservice-evolution/

[22] https://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052

[23] https://github.com/ticket-monster-msa/monolith/blob/master/monolith/src/test/java/org/jboss/examples/ticketmonster/test/rest/BookingServiceTest.java

[24] https://maven.fabric8.io/

[25] https://www.openshift.com/

[26] https://openshift.io/

[27] https://www.eclipse.org/che/

[28] http://blog.christianposta.com/deploy/blue-green-deployments-a-b-testing-and-canary-releases/

[29] https://github.com/ticket-monster-msa/monolith/tree/master/tm-ui-v1

[30] https://istio.io/

[31] http://blog.christianposta.com/microservices/00-microservices-patterns-with-envoy-proxy-series/

[32] http://blog.christianposta.com/microservices/application-network-functions-with-esbs-api-management-and-now-service-mesh/

[33] https://medium.com/@mattklein123/service-mesh-data-plane-vs-control-plane-2774e720f7fc

[34] https://istio.io/docs/tasks/traffic-management/ingress.html

[35] https://istio.io/docs/reference/config/traffic-rules/routing-rules.html

[36] https://www.turbinelabs.io/

[37] https://blog.turbinelabs.io/deploy-not-equal-release-part-one-4724bc1e726b

[38] http://www.apicur.io/

[39] https://hoverfly.io/

[40] https://github.com/ticket-monster-msa/monolith/blob/master/orders-service/src/test/java/org/ticketmonster/orders/HoverflyTest.java

[41] https://www.manning.com/books/testing-java-microservices

[42] https://twitter.com/alexsotob

[43] https://twitter.com/lightguardjp

[44] https://twitter.com/andygeede?lang=en

[45] http://blog.christianposta.com/

[46] https://twitter.com/christianposta

 

越忙越需要 GTD

GTD 最早于2002年由 David Allen 提出,自他撰写的畅销书 Getting Things Done 出版以来,“GTD” 已经成了一个术语,影响了大批致力于推动时间管理的图书及博客作者、企业管理者,甚至因此诞生了一批基于 GTD 的效率工具。它的内容从不过时,与时代发展同步行进,影响着全球成千上万的效率实践者。

专业人士

人们之所以对此乐此不疲,是因为 GTD 与[我想掌控自己的生活][我希望得到尊重与回报][我想要的是什么][我希望能着眼未来]关联密切——它并不仅仅解决工作问题,它试图帮人们理顺每一个日常,从全局角度审视人生。就像一个精密的神经系统,从任何角度唤醒神经元,都有对应的触发路径与反馈。

正在谱写乐曲的巴赫,正在向欧洲行军的拿破仑,正在创造艺术品的安迪沃霍尔,他们都需要处理大量信息并转化为自己的行为决策,以此体现自我意志。一个处理纷繁信息并无时无刻不在作出正确决策的人,在今天被称为专业人士。

毫无疑问,从 GTD 中获利最大的是专业人士。这里有一个虚拟形象,或许你见到过与他相似的人——前一晚工作了很长时间,他选择把工作带回家,结束已经是凌晨3点。他感到自己很少有时间去做喜欢的事情,比起这一点,没完成工作会更让他有负罪感。大部分时间他应对自如,但仍然感到重重压力。有些时候,他会把工作拖到最后时刻,再灵感迸发地完成它,每当这时候,他总后悔没有早点进入 aha moment.第二天早起赶到办公室,办公桌上堆积了许多很久没触碰过的文件。处理邮件到一半,同事拿着合同过来打断他,交谈完毕日历正好弹出5分钟后的会议提醒。结束后已经是中午,他和同事们在办公室一起用餐,想到自己上周忘记了一个重要约会。他感到很难让自己停下来,保持忙碌有一种安全感。他的勤勉为自己换来了尊重,在成功又琐碎的氛围中,他无暇着眼未来,他需要 GTD 来管理自己的工作与生活——

五个步骤

“通过行动让自己感觉良好,要比通过让自己感觉良好来实现更好的行动容易得多。”

  1. 收集引起我们注意的事务和信息

时间管理四象限法则把事务分为四种优先级:既紧急又重要、重要但不紧急、紧急但不重要、既不紧急也不重要。大部分时候,人们最优先处理第一种事务,把第二种事务作为长线供养,后两种被视为归档事务被忽略。这些事永远客观存在于生活中,人们无法如同精密仪器时刻不逾陈规。GTD 建议把所有事务不计前嫌统归到一起。有时候,正是一些前置的关联事务,让我们真正开始解决问题时毫不费力。另外 GTD 的两分钟法则认为:如果采取某项行动最多需要两分钟,就应该在作出决定的一刹那实施行动,这可以有效解决部分紧急任务。

“提升个人工作效率低最佳手段之一,就是拥有你乐于使用的管理工具。”

养成收集的习惯至关重要,匹配的管理工具能够安放所有收集到的事务。找到一款项目与任务管理工具,梳理所有引起注意的事务和信息。Teambition 的任务管理功能可以有效解决此类问题,在这里,你可以创建任何主题的事务,在下一步,我们将进行进一步归纳整理。

  1. 理清每个项目的意义和相关措施

进一步梳理经上一步统计到的所有事务。事务分为两种,一种是没有行动解决方案的,比如一个想法或者一个软件升级提醒,把这类事务安排到未来的日程,在日程下做好附注。另一类需要进一步行动解决方案的事务,比如参加会议、回顾工作或者洗车,设定每一步具体动作的完成时间。有时委派他人代劳也是完成事务的方法,如果你自己不是最佳的执行人选。在 Teambition 中,你可以指派任意任务给最适合完成它的人,无论他是你的同事、合作伙伴、学习搭档还是家庭成员。

  1. 组织整理:建立清单

到这一步事务开始变得明朗,大部分事务已经在时间线设定好位置。现在需要做的,是根据不同的情境,把事务归属到不同类别的清单。比如,“立即执行”清单,里面都是在2分钟内就能完成的简单任务。“等待”清单,指关注结果不关注过程的事,比如等待预定的电影票、用户对新提议的回复、等待新购买的空气净化器等。“外出事宜”清单,出门前看一看清单,能有效提升一路上能解决事务的数量与效率。Teambition 的自定义任务面板可以随心所欲地设定为不同主题的清单,你只需要在使用时重新打开他们就能一目了然。你有什么关于清单的好主意,请留言或邮件告诉给我们。

  1. 进行思考回顾

将所有任务在进行优先级排期与清单归纳后,进入回顾阶段——查看事务被着手解决的合理性。首先查看日程表,再看一看行动清单,根据情境选择恰当的回顾内容。人们总会放任自己的大脑纠缠于大量超过自身承载能力的任务数量。回顾的优点在于,尽可能冷静地重审自己的计划,重新夺回主动权而不至于疲于奔波。

  1. 选择行动

终于到了执行阶段,根据4种标准确认下一步的行动:情境、时间、精力、以及优先级。优先执行事先安排好的工作,再处理突发事件。有突发事件时,判断它是否值得你停下目前正在进行的工作,一旦被打断工作,重新进入工作状态将付出较高时间成本。实际上,并不存在被打断工作这回事,只是对新事物管理不善。

三个原则

三个原则的 Getting things done 精髓:

  1. 养成收集的习惯,整合所有工作与生活事务;
  2. 确定“下一步行”;
  3. 学会关注结果。

GTD 强调收集、思考并行动的重要性,五个步骤的最后一步才是真正的执行,前四步无一不是在确保事务解决的合理性与可行性。剥离开或关联或疏离的事项,一次只做一件事。

“一次只做一件事,沉浸通常伴随对特定任务的全身心关注,而且你通常会有掌控感,感到目标清晰。处于沉浸状态的个人通常明白接下来会发生什么事情,并能在执行整个任务的过程中得到及时反馈。”——认知科学家 Mihaly Csikszentmihalyi 《沉浸:关于最佳体验的心理学》

初入 GTD 的实践者,随着习惯的养成与方法的熟悉,会逐渐形成一套以自己为中心的地图,包含自己的角色、职责和兴趣,基于个人发展方向和需求而不断迭代,最终形成综合的全面生活管理系统。在任何情况下,用 GTD 的方法辅以匹配的工具,应对不同属性的事务,提升工作与生活效率。

Open source software for autonomous cars

Software

Honda Riding Assist原理及疑问讲解

在几天之前,大家看完Honda发布的自动平衡技术之后,相信心中都有不少疑问。大家都想知道其运作原理,老手会担心会否影响电单车原有的操控,买家也想知道新技术何时才能应用在市贩型号之上。今次为大家带来好消息,今天Honda进一步发表riding Assist的技术细节,保证人人都会开心满意。
当车速达到可以平衡车身时,前叉倾角收缩,回复至街车水平。

在规格表当中,我们经常会看到前倾角和拖曳距这两个名词,前倾角讲的是角度,拖曳距讲的是长度。前倾角是转向头轴线和垂线的夹角。而拖曳距就是轮轴与地面的垂线与转向头轴线与地面交点的距离。

前倾角和拖曳距直接影响着车子的操控性能,尤其是在过弯时候的表现。加大前倾角有两种效果,首先是会增加轴距。其次是会增加拖曳距。拖曳距太大会导致电单车转向效果变差,但同时直线会更稳定。

Honda所采用的名词分别是Positive和Negative trail length,比较难明和难记。看下图便较易记住:

上图是正常行车状态,头叉角度较直。
下图是慢车或停车状态,头叉较为向外哨出。
Honda的Riding Assist技术,便是主动地改变拖曳距,在慢车或静止时增长,在正常行车时变短。由于较长的拖曳距,在转动把手时会有更明显的反向作用,当车身倾倒时将把手向倾斜的方向转动,会起到明显的支撑作用。Honda便是在鹅颈位置装上两个马达,一个负责改变前倾角和拖曳距,另一个负责转动把手。透过感应器收集车身动态数据,两个马达同时运作便能自动平衡电单车,停车都不用落脚 。
好消息是,这项由本田机械人科技所延伸出来的应用技术Riding Assist,只涉及头担部份改动,其余部份包括车架、引擎和尾担都可沿用现有设计。理论上任何电单车都可以配上这套自动平衡系统,以后Honda电单车发售同一型号时,除了有ABS和DCT选择之外,还可以有Riding Assist,RA的选择。

试想像一下,座高870mm的CRF1000,当配备Riding Assist之后,身高只有160mm的女骑士,也不用担心落脚的问题。这项发明,将会彻底改变骑士选择机车的方向。

完美运行“Pokemon Go” 操作指南

出国

 

 

不多啰嗦了,内容如题,现在在中国大陆也可以正常玩儿 Pokemon Go 了!无论你用的是 iPhone 还是 Android 手机。

今天有消息称,中国区已经解开了锁区,能够正常游戏,目前在中国区连上 VPN 登陆后,就能够正常游戏,且在游戏中无需科学上网,不会出现之前搜索不到  GPS 以及没有任何精灵的情况。

但要注意的是, GPS 解锁不代表 Pokemon Go 在中国区已经上线,还是需要玩家们去美国区或澳大利亚区 App Store 进行下载。

iOS 平台下载方式

目前 Pokemon Go 在美国、澳大利亚区的 App Store 正式上线,并且在上线后就霸占了免费榜的首位。如果你已经有美区账号,直接登录 App Store 进行下载即可。

如果你还没有美区 App Store 账号,可以关注爱范儿(微信:ifanr 或扫描下方二维码)微信公众号,回复【美区】以获取最简单的美国 App Store 账号注册方法。

屏幕快照-2016-07-06-下午16.32.19-下午

Android 平台下载教程

Android 端的 Pokemon Go 是在 Google Play 商店上线的。爱范儿为读者们准备了Pokemon Go 的 Android 版本 APK 安装包,关注爱范儿微信公众号(在公众号搜索 ifanr)回复【宝可梦】或者【精灵】或者【Pokemon】就可以获取下载链接。

下载前的科普,什么是 Pokemon Go?

146566760274487800_a580xH

Pokemon Go 是任天堂推出的第一款手机增强现实游戏,玩家拿着手机,走到街上,就可以捕捉精灵宝可梦,升级精灵,进入道馆进行对战。具体玩儿法是怎样的,看下面这个视频与游戏截图就明白了。

腾讯视频

12.pic

下载游戏,登入 Google 账号进入游戏。

11.pic

设置自己的玩家形象,进入教程,先抓只妙蛙种子练练手,挥动精灵球的体验,很像曾经的游戏“扔纸团”。

13.pic

之后就可以进行游戏了,在不同的地点,玩家会碰到不同的精灵,并且在部分地标,可以收集免费的精灵球。

精灵训练师们快行动起来吧,如果你想进一步了解 Pokemon 的玩法,可以阅读下面这篇详细试玩体验。

Pokemon GO 试玩体验:要不是人民币玩家,就去苦练扔球吧

HTML5 视频使用指南

视频是 HTML5 中最受欢迎的特性之一。跟以前调用插件的做法相比,只要一个 <video> 就行的便利实在是今非昔比。除此之外,HTML5 视频对移动设备的友好也是 Flash 难望项背的。到了 2013 年,浏览器和各种移动设备对 HTML5 视频的支持已经相当成熟,尤其是移动设备上,HTML5 几乎是唯一实用的网页视频发布方式。

不过,HTML5 视频有个很大的问题:兼容性。固执地坚守老旧浏览器的用户甚至不算是这个问题的根源,而是 HTML5 规范中没有规定(也不可能规定)视频所用的容器和编解码技术,所以即使在不考虑缺乏 HTML5 支持的浏览器时,各种视频技术涉及的大量规格和授权类型,使不同的浏览器(甚至同一浏览器在不同平台上的版本)对 HTML5 视频的兼容性差别极大。要想让你的所有用户都能看到你发布的视频,你必须遵循一个标准的发布方法,而这就是本文的目的。

容器和编解码器

要发布兼容度最好的视频,有必要了解常用的视频容器和编解码器,以及各平台和浏览器的支持程度。

当我们看一个“视频”的时候,其实我们是在看视频,同时在听音频。一个视频文件中,至少储存了两条数据:一条是视频轨道、一条是音频轨道。这些数据封装在一个叫 容器 的文件格式中。MP4、MKV、AVI 等都是我们熟悉的容器格式,但是它们都只是装东西的袋子,还不能决定一个视频能否被支持。

当我们播放视频时,分离器会解析容器,找到其中的视频和音频数据流,根据它们的类型把它们传给相应的 编解码器。编解码器会解码这些数据流并通过播放器和操作系统输出到屏幕和扬声器上。

由此可见,播放器必须能识别容器,并拥有视频和音频数据流相应的编解码器,才能播放视频。浏览器和操作系统对视频的支持,其实就是对容器以及视频和音频编解码器组合的支持。

Web 支持的容器和编解码器

尽管视频容器和编解码器多如牛毛,但因为授权、效率或硬件解码等原因绝大部分在 Web 上都不受支持。如今在 Web 上常见的组合不外乎三种:

H.264 + AAC + MP4

H.264 也称为 MPEG-4 Part 10 或 MPEG-4 AVC,由 动态图像专家组 开发。这种格式可以应用在从低带宽、低性能的移动设备到高带宽、高性能的台式电脑的所有设备上,因此,它被划分为多个 ProfilesLevels,它们规定了编码的细节规格,以在编码效率和编解码所需计算性能之间取得平衡。例如手机等移动设备普遍可以支持 Baseline Profile(较新的设备如 iPhone、iPad 等也可支持 Main 甚至 High Profile),电脑则可以支持 Baseline、Main 和 High Profile。

H.264 的最大优点是支持硬件解码。在 iPhone 等移动设备以及较新的电脑上,有专门的硬件模块用于 H.264 的编解码工作,这可以减少不擅此道的 CPU 的负担,大大降低能耗并提高性能,这对 CPU 性能和电池容量有限的移动设备来说至关重要。不过 H.264 是一种专利技术,从法律上讲,要发布通过 H.264 编码的视频,需要有 MPEG LA Group 的授权。

AAC 是作为代替 MP3 的格式开发的,拥有远胜过 MP3 的编码效率和多声道支持。与 H.264 相似,AAC也分为针对一般设备的 LC-AAC 和针对高性能设备的 HE-AAC。Web 上绝大部分都使用 LC-AAC

AAC 有非常广泛的支持,iPhone 等移动设备,QuickTime、Windows Media Player 等闭源播放器,和 VLC 等开源播放器都支持 AAC。另外,AAC 同样也是一项专利技术,使用需要授权。

MP4 是从苹果的 QuickTime 的规格上发展而来的容器。MP4 视频有 mp4 和 m4v 两种常见的扩展名,后者是苹果为了与纯音频的 MP4 文件(m4a)区分而创造的。

WebM

WebM 是专门为 HTML5 视频设计的,由 Google 赞助开发,是开源、有免授权费专利的技术。目前,它由 VP8 视频编码、Vorbis 音频编码和一种基于 Matroska 的容器组成。

VP8 原来由 On2 所开发,是一种与 H.264 非常类似的编码格式(性能也很接近),Google 在 2010 年收购 On2 后便将 VP8 开放源代码。

Vorbis 是由 Xiph.Org 基金会 维护的开源音频编码格式,目的是与有专利限制的 MP3AAC 竞争,是开源世界最成功的音频编码格式之一。它的编码效率可以与 AAC 相比,大大高于 MP3

WebM 最近已新增了 VP9 视频编码和 Opus 音频编码的 WebM with VP9 版本,Chrome 30 开始已经可以支持。但目前除了基于 Webkit 的几种桌面浏览器(Chrome、Safari、Opera)的较新版本外,还几乎没有其他浏览器和平台支持 WebM with VP9。

Chrome、Firefox、Opera 原生支持 WebM,IE 和 Safari 则需要安装编解码器。WebM 视频的扩展名一般是 webm。

Theora + Vorbis + Ogg

Ogg 现由 Xiph.Org 基金会维护,也是开源、有免授权费专利的技术,但它的历史比 WebM 更久。它包含 Theora 视频编码、 Vorbis 音频编码和 Ogg 容器。

Theora 源自 VP3,与 WebM 的 VP8 一样都由 On2 所开发,不过 VP3 早在 2001 年就开放源代码了。

Chrome、Firefox、Opera 以及所有的主要 Linux 发行版原生支持 Ogg。Ogg 视频的扩展名一般是 ogv。

以下是未安装任何附加组件的情况下,主要浏览器和移动设备对以上三种组合的支持程度:

IE Chrome Firefox Opera Safari iOS Android
H.264 + AAC + MP4 9+ 4+ 21+ 1 3+ 3+ 2.1+ 2
WebM 6+ 4+ 10.6+ 2.3+
Theora + Vorbis + Ogg 4+ 3.5+ 10.5+

1:在 Windows 7 以上支持,在 Mac 和 Linux 上不支持
2:播放时需要特殊处理

如何做到最大兼容

从上表可以看出,没有一种格式可以兼容所有的情况,必须使用至少两种组合。

  • 要支持移动设备、IE 和 Mac,H.264 + AAC + MP4 是必须的
  • 要支持开源平台,需要 WebM 或 Theora + Vorbis + Ogg

除了 H.264 + AAC + MP4 必须使用外,WebM 和 Theora + Vorbis + Ogg 的支持范围比较相近,只不过前者的技术更新,压缩效率更高,而后者的历史更久,在较老的平台上也受到支持。可酌情挑选其中一种。以下是三种都使用的例子:

<video width="640" height="360" controls>
    <source src="code-rush.m4v" type='video/mp4; codecs="avc1.42E01E,mp4a.40.2"'>
    <source src="code-rush.webm" type='video/webm; codecs="vp8,vorbis"'>
    <source src="code-rush.ogv" type='video/ogg; codecs="theora,vorbis"'>
</video>

在这里要解释一下 <source> 里的 type 是视频文件的 MIME Type,它描述了视频的容器和编码类型。也可以不写 codecs 的内容,但写上可以让浏览器无需下载视频文件即可判断自己能否播放,从而选择正确的格式。

这里 MP4 的 type 比较复杂:

avc1.42E01E
这是视频编码规格:
Baseline – avc1.42E0xx(xx 是 Level,下同)
Main – avc1.4D40xx
High – avc1.6400xx
Level 是以 16 进制表示的:如 Level 3 是 1E(30 的 16 进制),Level 4.1 是 29(41 的 16 进制)
mp4a.40.2
这是音频编码规格,代表 LC-AAC

可以用 JavaScript 判断浏览器是否支持一种 MIME Type:

var canPlayMP4 = document.createElement("video").canPlayType('video/mp4; codecs="avc1.42E01E,mp4a.40.2"');
// "probably":浏览器认为它可以支持指定的容器和编码格式,应该可以播放
// "maybe":浏览器认为它可以支持指定的容器,但不确定能否支持编码格式,也许可以播放
// "":浏览器无法支持指定的容器

要测试你的浏览器对各种 MIME Type 的支持情况,点击此处

为 HTML5 制作视频

我们往往需要将视频素材转换成适合 HTML5 的格式。HandbrakeFFmpeg 是常用的工具,它们都是跨平台的开源软件。Handbrake 有图形界面和命令行两种使用方式,而 FFmpeg 是纯命令行的。此外,还有一些有趣的工具可供选择:Miro Video Converter 是傻瓜型的,可以转换我们提到的三种视频格式;MediaCoder 是一个国产的软件,支持的格式很多,用起来也最麻烦;Firefogg 是一个 Firefox 扩展,可以制作 WebM 和 Ogg。

建议使用 FFmpeg,请在官方下载页找到自己操作系统的预编译包使用。Windows 64bit 和 OS X 用户也可以下载我编译的版本:

FFmpeg for HTML5 Video

在 OS X 上可以用 Homebrew 来安装:

$ brew install ffmpeg --without-xvid --without-faac --without-lame --without-libvo-aacenc --with-libvpx --with-theora --with-fdk-aac --with-libvorbis --with-opus

制作 HTML5 视频时,需要考虑到用户的带宽。以 H.264 来说,常用画面规格的建议平均码率如下:

320×240/24p 640×360/24p 854×480/24p 1280×720/24p 1920×1080/24p
码率 200kbps 400kbps 800kbps 1500kbps 3000kbps
Profile 1 Baseline Main Main High High
Level 1 2 3 3 3.1 4

1:H.264 的 Profile 和 Level 与分辨率、帧率、码率的关系可以参见这张表格,也可以让转换软件自动选择。

如果对画质要求较高,或使用了更高的帧率,或使用 Theora 时,需要稍微提高码率。

另外 H.264 和 VP8 都支持 2-pass 编码,第一 pass 是扫描整个素材,记录下素材的一些特征,第二 pass 才是真正的编码。这种方法虽然比较花时间,但可以取得比 1-pass 更好的效果,时间充裕的话建议使用。

对于音频编码,AAC 和 Vorbis 的立体声码率均以 160kbps 为佳。

用 FFmpeg 制作 MP4 视频 – 文档
$ ffmpeg -pass 1 -fastfirstpass 1 -i INPUT -c:v libx264 -an -f rawvideo -y /dev/null
$ ffmpeg -pass 2 -i INPUT \
         -c:v libx264 -s 1280x720 -b:v 1500k -profile:v high -level 3.1 \
         -c:a libfdk_aac -ac 2 -b:a 160k -movflags faststart OUTPUT.m4v

重要的参数:

-pass
如果不使用 2-pass 编码,则只需要执行第二个命令,并去掉 -pass 2
-fastfirstpass
相当于 Handbrake 里的 Turbo First Pass,可以加快第一 pass 的速度
-an -f rawvideo -y /dev/null
这是在第一 pass 中不处理音频,并丢弃输出
-c:a libfdk_aac
libfdk_aac 也就是 Fraunhofer FDK AAC,是 FFmpeg 目前可以调用的最好的 AAC 编码器,但它并不是完全开源的,如果你的 FFmpeg 编译时没有包括它,可以换用 libfaac 或 FFmpeg 自带的 aac (由于是试验性的所以要加参数 -strict experimental),效果都不如 libfdk_aac
-movflags faststart
对网络视频而言十分重要,这可以让视频在下载完之前就开始播放
用 FFmpeg 制作 WebM 视频 – 文档
$ ffmpeg -pass 1 -i INPUT -c:v libvpx -an -f rawvideo -y /dev/null
$ ffmpeg -pass 2 -i INPUT \
         -c:v libvpx -s 1280x720 -b:v 1500k \
         -c:a libvorbis -ac 2 -b:a 160k OUTPUT.webm
用 FFmpeg 制作 Ogg 视频 – 文档
$ ffmpeg -i INPUT \
         -c:v libtheora -s 1280x720 -b:v 1500k \
         -c:a libvorbis -ac 2 -b:a 160k OUTPUT.ogv

FFmpeg 的详细用法请详见官方文档

添加字幕

HTML5 视频可以添加 WebVTT 字幕轨道,方法是在 <video> 内加上 <track>

<video width="640" height="360" controls>
    <source src="code-rush.m4v" type='video/mp4; codecs="avc1.42E01E,mp4a.40.2"'>
    <source src="code-rush.webm" type='video/webm; codecs="vp8,vorbis"'>
    <source src="code-rush.ogv" type='video/ogg; codecs="theora,vorbis"'>
    <track kind="subtitles" label="中文字幕" src="code-rush.chs.vtt" srclang="zh" default></track>
    <track kind="subtitles" label="英文字幕" src="code-rush.eng.vtt" srclang="en"></track>
</video>

以下是字幕文件的内容示例:

WEBVTT

1
00:00:02.150 --> 00:00:12.150
从1998年3月到1999年4月
一个独立纪录片摄制组跟随着网景公司的一个软件工程师团队
记录了他们公司和互联网历史上的分水岭

2
00:00:16.970 --> 00:00:20.240
我和很多到这里来寻找

3
00:00:20.240 --> 00:00:22.050
硅谷体验的人都聊过

事实上,它的格式和 SRT 字幕几乎一样,只不过秒和毫秒之间是点,而不是 SRT 的逗号,另外第一行要加上一个 WEBVTT 标识。

WebVTT 是非常新的技术,目前还处于草案阶段。以下是当前主要浏览器和移动设备的支持情况:

IE Chrome Firefox Opera Safari iOS Android
WebVTT 10+ 18+ 24+ 1 15+ 6+ 7+ 4.4+

1:默认禁用,在 about:config 中打开 media.webvtt.enabled 设置项来启用

要详细了解 WebVTT,可以看看这篇文章

不支持 HTML5

当然是 Flash fallback 了:

<video width="640" height="360" controls>
  <source src="code-rush.mp4" type='video/mp4; codecs="avc1.42E01E,mp4a.40.2"'>
  <source src="code-rush.webm" type='video/webm; codecs="vp8,vorbis"'>
  <source src="code-rush.ogv" type='video/ogg; codecs="theora,vorbis"'>
  <track kind="subtitles" label="中文字幕" src="code-rush.chs.vtt" srclang="zh" default></track>
  <track kind="subtitles" label="英文字幕" src="code-rush.eng.vtt" srclang="en"></track>
  <object type="application/x-shockwave-flash" data="http://releases.flowplayer.org/swf/flowplayer-3.2.1.swf" width="640" height="360">
    <param name="movie" value="http://releases.flowplayer.org/swf/flowplayer-3.2.1.swf" />
    <param name="allowFullScreen" value="true" />
    <param name="wmode" value="transparent" />
    <param name="flashVars" value="config={'playlist':['code-rush_poster.jpg',{'url':'code-rush.mp4','autoPlay':false}]}" />
    <img alt="Code Rush" src="code-rush_poster.jpg" width="640" height="360" title="No video playback capabilities, please download the video below" />
  </object>
</video>

<video> 标签里,按往常一样加上 Flash 播放器即可。注意:Flash 支持播放 H.264 视频,所以不需要再制作一个 FLV 出来。

Nginx区分PC或手机访问不同网站

近几年来,随着手机和pad的普及,越来越多的用户选择使用移动客户端访问网站,而为了获取更好的用户体验,就需要针对不同的设备显示出最合适的匹配,这样就是近年来流行的“响应式web设计”。

响应式web设计是一种纯前端技术js、css等实现的针对不同设备访问同一网址看到不同的布局,是页面内容更适合当前设备阅读。但这个不是本文的重点,重点还是放在nginx如何实现上来。

本文要讲的的是如何使用nginx区分pc和手机访问不同的网站,是物理上完全隔离的两套网站(一套移动端、一套pc端),这样带来的好处pc端和移动端的内容可以不一样,移动版网站不需要包含特别多的内容,只要包含必要的文字和较小的图片,这样会更节省流量。有好处当然也就会增加困难,难题就是你需要维护两套环境,并且需要自动识别出来用户的物理设备并跳转到相应的网站,当判断错误时用户可以自己手动切换回正确的网站。

下面以264查询网为实例来说明如何实现上面的需求。
明确的的需求:
1.制作两个站点PC端网站www.264.cn,和移动端网站m.264.cn
2.使用pc或移动设备访问任何一个域名都会跳到相应的站点。
3.用户可以选择访问移动版还是PC版网站,移动版网站始终有切换到PC版的链接,PC版当网站通过手机访问时会提供移动版网站的链接。
4.当用户选着访问其中一种类型的网站后,保存设置结果生效时间为24小时,当然长短可以自己设置。

简单的服务器端实现方法
有两套网站代码,一套PC版放在/usr/local/website/web,一套移动版放在/usr/local/website/mobile。只需要修改nginx的配置文件件,nginx通过UA来判断是否来自移动端访问,实现不同的客户端访问不同内容。
这种方法的缺点是移动端和PC端用同一个域名,存在黑帽的嫌疑,而且UA并不是总是判断的准确,如果判断错误的情况下,用户不能手动修改访问的网站类型。
关键的Nginx配置如下:

location / {
	#默认PC端访问内容
    root /usr/local/website/web;

	#如果是手机移动端访问内容
    if ( $http_user_agent ~ "(MIDP)|(WAP)|(UP.Browser)|(Smartphone)|(Obigo)|(Mobile)|(AU.Browser)|(wxd.Mms)|(WxdB.Browser)|(CLDC)|(UP.Link)|(KM.Browser)|(UCWEB)|(SEMC\-Browser)|(Mini)|(Symbian)|(Palm)|(Nokia)|(Panasonic)|(MOT\-)|(SonyEricsson)|(NEC\-)|(Alcatel)|(Ericsson)|(BENQ)|(BenQ)|(Amoisonic)|(Amoi\-)|(Capitel)|(PHILIPS)|(SAMSUNG)|(Lenovo)|(Mitsu)|(Motorola)|(SHARP)|(WAPPER)|(LG\-)|(LG/)|(EG900)|(CECT)|(Compal)|(kejian)|(Bird)|(BIRD)|(G900/V1.0)|(Arima)|(CTL)|(TDG)|(Daxian)|(DAXIAN)|(DBTEL)|(Eastcom)|(EASTCOM)|(PANTECH)|(Dopod)|(Haier)|(HAIER)|(KONKA)|(KEJIAN)|(LENOVO)|(Soutec)|(SOUTEC)|(SAGEM)|(SEC\-)|(SED\-)|(EMOL\-)|(INNO55)|(ZTE)|(iPhone)|(Android)|(Windows CE)|(Wget)|(Java)|(curl)|(Opera)" )
	{
		root /usr/local/website/mobile;
	}

	index index.html index.htm;
}

纯客户端js实现方式
下面这段代码放到首页<head>和</head>之间即可

<script type="text/javascript">// <![CDATA[
 if(/AppleWebKit.*Mobile/i.test(navigator.userAgent) || (/MIDP|SymbianOS|NOKIA|SAMSUNG|LG|NEC|TCL|Alcatel|BIRD|DBTEL|Dopod|PHILIPS|HAIER|LENOVO|MOT-|Nokia|SonyEricsson|SIE-|Amoi|ZTE/.test(navigator.userAgent))){
 	if(window.location.href.indexOf("?mobile")<0){
 		try{
 			if(/Android|webOS|iPhone|iPod|BlackBerry/i.test(navigator.userAgent)){
                                 //触屏手机版地址
 				window.location.href="http://m.264.cn";
 			}else if(/iPad/i.test(navigator.userAgent)){
                                 //pad版地址
 			}else{
                                 //普通手机版地址
 				window.location.href="http://wap.264.cn"
 			}
 		}catch(e){}
 	}
 }
 // ]]></script>

推荐的nginx区别手机和PC访问方法
利用前端js和后端nginx配合,js通过设置cookie来设定当前访问哪页面。

增加设置cookie的js代码,这段代码需要在移动网站和PC网站的所有页面都要放置。

function createCookie(name, value, days, domain, path) {
  var expires = '';
  if (days) {
    var d = new Date();
    d.setTime(d.getTime() + (days*24*60*60*1000));
    expires = '; expires=' + d.toGMTString();
  }
  domain = domain ? '; domain=' + domain : '';
  path = '; path=' + (path ? path : '/');
  document.cookie = name + '=' + value + expires + path + domain;
}

function readCookie(name) {
  var n = name + '=';
  var cookies = document.cookie.split(';');
  for (var i = 0; i < cookies.length; i++) {
     var c = cookies[i].replace(/^\s+/, '');
     if (c.indexOf(n) == 0) {
       return c.substring(n.length);
     }
   }
   return null;
 }
 
 function eraseCookie(name, domain, path) {
   setCookie(name, '', -1, domain, path);
 }
 
 

nginx增加如下配置,根据UA和cookie判断当前是移动端还是PC端访问

if ($http_user_agent ~* '(Android|webOS|iPhone|iPod|BlackBerry)') {
  set $mobile_request '1';
}
if ($http_cookie ~ 'mobile_request=full') {
  set $mobile_request '';
}
if ($mobile_request = '1') {
  rewrite ^.+ http://m.264.cn$uri;
}

移动版页面添加PC版链接
默认用户进来时会先判断UA,如果是手机端访问就会进入手机版,但也会存在误判进入手机版或者需要更多信息进入PC版,那么就需要在移动版的页面放入代码,让用户可以从移动版切换到web版并且下次访问会保留设置。

<a onclick="setCookie('iphone_mode', 'full', 1, '264.cn')" href="http://www.264.cn">
  电脑版
</a>

如果用户访问不正确时,点击电脑版链接就可以进入PC版网站,并且24小时内再次访问会记忆上次访问的网站类型设置。

PC版网站增加访问手机版的链接
在PC版的网站适当的地方加入下面的链接让用户可以切换到手机版的网站。

<a onclick="deleteCookie('mobile_mode', '264.cn');" href="http://m.264.cn">
  手机版
</a>

完整的nginx端配置,当然是去掉了与本文功能无关的配置,并不是一个完可用的配置,只是给大家一个整体的框架。

PC版网站配置

upstream app_server {
  server 0.0.0.0:9001;
}

server {
  listen 80;
  server_name www.264.cn;

  root /path/to/main_site;
  # ...

  location / {
    proxy_set_header X-Real-IP $remote_addr;
    # ...

    if ($http_user_agent ~* '(Android|webOS|iPhone|iPod|BlackBerry)') {
      set $mobile_request '1';
    }
    if ($http_cookie ~ 'mobile_request=full') {
      set $mobile_request '';
    }
    if ($mobile_request = '1') {
      rewrite ^.+ http://m.264.cn$uri;
    }

    # serve cached pages ...

    if (!-f $request_filename) {
      proxy_pass http://app_server;
      break;
    }
  }
}

手机移动版配置

upstream m_app_server {
server 0.0.0.0:9001;
}

server {
  listen 80;
  server_name m.264.cn;

  root /path/to/mobile_site;
  # ...

  location / {
    proxy_set_header X-Real-IP $remote_addr;
    # ...

    if ($http_user_agent ~* '(Android|webOS|iPhone|iPod|BlackBerry)') {
      set $mobile_request '1';
    }
    if ($http_cookie ~ 'mobile_request=full') {
      set $mobile_request '';
    }
    if ($mobile_request != '1') {
      rewrite ^.+ http://www.264.cn$uri;
    }

    # serve cached pages ...

    if (!-f $request_filename) {
      proxy_pass http://m_app_server;
      break;
    }
  }
}

 

Nginx区分PC或手机访问不同网站

Nginx CORS实现JS跨域

1. 什么是跨域

简单地理解就是因为JavaScript同源策略的限制,a.com 域名下的js无法操作b.com或是c.a.com域名下的对象。

同源是指相同的协议、域名、端口。特别注意两点:

  • 如果是协议和端口造成的跨域问题“前台”是无能为力的,
  • 在跨域问题上,域仅仅是通过“协议+域名+端口”来识别,两个不同的域名即便指向同一个ip地址,也是跨域的。

2. 跨域解决方案

跨域解决方案有多种,大多是利用JS Hack:

3. CORS

CORS: 跨域资源共享(Cross-Origin Resource Sharing)http://www.w3.org/TR/cors/

当前几乎所有的浏览器(Internet Explorer 8+, Firefox 3.5+, Safari 4+和 Chrome 3+)都可通过名为跨域资源共享(Cross-Origin Resource Sharing)的协议支持ajax跨域调用。(see: http://caniuse.com/#search=cors)

Chrome, Firefox, Opera and Safari 都使用的是 XMLHttpRequest2 对象, IE使用XDomainRequest。XMLHttpRequest2的Request属性:open()、setRequestHeader()、timeout、withCredentials、upload、send()、send()、abort()。

XMLHttpRequest2的Response属性:status、statusText、getResponseHeader()、getAllResponseHeaders()、entity、overrideMimeType()、responseType、response、responseText、responseXML。

启用 CORS 请求

假设您的应用已经在 example.com 上了,而您想要从 www.example2.com 提取数据。一般情况下,如果您尝试进行这种类型的 AJAX 调用,请求将会失败,而浏览器将会出现“源不匹配”的错误。利用 CORS,www.example2.com 服务端只需添加一个HTTP Response头,就可以允许来自 example.com 的请求:

Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Credentials: true(可选)

可将 Access-Control-Allow-Origin 添加到某网站下或整个域中的单个资源。要允许任何域向您提交请求,请设置如下:

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true(可选)

其实,该网站 (html5rocks.com) 已在其所有网页上均启用了 CORS。启用开发人员工具后,您就会在我们的响应中看到 Access-Control-Allow-Origin 了。

提交跨域请求

如果服务器端已启用了 CORS,那么提交跨域请求就和普通的 XMLHttpRequest 请求没什么区别。例如,现在 example.com 可以向 www.example2.com 提交请求了:

var xhr = new XMLHttpRequest();
// xhr.withCredentials = true; //如果需要Cookie等
xhr.open('GET', 'http://www.example2.com/hello.json');
xhr.onload = function(e) {
  var data = JSON.parse(this.response);
  ...
}
xhr.send();

4. 服务端Nginx配置

要实现CORS跨域,服务端需要这个一个流程:http://www.html5rocks.com/static/images/cors_server_flowchart.png

对于简单请求,如GET,只需要在HTTP Response后添加Access-Control-Allow-Origin。

对于非简单请求,比如POST、PUT、DELETE等,浏览器会分两次应答。第一次preflight(method: OPTIONS),主要验证来源是否合法,并返回允许的Header等。第二次才是真正的HTTP应答。所以服务器必须处理OPTIONS应答。

http://enable-cors.org/server_nginx.html这里是一个nginx启用COSR的参考配置。

流程如下:

  1. 首先查看http头部有无origin字段;
  2. 如果没有,或者不允许,直接当成普通请求处理,结束;
  3. 如果有并且是允许的,那么再看是否是preflight(method=OPTIONS);
  4. 如果是preflight,就返回Allow-Headers、Allow-Methods等,内容为空;
  5. 如果不是preflight,就返回Allow-Origin、Allow-Credentials等,并返回正常内容。

用伪代码表示:

location /pub/(.+) {
    if ($http_origin ~ <允许的域(正则匹配)>) {
        add_header 'Access-Control-Allow-Origin' "$http_origin";
        add_header 'Access-Control-Allow-Credentials' "true";
        if ($request_method = "OPTIONS") {
            add_header 'Access-Control-Max-Age' 86400;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE';
            add_header 'Access-Control-Allow-Headers' 'reqid, nid, host, x-real-ip, x-forwarded-ip, event-type, event-id, accept, content-type';
            add_header 'Content-Length' 0;
            add_header 'Content-Type' 'text/plain, charset=utf-8';
            return 204;
        }
    }
    # 正常nginx配置
    ......
}

但是由于Nginx 的 if 是邪恶的,所以配置就相当地让人不爽(是我的配置不够简洁吗?)。下面nginx-spdy-push里/pub接口启用CORS的配置:

# push publish
# broadcast channel name must start with '_'
# (i.e., normal channel must not start with '_')

# GET    /pub/channel_id -> get statistics about a channel
# POST   /pub/channel_id -> publish a message to the channel
# DELETE /pub_admin?id=channel_id -> delete the channel

#rewrite_log on;

# server_name test.gw.com.cn
# listen      2443 ssl spdy

location ~ ^/pub/([-_.A-Za-z0-9]+)$ {

    set $cors "local";

    # configure CORS based on https://gist.github.com/alexjs/4165271
    # (See: http://www.w3.org/TR/2013/CR-cors-20130129/#access-control-allow-origin-response-header )

    if ( $http_origin ~* "https://.+\.gw\.com\.cn(?=:[0-9]+)?" ) {
        set $cors "allow";
    }
    if ($request_method = "OPTIONS") {
        set $cors "${cors}options";
    }

    # if CORS request is not a simple method
    if ($cors = "allowoptions") {
        # Tells the browser this origin may make cross-origin requests
        add_header 'Access-Control-Allow-Origin' "$http_origin";
        # in a preflight response, tells browser the subsequent actual request can include user credentials (e.g., cookies)
        add_header 'Access-Control-Allow-Credentials' "true";

        # === Return special preflight info ===

        # Tell browser to cache this pre-flight info for 1 day
        add_header 'Access-Control-Max-Age' 86400;

        # Tell browser we respond to GET,POST,OPTIONS in normal CORS requests.
        # Not officially needed but still included to help non-conforming browsers.
        # OPTIONS should not be needed here, since the field is used
        # to indicate methods allowed for 'actual request' not the preflight request.
        # GET,POST also should not be needed, since the 'simple methods' GET,POST,HEAD are included by default.
        # We should only need this header for non-simple requests  methods (e.g., DELETE), or custom request methods (e.g., XMODIFY)
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE';

        # Tell browser we accept these headers in the actual request
        add_header 'Access-Control-Allow-Headers' 'reqid, nid, host, x-real-ip, x-forwarded-ip, event-type, event-id, accept, content-type';

        # === response for OPTIONS method ===

        # no body in this response
        add_header 'Content-Length' 0;
        # (should not be necessary, but included for non-conforming browsers)
        add_header 'Content-Type' 'text/plain, charset=utf-8';
        # indicate successful return with no content
        return 204;
    }

    if ($cors = "allow") {
        rewrite /pub/(.*) /pub_cors/$1 last;
    }

    if ($cors = "local") {
        rewrite /pub/(.*) /pub_int/$1 last;
    }
}

location ~ /pub_cors/(.*) {
    internal;
    # Tells the browser this origin may make cross-origin requests
    add_header 'Access-Control-Allow-Origin' "$http_origin";
    # in a preflight response, tells browser the subsequent actual request can include user credentials (e.g., cookies)
    add_header 'Access-Control-Allow-Credentials' "true";

    push_stream_publisher                   admin; # enable delete channel
    set $push_stream_channel_id             $1;

    push_stream_store_messages              on;  # enable /sub/ch.b3
    push_stream_channel_info_on_publish     on;
}

location ~ /pub_int/(.*) {
  #  internal;
    push_stream_publisher                   admin; # enable delete channel
    set $push_stream_channel_id             $1;

    push_stream_store_messages              on;  # enable /sub/ch.b3
    push_stream_channel_info_on_publish     on;
}

5. 客户端javascript代码

下面是https://spdy.gw.com.cn/sse.html里的代码

var xhr = new XMLHttpRequest();
//xhr.withCredentials = true;
xhr.open("POST", "https://test.gw.com.cn:2443/pub/ch1", true);
// xhr.setRequestHeader("accept", "application/json");

xhr.onload = function()  {
  $('back').innerHTML = xhr.responseText;
  $('ch1').value = + $('ch1').value + 1;
}
xhr.onerror = function() {
  alert('Woops, there was an error making the request.');
};

xhr.send($('ch1').value);

页面的域是https://spdy.gw.com.cn, XMLHttpRequest的域是https://test.gw.com.cn:2443,不同的域,并且Post方式。

用Chrome测试,可以发现有一次OPTIONS应答。如过没有OPTIONS应答,可能是之前已经应答过,被浏览器缓存了'Access-Control-Max-Age' 86400;,清除缓存,再试验。

6. 相关链接

在nginx上使用php代理运行cgi程序

我们用到的很多开源程序比如mailman, nagios等等,都有WEB端管理界面。在那个Apache一家独大的年代,这个问题可以很好解决,因为apache本身可以运行cgi程序。但随着 nginx服务器的大规模应用,而恰好nginx又没有cgi模块,所以我们不得不采用一些变通的手段来解决它。

在网上广为流传的解决方法是一个老外写的perl脚本,但这个脚本本身有很多问题,而且需要在后台启动一个守护进程,本人对用perl写的网络服务守护进程的稳定性很怀疑,在看了它的代码后,发现用PHP即可很好的解决这个问题。

CGI其实本质上就是一个普通的二进制程序,你可以在后台直接运行它。而服务器要做的事就是将WEB传递的变量作为参数传递给这个程序并执行,而将执行返回的结果显示到页面上。

明白了这个道理,我们就可以开始着手解决这个问题了。其过程无非就是将PHP作为一个proxy,使其运行指定的程序,并把程序输出结果echo出来。

我们把这个PHP脚本命名为cgi.php,把它随便放到一个你认为合适的位置,然后用rewrite将后缀为cgi的请求都转发到cgi.php上。以下为参考的配置格式

#rewrite cgi请求到cgi.php上,并把cgi文件名作为php的pathinfo
rewrite ^/nagios/cgi-bin/(.*) /cgi.php/$1 break;
 
location /nagios/
{
    gzip off;
    alias /usr/local/nagios/share/;
    index index.html index.htm index.php;
}
 
location ~ .*\.php(\/.*)*$ {
    fastcgi_pass    127.0.0.1:9000;
    fastcgi_index   index.php;
    include fcgi.conf;
    fastcgi_param SCRIPT_FILENAME /usr/local/nagios/share$fastcgi_script_name;
 
    #pathinfo必须设置
    fastcgi_param  PATH_INFO $fastcgi_script_name;
 
    #以下两个为cgi.php需要用到的变量名,分别为cgi程序目录,和cgi默认index程序
    fastcgi_param  CGI_BASE  /usr/local/nagios/sbin;
    fastcgi_param  CGI_INDEX status.cgi;
}

注意上面配置文件的注释部分,在你自己设置的时候必须填上合适的值。下面就是最重要的cgi.php文件了

<?php
/*
   use php to execute mailman cgi app
   hack by 70 (magike.net@gmail.com)
   https://joyqi.com
 */
 
// get cgi base from fastcgi param
$cgi_base = '';
if (isset($_SERVER['CGI_BASE'])) {
    $cgi_base = rtrim($_SERVER['CGI_BASE'], '/') . '/';
} else {
    die('PLEASE CONFIGURE YOUR CGI_BASE PARAM');
}
 
// get pathinfo
$pathinfo = '';
if (isset($_SERVER['PATH_INFO'])) {
    $pathinfo = $_SERVER['PATH_INFO'];
} else if (isset($_SERVER['CGI_INDEX'])) {
    $pathinfo = $_SERVER['CGI_INDEX'];
} else {
    die('PLEASE CONFIGURE YOUR PATH_INFO PARAM');
}
 
// get real cgi path
$cgi_path = $cgi_base;
$cgi_file = trim($pathinfo, '/');
$cgi_file_levels = explode('/', $cgi_file);
$cgi_file_exists = false;
 
while (count($cgi_file_levels) > 0) {
    $cgi_path = $cgi_path . '/' . array_shift($cgi_file_levels);
 
    if (is_file($cgi_path)) {
        $cgi_file_exists = true;
        break;
    }
}
 
if (!$cgi_file_exists) {
    die('NOT EXISTS PAGE!' . $cgi_file);
}
 
$cgi_pathinfo = '';
if (!empty($cgi_file_levels)) {
    $cgi_pathinfo = '/' . implode('/', $cgi_file_levels);
}
 
if (is_readable($cgi_path)) {
 
    $descriptorspec = array(
            0 => array("pipe", "r"),  // stdin is a pipe that the child will read from
            1 => array("pipe", "w"),  // stdout is a pipe that the child will write to
            2 => array("file", "/tmp/error-output.txt", "a") // stderr is a file to write to
            );
 
    $cwd = $cgi_base;
    $env = $_ENV;
 
    $env['SCRIPT_FILENAME'] = $cgi_path;
    $env['SCRIPT_NAME'] = $cgi_file;
    $env['DOCUMENT_ROOT'] = CGI_BASE;
    $env['PATH_INFO'] = $cgi_pathinfo;
 
    // http auth support (nagios etc.)
    if (isset($_SERVER['PHP_AUTH_USER'])) {
        $env['REMOTE_USER'] = $_SERVER['PHP_AUTH_USER'];
    }
 
    $process = proc_open($cgi_path, $descriptorspec, $pipes, $cwd, $env);
    if (is_resource($process)) {
        $stdin = file_get_contents("php://input");
 
        if (!empty($stdin)) {
            fwrite($pipes[0], $stdin);
            fclose($pipes[0]);
        }
 
        //stream_set_blocking($pipes[1], 0);
        stream_set_timeout($pipes[1], 3);
        $result = stream_get_contents($pipes[1]);
        fclose($pipes[1]);
        $return_value = proc_close($process);
 
        list($header, $body) = preg_split("/\r?\n\r?\n/", $result, 2);
 
        $headers = explode("\n", $header);
        foreach ($headers as $line) {
            header(trim($line));
        }
 
        echo $body;
    } else {
        die('ERROR APPLICATION!');
    }
 
} else {
    die('ERROR PAGE!' . $cgi_path);
}

Nginx配置免费SSL证书StartSSL,解决Firefox不信任问题

先在StartSSL上申请免费一年的SSL证书,具体过程网上很多教程。然后把申请到的key和crt文件上传到服务器,比如/usr/local/nginx/certs/.

 Nginx配置SSL证书

直接贴上我的nginx的部分配置:

server {
        listen 443;
	server_name   liuzhichao.com www.liuzhichao.com ;
        ssl on;
        ssl_certificate /usr/local/nginx/ssl/ssl.crt;
        ssl_certificate_key /usr/local/nginx/ssl/ssl.key;if($http_transfer_encoding ~* chunked){return444;}

	gzip on;if(-d $request_filename){
		rewrite ^/(.*)([^/])$ $scheme://$host/$1$2/ permanent;}

	 root   /home/wwwroot/;

	 ssi off;
	 ssi_silent_errors off;
	 ssi_types text/shtml;

	 location /{
		 index  index.html index.htm index.shtml index.php;
		 autoindex	off;}

	location /nginx_status {
		stub_status on;
		access_log off;}

	 location ~(favicon.ico){ 
		 log_not_found off;
		 access_log   off;}

	 location ~* \.(gif|jpg|jpeg|png|bmp|swf)$ {
		 expires 1y;}

	 location ~* \.(js|css)$ {
		 expires 7d;}#------------
	 location ~*^(.+)\.(php[3-9]?|phtm[l]?)(\/.*)*$ {set $real_script_name $1.$2;set $path_info $3;if(!-f $document_root$real_script_name){return404;}

		  fastcgi_pass 127.0.0.1:8999; fastcgi_param HTTPS on;
		  include enable_php.conf;}}

现在重启Nginx,Chrome应该能正常显示Https.如果只想使用Https连接,可以再添加一个server,然后跳转到https

server {
        listen 80;
	server_name   liuzhichao.com www.liuzhichao.com ;
        rewrite     ^   https://$server_name$request_uri? permanent;}

 解决Firefox不信任StartSSL证书问题

wget http://cert.startssl.com/certs/ca.pem
wget http://cert.startssl.com/certs/sub.class1.server.ca.pem
cat ca.pem sub.class1.server.ca.pem >> ca-certs.crt
cat ca-certs.crt >> ssl.crt

再次重启Nginx,本想这下Firefox也应该能正常识别证书了,但是重启Nginx遇到了SSL: error:0906D066:PEM routines:PEM_read_bio:bad end line error错误。

[emerg]: SSL_CTX_use_certificate_chain_file("/usr/local/nginx/certs/ssl.crt")
 failed (SSL: error:0906D066:PEM routines:PEM_read_bio:bad end line error:140DC009:SSL routines:SSL_CTX_use_certificate_chain_file:PEM lib)
configuration file /usr/local/nginx/conf/nginx.conf test failed

这个的意思就是server.crt读取到意外错误行.这是因为我们在合并StartSSL提供的crt证书时,直接cat到了ssl.crt里。使用vi或者nano命令打开并编辑ssl.crt,找到:

-----END CERTIFICATE----------BEGIN CERTIFICATE-----

修改为:

-----END CERTIFICATE----------BEGIN CERTIFICATE-----

保存这个crt文件,再次重启Nginx服务,输入申请证书时私钥的密码,启动成功后,现在使用Firefox访问网站也能信任证书了。