做人呢最重要的就是开心

装饰者模式

动态地将责任附加到对象上。想要扩展功能,装饰者提供有别于继承的另一种选择。

装饰者模式 符合开放-关闭原则。继承 虽然属于扩展的形式之一,但是在设计中,推崇多用组合少用继承的说法。但是好像 GO 里面没有继承的说法,只有组合。所以用 GO 实现装饰者模式还是很方便的。装饰者的主要目的就是对基础对象不断的扩展功能,虽然在一定程度上保持了扩展性,但是如果过度使用会出现众多小对象,会使程序变得很复杂。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//  饮料接口
type Beverage interface {
getDescription() string
cost() int
}

// 实现咖啡的过程
type Coffee struct {
description string
}

func(this Coffee) getDescription() string {
return this.description
}

func(this Coffee) cost() int {
return 1
}
// Mocha 实现
type Mocha struct {
beverage Beverage
description string
}

func(this Mocha) getDescription() string {
return fmt.Sprintf("%s, %s", this.beverage.getDescription(), this.description)
}

func(this Mocha) cost() int {
return this.beverage.cost() + 1
}

// Whip 实现
type Whip struct {
beverage Beverage
description string
}

func(this Whip) getDescription() string {
return fmt.Sprintf("%s, %s", this.beverage.getDescription(), this.description)
}

func(this Whip) cost() int {
return this.beverage.cost() + 1
}

func main() {
var beverage Beverage
// 买了一杯咖啡
beverage = Coffee{description:"houseBlend"}
// 给咖啡加上 Mocha
beverage = Mocha{beverage:beverage, description:"Mocha"}
// 给咖啡加上 Whip
beverage = Whip{beverage:beverage, description:"whip"}
// 最后计算 Coffee 的价格
fmt.Println(beverage.getDescription(), ", cost is ", beverage.cost())
}

装饰模式是原始对象的扩展,不断的增加功能。

在《程序员的思维修炼:开发潜能认知的九堂课》这本书中,介绍了一种从新手专家的成长模型,既“德雷福斯模型”。 简单点的接介绍,就是如何从一个新手成长为一个专家的修炼套路。

德雷福斯模型由德雷福斯兄弟在 20 世纪 70 年代研究提出,德雷福斯兄弟考察了行业技术能手,包括商用客机飞行员和世界著名国际象棋大师 。他们的研究表明,从新手到专家要经历巨大的变化。

德雷福斯模型描述了我们的能力、态度、素质和视角在不同的技术水平下是如何变化的,以及为什么会有变化。

对于所有的事情,你既不是“专家”也不是“新手”,你只是处于某个特定技能领域中的某个水平阶段。虽然你可能只是烹饪新手,但却可能是跳伞专家。大多数非残障成人在直立行走方面都是专家——无需计划或者思考。这已经变成了本能。大多数人在税务规划方面都是新手。如果提供足够多的明确指令,我们就能够完成它,但是事实上我们不知道那是怎么回事(不明白为什么这些规则如此神奇)。

德雷福斯模型分为以下五个阶段

新手

新手是每个开始学习任何技能的人一开始的阶段,包括未入门的外行人,这一阶段的人,经验很少或者根本就没有,而所谓的经验,指的是,通过实践这项技术促进了思维的改变。明白到这点是很重要的,因为很多人以为,经验就是指资历(单纯时间而言),其实并不是这样,就好像一个软件开发人员号称他有十年的经验,其实每天都在重复做一件事,并没有任何改进,而且自己本身对于自己的职业也没有进一步的认识,这样的经验就算是100年,也就相当于别人的1年,别说什么煤油翁的故事。很抱歉,像软件开发这领域,技术的更新是很快的,甚至在我写这篇文章的时候,在外国某个实验室里可能就已经有新的语言或者其他东西出来了,只是我们没有用到而已(尤其是中国这边,技术的更新与外面脱节很多)。当然,很多人对于“促进思维的改变”很好奇,什么是“促进思维的改变”?所谓思维的改变,就好像你从面向过程的编程向面向对象编程这方面转变,那么,你的思维一定会发生转变,因为你的思维相比以前,已经向更高的抽象这方面发展,要是进一步学习动态语言,就更是如此。思维的转变一定会发生的,因为这是你在实践中所体会到的,所领悟到的,甚至是别人从没有发现过的新的用法

说了那么多有关于经验的介绍,那么,什么是新手呢?如何定义一个新手,其实很简单,就是新手面对问题的态度。这是非常简单的判断方法,因为 经验的差距,使得新手在面对问题,尤其是突发问题的表现,明显是与在新手上面更高的层次是有区别的。那么,新手在面对问题的时候,会怎么做呢?就是举手无措!新手害怕面对问题,因为他们不认为自己能够解决问题,认为自己的能力不足,所以不敢主动去解决问题,更多是向更高层次的人求助(好吧,我之前就是这样,在一个陌生的领域遇到一个问题,结果自己就慌了,因为自己根本就不知道怎么办,就算找到问题的症结所在,但还是不知道要怎么办)。不仅是这样,新手更喜欢指令性的命令,什么是指令性的命令呢?想想当我们开始煮菜的时候,跟着菜谱学做菜的时候,我们是不是严格按照菜谱上一条一条执行的呢?像是该加多少面粉啊,该在什么时候加多少水啊,或者该用多少温度加温多久啊。。。但是一旦最后煮出来的菜并不是菜谱上的那个样子和味道,我们就会慌了,怎么回事,我明明跟着菜谱上的指令严格来做的啊?如果是新手以上的级别,是可以找出问题的,甚至在一开始就已经发现问题,像是专家,就已经在一开始不知不觉中将问题解决了(是的,不知不觉,因为他们很少会犯低级错误,甚至已经是他们潜意识中就在避免的)。但是新手不行啊,他们需要明确的解决问题的指令,像是看看煤气有没有开这个脑残的提示他们也会去做的,因为这时他们已经失去了思考,完全交给了所谓的权威

新手还非常在乎成功,他们非常渴求能够马上将当前的目标完成。这个症结之所以存在,也是因为他们缺乏经验,因为 他们害怕面对问题,而快速成功意味着不用遇到太多甚至是没有问题。他们从没有想过这个方法到底对还是错,只是想要知道这个方法能否成功而已,而我们知道,在软件开发这方面,方法能够奏效并不代表这个方法就是适合的。

