Golang框架选型比较: goframe, beego, iris和gin

由于工作需要,这些年来也接触了不少的开发框架,Golang的开发框架比较多,不过基本都是Web”框架”为主。这里稍微打了个引号,因为大部分”框架”从设计和功能定位上来讲,充其量都只能算是一个组件,需要项目使用的话得自己四处再去找找其他的组件,或者自己造轮子。如果用于Web开发,这些”框架”的Web开发能力均已完备,无太大差别,且均是自标准库net/http.Server的二次封装。由于框架众多,这里笔者只选择了几个曾做过技术选型评估、较为熟悉,且目前比较流行和典型的Golang”框架”,从适用于业务项目开发框架的角度,做一个简单的横向比较,以便大家在项目框架选型时做个参考。

评估指标

指标
说明
指标
说明
基本介绍 来源各自官网。
模块化设计 是否支持模块化插拔设计、模块之间低耦合设计,是否可以独立使用其中某部分组件。
模块完善度 框架提供的功能模块是否丰富。模块能否能覆盖日常普遍的开发需求。
使用易用性 易用性不仅仅是值框架好不好用,更多是团队能否在低成本下快速接入,长期来看能否低成本维护。
文档完善性 参考官网提供的介绍资料,包括但不限于:文档、视频、示例、案例资料。同时,本地中文文档支持也是参考项。
工程化完备 是否能够快速接入项目开发,是否提供项目接入规范、设计模式、开发工具链,文档是否完善、源码是否易读、是否便于长期维护。
开发模式 框架适用的开发模式,或者官方推荐的开发模式。
工程规范 项目接入时的开发规范,如目录规范、设计规范、编码规范、命名规范等。
社区活跃 官方与社区沟通是否便捷,问题是否能够快速解答,BUG是否能够快速响应处理。
开发工具链 项目开发时使用到的CLI开发工具,如初始化项目、交叉编译、代码生成、swagger、热编译能力等等。
Web: 性能测试 来源第三方评测 https://github.com/the-benchmarker/web-frameworks 。
Web: 路由冲突处理 存在路由注册冲突时有无良好的解决方案,在业务项目开发中比较常见。
Web: 域名支持 Web路由是否支持域名绑定,甚至多域名的绑定。
Web: 文件服务 Web服务是否提供静态资源的访问能力。
Web: 优雅重启/关闭 Web服务在重启时不会影响请求执行,关闭时会等待正在执行的请求处理完,新请求不再接入。
ORM 框架是否自带ORM组件,ORM组件是业务项目的核心组件。无论是自研还是通过第三方组件引入。
Session 框架是否提供会话管理组件,无论是通用型Session组件,还是仅针对于Web服务的Session组件。
I18N支持 国际化组件支持(常用但非核心组件)。
配置管理 配置管理也是框架需要完备的核心组件能力。
日志组件 日志组件也是框架需要完备的核心组件能力。
数据校验 数据校验也是框架需要完备的核心组件能力。
缓存管理 缓存管理也是框架需要完备的核心组件能力。无论是内存还是Redis,无论是自研还是通过第三方组件引入。
资源打包 支持将依赖的文件资源例如静态资源、配置文件等固定文件编译到可执行文件中。框架组件自动支持资源检索。
链路跟踪 框架是否具备分布式链路跟踪能力,分布式跟踪在微服务架构中是必不可少的能力。
测试框架 框架是否支持单元测试接入,提供单元测试接入规范。无论是使用标准库还是第三方测试框架。
突出优点 比较明显的几点优点。
突出缺点 比较明显的几点缺点。

横向比较

  • 以下部分对比参数涉及评分的部分,满分总共按照10分为标准。
  • 如果标记为”-“的部分,表示不支持或者需要引入第三方插件支持。
  • 以下特性如果官网提供文档则直接提供文档地址,找不到文档但是笔者知道有就会简单标注。
  • 各个”框架”功能特性实现不同,在文档、功能、易用性上存在较大差异,各位朋友可自行查阅链接。

 

GoFrame
Beego
Iris
Gin
比较版本 v1.15.2 v1.12.3 v12.0.2 v1.6.3
项目类型 开源(国内) 开源(国内) 开源(海外) 开源(海外)
开源协议 MIT Apache-2 BSD-3-Clause MIT
框架类型 模块化框架 Web框架 Web”框架” Web”框架”
基本介绍 工程完备、简单易用,模块化、高质量、高性能、企业级开发框架。 最简单易用的企业级Go应用开发框架。 目前发展最快的Go Web框架。提供完整的MVC功能并且面向未来。 一个Go语言写的HTTP Web框架。它提供了Martini风格的API并有更好的性能。
项目地址 github.com/gogf/gf github.com/beego/beego github.com/kataras/iris github.com/gin-gonic/gin
官网地址 goframe.org beego.me iris-go.com gin-gonic.com
模块化设计
模块完善度 10 6 4 2
使用易用性 9 9 9 10
文档完善度 10 8 6 4
工程化完备 10 8 5 1
社区活跃 9 10 9 10
开发模式 模块引入三层架构、MVC MVC MVC
工程规范 分层设计对象设计 项目结构
开发工具链 gf工具链 bee工具链
Web: 性能测试 8 8 8 9
Web: HTTPS HTTPS & TLS 支持 CustomHttpConfiguration 支持
Web: HTTP2 支持 支持
Web: WebSocket WebSocket服务
Web: 分组路由 路由注册-分组路由 Namespace GroupingRoutes
Web: 路由冲突处理
Web: 域名支持 域名绑定
Web: 文件服务 静态文件服务 静态文件处理 ServingStaticFiles
Web: 多端口/实例 多端口监听多实例监听 RunMultipleServiceUsingIris
Web: 优雅重启/关闭 平滑重启特性 热升级 GracefulShutdownOrRestart GracefulRestartOrStop
ORM ORM文档 ORM文档
Session Session Session
I18N支持 I18N I18N Localization
模板引擎 模板引擎 View设计 TemplateRendering HtmlRendering
配置管理 配置管理 参数配置 CustomHttpConfig
日志组件 日志组件 Logging
数据校验 数据校验 表单数据验证 CustomValidators
缓存管理 缓存管理 Cache
资源打包 资源管理 bee工具bale命令
链路跟踪 链路跟踪
测试框架 单元测试 Testing Testing
突出优点 goframe主要以工程化和企业级方向为主,特别是模块化设计和工程化设计思想非常棒。针对业务项目而言,提供了开发规范、项目规范、命名规范、设计模式、开发工具链、丰富的模块、高质量代码和文档,社区活跃。作者也是资深的PHP开发者,PHP转Go的小伙伴会倍感亲切。 beego开源的比较早,最早的一款功能比较全面的Golang开发框架,一直在Golang领域有着比较大的影响力,作者谢大多年组织着国内影响力比较大GopherCN活动。beego有着比较丰富的开发模块、开箱即用,提供了基于MVC设计模式的项目结构、开发工具链,主要定位为Web开发,当然也可以用于非Web项目开发。 iris主要侧重于Web开发,提供了Web开发的一系列功能组件,基于MVC开发模式。iris这一年发展比较快,从一个Web Server的组件,也慢慢朝着beego的设计方向努力。 gin专注于轻量级的Web Server,比较简单,易于理解,路由和中间件设计不错,可以看做替代标准库net/http.Server的路由加强版web server。献给爱造轮子的朋友们。
突出缺点 开源时间较晚,推广过于佛系,目前主要面向国内用户,未推广海外。 起步较早,自谢大创业后,近几年发展较慢。非模块化设计,对第三方重量级模块依赖较多。 号称性能最强,结果平平。非模块化设计。最近两年开始朝beego方向发展,但整体框架能力还不完备,需要加油。 功能简单易用,既是优点,也是缺点。

经验分享

不同的需求场景,存在不同的选择。选择适合的工具,解决适合的问题。

开源不存在孰好孰坏之分,开源作者能够本着开源精神给大家分享技术成果用以学习和使用,这本身就是一件非常不易并且值得称道的事情。

最后,笔者在这里跟大家分享一下自己所在团队的情况,以及在Golang技术栈转型过程中所走的弯路,希望能在框架选型这一环节,能给大家作一定参考。

团队最初痛点

团队转型Golang技术栈的一些背景。主要几点:

  1. 团队后端最初的主要技术栈为PHP,由于业务发展需要,进行微服务改造。第一版微服务采用了PHP+JsonRpc的通信方式。
  2. 随着项目增多,公司也组件了自己的DevOps团队,底层部署转向了Docker+Kubernetes容器架构,并且引入了Golang技术栈。
  3. 由于一些痛点,通过一段时间对PHPGolang的比较,团队决定快速转型Golang技术栈,主要痛点如下:
    1. PHP项目在业务复杂后、项目中后期的开发和维护成本整体偏高。主要原因还是其过高的灵活性,非结构化的变量设计,参差不齐的开发人员素质。
    2. 上云容器化部署后,PHPDevOps效率太低。复杂的Composer版本管理,超大的Docker镜像大小,都影响着DevOps的效率。相比较而言,Golang效率极其高效。
    3. JsonRpc通信协议设计下,接口的扩展性和灵活性很高,但服务之间很难快速确定接口的输入与输出定义,只能根据文档和示例进行对接和维护。由于代码和文档分离,大部分场景下接口文档维护往往滞后于接口变化。随着服务的不断增加,非结构化的通信协议管理使得服务接口的开发和维护成本进一步提高。
    4. JsonRpc的通信协议本质基于HTTP1.x+Json,执行效率过低,算不上真正的微服务通信协议,很难对接上主流的服务治理框架。相比较基于HTTP2.xgRPC协议有着成熟微服务开发框架和服务治理解决方案。
    5. 业务梳理的考量,PHPGolang技术栈的迁移,其实也是一次技术重构的契机,在技术重构的过程中也重新梳理业务系统设计,偿还技术债务。