所以,新手就是无法根据当前发生问题的根点思考出问题的症结所在,并进而想出解决问题的方法出来。他们之所以会表现出这样的行为,就是因为他们的经验太少或者无法根据当前的情境将问题转化为自己以前解决过的问题类型,因为他们缺乏抽象的能力,将问题抽象成一个模型,然后,在遇到类似的问题时能够马上反应过来。这个能力实在是太过了,因为真的很难,但是新手以上的级别就能做到,因为他们已经通过自己大量实践将这个能力掌握了,在每次解决问题的时候都会将这个问题的症结和解决方法记录进自己的“数据库”里,然后根据情境进行匹配。所以,要想摆脱新手这个级别,关键还是多实践,多思考,多总结,这样就能通过积累而发生质的变化。

高级新手

高级新手就是比新手更上的一个层次,这个层次已经具有一定的经验,并且自我解决问题的能力已经得到大幅提升,具有初步的情境匹配的能力,但是,高级新手之所以还有“新手”这两个字,就是因为他们对于情境的处理能力还是不足的。高级新手已经能够单独解决任务了,他们已经不需要像新手那样战战兢兢,在遇到问题的时候,能够根据自己过去的经验寻找解决问题的方法,这时的他们就会表现出一个很明显区别于新手的特点:他们不像新手那样,会将文档从头看到尾,他们是会选择快速的查找相关的方法,因为他们知道,解决这个问题只需要这个方法就行,但是从来不会去追根刨底,为什么是这个方法。但是,解决问题的基础依然是当前的问题与自己过去解决过的实在太相似了,如果是表面毫不相关的问题,他们依然还会陷入迷茫中,这是因为他们提取情境的能力仍旧存在问题,因此只能记住具体的问题类型而没有发现到问题之间的相同点,进而用相同的方法解决。而且,他们还看不到情境的延伸,一个情境是会延伸的,如果它与实际的问题连在一块,那么你就会发现,一个情境的背后是无数小情境,解决大的情境是很难的,但是解决一个小情境还是很简单的,这就是一般解决大情境的方法,但是高级新手是不会看到的,就像我们经常遇到的,我们无法将一个庞大的问题肢解,我们需要更高层次的人来为我们肢解,然后告诉我们先解决哪一个。

胜任者

要达到胜任者,还是需要很大的努力,因为胜任者在团队中担当的任务非常大,甚至可以说是团队的核心。胜任者的具体表现是什么呢?胜任者不仅能够解决问题,还能发现新的问题,他们能够从目前的问题的解决中发现新的问题并且将它切实的解决掉。这种能力是因为他们过去强大的经验所致,他们知道,这样的问题用这样的方法,会有什么副作用,而我们又要怎样来解决这个副作用。他们在遇到问题的时候能够将问题层层肢解并且针对每个部分提出相应的解决计划,之所以说他们是团队的核心,就是因为一个团队的方案啊,计划啊,基本上都是由他们提出的(很多人会问,更上面的人呢?这是后面要讲的,因为你要知道,一个团队大部分情况下,是不需要胜任者以上的级别的)。这时的胜任者就是一个中转点,因为他们能够与更上面的人沟通,又能统帅下面的新手们,所以,这种人一般都是领导者级别的。但是,胜任者在情境上的处理并不是很成熟,他们只是因为处理过的经验的积累以及严谨的思考使得他们能够解决任何复杂的问题,但是还是不能关注一些情境的细节,甚至是选择忽略,如果说忽略这个细节也能解决问题而且还不会发生什么不好的副作用,他们是会忽略的。

精通者

精通者的一个明显的问题就是他们对于所处的技术领域具有全局思维,他们围绕着自己的技术领域,积极寻找更大的概念框架,更喜欢从他人身上学习,就算只是听别人讲自己的失败经验,也能够从中受益,因为他们已经能够充分理解情境了。他们明白当前的情境是怎样的,情境的细节他们也能把握,甚至在解决问题的时候就已经将这个细节包含进去。他们能够运用计算机的所谓的格言经验(所谓的格言经验,就是我们在学习的时候经常会在一些大师的作品中看到的,一些类似于聊家常的经验,如:“测试一切可能出错的东西”,但是对于新手,他根本就不知道要测试什么),因为他们知道,这样的格言适合什么情境,就是因为他们对情境的理解非常充分。他们也能充分运用任何当前技术的任何复杂的方式,比如说,新手的杀手,设计模式,一旦用不好就会出现问题,就算用了,也会存在有没有必要的问题,但是精通者就能运用自如,因为他非常清楚自己的问题所处的情境到底是怎样的。精通者还有一个更重要的特点,这个特点也是精通者能够成为专家的关键,就是他们能够通过有效的反馈和思考来不断调整自己或者不断进步。反馈和思考是非常重要的,这是掌握任何技能所必须的能力

专家

专家就真的是各个行业的魔法师或者规则的制定者。他们永远都在寻找更好的解决方法,就算是一个小问题,他们也在努力研究怎样更加优美的解决。当然,这样自然是不用说的,因为他们已经是这个行业的最高者,自然必须拥有这个能力。但是,判断一个人是否是专家,还得靠他是怎样解决问题的。专家解决问题更多是根据他们的直觉。他们会觉得,这里会存在问题,而且这样做就能解决,但是你问他为什么会想到,他可能就想不到答案,因为这就好像是一看到这个问题就知道问题的答案一样,就好像你一说出来的就是家乡的方言一样,如果问你,为什么会说方言,你能答得出来吗。这就是一个完全内化的阶段,他们将所有情境和解决这个情境的方法完全内化了,并不是看到情境就想到方法,而是情境本来就和方法连在一块,就像我们解数学题一样,问题本身就是答案。所以,成为专家的关键,就是锻炼自己的直觉,将情境和解决情境的办法连在一块。这可是一个非常漫长的过程,当然,如果你真的非常有天赋,那就另当别论,所以,我们还是要老老实实从新手开始积累实践经验吧。

你在哪个阶段呢?

文章转自网络

awk 是处理文本文件的一个应用程序,几乎所有 Linux 系统都自带这个程序。

它依次处理文件的每一行,并读取里面的每一个字段。对于日志、CSV 那样的每行格式相同的文本文件,awk 可能是最方便的工具。

awk 其实不仅仅是工具软件,还是一种编程语言。不过,本文只介绍它的命令行用法,对于大多数场合,应该足够用了。

基本用法

awk 的基本用法就是下面的形式。

#格式
$ awk 动作 文件名

示例

1
$ awk '{print $0}' demo.txt

上面示例中,demo.txtawk 所要处理的文本文件。前面单引号内部有一个大括号,里面就是每一行的处理动作 print $0。其中,print 是打印命令,$0 代表当前行,因此上面命令的执行结果,就是把每一行原样打印出来。

下面,我们先用标准输入(stdin)演示上面这个例子。

1
$ echo 'this is a test' | awk '{print $0}'

this is a test

上面代码中,print $0 就是把标准输入 this is a test ,重新打印了一遍。

awk 会根据空格和制表符,将每一行分成若干字段,依次用 $1$2$3 代表第一个字段、第二个字段、第三个字段等等。

1
$ echo 'this is a test' | awk '{print $3}'

a

上面代码中,$3 代表 this is a test 的第三个字段a。

下面,为了便于举例,我们把 /etc/passwd 文件保存成 demo.txt

root:x:0:0:root:/root:/usr/bin/zsh
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync

这个文件的字段分隔符是冒号(:),所以要用-F参数指定分隔符为冒号。然后,才能提取到它的第一个字段。

1
$ awk -F ':' '{ print $1 }' demo.txt

root
daemon
bin
sys
sync

变量

除了 $ + 数字表示某个字段,awk 还提供其他一些变量。

变量 NF 表示当前行有多少个字段,因此 $NF 就代表最后一个字段。

1
$ echo 'this is a test' | awk '{print $NF}'

test

$(NF-1) 代表倒数第二个字段。

1
$ awk -F ':' '{print $1, $(NF-1)}' demo.txt

root /root
daemon /usr/sbin
bin /bin
sys /dev
sync /bin

上面代码中,print 命令里面的逗号,表示输出的时候,两个部分之间使用空格分隔。

变量 NR 表示当前处理的是第几行。

1
$ awk -F ':' '{print NR ") " $1}' demo.txt

1) root
2) daemon
3) bin
4) sys
5) sync

上面代码中,print 命令里面,如果原样输出字符,要放在双引号里面。

awk 的其他内置变量如下:

FILENAME:当前文件名
FS:字段分隔符,默认是空格和制表符。
RS:行分隔符,用于分割每一行,默认是换行符。
OFS:输出字段的分隔符,用于打印时分隔字段,默认为空格。
ORS:输出记录的分隔符,用于打印时分隔记录,默认为换行符。
OFMT:数字输出的格式,默认为%.6g。

函数

awk 还提供了一些内置函数,方便对原始数据的处理。

函数 toupper() 用于将字符转为大写。

1
$ awk -F ':' '{ print toupper($1) }' demo.txt

ROOT
DAEMON
BIN
SYS
SYNC

上面代码中,第一个字段输出时都变成了大写。

其他常用函数如下。

  • tolower():字符转为小写。
  • length():返回字符串长度。
  • substr():返回子字符串。
  • sin():正弦。
  • cos():余弦。
  • sqrt():平方根。
  • rand():随机数。

awk 内置函数的完整列表,可以查看手册。

条件

awk 允许指定输出条件,只输出符合条件的行。

输出条件要写在动作的前面。

$ awk ‘条件 动作’ 文件名

请看下面的例子。

1
$ awk -F ':' '/usr/ {print $1}' demo.txt

root
daemon
bin
sys

上面代码中,print 命令前面是一个正则表达式,只输出包含 usr 的行。

下面的例子只输出奇数行,以及输出第三行以后的行。

输出奇数行

1
$ awk -F ':' 'NR % 2 == 1 {print $1}' demo.txt

root
bin
sync

输出第三行以后的行

1
$ awk -F ':' 'NR >3 {print $1}' demo.txt

sys
sync

下面的例子输出第一个字段等于指定值的行。

1
$ awk -F ':' '$1 == "root" {print $1}' demo.txt

root

1
$ awk -F ':' '$1 == "root" || $1 == "bin" {print $1}' demo.txt

root
bin

if 语句

awk 提供了if结构,用于编写复杂的条件。

1
$ awk -F ':' '{if ($1 > "m") print $1}' demo.txt

root
sys
sync
上面代码输出第一个字段的第一个字符大于 m 的行。

if 结构还可以指定 else 部分。

1
$ awk -F ':' '{if ($1 > "m") print $1; else print "---"}' demo.txt

root
sys
sync

文章转载 阮一峰的网络日志

观察者模式

最近在看设计模式,正好也在看 GO,所以之后都会用 GO 来学习基础的设计模式。当然不会很复杂去演示某一个模式,只需要有这个概念就可以了。如果工作中生硬的去套用模式,这肯定是不可取的。模式是代码实践的结果,没有一定的代码量,是无法理解其之精髓。这是我一直以来的对模式的看法。当然书本的知识会让学习更快接触模式的思维。

观察者模式(Observer Pattern):定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式又叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。

观察者模式是一种对象行为型模式。

GO 的简单实现:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
 package main

import "fmt"

type Subject interface {
AddObserver(observer Observer)
RemoveObserver(observer Observer)
Notify()
}

type Observer interface {
Update()
}

type Weather struct {
observers []Observer
}

func(this *Weather) AddObserver(observer Observer) {
this.observers = append(this.observers, observer)
}

func(this *Weather) RemoveObserver(observer Observer) {
for i,v := range this.observers {
if v == observer {
this.observers = append(this.observers[:i], this.observers[i+1:]...)
}
}
}

func(this *Weather) Notify() {
for _,v :=range this.observers {
v.Update()
}
}

type Ob1 struct {}
func(this Ob1) Update() {
fmt.Println("this is Ob1")
}

type Ob2 struct {}
func(this Ob2) Update() {
fmt.Println("this is Ob2")
}

type Ob3 struct {}
func(this Ob3) Update() {
fmt.Println("this is Ob3")
}
var (
Sub Subject
ob1, ob2, ob3 Observer
)
func main() {
Sub, ob1, ob2, ob3 = &Weather{}, Ob1{}, Ob2{},Ob3{}
Sub.AddObserver(ob1)
Sub.AddObserver(ob2)
Sub.AddObserver(ob3)
Sub.RemoveObserver(ob2)
Sub.Notify()
}

今天突然想看看客户端是如何与 redis server 交互的,所以就想着简单实现一下 redis 的客户端。当我们在使用 redis 的时候,redis 官方也提供了 redis-cli 客户端予以使用,通过一下命令操作,那么依据此,是不是客户端可以这么做呢?是不是遵从着某种 特定的协议呢?

首先通过 Tcp 连接到 redis-server, 保证可通。利用 GO 提供的 net 包,可以很轻松的实现。但是在这之前先定义个 interface,面向对象嘛#滑稽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type redis interface {
set(key string, value string) (bool, error)
get(key string) string
del(key string) int
}
type Client struct {
Conn net.Conn
}
// 连接, 特简单
func connect(host string) net.Conn {
conn, err := net.Dial("tcp", host)
if err != nil {
log.Fatalln(err)
}
return conn
}

当使用 redis-cli 的时候,提供的 cli 命令操作。当然 redis 的提供的很多的 API 操作,单下面的例子就以 set get 为例。主要是操作字符串。对于 set 是这样的

1
2
> set blog njphper
> ok