进一步的痛点

Golang确实足够简单,相比较其他的解释类开发语言,没有过多的语法糖和语言特性,因此团队上手很快,并快速完成了一部分业务系统的技术重构。但随之而来的是更加严重的痛点。主要几点:

  1. 轮子过多:Golang实在太简单了,以至于我们的团队成员爆发了压抑许久的闷骚劲,充分发挥”造后不管”的造轮精神,开发/封装了许多大大小小的轮子。这些轮子均能满足最基本的功能,例如:日志、配置、缓存等等。但轮子并不是实现一个基础功能的半成品就了事,需要保证功能性、稳定性、扩展性和可维护性,要能结合更多生产实践验证,更需要能够长期维护、持续进行迭代改进。否则,就是一堆大小不一的成人玩具。造轮一时爽,维护火葬场。直到现在,我们还在为分散在100多个Golang项目中的数十个成人玩具做大统一的事情痛苦不已。当然,这个问题也跟组织架构和团队管理也有很大关系。
  2. 不成体系:
    1. 我们坚信一个package只做一件事情,并且特地使用单仓库包的形式进行包管理,相当于每个package都是独立维护的git仓库。其实单仓库包package设计并不存在必要性,反而独立的单仓库包提高了组件和框架的维护成本。
    2. 这种单仓库包设计难以形成技术体系,在团队技术管理上,难以形成统一的技术框架。单仓包显得很孤立,而一个技术体系的建立除了需要制定规范和标准,更需要技术框架来准确落地。一个成体系的、统一的技术框架,至少涉及到数十个基础技术组件,不可能完全孤立设计。每一个package的基础功能实现都很简单,但是如何能够统一组织在一起却不是一件简单的事情,这需要团队的技术管理者需要有一定的技术底蕴、格局和前瞻性,而不是和普通开发者那样眼界只能局限于package本身。
    3. 这种孤立的单仓库包设计,对于业务项目的规范化约束不强,每一个组件都可以独立替换,也至于痛点1的问题越发严重(连日志组件都好几套,虽然都满足基本的日志规范设计)。最终,我们最初引以为傲的单仓库包设计,最终变成了一堆散沙。例如,就连需要增加标准化的链路跟踪功能,由于单仓库包过于散乱和不统一,使得推进改进成本极其高昂。
    4. 除了使得技术体系难以建立,技术规范难以准确落地之外,每个组件的易用性也设计得较差。举个简单例子,我们的日志组件、缓存组件、数据库组件、HTTP/gRPC Server组件都需要对接配置管理功能,单仓包需要保证低耦合设计,因此开发者在使用的时候需要先手动读取配置、并转换为目标配置对象、并注入到对应的组件初始化方法中,随后才能将该对象运用到业务逻辑中,若干个业务项目均是重复此步骤。其实goframe在这块的易用性设计就挺不错,每个包当然是独立设计的,在统一的技术框架体系下,再独立提供一个耦合的单例模块将常用的对象进行单例化封装,自动实现配置读取、配置对象转换、配置对象注入及组件对象初始化,开发者仅需要调用一个单例方法即可。而这个常用单例模块,成为了我们技术框架体系的一部分,极大地提高了业务项目的开发和维护效率。
  3. 版本不一致:在业务项目不断增多之后,轮子版本不一致性也越来越明显。什么是版本不一致?举个例子。我们有个轮子叫做httpClient,总共发布了10来个版本;我们总共有100多个Golang项目,几乎每个版本都在使用。我们提交了一个bug fix,却难以让所有项目都能更新。对其他的轮子也是类似的情况,况且我们也有数十个各种轮子,被各个项目独立使用中。

经过反思总结,总结了以下几点:

  1. 团队需要一个统一的技术框架,而不是东拼西凑的一堆单仓库包。
  2. 我们只需要维护一个框架的版本,而不是维护数十个单仓库包的版本。
  3. 框架的组件必须模块化、低耦合设计,保证内部组件也可以独立引用。
  4. 核心组件严格禁止单仓库包设计,并且必须由框架统一维护。

最终的抉择

走过这么多弯路之后,我们决心建立一套成体系的Golang开发框架。除了要求团队能够快速学习,维护成本低,并且我们最主要的诉求,是核心组件不能是半成品,框架必须是上过大规模生产验证的,稳定和成熟的。随着,我们重新对行业中流行的技术框架做了技术评估,包括上面说的那些框架。原本的初衷是想将内部的各个轮子统一做一个成体系的框架,在开源项目中找一些有价值的参考。

后来找到了goframe,仔细评估和学习了框架设计,发现框架设计思想和我们的经验总结如出一则!

这里不得不提一件尴尬的事情。其实最开始转Golang之前(2019年中旬)也做过一些调研,那时goframe版本还不高,并且我们负责评估的团队成员有一种先入为主的思想,看到模块和文档这么多,感觉应该挺复杂,性能应该不高,于是没怎么看就PASS。后来选择一些看起来简单的开源轮子自己做了些二次封装。

这次经过一段时间的仔细调研和源码学习,得出一个结论,goframe框架的框架架构、模块化和工程化设计思想非常棒,执行效率很高,模块不仅丰富,而且质量之高,令人惊叹至极!相比较我们之前写的那些半成品轮子,简直就是小巫见大巫。团队踩了一年多的坑,才发现团队确实需要一个统一的技术框架而不是一堆不成体系的轮子,其实人家早已给了一条明光大道,并且一直在前面默默努力。

经过团队内部的调研和讨论,我们决定使用goframe逐步重构我们的业务项目。由于goframe是模块化设计的,因此我们也可以对一些模块做必要的替换。重构过程比较顺利,基础技术框架的重构并不会对业务逻辑造成什么影响,反而通过goframe的工程化思想和很棒的开发工具链,在统一技术框架后,极大地提高了项目的开发和维护效率,使得团队可以专心于业务开发,部门也陆续有了更多的产出。目前我们已经有大部门业务项目专向了goframe。平台每日流量千万级别。

最后,感谢开源作者们的默默贡献!我们也在努力推动团队秉着来源社区,回馈社区的思想,未来也会更多参与社区贡献。

Golang 微框架 Gin 简介

所谓框架

框架一直是敏捷开发中的利器,能让开发者很快的上手并做出应用,甚至有的时候,脱离了框架,一些开发者都不会写程序了。成长总不会一蹴而就,从写出程序获取成就感,再到精通框架,快速构造应用,当这些方面都得心应手的时候,可以尝试改造一些框架,或是自己创造一个。

曾经我以为Python世界里的框架已经够多了,后来发现相比golang简直小巫见大巫。golang提供的net/http库已经很好了,对于http的协议的实现非常好,基于此再造框架,也不会是难事,因此生态中出现了很多框架。既然构造框架的门槛变低了,那么低门槛同样也会带来质量参差不齐的框架。

考察了几个框架,通过其github的活跃度,维护的team,以及生产环境中的使用率。发现Gin还是一个可以学习的轻巧框架。

Gin

Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确,已经发布了1.0版本。具有快速灵活,容错方便等特点。其实对于golang而言,web框架的依赖要远比Python,Java之类的要小。自身的net/http足够简单,性能也非常不错。框架更像是一些常用函数或者工具的集合。借助框架开发,不仅可以省去很多常用的封装带来的时间,也有助于团队的编码风格和形成规范。

在 Go语言开发的 Web 框架中,有两款著名 Web 框架分别是 Martini 和 Gin,两款 Web 框架相比较的话,Gin 自己说它比 Martini 要强很多。

Gin 是 Go语言写的一个 web 框架,它具有运行速度快,分组的路由器,良好的崩溃捕获和错误处理,非常好的支持中间件和 json。总之在 Go语言开发领域是一款值得好好研究的 Web 框架,开源网址:https://github.com/gin-gonic/gin

首先需要安装,安装比较简单,使用go get即可:

go get gopkg.in/gin-gonic/gin.v1

Hello World

使用Gin实现Hello world非常简单,创建一个router,然后使用其Run的方法:

import (
    "gopkg.in/gin-gonic/gin.v1"
    "net/http"
)

func main(){
    
    router := gin.Default()

    router.GET("/", func(c *gin.Context) {
        c.String(http.StatusOK, "Hello World")
    })
    router.Run(":8000")
}

简单几行代码,就能实现一个web服务。使用gin的Default方法创建一个路由handler。然后通过HTTP方法绑定路由规则和路由函数。不同于net/http库的路由函数,gin进行了封装,把request和response都封装到gin.Context的上下文环境。最后是启动路由的Run方法监听端口。麻雀虽小,五脏俱全。当然,除了GET方法,gin也支持POST,PUT,DELETE,OPTION等常用的restful方法。

restful路由

gin的路由来自httprouter库。因此httprouter具有的功能,gin也具有,不过gin不支持路由正则表达式:

func main(){
    router := gin.Default()
    
    router.GET("/user/:name", func(c *gin.Context) {
        name := c.Param("name")
        c.String(http.StatusOK, "Hello %s", name)
    })
}
冒号:加上一个参数名组成路由参数。可以使用c.Params的方法读取其值。当然这个值是字串string。诸如/user/rsj217,和/user/hello都可以匹配,而/user//user/rsj217/不会被匹配。
curl http://127.0.0.1:8000/user/rsj217
Hello rsj217%                                                                 curl http://127.0.0.1:8000/user/rsj217/
404 page not found%                                                                curl http://127.0.0.1:8000/user/
404 page not found%
冒号:加上一个参数名组成路由参数。可以使用c.Params的方法读取其值。当然这个值是字串string。诸如/user/rsj217,和/user/hello都可以匹配,而/user//user/rsj217/不会被匹配。
curl http://127.0.0.1:8000/user/rsj217
Hello rsj217%                                                                 curl http://127.0.0.1:8000/user/rsj217/
404 page not found%                                                              curl http://127.0.0.1:8000/user/
404 page not found%

除了:,gin还提供了*号处理参数,*号能匹配的规则就更多。

func main(){
    router := gin.Default()
    
    router.GET("/user/:name/*action", func(c *gin.Context) {
        name := c.Param("name")
        action := c.Param("action")
        message := name + " is " + action
        c.String(http.StatusOK, message)
    })
}

访问效果如下

curl http://127.0.0.1:8000/user/rsj217/
rsj217 is /%                                                                  curl http://127.0.0.1:8000/user/rsj217/中国
rsj217 is /中国%

query string参数与body参数

web提供的服务通常是client和server的交互。其中客户端向服务器发送请求,除了路由参数,其他的参数无非两种,查询字符串query string和报文体body参数。所谓query string,即路由用,用?以后连接的key1=value2&key2=value2的形式的参数。当然这个key-value是经过urlencode编码。

query string

对于参数的处理,经常会出现参数不存在的情况,对于是否提供默认值,gin也考虑了,并且给出了一个优雅的方案:

func main(){
    router := gin.Default()
    router.GET("/welcome", func(c *gin.Context) {
        firstname := c.DefaultQuery("firstname", "Guest")
        lastname := c.Query("lastname")

        c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
    })
  router.Run()
}

使用c.DefaultQuery方法读取参数,其中当参数不存在的时候,提供一个默认值。使用Query方法读取正常参数,当参数不存在的时候,返回空字串:

curl http://127.0.0.1:8000/welcome
Hello Guest %                                                                 curl http://127.0.0.1:8000/welcome\?firstname\=中国
Hello 中国 %                                                                  curl http://127.0.0.1:8000/welcome\?firstname\=中国\&lastname\=天朝
Hello 中国 天朝%                                                              curl http://127.0.0.1:8000/welcome\?firstname\=\&lastname\=天朝
Hello  天朝%
curl http://127.0.0.1:8000/welcome\?firstname\=%E4%B8%AD%E5%9B%BD
Hello 中国 %

之所以使用中文,是为了说明urlencode。注意,当firstname为空字串的时候,并不会使用默认的Guest值,空值也是值,DefaultQuery只作用于key不存在的时候,提供默认值。

body

http的报文体传输数据就比query string稍微复杂一点,常见的格式就有四种。例如application/jsonapplication/x-www-form-urlencoded, application/xmlmultipart/form-data。后面一个主要用于图片上传。json格式的很好理解,urlencode其实也不难,无非就是把query string的内容,放到了body体里,同样也需要urlencode。默认情况下,c.PostFROM解析的是x-www-form-urlencodedfrom-data的参数。

func main(){
    router := gin.Default()
    router.POST("/form_post", func(c *gin.Context) {
        message := c.PostForm("message")
        nick := c.DefaultPostForm("nick", "anonymous")

        c.JSON(http.StatusOK, gin.H{
            "status":  gin.H{
                "status_code": http.StatusOK,
                "status":      "ok",
            },
            "message": message,
            "nick":    nick,
        })
    })
}

与get处理query参数一样,post方法也提供了处理默认参数的情况。同理,如果参数不存在,将会得到空字串。

curl -X POST http://127.0.0.1:8000/form_post -H "Content-Type:application/x-www-form-urlencoded" -d "message=hello&nick=rsj217" | python -m json.tool
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   104  100    79  100    25  48555  15365 --:--:-- --:--:-- --:--:-- 79000
{
    "message": "hello",
    "nick": "rsj217",
    "status": {
        "status": "ok",
        "status_code": 200
    }
}

发送数据给服务端,并不是post方法才行,put方法一样也可以。同时querystring和body也不是分开的,两个同时发送也可以:

func main(){
    router := gin.Default()
    
    router.PUT("/post", func(c *gin.Context) {
        id := c.Query("id")
        page := c.DefaultQuery("page", "0")
        name := c.PostForm("name")
        message := c.PostForm("message")
        fmt.Printf("id: %s; page: %s; name: %s; message: %s \n", id, page, name, message)
        c.JSON(http.StatusOK, gin.H{
            "status_code": http.StatusOK,
        })
    })
}

上面的例子,展示了同时使用查询字串和body参数发送数据给服务器。

文件上传

上传单个文件

前面介绍了基本的发送数据,其中multipart/form-data转用于文件上传。gin文件上传也很方便,和原生的net/http方法类似,不同在于gin把原生的request封装到c.Request中了。

func main(){
    router := gin.Default()
    
    router.POST("/upload", func(c *gin.Context) {
        name := c.PostForm("name")
        fmt.Println(name)
        file, header, err := c.Request.FormFile("upload")
        if err != nil {
            c.String(http.StatusBadRequest, "Bad request")
            return
        }
        filename := header.Filename

        fmt.Println(file, err, filename)

        out, err := os.Create(filename)
        if err != nil {
            log.Fatal(err)
        }
        defer out.Close()
        _, err = io.Copy(out, file)
        if err != nil {
            log.Fatal(err)
        }
        c.String(http.StatusCreated, "upload successful")
    })
    router.Run(":8000")
}

使用c.Request.FormFile解析客户端文件name属性。如果不传文件,则会抛错,因此需要处理这个错误。一种方式是直接返回。然后使用os的操作,把文件数据复制到硬盘上。

使用下面的命令可以测试上传,注意upload为c.Request.FormFile指定的参数,其值必须要是绝对路径:

curl -X POST http://127.0.0.1:8000/upload -F "upload=@/Users/ghost/Desktop/pic.jpg" -H "Content-Type: multipart/form-data"

表单上传

上面我们使用的都是curl上传,实际上,用户上传图片更多是通过表单,或者ajax和一些requests的请求完成。下面展示一下web的form表单如何上传。

我们先要写一个表单页面,因此需要引入gin如何render模板。前面我们见识了c.String和c.JSON。下面就来看看c.HTML方法。

首先需要定义一个模板的文件夹。然后调用c.HTML渲染模板,可以通过gin.H给模板传值。至此,无论是String,JSON还是HTML,以及后面的XML和YAML,都可以看到Gin封装的接口简明易用。

创建一个文件夹templates,然后再里面创建html文件upload.html:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>upload</title>
</head>
<body>
<h3>Single Upload</h3>
<form action="/upload", method="post" enctype="multipart/form-data">
<input type="text" value="hello gin" />
<input type="file" name="upload" />
<input type="submit" value="upload" />
</form>


<h3>Multi Upload</h3>
<form action="/multi/upload", method="post" enctype="multipart/form-data">
<input type="text" value="hello gin" />
<input type="file" name="upload" />
<input type="file" name="upload" />
<input type="submit" value="upload" />
</form>

</body>
</html>

upload 很简单,没有参数。一个用于单个文件上传,一个用于多个文件上传。

    router.LoadHTMLGlob("templates/*")
    router.GET("/upload", func(c *gin.Context) {
        c.HTML(http.StatusOK, "upload.html", gin.H{})
    })

使用LoadHTMLGlob定义模板文件路径。

参数绑定

我们已经见识了x-www-form-urlencoded类型的参数处理,现在越来越多的应用习惯使用JSON来通信,也就是无论返回的response还是提交的request,其content-type类型都是application/json的格式。而对于一些旧的web表单页还是x-www-form-urlencoded的形式,这就需要我们的服务器能改hold住这多种content-type的参数了。

Python的世界里很好解决,毕竟动态语言不需要实现定义数据模型。因此可以写一个装饰器将两个格式的数据封装成一个数据模型。golang中要处理并非易事,好在有gin,他们的model bind功能非常强大。

 

type User struct {
    Username string `form:"username" json:"username" binding:"required"`
    Passwd   string `form:"passwd" json:"passwd" bdinding:"required"`
    Age      int    `form:"age" json:"age"`
}

func main(){
    router := gin.Default()
    
    router.POST("/login", func(c *gin.Context) {
        var user User
        var err error
        contentType := c.Request.Header.Get("Content-Type")

        switch contentType {
        case "application/json":
            err = c.BindJSON(&user)
        case "application/x-www-form-urlencoded":
            err = c.BindWith(&user, binding.Form)
        }

        if err != nil {
            fmt.Println(err)
            log.Fatal(err)
        }

        c.JSON(http.StatusOK, gin.H{
            "user":   user.Username,
            "passwd": user.Passwd,
            "age":    user.Age,
        })

    })

}