类似这样的一个操作,如果将这里看成一个 im 服务的话, 说明在这里我们向 redis 服务器发送了一个“ set blog njphper” 字符串,redis-server 在收到这个字符串的后,进行了一系列操作,然后返回之后的状态。那么这里肯定会约束双方以怎么样的协议去发送以及返回。好了,这里就需要借助文档了,看一下 redis 协议文档 https://redis.io/topics/protocol
会看到以下信息:

In RESP, the type of some data depends on the first byte:

  • For Simple Strings the first byte of the reply is “+” // 字符串返回的第一个字符是+
  • For Errors the first byte of the reply is “-“ // 错误返回的第一个字符串是 -
  • For Integers the first byte of the reply is “:” // 整型返回的第一个字符是 :
  • For Bulk Strings the first byte of the reply is “$” // bulk字符第一个返回$
  • For Arrays the first byte of the reply is “*” // 对于array第一个字符是 *

以上是服务端返回的信息,对于客户端而言,必须当 "\r\n" (CRLF) 结束,当然服务端也是,但是他们之间有一点区别。下面再说。因为 redis 的协议足够简单,所以操作起来还是很方便的。 这里实现以下 set , get 以及 del 操作

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
39
40
41
42
43
44
45
46
47
48
49
50
func(client Client) set(key string, value string) (bool, error) {
var (
res bool
err error
)

client.Conn.Write([]byte(fmt.Sprintf("set %s %s \r\n", key, value)))
reader := bufio.NewReader(client.Conn)
line, _ , err := reader.ReadLine()
if err != nil {
log.Fatalln(err)
}
switch string(line[0]) {
case "+":
res, err = true, nil
case "-":
res, err = false, errors.New(string(line[1:]))
}
// 清空 buff
reader.Reset(client.Conn)
return res, err
}

// 获取字符串
func(client Client) get(key string) string {
_, err := client.Conn.Write([]byte(fmt.Sprintf("get %s \r\n", key)))
if err != nil {
log.Fatalln(err)
}
reader := bufio.NewReader(client.Conn)
// 第一行 redis 返回的状态,这里可以进行一些判断之类的
reader.ReadLine()
// 第二行才是 value 值
line, _ , err := reader.ReadLine()
// 清空 buff
reader.Reset(client.Conn)
return string(line)
}

// 删除字符串
func(client Client) del(key string) int {
_, err := client.Conn.Write([]byte(fmt.Sprintf("del %s \r\n", key)))
if err != nil {
log.Fatalln(err)
}
reader := bufio.NewReader(client.Conn)
line, _ , err := reader.ReadLine()
code, _ := strconv.Atoi(string(line[1:]))
return code
}

来测试一下看看,有没有成功?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 var client redis
conn := connects("127.0.0.1:6379")
client = Client{Conn: conn}
fmt.Println(client.set("hi", "见见空空"))
// 有返回值
fmt.Println(client.get("hi"))
// 设置
fmt.Println(client.set("name", "hello"))
// 获取
fmt.Println(client.get("name"))
// 删除 ,返回了 in(1)
fmt.Println(client.del("name"))
// nil
fmt.Println(client.get("name"))

这里只是简单了解一下 redis,如果需要更加健壮的 redis 客户端,还是找一些开源包比较靠谱,毕竟轮子不需要再造一遍,可以了解,但没必要在自己花费精力造一遍 。这里还要提一下,go 的 interface 真好用,个人比较虽然倾向这种隐示的实现

Golang的面向对象机制与Smalltalk或者Java等传统面向对象的编程语言不同。传统面向对象编程语言的一个重要特性是继承机制。因为继承机制支持在关联对象间进行代码复用和数据共享。继承机制曾在代码复用和数据共享的设计模式占据主导地位,但是目前组合这一古老的技术重新焕发了活力。

本篇文章转自 Tim Henderson的 "Object Oriented Inheritance in Go", 原文地址是 http://hackthology.com/object-oriented-inheritance-in-go.html 。非常感谢李浩和骏奇对于这篇文章的翻译。

在我们探讨如何在** Go** 中实现继承机制之前(Golong 中的继承机制和其他语言 (Java) 的继承机制有区别),我们先看一下 Java 中如何实现继承机制。

继承与组合

让我们先看一下我最喜欢的话题之一:编译器!编译器由管道转换构成,该管道读取 text 文本并将其转化为机器代码、汇编语言、字节码或者其他的编程语言。管道首先会使用语法分析器对目标变成语言进行语法分析。一般情况下文本会被分解为不同的组成部分,例如:关键词、标识符、标点和数字等等。每个组成部分都会被相应的数据类型标记。例如下面这个 Java 数据类型:

1
public class Main {}

这些组成部分(可以称作标记)如下所示:

public keyword, “public”
class keyword, “class”
idenitifier, “Main”
left-bracket, “{“
right-bracket, “}”

这些标记可以划分为两个部分:

标记类型
语义部分

这会导致我们进行如下的 Java 设计方式:

1
2
3
public enum TokenType {
KEYWORD, IDENTIFIER, LBRACKET, RBRACKET, ...
}
1
2
3
4
5
public class Token {
public TokenToken type;
**Here, I think TokenToken should be "TokenType"**
public String lexeme;
}

对于一些标记类型来说,例如数值常量,标记类型最好能够将包含这些属性信息。就数值常量来说,在他的标记类型里应该包括常量值这一属性。实现这一设计的传统方式是使用继承机制产生 Token 子类。

1
2
3
public class IntegerConstant extends Token {
public long value;
}

另外一种完成该设计的方式是利用组合方式产生 IntegerConstantIntegerConstant 包含 token 的引用:

1
2
3
4
public class IntegerConstant {
public Token type;
public long value;
}

在这个例子中,继承机制是一个比较恰当的选择。理由是语法分析器需要返回一个通用类型。考虑一下语法分析器的接口设计:

1
2
3
4
5
6
public class Lexer {
public Lexer(InputStream in)
public boolean EOF()
public Token peek() throws Error
public Token next() throws Error
}

在继承机制中,IntegerConstant 属于Token类型,所以它可以在Lexer中调用。这不是唯一可用或者最好的设计,但是这种设计方式是有效的。让我们看一下 Go 是如何完成这一目的的。

Inheritance and Composition in Go

Go 中实现组合是一件十分容易的事情。简单组合两个结构体就能够构造一个新的数据类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type TokenType uint16

const (
KEYWORD TokenType = iota
IDENTIFIER
LBRACKET
RBRACKET
INT
)

type Token struct {
Type TokenType
Lexeme string
}

type IntegerConstant struct {
Token *Token
Value uint64
}

这就是 Go 中实现代码和数据共享的常用方式。然而如果你想实现继承机制,我们该如何去做?

Why would you want to use inheritance in go

一个可选的方案是将 Token 设计成接口类型。这种方案在 Java 和** Go** 都适用:

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
type Token interface {
Type() TokenType
Lexeme() string
}

type Match struct {
toktype TokenType
lexeme string
}

type IntegerConstant struct {
token Token
value uint64
}

func (m *Match) Type() TokenType {
return m.toktype
}

func (m *Match) Lexeme() string {
return m.lexeme
}

func (i *IntegerConstant) Type() TokenType {
return i.token.Type()
}

func (i *IntegerConstant) Lexeme() string {
return i.token.Lexeme()
}

func (i *IntegerConstant) Value() uint64 {
return i.value
}

这样分析器就可以返回满足 MatchIntegerConstant 类型的 Token 接口。

继承机制的简化版

上面的实现方案的一个问题是 *IntegerConstant 的方法调用中,出现了重复造轮子的问题。但是我们可以使用Go 内建的嵌入机制来避免此类情况的出现。嵌入机制(匿名嵌入)允许类型之前共享代码和数据。

1
2
3
4
5
6
7
8
type IntegerConstant struct {
Token
value uint64
}

func (i *IntegerConstant) Value() uint64 {
return i.value
}

IntegerConstant中 匿名嵌入了** Token** 类型,使得 IntegerConstant “继承“了 Token 的字段和方法。很酷的方法!我们可以这样写代码:

1
2
3
4
t := IntegerConstant{&Match{KEYWORD, "wizard"}, 2}
fmt.Println(t.Type(), t.Lexeme(), t.Value())
x := Token(t)
fmt.Println(x.Type(), x.Lexeme())