先定义一个User模型结构体,然后针对客户端的content-type,一次使BindJSONBindWith方法。

curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/x-www-form-urlencoded" -d "username=rsj217&passwd=123&age=21" | python -m json.tool
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    79  100    46  100    33  41181  29543 --:--:-- --:--:-- --:--:-- 46000
{
    "age": 21,
    "passwd": "123",
    "username": "rsj217"
}
curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/x-www-form-urlencoded" -d "username=rsj217&passwd=123&new=21" | python -m json.tool
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    78  100    45  100    33  37751  27684 --:--:-- --:--:-- --:--:-- 45000
{
    "age": 0,
    "passwd": "123",
    "username": "rsj217"
}
curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/x-www-form-urlencoded" -d "username=rsj217&new=21" | python -m json.tool
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0curl: (52) Empty reply from server
No JSON object could be decoded

可以看到,结构体中,设置了binding标签的字段(username和passwd),如果没传会抛错误。非banding的字段(age),对于客户端没有传,User结构会用零值填充。对于User结构没有的参数,会自动被忽略。

改成json的效果类似:

curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/json" -d '{"username": "rsj217", "passwd": "123", "age": 21}' | python -m json.tool
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    96  100    46  100    50  32670  35511 --:--:-- --:--:-- --:--:-- 50000
{
    "age": 21,
    "passwd": "123",
    "username": "rsj217"
}
curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/json" -d '{"username": "rsj217", "passwd": "123", "new": 21}' | python -m json.tool
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    95  100    45  100    50  49559  55066 --:--:-- --:--:-- --:--:-- 50000
{
    "age": 0,
    "passwd": "123",
    "username": "rsj217"
}
curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/json" -d '{"username": "rsj217", "new": 21}' | python -m json.tool
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0curl: (52) Empty reply from server
No JSON object could be decoded
curl -X POST http://127.0.0.1:8000/login -H "Content-Type:application/json" -d '{"username": "rsj217", "passwd": 123, "new": 21}' | python -m json.tool
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0curl: (52) Empty reply from server
No JSON object could be decoded

当然,gin还提供了更加高级方法,c.Bind,它会更加content-type自动推断是bind表单还是json的参数。

    router.POST("/login", func(c *gin.Context) {
        var user User
        
        err := c.Bind(&user)
        if err != nil {
            fmt.Println(err)
            log.Fatal(err)
        }

        c.JSON(http.StatusOK, gin.H{
            "username":   user.Username,
            "passwd":     user.Passwd,
            "age":        user.Age,
        })

    })

多格式渲染

既然请求可以使用不同的content-type,响应也如此。通常响应会有html,text,plain,json和xml等。
gin提供了很优雅的渲染方法。到目前为止,我们已经见识了c.String, c.JSON,c.HTML,下面介绍一下c.XML。

    router.GET("/render", func(c *gin.Context) {
        contentType := c.DefaultQuery("content_type", "json")
        if contentType == "json" {
            c.JSON(http.StatusOK, gin.H{
                "user":   "rsj217",
                "passwd": "123",
            })
        } else if contentType == "xml" {
            c.XML(http.StatusOK, gin.H{
                "user":   "rsj217",
                "passwd": "123",
            })
        }

    })

结果如下:

curl http://127.0.0.1:8000/render\?content_type\=json
{"passwd":"123","user":"rsj217"}
curl http://127.0.0.1:8000/render\?content_type\=xml
<map><user>rsj217</user><passwd>123</passwd></map>%

重定向

gin对于重定向的请求,相当简单。调用上下文的Redirect方法:

    router.GET("/redict/google", func(c *gin.Context) {
        c.Redirect(http.StatusMovedPermanently, "https://google.com")
    })

分组路由

熟悉Flask的同学应该很了解蓝图分组。Flask提供了蓝图用于管理组织分组api。gin也提供了这样的功能,让你的代码逻辑更加模块化,同时分组也易于定义中间件的使用范围。

    v1 := router.Group("/v1")

    v1.GET("/login", func(c *gin.Context) {
        c.String(http.StatusOK, "v1 login")
    })

    v2 := router.Group("/v2")

    v2.GET("/login", func(c *gin.Context) {
        c.String(http.StatusOK, "v2 login")
    })

访问效果如下:

curl http://127.0.0.1:8000/v1/login
v1 login%                                                                                 
curl http://127.0.0.1:8000/v2/login
v2 login%

middleware中间件

golang的net/http设计的一大特点就是特别容易构建中间件。gin也提供了类似的中间件。需要注意的是中间件只对注册过的路由函数起作用。对于分组路由,嵌套使用中间件,可以限定中间件的作用范围。中间件分为全局中间件,单个路由中间件和群组中间件。

全局中间件

先定义一个中间件函数:

func MiddleWare() gin.HandlerFunc {
    return func(c *gin.Context) {
        fmt.Println("before middleware")
        c.Set("request", "clinet_request")
        c.Next()
        fmt.Println("before middleware")
    }
}

该函数很简单,只会给c上下文添加一个属性,并赋值。后面的路由处理器,可以根据被中间件装饰后提取其值。需要注意,虽然名为全局中间件,只要注册中间件的过程之前设置的路由,将不会受注册的中间件所影响。只有注册了中间件一下代码的路由函数规则,才会被中间件装饰。

    router.Use(MiddleWare())
    {
        router.GET("/middleware", func(c *gin.Context) {
            request := c.MustGet("request").(string)
            req, _ := c.Get("request")
            c.JSON(http.StatusOK, gin.H{
                "middile_request": request,
                "request": req,
            })
        })
    }

使用router装饰中间件,然后在/middlerware即可读取request的值,注意在router.Use(MiddleWare())代码以上的路由函数,将不会有被中间件装饰的效果。

curl  http://127.0.0.1:8000/middleware
{"middile_request":"clinet_request","request":"clinet_request"}

如果没有注册就使用MustGet方法读取c的值将会抛错,可以使用Get方法取而代之。

上面的注册装饰方式,会让所有下面所写的代码都默认使用了router的注册过的中间件。

单个路由中间件

当然,gin也提供了针对指定的路由函数进行注册。

    router.GET("/before", MiddleWare(), func(c *gin.Context) {
        request := c.MustGet("request").(string)
        c.JSON(http.StatusOK, gin.H{
            "middile_request": request,
        })
    })

把上述代码写在 router.Use(Middleware())之前,同样也能看见/before被装饰了中间件。

群组中间件

群组的中间件也类似,只要在对于的群组路由上注册中间件函数即可:

authorized := router.Group("/", MyMiddelware())
// 或者这样用:
authorized := router.Group("/")
authorized.Use(MyMiddelware())
{
    authorized.POST("/login", loginEndpoint)
}

群组可以嵌套,因为中间件也可以根据群组的嵌套规则嵌套。

中间件实践

中间件最大的作用,莫过于用于一些记录log,错误handler,还有就是对部分接口的鉴权。下面就实现一个简易的鉴权中间件。

    router.GET("/auth/signin", func(c *gin.Context) {
        cookie := &http.Cookie{
            Name:     "session_id",
            Value:    "123",
            Path:     "/",
            HttpOnly: true,
        }
        http.SetCookie(c.Writer, cookie)
        c.String(http.StatusOK, "Login successful")
    })

    router.GET("/home", AuthMiddleWare(), func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"data": "home"})
    })

登录函数会设置一个session_id的cookie,注意这里需要指定path为/,不然gin会自动设置cookie的path为/auth,一个特别奇怪的问题。/homne的逻辑很简单,使用中间件AuthMiddleWare注册之后,将会先执行AuthMiddleWare的逻辑,然后才到/home的逻辑。

AuthMiddleWare的代码如下:

func AuthMiddleWare() gin.HandlerFunc {
    return func(c *gin.Context) {
        if cookie, err := c.Request.Cookie("session_id"); err == nil {
            value := cookie.Value
            fmt.Println(value)
            if value == "123" {
                c.Next()
                return
            }
        }
        c.JSON(http.StatusUnauthorized, gin.H{
            "error": "Unauthorized",
        })
        c.Abort()
        return
    }
}

从上下文的请求中读取cookie,然后校对cookie,如果有问题,则终止请求,直接返回,这里使用了c.Abort()方法。

In [7]: resp = requests.get('http://127.0.0.1:8000/home')

In [8]: resp.json()
Out[8]: {u'error': u'Unauthorized'}

In [9]: login = requests.get('http://127.0.0.1:8000/auth/signin')

In [10]: login.cookies
Out[10]: <RequestsCookieJar[Cookie(version=0, name='session_id', value='123', port=None, port_specified=False, domain='127.0.0.1', domain_specified=False, domain_initial_dot=False, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={'HttpOnly': None}, rfc2109=False)]>

In [11]: resp = requests.get('http://127.0.0.1:8000/home', cookies=login.cookies)

In [12]: resp.json()
Out[12]: {u'data': u'home'}

异步协程

golang的高并发一大利器就是协程。gin里可以借助协程实现异步任务。因为涉及异步过程,请求的上下文需要copy到异步的上下文,并且这个上下文是只读的。

    router.GET("/sync", func(c *gin.Context) {
        time.Sleep(5 * time.Second)
        log.Println("Done! in path" + c.Request.URL.Path)
    })

    router.GET("/async", func(c *gin.Context) {
        cCp := c.Copy()
        go func() {
            time.Sleep(5 * time.Second)
            log.Println("Done! in path" + cCp.Request.URL.Path)
        }()
    })