(可以在这里试一下 :https://play.golang.org/p/PJW7VShpE0)

我们没有编写 Type()Value() 方法的代码,但是 *IntegerConstant 也实现了 Token 接口,非常棒。

结构体的”继承”机制

Go 中有三种方式完成”继承“机制,您已经看到了第一种实现方式:在结构体的第一个字段匿名嵌入接口类型。你还可以利用结构体实现其他两种”继承“机制:
1.匿名嵌入结构体实例

1
2
3
4
type IntegerConstant struct {
Match
value uint64
}

2.匿名嵌入结构体实例指针

1
2
3
4
type IntegerConstant struct {
*Match
value uint64
}

在所有的例子中,与正常嵌入类型不同的是我们使用匿名嵌入。然而,这个字段还是有字段名称的,名称是嵌入类型名称。在IntegerConstantMatch 字段中,字段名称是 Match,无论嵌入类型是实例还是指针。

在以上的方案中,你不能嵌入与嵌入类型相同的方法名。例如结构体 Bar 匿名嵌入结构体 Foo 后,就不能拥有名称为 Foo 的方法,同样也不能实现 type Fooer interface { Foo() } 接口类型。

共享代码、共享数据或者两者兼得

相比于 JavaGo 在继承和聚合之间的界限是很模糊的。Go 中没有 extends 关键词。在语法的层次上,继承看上去与聚合没有什么区别。Go聚合继承唯一的不同在于,继承自其他结构体的 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
type Pet struct {
name string
}

type Dog struct {
Pet
Breed string
}

func (p *Pet) Speak() string {
return fmt.Sprintf("my name is %v", p.name)
}

func (p *Pet) Name() string {
return p.name
}

func (d *Dog) Speak() string {
return fmt.Sprintf("%v and I am a %v", d.Pet.Speak(), d.Breed)
}

func main() {
d := Dog{Pet: Pet{name: "spot"}, Breed: "pointer"}
fmt.Println(d.Name())
fmt.Println(d.Speak())
}

(可以试一下 https://play.golang.org/p/Pmkd27Nqqy)

输出:

spot
my name is spot and I am a pointer

嵌入式继承机制的的局限

相比于** Java Go** 的继承机制的作用是非常有限的。有很多的设计方案可以在 Java 轻松实现,但是 Go 却不可能完成同样的工作。让我们看一下:

Overriding Methods

上面的 Pet 例子中,Dog 类型重载了 Speak() 方法。然而如果 Pet 有另外一个方法 Play() 被调用,但是 Dog 没有实现 Play() 的时候,Dog 类型的 Speak() 方法则不会被调用。

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
package main

import (
"fmt"
)

type Pet struct {
name string
}

type Dog struct {
Pet
Breed string
}

func (p *Pet) Play() {
fmt.Println(p.Speak())
}

func (p *Pet) Speak() string {
return fmt.Sprintf("my name is %v", p.name)
}

func (p *Pet) Name() string {
return p.name
}

func (d *Dog) Speak() string {
return fmt.Sprintf("%v and I am a %v", d.Pet.Speak(), d.Breed)
}

func main() {
d := Dog{Pet: Pet{name: "spot"}, Breed: "pointer"}
fmt.Println(d.Name())
fmt.Println(d.Speak())
d.Play()
}

(试一下 https://play.golang.org/p/id-aDKW8L6)

输出:

spot
my name is spot and I am a pointer
my name is spot

但是 Java 中就会像我们预想的那样工作:

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
39
40
41
public class Main {
public static void main(String[] args) {
Dog d = new Dog("spot", "pointer");
System.out.println(d.Name());
System.out.println(d.Speak());
d.Play();
}
}

class Pet {
public String name;

public Pet(String name) {
this.name = name;
}

public void Play() {
System.out.println(Speak());
}

public String Speak() {
return String.format("my name is %s", name);
}

public String Name() {
return name;
}
}

class Dog extends Pet {
public String breed;

public Dog(String name, String breed) {
super(name);
this.breed = breed;
}

public String Speak() {
return String.format("my name is %s and I am a %s", name, breed);
}
}

输出:

$ javac Main.java && java Main
spot
my name is spot and I am a pointer
my name is spot and I am a pointer

这个明显的区别是因为 Go 从根本上阻止了抽象方法的使用。让我们看看下面这个例子:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package main

import (
"fmt"
)

type Pet struct {
speaker func() string
name string
}

type Dog struct {
Pet
Breed string
}

func NewPet(name string) *Pet {
p := &Pet{
name: name,
}
p.speaker = p.speak
return p
}

func (p *Pet) Play() {
fmt.Println(p.Speak())
}

func (p *Pet) Speak() string {
return p.speaker()
}

func (p *Pet) speak() string {
return fmt.Sprintf("my name is %v", p.name)
}

func (p *Pet) Name() string {
return p.name
}

func NewDog(name, breed string) *Dog {
d := &Dog{
Pet: Pet{name: name},
Breed: breed,
}
d.speaker = d.speak
return d
}

func (d *Dog) speak() string {
return fmt.Sprintf("%v and I am a %v", d.Pet.speak(), d.Breed)
}

func main() {
d := NewDog("spot", "pointer")
fmt.Println(d.Name())
fmt.Println(d.Speak())
d.Play()
}

(试一下 https://play.golang.org/p/9iIb2px7jH)

输出:

spot
my name is spot and I am a pointer
my name is spot and I am a pointer

现在跟我们预想的一样了,但是跟Java相比略显冗长和晦涩。你必须手工重载方法签名。而且,代码在结构体未正确初始化的情况下会崩溃,例如当调用 Speak() 时,speaker() 却没有完成初始化工作的时候。

SubtypingJava 中,Dog 继承自 Pet ,那么 Dog 类型就是 Pet 子类。这意味着在任何需要调用 Pet 类型的场景都可以使用 Dog 类型替换。这种关系称作多态性,但 Go 的结构体类型不存在这种机制。
让我们看下面的例子:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package main

import (
"fmt"
)

type Pet struct {
speaker func() string
name string
}

type Dog struct {
Pet
Breed string
}

func NewPet(name string) *Pet {
p := &Pet{
name: name,
}
p.speaker = p.speak
return p
}

func (p *Pet) Play() {
fmt.Println(p.Speak())
}

func (p *Pet) Speak() string {
return p.speaker()
}

func (p *Pet) speak() string {
return fmt.Sprintf("my name is %v", p.name)
}

func (p *Pet) Name() string {
return p.name
}

func NewDog(name, breed string) *Dog {
d := &Dog{
Pet: Pet{name: name},
Breed: breed,
}
d.speaker = d.speak
return d
}

func (d *Dog) speak() string {
return fmt.Sprintf("%v and I am a %v", d.Pet.speak(), d.Breed)
}

func Play(p *Pet) {
p.Play()
}

func main() {
d := NewDog("spot", "pointer")
fmt.Println(d.Name())
fmt.Println(d.Speak())
Play(d)
}

(试一下 https://play.golang.org/p/e1Ujx0VhwK)

输出:

prog.go:62: cannot use d (type *Dog) as type *Pet in argument to Play

然而,接口类型中存在子类化的多态机制!

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package main

import (
"fmt"
)

type Pet interface {
Name() string
Speak() string
Play()
}

type pet struct {
speaker func() string
name string
}

type Dog interface {
Pet
Breed() string
}

type dog struct {
pet
breed string
}

func NewPet(name string) *pet {
p := &pet{
name: name,
}
p.speaker = p.speak
return p
}

func (p *pet) Play() {
fmt.Println(p.Speak())
}

func (p *pet) Speak() string {
return p.speaker()
}

func (p *pet) speak() string {
return fmt.Sprintf("my name is %v", p.name)
}

func (p *pet) Name() string {
return p.name
}

func NewDog(name, breed string) *dog {
d := &dog{
pet: pet{name: name},
breed: breed,
}
d.speaker = d.speak
return d
}

func (d *dog) speak() string {
return fmt.Sprintf("%v and I am a %v", d.pet.speak(), d.breed)
}

func Play(p Pet) {
p.Play()
}

func main() {
d := NewDog("spot", "pointer")
fmt.Println(d.Name())
fmt.Println(d.Speak())
Play(d)
}

(试一下 https://play.golang.org/p/WMH-cr4AJf)

输出:

spot
my name is spot and I am a pointer
my name is spot and I am a pointer

所以接口类型可以用来实现子类化的机制。但是如果你想正确的实现方法重载,需要了解以上的技巧。

Conclusion

事实上,虽然这不是Go的主打特性,但是Go语言在结构体嵌入结构体或者接口方面的能力确实为实际工作增加了很大的灵活性。Go的这些特性为我们解决实际问题提供了新的解决方案。但是相较于Java等语言,由于Go缺少子类化和方法重载支持还有存在一些局限性。Go含有一项Java没有的特性–接口嵌入。关于接口嵌入的细节请参考Golang的官方文档的Embedding部分。

切片

Go 的切片很强大,一般用到切片的地方就不会用到数组了,所以今天就来测试一下关于切片的引用,容量的等等

1
2
// 定义一个长度为3, 容量为9的切片 
s := make([]string, 3, 9)
1
2
// 初始化元素到 s 里面
copy(s, []string{"a", "b", "c"})

网上资料的都说了切片是对底层数组的引用,那么对于追加之后超过的切片容量的呢?会怎么处理呢。首先看一下为超过容量。

1
2
3
4
5
6
d := []string{"d", "e", "f"}
s1 := append(s, d...)
fmt.Println(s1)
s[0] = "z"
fmt.Println(cap(s1))
fmt.Println(s1)

创建一个 d 切片
然后将 d 追加到 s 中并返回给一个 s1
打印 s1 => [a b c d e f]
改变 s 第一个元素
打印查看 s1 cap 的大小为 9
打印 s1 => [z b c d e f]
结果是不一致,的确是对底层数组的引用

在来看一个例子

1
2
3
4
5
6
d := []string{"d", "e", "f", "d", "e", "f", "d", "e", "f"}
s1 := append(s, d...)
fmt.Println(s1)
s[0] = "z"
fmt.Println(cap(s1))
fmt.Println(s1)

创建一个 d 切片
然后将 d 追加到 s 中并返回给一个 s1
打印 s1 => [a b c d e f d e f d e f]
改变 s 第一个元素
打印查看 s1 cap 的大小为 18
打印 s1 => [a b c d e f d e f d e f]
结果是一致,说明没有对 s 底层的数组进行引用了,而是开辟了新的 slice

websocket

这一篇继续上一篇文章的之后, 进行帧包的解析,当然是简洁的实现文本消息, 因为帧包里面的信息很多,就不一一弄了, 有兴趣的可以试

1
2
3
4
5
6
7
8
9
10
//定义一个64位的长度整形
var datalength int64
//定义个结构体
type webSocket struct {
Mask []byte
Conn net.Conn
}
func WebSocket(conn net.Conn) *webSocket {
return &webSocket{Conn:conn}
}

Websocket帧格式

0                    1                   2                    3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+------+-+-------------+--------------------------------+
|F|R|R|R|opcode|M| Payload Len |     extended payload length    |
|I|S|S|S|  (4) |A|    (7)      |        (16/63)                 |
|N|V|V|V|      |S|             |     (if payload len = 126/127) |
| |1|2|3|      |K|             |                                |
+-+-+-+-+------+-+-------------+--------------------------------+
|    Extended payload length continued, if payload len == 127   |
+------------------------------+--------------------------------+
|                              | Masking-key, if Mask set To 1  |
+------------------------------+--------------------------------+
|    Masking-key (continued)   |    Payload Data                |
+------------------------------+- - -  - - - - -  - - -  - - - -+
|                    Payload Data continued                     |
+- - - - -  - - - - - - - - - - - - - - -  - - - - - - - - - - -+
|                    Payload Data continued                     |
+----------------------------------------------------------------

下面一一解释包的说明
FIN 长度1位,表示是否是最后一帧,为1则表示最后一帧,为0则表示还有后续帧
RSV 长度三位 默认都是0 如果服务端与客户端没有协商,那么非0则认为是一个错误的帧
opcode 表示帧格式,占4位,格式如下

0x00,表示继续帧
0x01,表示文本帧
0x02,表示二进制帧
0x03-0x07,保留的未定义非控制帧
0x08,连接关闭帧
0x09,表示ping
0xA,表示pong
0xB-0xF,用于保留的控制帧

MASK,1位,定义负载数据是否使用掩码,1为使用掩码,0为不使用掩码

Payload Length,7位,7+16位,7+64位,定义负载数据的长度,以字节为单位。这部分如果为0-125,则负载长度则就是这段定义的长度,如果为126,之后的 Extend payload Length 16位将作为负载长度,如果为127,那么之后的Extend payload Length 64位将作为负载长度。

Masking-key 0 或者 32位,mask位设为0,则该字段缺失(不过协议要求,所有的帧都需要使用mask)

Payload data 负载数据=扩展数据+应用数据

包解析

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
//读取
func (this *webSocket)readFrame() string {
//解析第一个字节位
first := make([]byte, 1)
this.Conn.Read(first)
//获取FIN值,0代表数据未结束 1 代表数据结束
FIN := first[0] >> 7
RSV1 := first[0] >> 6 & 1
RSV2 := first[0] >> 5 & 1
RSV3 := first[0] >> 4 & 1
log.Println(FIN, RSV1, RSV2, RSV3)
OPCODE := first[0] & 0xF
log.Println(OPCODE)
//解析第二个字节位
second := make([]byte, 1)
this.Conn.Read(second)
//获取MASK值
MASK := second[0] >> 7
log.Println(MASK)
payLength := second[0] & 0x7F
fmt.Println(int(payLength))
datalength = int64(payLength)

//如果payload 长度为126则读取后面的两个字节 数据长度
if payLength == 126 {
extendByte := make([]byte, 2)
this.Conn.Read(extendByte)
datalength = uint16(binary.BigEndian.Uint16(extendByte))
}
//如果payload 长度为127则读取后面的两个字节 数据长度
if payLength == 127 {
extendByte := make([]byte, 8)
this.Conn.Read(extendByte)
datalength = unit64(binary.BigEndian.Uint64(extendByte))
}

// 读取Masking-key
maskSec := make([]byte, 4)
if MASK == 1 {
this.Conn.Read(maskSec)
}
//读取数据
data := make([]byte, datalength)
this.Conn.Read(data)
if MASK == 1 {
var i int64
for i = 0; i < datalength; i++ {
data[i] ^= maskSec[i % 4]
}
}
fmt.Println(FIN)
// 如果FIN 为 1 表示是最后一个数据帧
if FIN == 1 {
return string(data)
}

getNextData := this.readFrame()

data = append(data, getNextData...)

return string(data)
}

//发送包
/**
给客户端发送数据, 也要以帧格式发送, 这里以不做掩码处理, 如果需要就自行改变帧的二进制格式
*/
func (this *webSocket) writeFrame(data []byte) {

length := len(data)
buf := make([]byte, 10+length)

// 数据开始和结束的位置
payloadStart := 2

// 数据帧的第一个字节, 不支持分片,且值能发送文本类型数据 二进制格式为 1000 0001
buf[0] = 0x81
// 数据帧第二个字节,服务器发送的数据不需要进行掩码处理
if length < 125 {
buf[1] = byte(0x00) | byte(length)
} else if (length > 125 && length < 65536) {
buf[1] = byte(0x00) | 126
binary.BigEndian.PutUint16(buf[payloadStart:], uint16(length))
payloadStart += 2
} else {
buf[1] = byte(0x00) | 127
binary.BigEndian.PutUint64(buf[payloadStart:], uint64(length))
payloadStart += 8
}
// 复制
copy(buf[payloadStart:], data)
this.Conn.Write(buf)
}

完整代码

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
package main

import (
"net"
"fmt"
"log"
"encoding/binary"
"strings"
"crypto/sha1"
"io"
"encoding/base64"
)

const (
WEBSOCKET_KEY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
// 文本数据帧类型
)
var datalength int64

type webSocket struct {
Mask []byte
Conn net.Conn
}

func WebSocket(conn net.Conn) *webSocket {
return &webSocket{Conn:conn}
}
func main() {
connect()
}


func connect() {
ln, err := net.Listen("tcp", ":8000")

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

for {
conn, err := ln.Accept()
if err != nil {
fmt.Println(err)
}

for {
handConnect(conn)
}
}
}

func handConnect(conn net.Conn) {
content := make([]byte, 1024)
n, err := conn.Read(content)
if err != nil {
fmt.Println(err)
}
fmt.Println(fmt.Sprintf("读取%d个字节", n))

header := parseHeaders(string(content))
fmt.Println(header["Sec-WebSocket-Key"])

secret := getSecret(header["Sec-WebSocket-Key"])

response := "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
response += "Upgrade: websocket\r\n"
response +="Connection: Upgrade\r\n"
response +="Sec-WebSocket-Accept: " + secret +"\r\n"
response += "\r\n"

conn.Write([]byte(response))

for {
ws := WebSocket(conn)
data := ws.readFrame()
fmt.Println("data is :", data)

ws.writeFrame([]byte("i recive a message as you konw"))
}

}

func (this *webSocket)readFrame() string {
//解析第一个字节位
first := make([]byte, 1)
this.Conn.Read(first)
//获取FIN值,0代表数据未结束 1 代表数据结束
FIN := first[0] >> 7
RSV1 := first[0] >> 6 & 1
RSV2 := first[0] >> 5 & 1
RSV3 := first[0] >> 4 & 1
log.Println(FIN, RSV1, RSV2, RSV3)
OPCODE := first[0] & 0xF
log.Println(OPCODE)
//解析第二个字节位
second := make([]byte, 1)
this.Conn.Read(second)
//获取MASK值
MASK := second[0] >> 7
log.Println(MASK)
payLength := second[0] & 0x7F
fmt.Println(int(payLength))
datalength = int64(payLength)

//如果payload 长度为126则读取后面的两个字节 数据长度
if payLength == 126 {
extendByte := make([]byte, 2)
this.Conn.Read(extendByte)
datalength = int64(binary.BigEndian.Uint16(extendByte))
}
//如果payload 长度为127则读取后面的两个字节 数据长度
if payLength == 127 {
extendByte := make([]byte, 8)
this.Conn.Read(extendByte)
datalength = int64(binary.BigEndian.Uint16(extendByte))
}

// 读取Masking-key
maskSec := make([]byte, 4)
if MASK == 1 {
this.Conn.Read(maskSec)
}
//读取数据
data := make([]byte, datalength)
this.Conn.Read(data)
if MASK == 1 {
var i int64
for i = 0; i < datalength; i++ {
data[i] ^= maskSec[i % 4]
}
}
fmt.Println(FIN)
// 如果FIN 为 1 表示是最后一个数据帧
if FIN == 1 {
return string(data)
}

getNextData := this.readFrame()

data = append(data, getNextData...)

return string(data)
}

/**
给客户端发送数据, 也要以帧格式发送, 这里以不做掩码处理, 如果需要就自行改变帧的二进制格式
*/
func (this *webSocket) writeFrame(data []byte) {

length := len(data)
buf := make([]byte, 10+length)

// 数据开始和结束的位置
payloadStart := 2

// 数据帧的第一个字节, 不支持分片,且值能发送文本类型数据 二进制格式为 1000 0001
buf[0] = 0x81
// 数据帧第二个字节,服务器发送的数据不需要进行掩码处理
if length < 125 {
buf[1] = byte(0x00) | byte(length)
} else if (length > 125 && length < 65536) {
buf[1] = byte(0x00) | 126
binary.BigEndian.PutUint16(buf[payloadStart:], uint16(length))
payloadStart += 2
} else {
buf[1] = byte(0x00) | 127
binary.BigEndian.PutUint64(buf[payloadStart:], uint64(length))
payloadStart += 8
}
// 复制
copy(buf[payloadStart:], data)
this.Conn.Write(buf)
}

func parseHeaders(content string) map[string]string {
h := strings.Split(content, "\r\n")
header := make(map[string]string, 0)
for _, value := range h {
v := strings.Split(value, ":")

if len(v) >= 2 {
header[strings.Trim(v[0], " ")] = strings.Trim(v[1], " ")
}
}

return header
}

func getSecret(key string) string{
key += WEBSOCKET_KEY

res := sha1.New()
io.WriteString(res, key)

return base64.StdEncoding.EncodeToString(res.Sum(nil))
}

试一试吧,前台

Websocket

websocket作为h5新的API, 基于HTTP的新协议, 具体资料请自行搜索

Websocket标准头信息

GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: 127.0.0.1:8000
Origin: http://127.0.0.1:8000
Sec-WebSocket-Key: hj0eNqbhE/A0GkBXDRrYYw==
Sec-WebSocket-Version: 13

Sec-WebSocket-Key是由客户端(浏览器)发起的, 主要就是要根据这个来进行握手

Websocket标准response

HTTP/1.1 101 Web Socket Protocol Handshake
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept:secret

握手的关键就在这个响应的Sec-WebSocket-Accept,服务要通过Sec-WebSocket-Key和常量字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11合并成新的字符串之后进行sha1加密, 然后在进行base64加密之后得到Sec-WebSocket-Accept, 返回给客户端认证握手。

GO实现

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package main

import (
"net"
"fmt"
"strings"
"crypto/sha1"
"io"
"encoding/base64"
)

const (
WEBSOCKET_KEY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
)

func main() {
connect()
}


func connect() {
//监听
ln, err := net.Listen("tcp", ":8000")
if err != nil {
fmt.Println(err)
}

for {
//接受客户端连接
conn, err := ln.Accept()
if err != nil {
fmt.Println(err)
}

for {
handConnect(conn)
}
}
}

func handConnect(conn net.Conn) {
content := make([]byte, 1024)
n, err := conn.Read(content)
if err != nil {
fmt.Println(err)
}
fmt.Println(fmt.Sprintf("读取%d个字节", n))

header := parseHeaders(string(content))
fmt.Println(header["Sec-WebSocket-Key"])

secret := getSecret(header["Sec-WebSocket-Key"])

response := "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
response += "Upgrade: websocket\r\n"
response +="Connection: Upgrade\r\n"
response +="Sec-WebSocket-Accept: " + secret +"\r\n"
response += "\r\n"

conn.Write([]byte(response))

}

func parseHeaders(content string) map[string]string {
h := strings.Split(content, "\r\n")
header := make(map[string]string, 0)
for _, value := range h {
v := strings.Split(value, ":")
//注意这里切片长度一定要大于2
if len(v) >= 2 {
//空格也必须去除
header[strings.Trim(v[0], " ")] = strings.Trim(v[1], " ")
}
}
return header
}

func getSecret(key string) string{
key += WEBSOCKET_KEY
res := sha1.New()
io.WriteString(res, key)
return base64.StdEncoding.EncodeToString(res.Sum(nil))
}

上面就是简单的实现, 然后就是如何解析frame。

Laravel

问题可能出现的场景 可能就是你没有自己定义登录中间件, 使用默认的auth的时候。

当我在使用了Laravel的默认的用户认证Trait时候, 并且已经在auth配置文件配置了guards。重写该门面方法。

1
2
3
4
protected function guard()
{
return Auth::guard('admin');
}

照理讲当你自定义完该方法, 并且切换了guard之后, 就应该可以了, 可是在并不能通过,尝试了找到了登录验证方法

1
2
3
4
5
6
7
8
9
protected function sendLoginResponse(Request $request)
{
$request->session()->regenerate();

$this->clearLoginAttempts($request);

return $this->authenticated($request, $this->guard()->user())
?: redirect()->intended($this->redirectPath());
}

我通过打印$this->gurad()->user()之后发现已经验证成功, 可是并不能通过, 所以问题出现在哪里呢?登录之后应该会自动跳转到$this->redirectPath();就是你自定义跳转的页面, 查看该页面的路由, 发现我用了auth的中间件,来看一下auth中间件的做的事儿
发现这个参数

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
public function handle($request, Closure $next, ...$guards)
{
$this->authenticate($guards);

return $next($request);
}

/**
* Determine if the user is logged in to any of the given guards.
*
* @param array $guards
* @return void
*
* @throws \Illuminate\Auth\AuthenticationException
*/
protected function authenticate(array $guards)
{
if (empty($guards)) {
return $this->auth->authenticate();
}

foreach ($guards as $guard) {
if ($this->auth->guard($guard)->check()) {
return $this->auth->shouldUse($guard);
}
}

throw new AuthenticationException('Unauthenticated.', $guards);
}

…$gurad可变参数,这里就没错了, 在auth中间还需要认证guard, 这里在authenticate也可以发现,auth中间件默认使用的是auth配置里面default的门面。具体可以查看Auth源码。 这里就不去解释了。但是如果你切换了guard的话, 你就必须加上认证的guards。
在你的路由文件这样写 ->middleware(‘auth:admin’);当然从这个中间件来看, 支持多个guard。 具体在文档的 中间件可以查看