在请求的时候,sleep5秒钟,同步的逻辑可以看到,服务的进程睡眠了。异步的逻辑则看到响应返回了,然后程序还在后台的协程处理。

自定义router

gin不仅可以使用框架本身的router进行Run,也可以配合使用net/http本身的功能:

func main() {
    router := gin.Default()
    http.ListenAndServe(":8080", router)
}

或者

func main() {
    router := gin.Default()

    s := &http.Server{
        Addr:           ":8000",
        Handler:        router,
        ReadTimeout:    10 * time.Second,
        WriteTimeout:   10 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }
    s.ListenAndServe()
}

当然还有一个优雅的重启和结束进程的方案。后面将会探索使用supervisor管理golang的进程。

总结

Gin是一个轻巧而强大的golang web框架。涉及常见开发的功能,我们都做了简单的介绍。关于服务的启动,请求参数的处理和响应格式的渲染,以及针对上传和中间件鉴权做了例子。更好的掌握来自实践,同时gin的源码注释很详细,可以阅读源码了解更多详细的功能和魔法特性。

php8 源码编译安装

简述

PHP 团队于2020年11月26日宣布 PHP 8 正式发布!这意味着将不会有 PHP 7.5 版本。PHP8 目前正处于非常活跃的开发阶段,所以在接下来的几个月里,情况可能会发生很大的变化。我也分享一些研究PHP 8 的心得,希望PHPer大家一起共同进步。首先说一下最受关注的JIT。

JIT

由于 PHP 8 是一个新的大版本,因此升级版本,代码被破坏的可能性更高。如果项目始终保持运行 PHP 的最新版本,那么升级相对来说就会轻松很多,因为在 7. * 版本中,大多数重大更改均已弃用。除重大更改外,PHP 8 还带来了一些不错的新功能,比如说 JIT 编译器 , 联合类型 , 属性,以及更多。很多人可能对JIT有很深的误解,觉得引入JIT之后性能就能提高10倍跟V8平起平坐了,事实上不是这样的。JIT技术的水很深,动态语言的JIT尤其困难,V8的诞生几乎可以说是一个技术奇迹。以PHP社区的技术水平,我谨慎地不看好他们解决这个问题的能力,毕竟Facebook的HHVM也没有完全解决,最后是靠Hacklang补全PHP的语法功能之后才基本圆满解决的。

 

动态语言的JIT本质要解决的问题之中,生成汇编只是一小部分,对于弱类型和动态类型语言来说,优化内存布局也是重点。例如,对于JavaScript和Python来说,以前对象内部是一个HashMap,这种数据结构的访问效率比较低,导致访问对象的每个属性都很慢,在JIT之后会将它优化成类似C++的平铺式的布局,将属性的值按顺序放在特定的位置上,这就带来一些新的要求:

  1. 没有类型标注的情况下,JIT只能猜测类型而无法肯定,那么使用优化的类型布局之前需要进行额外的检测,判断是否的确为预想的类型;

  2. 属性的类型也需要进一步推测,使用时也需要检验;

  3. JavaScript、Python乃至PHP都支持在对象创建之后为它添加新的属性。之前符合推测的类型后来添加或者删除了属性,要怎么处理?

 

  除此之外,调用函数时候如何优化调用开销也是一个重点,本质上跟优化对象的内存布局是类似的,可以将传入参数看成是构建一个有多个属性的对象,每个属性的类型不同。局部变量也需要有选择性地优化到寄存器、栈和堆当中。

PHP在这里的优势是支持类型标注,缺点是所有Hacklang里面修改掉的部分:

1. 不支持泛型,尤其是array类型不支持泛型。将一个变量类型标注为array几乎没有任何帮助,PHP中的array可以是顺序表也可以是hashmap,还可以混着,value的类型也不确定,这些都对类型优化有很高要求。Hacklang就推荐废掉array改用vector等几个确定类型且支持泛型的数据结构。

  2. reference这个功能,这个功能非常容易成为内存布局优化的障碍,也会阻碍JIT生成高效代码,尤其是数组中可以存储reference这件事,JIT编译器完全无法从字面上判断某条对array元素赋值的语句是否会影响环境中的其它变量的值。这也是为什么Hacklang直接删掉了这个功能。

  3. 其他参考Hacklang的变更

之前版本(PHP7)抠解释器实现带来的性能优化也会是一个阻碍,JIT的时候这些都得放弃掉,因为内存布局不一样了,这样可能导致最初的时候许多应用JIT反而变慢。所以,PHP8如果解决不了这些问题,最大的可能是许多microbenchmark速度大幅上升,但整体应用性能持平,自娱自乐。

源码编译安装

1. 安装依赖

yum -y install autoconf freetype gd libpng libpng-devel libjpeg libxml2 libxml2-devel zlib curl curl-devel net-snmp-devel libjpeg-devel

2. oniguruma 依赖包

yum install autoconf automake libtool
wget https://github.com/kkos/oniguruma/archive/v6.9.4.tar.gz -O oniguruma-6.9.4.tar.gz
tar xf oniguruma-6.9.4.tar.gz && cd oniguruma-6.9.4
./autogen.sh && ./configure --prefix=/usr
make && make install

4.新建用户

userdel www
groupadd www
useradd -g www -M -d /data/www -s /sbin/nologin www &> /dev/null

5. 下载源码包

wget  https://www.php.net/distributions/php-8.0.7.tar.gz

6. 解压

tar -zxvf php-8.0.7.tar.gz
cd php-8.0.7

7. 编译和安装 (选择自己需要的安装)

./configure --prefix=/usr/local/php \
--with-config-file-path=/usr/local/php \
--enable-mbstring  \
--enable-ftp  \
--enable-gd   \
--enable-gd-jis-conv \
--enable-mysqlnd \
--enable-pdo   \
--enable-sockets   \
--enable-fpm   \
--enable-xml  \
--enable-soap  \
--enable-pcntl   \
--enable-cli   \
--with-openssl  \
--with-mysqli=mysqlnd   \
--with-pdo-mysql=mysqlnd   \
--enable-mysqlnd-compression-support \
--with-pear   \
--with-zlib  \
--with-iconv  \
--with-curl   \
--build=arm-linux && make && make install

7. 开始配置

新增环境变量

vim /etc/profile
##最后增加 
export PHP=/usr/local/php
export PATH=$PHP/bin:$PHP/sbin:$PATH
source /etc/profile 

8. 配置PHP

cp php.ini-production /usr/local/php/etc/php.ini
cp /usr/local/php/etc/php-fpm.conf.default /usr/local/php/etc/php-fpm.conf
cp /usr/local/php/etc/php-fpm.d/www.conf.default /usr/local/php/etc/php-fpm.d/www.conf
cp sapi/fpm/init.d.php-fpm /etc/init.d/php-fpm
chmod +x /etc/init.d/php-fpm

9. 测试

[root@localhost www]# php -v 
PHP 8.0.7 (cli) (built: Jun 25 2021 10:10:16) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.7, Copyright (c) Zend Technologies

 

编译过程错误解决

若在 configure 过程中,遇到提示 configure: error,留意看下方的错误详细信息。以下 列出了在配置过程中出现的错误情况之关键信息摘要,未列举到的其它错误情况,解决方 法如出一辙:

Error #1:

configure: error: Package requirements (libxml-2.0 >= 2.7.6) were not met:
No package ‘libxml-2.0’ found
解决方法:

yum install libxml2-devel.x86_64

Error #2:
configure: error: Package requirements (sqlite3 > 3.7.4) were not met:
No package ‘sqlite3’ found

解决方法:

yum install sqlite-devel.x86_64

安装配置匿名访问网络Tor洋葱浏览器

概述

tor工作原理

Tor由已经安装了Tor软件的电脑连接网络而成。它之所以被称为onion,是因为它的结构就跟洋葱相同,你只能看出它的外表,而想要看到核心,就必须把它层层的剥开。即每个路由器间的传输都经过点对点密钥(symmetric key)来加密,形成有层次的结构。它中间所经过的各节点,都好像洋葱的一层皮,把客户端包在里面,算是保护信息来源的一种方式。

用 Tor 创建一条私有网络路径时,用户的软件或客户端通过网络上的中继递增地建立一条由若干加密连接组成的环路(circuit)。环路一次扩展一跳(hop),环路上的中继仅仅知道它从哪一个中继接收数据以及向哪一个中继发送数据。没有一台单独的中继会知道数据包的完整路径。客户端与环路上的每一跳都协商一组独立的密钥,这样可以保证数据通过任何一跳时都无法跟踪。

tor browser浏览器

tor browser的最大特点就是匿名性。

  • 您的互联网服务提供商和任何本地的监视者都将无法查看您的连接、跟踪您的网络活动,包括您所访问网站的名称和地址。
  • 您使用的网站和服务的运营商以及任何监视它们的人都将看到连接来自 Tor 网络而不是您的互联网IP地址,并且不知道您是谁,除非您明确标识自己。

安装

1. Tor安装配置

首先我们下载安装tor browser
下载地址: https://tor-browser.en.softonic.com/

安装时根据网络环境选择直连安装、网络受限安装(内置网桥连接、获取网桥连接、代理连接)。直连安装是网络可直接访问国外受限站点的网络环境;网络受限安装是当前网络不能直接访问国外受限站点,如有通过代理可访问受限站点选择代理连接,没有代理可选择内置网桥连接或获取网桥连接。

获取Tor网桥方法:

(一)mail自助获取tor的桥接ip
用 gmail等给 [email protected] 发一封内容为 get bridges 的邮件,主题随意,或标题 get transport obfs3 正文 get transport obfs3 或 标题 get transport scramblesuit 正文 get transport scramblesuit

(二)直接随机获取网桥ip,https://bridges.torproject.org/打开网页获得网桥ip。

获得的网桥样子是:

obfs4 139.59.140.35:8491 B1137CEB247F5FF9B57DA3160E6931D0269FF010 cert=g92HymBTzn1KzyizGjJkXxT8YRJw/QC3LEfoVFWuv9but3AqLd916ZoB+fo/6xJqWB0faQ iat-mode=0
obfs4 185.178.93.141:9060 05012FE073A4B522580E6FE77F3FD92E1ABF6FD1 cert=377UalzVX10eI5raeIVmJFFvq0FpKEF12igXoKe2rMJWGJxScKzMm9dph9m4v8hz0y3hDg iat-mode=0
obfs4 192.241.152.208:444 05277333D6A14B6E706CF5A189C57C97C471189B cert=2j8mktwt8pA4LGmqBUFeDfkGbVE0U0MqYBIbt6/ePMRrlrk9UhHQeeyw8WqgGPa1xYzyaQ iat-mode=0

填加网桥:

** 请注意:只添加前面的地址和端口, 最后面的长串数字字母是数字指纹,不要添加。

Tor官方其实也更推荐obfs4 https://2019.www.torproject.org/docs/pluggable-transports

代理连接:

这种方法比较常用,原因是比较快,可以用免费的节点分享网址或者自己搭建(以后讲方法,欢迎关注)
免费的节点分享网址:http://free-proxy.cz/en

2. Tor高级选项配置方法

在 Tor Browser\Data\Tor目录中找到torrc文件,用记事本等文本编辑器打开这个文件,可以看到里面有一些参数,这个文件是tor的核心设置文件。

以下为排除的节点(StrictNodes 1为坚决执行)
ExcludeNodes {cn},{hk},{mo},{sg},{th},{pk},{by},{ru},{ir},{vn},{ph},{my},{cu}
ExcludeExitNodes {cn},{hk},{mo},{sg},{th},{pk},{by},{ru},{ir},{vn},{ph},{my},{cu}
StrictNodes 1
注:
ExcludeNodes 是指排除节点,即把括号中的国家的节点从tor链路上除去;
ExcludeExitNodes 是指“排除“出口”节点”,,即tor的出口节点要排除括号中的国家的节点。

指定出口节点:

StrictExitNodes 1
ExitNodes {us}
这里us是指限定美国的ip为出口ip,你可以改为任何国家,国家代码请参考:https://zh.wikipedia.org/zh-cn/ISO_3166-1

补充:opera浏览器自带VPN功能同样可以访问国外受限站点。将计算机区域设置为中国台湾,安装好opera并在opera设置里将首选语言选为繁体,清除缓除重启opera,再到opera设置的高级选项里的隐私安全设置里即有VPN启用选择项,启用VPN后可在浏览器地址栏看到VPN字样,点击此字样可定位VPN位置。如果当前网络环境本身就在(已启用其他代理)国外IP,就不需改变计算机区域位置及opera语言设置,直接到opera设置高级选择项就可选择是否开启VPN功能。

3.Tor 的匿名性(挺好)

Tor 的一些特性使得其匿名性略高于普通浏览器的无痕浏览模式:参考 Chrome 无痕浏览的工作原理;以及 Tor 安全设置说明:https://tb-manual.torproject.org/zh-CN/security-settings/

访问 https://ip.sb/ 或 https://whoer.net/zh 查看你的IP;既不是你的真实IP(网络运营商分配的IP),也不是你的代理IP(机场),而是 Tor 的三层叠加的最后一层 IP;

X-ui,支持多协议多用户的Xray 面板!V2-ui全面停止更新

前言

sprov 这位大神开发的这套面板程序,很是方便,可以可视化的搭建SS、V2ray、Xray、Trojan等热门的协议,并且可以实时看到 VPS 的性能状态以及流量的使用情况。

那在经过将近两年的不断更新之后,V2-ui面板,迎来了一个比较大的转折——停止更新了。

sprov 大神又用 GO 语言重新开发了一套面板 X-UI。那这套面板相比原来的 V2-ui,兼容性更强,也便于维护, GO 语言的性能更好,而且内存占用也会相对的低一些。

功能介绍

  • 系统状态监控
  • 支持多用户多协议,网页可视化操作
  • 支持的协议:vmess、vless、trojan、shadowsocks、dokodemo-door、socks、http
  • 支持配置更多传输配置
  • 流量统计,限制流量限制到期时间
  • 可自定义 xray 配置模板
  • 支持 https 访问面板(自备域名 + ssl 证书)
  • 更多高级配置项,详见该项目的 GitHub:点击访问

安装&升级

bash <(curl -Ls https://raw.githubusercontent.com/vaxilu/x-ui/master/install.sh)

根据提示安装后,在浏览器中打开 http://<服务器 IP>:<端口>即可访问面板,默认用户名和密码都是 admin。

登陆后请修改用户名和密码

更换面板端口

x-ui 打开脚本面板,更换面板端口为你想用的端口即可,记得开放防火墙

添加节点

添加节点的过程 youtube 有很多视频,不再赘述

最简单的就是添加一个 vmess+websocket,其他保持全默认即可

手动安装&升级

  1. 首先从 https://github.com/vaxilu/x-ui/releases 下载最新的压缩包,一般选择amd64架构
  2. 然后将这个压缩包上传到服务器的/root/目录下,并使用root用户登录服务器

如果你的服务器 cpu 架构不是amd64,自行将命令中的amd64替换为其他架构

cd /root/
rm x-ui/ /usr/local/x-ui/ /usr/bin/x-ui -rf
tar zxvf x-ui-linux-amd64.tar.gz
chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh
cp x-ui/x-ui.sh /usr/bin/x-ui
cp -f x-ui/x-ui.service /etc/systemd/system/
mv x-ui/ /usr/local/
systemctl daemon-reload
systemctl enable x-ui
systemctl restart x-ui

建议系统

  • CentOS 7+
  • Ubuntu 16+
  • Debian 8+

常见问题

从 v2-ui 迁移

首先在安装了 v2-ui 的服务器上安装最新版 x-ui,然后使用以下命令进行迁移,将迁移本机 v2-ui 的所有 inbound 账号数据至 x-ui,面板设置和用户名密码不会迁移

迁移成功后请关闭 v2-ui并且重启 x-ui,否则 v2-ui 的 inbound 会与 x-ui 的 inbound 会产生端口冲突

x-ui v2-ui

更新系统

安装相关依赖
centos 系统下

yum update -y                                                                                                
apt-get update -y && apt-get install curl -y

ubuntu 系统下

apt update -y
apt-get update -y && apt-get install curl -y

若出现 bash curl: command not found 等错误,请先安装 curl

CentOS 系统:

yum install curl -y

Debian 或 Ubuntu 系统:

apt install curl -y

错误提示:-bash curl: command not found

Ubuntu、Debian 如果出现无法安装 Curl, 需要先升级

sudo apt-get update

申请 SSL 证书

下面环境的安装方式,大家根据自己的系统选择命令安装就好了。

更新及安装组件

apt update -y          # Debian/Ubuntu 命令
apt install -y curl    #Debian/Ubuntu 命令
apt install -y socat    #Debian/Ubuntu 命令
yum update -y          #CentOS 命令
yum install -y curl    #CentOS 命令
yum install -y socat    #CentOS 命令

安装 Acme 脚本

curl https://get.acme.sh | sh

80 端口空闲的证书申请方式

自行更换代码中的域名、邮箱为你解析的域名及邮箱

~/.acme.sh/acme.sh --register-account -m [email protected]
~/.acme.sh/acme.sh  --issue -d mydomain.com   --standalone

安装证书到指定文件夹

自行更换代码中的域名为你解析的域名

~/.acme.sh/acme.sh --installcert -d mydomain.com --key-file /root/private.key --fullchain-file /root/cert.crt

FunAdmin开发框架系统 支持PHP8.0(ThinkPHP6.X + Layui +Requirejs)

介绍

FunAdmin是基于ThinkPHP6.X + Layui +Requirejs的敏捷开发的后台管理系统。

演示站点

演示地址: fundemo.funadmin.com (账号:admin,密码:123456。备注:只有查看信息的权限

主要特性

  • 基于auth的权限管理系统
    • 支持无限级父子级权限继承,父级的管理员可任意增删改子级管理员及权限设置
    • 支持管理员多角色管理
  • CURD 命令行模式助您快速开发
  • 完善的菜单管理
    • 分模块管理
    • 无限极菜单
  • 二级域名部署支持,同时域名支持绑定到插件
  • 多语言支持,服务端及客户端支持
  • 丰富的插件市场
  • 完善的上传插件组件功能
    • 本地存储
    • 阿里云OSS建议使用
    • 腾讯云COS
    • 七牛云OSS
  • 完善的前端功能组件开发
    • 自适应手机、平板、PC,自使用IE+谷歌+火狐等浏览器
    • 基于RequireJS进行JS模块管理,按需加载
    • 对form表单二次封装,只需要使用函数就可以展示表
    • 对layui table ,form表格再次封装,方便使用
  • 完善的后台操作日志
    • 记录用户的详细操作信息 操作ip 等
  • 文件附件管理
  • 后台路径自定义,防止别人找到对应的后台地址

项目介绍

FunAdmin 基于thinkphp6.X +Layui2.6.*+requirejs开发权限(RBAC)管理框架,框架中集成了权限管理、模块管理、插件管理、后台支持多主题切换、配置管理、会员管理等常用功能模块,以方便开发者快速构建自己的应用。框架专注于为中小企业提供最佳的行业基础后台框架解决方案,执行效率、扩展性、稳定性值得信赖,操作体验流畅,使用非常优化,欢迎大家使用及进行二次开发。

  • PHP支持php8.0的快速开发框架 建议使用PHP8
  • 这是一个有趣的后台管理系统,这是可以让你节约时间的系统
  • 这是一款快速、高效、便捷、灵活敏捷的应用开发框架。
  • 系统采用最新版TinkPHP6框架开发,底层安全可靠,数据查询更快,运行效率更高,网站速度更快, 后续随官网升级而升级
  • 密码动态加密,相同密码入库具有唯一性,用户信息安全牢固,告别简单md5加密
  • 自适应前端,桌面和移动端访问界面友好简洁,模块清晰
  • 兼容ie11 + firefox + Chrome +360 等浏览器
  • UI组件化,只需要写函数就可以成就后台表单
  • 内置CURD 命令行模式,帮助助您快速开发系统
  • 模块化:全新的架构和模块化的开发机制,便于灵活扩展和二次开发。
  • 强大的表单管理,只需要使用函数即可成就表单
  • layui采用最新layui2.6.X 框架
  • 适用范围:可以开发OA、ERP、BPM、CRM、WMS、TMS、MIS、BI、电商平台后台、物流管理系统、快递管理系统、教务管理系统等各类管理软件。
  • require.js 模块化开发 一个命令即可打包js,css ; node r.js -o backend-build.js
  • restful api 接口,接口使用jwt接口验证等
  • …更多功能尽请关注

环境要求:

  • PHP >= 7.4 支持php8.0
  • PDO PHP Extension
  • MBstring PHP Extension
  • CURL PHP Extension
  • 开启静态重写
  • 要求环境支持pathinfo
  • Mysql 5.7及以上
  • Apache 或 Nginx

功能特性

  • 严谨规范: 提供一套有利于团队协作的结构设计、编码、数据等规范。
  • 高效灵活: 清晰的分层设计,解耦设计更能灵活应对需求变更。
  • 严谨安全: 清晰的系统执行流程,严谨的异常检测和安全机制,详细的日志统计,为系统保驾护航。
  • 组件化: 完善的组件化设计,丰富的表单组件,让开发列表和表单更得心应手。无需前端开发,省时省力。
  • 简单上手快: 结构清晰、代码规范、在开发快速的同时还兼顾性能的极致追求。
  • 自身特色: 权限管理、组件丰富、第三方应用多、分层解耦化设计和先进的设计思想。
  • 高级进阶: 分布式、负载均衡、集群、Redis、分库分表。

插件

  • ** CMS内容管理插件(免费)
  • ** BBS社区插件
  • ** 编辑器插件
  • ** 微信管理插件
  • ** 更多请查看 插件列表

开发者信息

  • 开源协议:Apache 2.0

Centos 7安装配置NTP网络时间同步服务器

现在的系统大多是微服务化架构,且部署方式大多为分布式集群化部署,也就是说系统的各个组件分布在不同的服务器中运行,导致时间同步对于系统的正常运行来说尤为重要, 由于时间不同同步,会导致系统出现各种异常

NTP是网络时间协议(Network Time Protocol),它是用来同步网络中各个计算机的时间的协议。本文,我们采用ntpd做时间同步,ntpd不仅仅是时间同步服务器,它还可以做客户端与标准时间服务器进行同步时间,而且是平滑同步

时间时区概念理解:

GMT、UTC、CST、DST

UTC:

整个地球分为二十四时区,每个时区都有自己的本地时间,在国际无线电通信场合,为了统一起见,使用一个统一的时间,称为通用协调时(UTC:Universal Time Coordinated)。

GMT:

格林威治标准时间 (Greenwich Mean Time)指位于英国伦敦郊区的皇家格林尼治天文台的标准时间,因为本初子午线被定义在通过那里的经线(UTC与GMT时间基本相同)。

CST:

中国标准时间 (China Standard Time)

GMT + 8 = UTC + 8 = CST

DST:
夏令时(Daylight Saving Time) 指在夏天太阳升起的比较早时,将时间拨快一小时,以提早日光的使用(中国不使用)。

1、查看当前服务器时区&列出时区并设置时区(如已是正确时区,请略过):

查看当前系统时间、时区

$ timedatectl 
Local time: Thu 2018-10-11 13:03:04 CST
Universal time: Thu 2018-10-11 05:03:04 UTC
RTC time: Thu 2018-10-11 01:17:11
Time zone: Asia/Shanghai (CST, +0800)
NTP enabled: no
NTP synchronized: no
RTC in local TZ: no
DST active: n/a

$ timedatectl status
Local time: Thu 2018-10-11 13:03:09 CST
Universal time: Thu 2018-10-11 05:03:09 UTC
RTC time: Thu 2018-10-11 01:17:16
Time zone: Asia/Shanghai (CST, +0800)
NTP enabled: no
NTP synchronized: no
RTC in local TZ: no
DST active: n/a

列出全世界所有的时区

# timedatectl list-timezones | grep Asia
...
Asia/Hong_Kong
Asia/Shanghai
Asia/TaipeiAsia/Urumqi
...

设置时区

# 方法1:
# 将时区设置为上海
$ timedatectl set-timezone Asia/Shanghai 
#方法2:
# 直接修改符号链接
$ rm /etc/localtime$ ln -s ../usr/share/zoneinfo/Asia/Shanghai /etc/localtime

若坚持要手动修改时间,先timedatectl set-ntp no。

# 设置日期和时间
$ timedatectl set-time '2018-10-11 09:00:00'
# 设置日期
$ timedatectl set-time '2018-10-11'
# 设置时间
$ timedatectl set-time '09:00:00'

# 方法2:使用date

$ date -s '2018-10-11 09:00:00'

同步系统时间到硬件时间

# 方法1:不建议硬件时间随系统时间变化
# 设置硬件时间随系统时间变化
$ timedatectl set-local-rtc 1
# 设置硬件时间不随系统时间变化
$ timedatectl set-local-rtc 0

# 方法2:
$ hwclock --systohc

是否启用自动同步时间

# 启用|停用自动同步时间
$ timedatectl set-ntp yes|no

# 上面的命令其实是启用、停用时间服务器,若安装了chrony服务,则等同于对该服务启停,若只安装了ntp,则是对ntp服务启停。
# 对chrony服务启停
$ systemctl start|stop chronyd
# 对ntp服务启停
$ systemctl start|stop ntpd

2、确认是否已安装ntp

【命令】rpm –qa | grep ntp

若只有ntpdate而未见ntp,则需删除原有ntpdate。如:

ntpdate-4.2.6p5-22.el7_0.x86_64

fontpackages-filesystem-1.44-8.el7.noarch

python-ntplib-0.3.2-1.el7.noarch

删除已安装ntp
【命令】yum –y remove ntpdate-4.2.6p5-22.el7.x86_64

重新安装ntp
【命令】yum –y install ntp

3、安装NTP

yum install -y ntp

说明:如果本机不支持联网,则可以找一台可以上网的服务器把安装包下载下来,然后做离线安装:

yum install --downloadonly --downloaddir=/home ntp

修改NTP配置

/etc/ntp.conf

#在配置中增加以下配置:
#允许上层时间服务器主动修改本机时间

restrict time.pool.aliyun.com nomodify notrap noquery

#允许所有主机通过本机同步时间

restrict default nomodify notrap

#外部时间服务器不可用时,以本地时间作为时间服务

server time.pool.aliyun.com iburst

server 127.127.1.0
fudge 127.127.1.0 stratum 10

修改完成后保存退出并重启ntp(systemctl restart ntpd)。

说明:
#nomodfiy:客户端不能更改服务端的时间参数,但是客户端可以通过服务端进行网络校时。
#noquery:不提供客户端的时间查询。
#notrap:不提供trap这个远程时间登录的功能

4、启动NTP服务&开机启动设置

#启动NTP服务

systemctl start ntpd

#将NTP服务设置为开机启动

systemctl enable ntpd

5、chrony的安装配置

安装chrony

$ yum -y install chrony

配置chrony

$ vim /etc/chrony.conf
server ntp1.alyun.com
server ntp2.alyun.com
server ntp3.alyun.com
#server 0.centos.pool.ntp.org iburst...

启动chrony

$ systemctl start chronyd

6、配置计划任务,使用ntpdate同步时间

# 启动并开机启动计划任务cron
$ systemctl start crond
$ systemctl enable crond

# 配置计划任务,每5分钟同步一次
$ crontab -e
*/5 * * * * /usr/sbin/ntpdate ntp1.aliyun.com

tp6 解决跨域问题

在和前端js对接接口的时候遇到跨域问题,记录一下

方法一
通过route文件配置
文件位置 : app/api/route/app.php

<?php
/**
 * Created by PhpStorm.
 * User: 雪后西溏 <[email protected]>
 * Date: 2021-02-23
 * Time: 16:06
 */

use think\facade\Route;

// 手写资源路由
Route::group(':v/:c', function () {
    Route::get('', '/:v.:c/index');
    Route::get('create', '/:v.:c/create');
    Route::get(':id/edit', '/:v.:c/edit');
    Route::get(':id', '/:v.:c/read');
    Route::post('', '/:v.:c/save');
    Route::put(':id', '/:v.:c/update');
    Route::delete(':id', '/:v.:c/delete');
})->allowCrossDomain([
    /** 设置跨域允许的header头信息,新增token字段 */
    'Access-Control-Allow-Headers'     => 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With, authKey, Accept, Origin, token',
    /** 允许所有请求 */
    'Access-Control-Allow-Origin'      => '*',
    'Access-Control-Allow-Credentials' => 'true',
]);

更新后写法

<?php
/**
 * Created by PhpStorm.
 * User: 雪后西溏 <[email protected]>
 * Date: 2021-06-16
 * Time: 10:14
 */

use think\facade\Route;

// 开启资源路由
Route::resource(':v/:c', ':v.:c');
// 配置跨域
Route::allowCrossDomain([
    /** 设置跨域允许的header头信息 , 增加token字段 */
    'Access-Control-Allow-Headers'     => 'Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With, authKey, Accept, Origin, token',
    //    允许所有请求
    'Access-Control-Allow-Origin'      => '*',
]);

参见 官方文档
https://www.kancloud.cn/manual/thinkphp6_0/1037507

 

方法二

设置全局中间件

在 ./app/middleware.php 文件中启用内置中间件即可

\think\middleware\AllowCrossDomain::class
<?php
// 全局中间件定义文件
return [
    // 全局请求缓存
    // \think\middleware\CheckRequestCache::class,
    // 多语言加载
    // \think\middleware\LoadLangPack::class,
    // Session初始化
    // \think\middleware\SessionInit::class
    \think\middleware\AllowCrossDomain::class,
];

 

 

linux命令总结之date命令

命令简介:

date 根据给定格式显示日期或设置系统日期时间。print or set the system date and time

指令所在路径:/bin/date

命令语法:

date [OPTION]… [+FORMAT]

date [-u|–utc|–universal] [MMDDhhmm[[CC]YY][.ss]]

命令参数:

参数 描述
-d 显示字符串描述的时间
-f 显示DATEFILE文件中的每行时间
-r 显示文件的最后修改时间
-R 以RFC-2822兼容日期格式显示时间
-rfc-2822 以RFC-2822兼容日期格式显示时间
-s 设置时间为string
-u 显示或设定为Coordinated Universal Time时间格式
–help 显示date命令的帮助信息
–version 显示date命令的版本信息

Format参数格式

要说写这位程序的 David MacKenzie老兄,真是事无巨细啊,居然整了这么多格式参数,佩服佩服。

参数 描述
%% 显示字符%
%a 星期几的缩写(Sun..Sat)
%A 星期几的完整名称(Sunday…Saturday)
%b 月份的缩写(Jan..Dec)
%B 月份的完整名称(January..December)
%c 日期与时间。只输入date指令也会显示同样的结果
%C 世纪(年份除100后去整) [00-99]
%d 日期(以01-31来表示)。
%D 日期(含年月日)。
%e 一个月的第几天 ( 1..31)
%F 日期,同%Y-%m-%d
%g 年份(yy)
%G 年份(yyyy)
%h 同%b
%H 小时(00..23)
%I 小时(01..12)
%j 一年的第几天(001..366)
%k 小时( 0..23)
%l 小时( 1..12)
%m 月份(01..12)
%M 分钟(00..59)
%n 换行
%N 纳秒(000000000..999999999)
%p AM or PM
%P am or pm
%r 12小时制时间(hh:mm:ss [AP]M)
%R 24小时制时间(hh:mm)
%s 从00:00:00 1970-01-01 UTC开始的秒数
%S 秒(00..60)
%t 制表符
%T 24小时制时间(hh:mm:ss)
%u 一周的第几天(1..7); 1 表示星期一
%U 一年的第几周,周日为每周的第一天(00..53)
%V 一年的第几周,周一为每周的第一天 (01..53)
%w 一周的第几天 (0..6); 0 代表周日
%W 一年的第几周,周一为每周的第一天(00..53)
%x 日期(mm/dd/yy)
%X 时间(%H:%M:%S)
%y 年份(00..99)
%Y 年份 (1970…)
%z RFC-2822 风格数字格式时区(-0500)
%Z 时区(e.g., EDT), 无法确定时区则为空

 

使用示例:

1: 查看date命令帮助信息

 date --help
 man date

2:运用-d参数

[root@Gin scripts]# date
Sun Jan 29 10:46:03 CST 2017
[root@Gin scripts]# date -d now
Sun Jan 29 10:46:07 CST 2017
[root@Gin scripts]# date -d 'next monday'
Mon Jan 30 00:00:00 CST 2017
[root@Gin scripts]# date -d yesterday +%Y%m%d
20170128

3:显示文件中的时间

[root@Gin scripts]# cat >date.txt
2013-11-17 10:54:00
2013-11-22 11:11:11

[root@Gin scripts]# more date.txt
2013-11-17 10:54:00
2013-11-22 11:11:11

[root@Gin scripts]# date -f date.txt
Sun Nov 17 10:54:00 CST 2013
Fri Nov 22 11:11:11 CST 2013

4:显示文件最后修改的时间

[root@Gin scripts]# date -r date.txt
Sun Jan 29 10:49:11 CST 2017

5:按各种格式显示当前日期时间
这个命令的格式参数实在是太多了,在此没有必要每一个参数都尝试一遍,大家可以对照Format参数表,自己敲一敲命令实践一下。

[root@Gin scripts]# date +%Y
2017
[root@Gin scripts]# date +%m
01
[root@Gin scripts]# date +%D
01/29/17
[root@Gin scripts]# date '+%Y-%m-%d'
2017-01-29
[root@Gin scripts]# date +%Y-%m-%d
2017-01-29
[root@Gin scripts]# date +%m/%d/%y
01/29/17

6: 设置系统时间

[root@Gin scripts]# date -s "2016-11-11 00:00:00"
Fri Nov 11 00:00:00 CST 2016
[root@Gin scripts]# date
Fri Nov 11 00:00:05 CST 2016

7:请给出如下格式的date命令,如:11-02-26。再给出实现按周输出,如:周六为6,请分别给出命令

[root@Gin scripts]# date +%y-%m-%d
17-01-29
[root@Gin scripts]# date +%F
2017-01-29
[root@Gin scripts]# date "+%Y-%m-%d %H:%M:%S"
2017-01-29 10:57:02

生产场景中常用时间格式来打包数据:

[root@Andy andy]# tar zcvf etc-$(date +%F).tar.gz /etc
[root@Andy andy]# tar zcvf etc-`date +%F`.tar.gz /etc
解析命令方法:反引号,或$(),如上
[root@Andy andy]# date +%w  ##显示周,0-6

显示过去与未来时间:

[root@Andy andy]# date +%F
2016-11-16
[root@Andy andy]# date +%F -d "-1day"
2016-11-15
[root@Andy andy]# date +%F -d "+2day"
2016-11-18
[root@Andy andy]# date +%F -d "+24hour"
2016-11-17
[root@Andy andy]# date +%F-%H -d "+2hour"
2016-11-16-14

centos设置时区且自动同步

Linux的时间分为System Clock(系统时间)和Real Time Clock (硬件时间,简称RTC)。

系统时间:指当前Linux Kernel中的时间。

硬件时间:主板上有电池供电的时间。

查看系统时间的命令: #date

设置系统时间的命令: #date –set(月/日/年 时:分:秒)

例:#date –set “10/11/10 10:15”

查看硬件时间的命令: # hwclock

设置硬件时间的命令: # hwclock –set –date = (月/日/年 时:分:秒)

上述提到的是手动设置时间到一个时间点,可能与当前网络的时间有误差。下面介绍一下与时间服务器上的时间同步的方法

 

1.查看时间

[root@bogon ~]# date
Thu Apr 4 11:24:24 CST 2019

2.使用date -s修改时间

[root@bogon ~]# date -s "2019/4/4 11:26" 
Thu Apr  4 11:26:00 CST 2019

3.修改时区

使用新的时区文件覆盖系统默认时区
 # cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

4.安装ntpdate

yum install ntpdate -y

5.同步网络时间

ntpdate ntp1.aliyun.com

6.保存为bios时间

同步BIOS时钟,强制将系统时间写入CMOS,使之永久生效,避免系统重启后恢复成原时间。
# clock -w

7.设置crontab同步时间

59 23 * * *    ntpdate ntp1.aliyun.com
*  *  *  *  *  command 
分 时 日 月 周 命令 
第1列表示分钟1~59 每分钟用*或者 */1表示 
第2列表示小时1~23(0表示0点) 
第3列表示日期1~31 
第4列表示月份1~12 
第5列标识号星期0~6(0表示星期天) 
第6列要运行的命令

8.查看bios时间

[root@bogon ~]# hwclock  -r
Thu 04 Apr 2019 11:35:20 AM CST  -0.365289 seconds