golang的socket的分布式部署

随着业务的发展,线上部署肯定不会是一台服务器,当多个服务器的时候,前面的程序是无法满足的,当一个用户连接到主机A,而另一个用户连接到主机B,这两个用户又如何通信呢?

本篇我们来解决这个问题,我们通过在生成的连接标识到带有相关信息和转发机制来实现,代码如下:

package main

import (
	"bufio"
	"encoding/json"
	"flag"
	"fmt"
	"math/rand"
	"net"
	"strings"
	"sync"
	"time"
)

var connList sync.Map
var Ip, Port string

type UserMsg struct {
	From string `json:"from"`
	To   string `json:"to"`
	Body string `json:"body"`
	Cmd  string `json:"cmd"`
}

func getLocalIpV4() string {
	inters, err := net.Interfaces()
	if err != nil {
		panic(err)
	}
	for _, inter := range inters {
		// 判断网卡是否开启,过滤本地环回接口
		if inter.Flags&net.FlagUp != 0 && !strings.HasPrefix(inter.Name, "lo") {
			// 获取网卡下所有的地址
			addrs, err := inter.Addrs()
			if err != nil {
				continue
			}
			for _, addr := range addrs {
				if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
					//判断是否存在IPV4 IP 如果没有过滤
					if ipnet.IP.To4() != nil {
						return ipnet.IP.String()
					}
				}
			}
		}
	}
	return ""
}

//{"from":"aaa","to":"bbb","body":"aaa","cmd":"msg"}

func randName() string {
	var letterRunes = []rune("1234567890")
	rand.Seed(time.Now().UnixNano())
	b := make([]rune, 2)
	for i := range b {
		b[i] = letterRunes[rand.Intn(len(letterRunes))]
	}
	return Ip + ":" + Port + ":" + string(b)
}

func forwardMsg(ip, port, msg string) {
	conn, _ := net.Dial("tcp", ip+":"+port)
	defer conn.Close()
	conn.Write([]byte(msg))
	time.Sleep(time.Second)
}

func process(conn net.Conn) {
	// 处理完关闭连接
	defer conn.Close()
	name := randName()
	connList.Store(name, conn)
	conn.Write([]byte(" you is  : " + name + "\r\n"))
	// 针对当前连接做发送和接受操作
	for {
		reader := bufio.NewReader(conn)
		// 读取字符串, 直到碰到回车返回
		str, err := reader.ReadString('\n')
		// 数据读取正确
		if err == nil {
			// 去掉字符串尾部的回车
			newStr := strings.TrimSpace(str)
			var strStruct UserMsg
			err := json.Unmarshal([]byte(newStr), &strStruct)
			if err != nil {
				fmt.Println("json error :" + err.Error() + " ; " + newStr)
			} else {
				to := strStruct.To
				toArr := strings.Split(to, ":")
				if toArr[0] == Ip && toArr[1] == Port {
					toUser, _ := connList.Load(to)
					toUserConn, ok := toUser.(net.Conn)
					if !ok {
						fmt.Println("to user fail")
					} else {
						if strStruct.Cmd == "msg" {
							toUserConn.Write([]byte(strStruct.From + " say : " + strStruct.Body + "\r\n"))
							fmt.Println("send msg")
						} else {
							fmt.Println("error msg")
						}
					}
				} else { //转发给其他服务端
					forwardMsg(toArr[0], toArr[1], str+"\n")
					fmt.Println("forwardMsg " + toArr[0] + toArr[1] + str)
				}
			}
		} else {
			// 发生错误
			fmt.Println("Session closed")
			conn.Close()
			break
		}
	}
}

func main() {
	flag.StringVar(&Port, "Port", "1215", "端口")
	flag.Parse()
	Ip = getLocalIpV4()
	// 建立 tcp 服务
	listen, err := net.Listen("tcp", Ip+":"+Port)
	if err != nil {
		fmt.Printf("listen failed, err:%v\n", err)
		return
	}
	fmt.Println("listen " + Ip + ":" + Port)
	for {
		// 等待客户端建立连接
		conn, err := listen.Accept()
		if err != nil {
			fmt.Printf("accept failed, err:%v\n", err)
			continue
		}
		// 启动一个单独的 goroutine 去处理连接
		go process(conn)
	}
}

这是服务端代码,我们发消息的格式如下:

{"from":"aaa","to":"xxx.xxx.xxx.xxx:xx:xx","body":"1234567","cmd":"msg"}

这样我们就可能实现一个可以分布属部署的程序了.

发表在 好文推荐 | 标签为 | 留下评论

golang的socket的单聊

接下来我们来演示如何实现单聊,既然群聊可以,那么单聊就是在于如何区分用户,然后找到用户发送消息即可.

package main

/*
* 单聊--一对一聊天
 */

import (
	"bufio"
	"encoding/json"
	"fmt"
	"math/rand"
	"net"
	"strings"
	"sync"
	"time"
)

//保存连接
var connList sync.Map

//消息结构{"from":"aaa","to":"bbb","body":"aaa","cmd":"sendMsg"}
type UserMsg struct {
	From string `json:"from"`
	To   string `json:"to"`
	Body string `json:"body"`
	Cmd  string `json:"cmd"`
}

//随机字符串
func randName() string {
	var letterRunes = []rune("123456789")
	rand.Seed(time.Now().UnixNano())
	b := make([]rune, 6)
	for i := range b {
		b[i] = letterRunes[rand.Intn(len(letterRunes))]
	}
	return string(b)
}

func DealConn(conn net.Conn) {
	// 处理完关闭连接
	defer conn.Close()
	fmt.Println("new conn\n")
	name := randName()
	connList.Store(name, conn)
	conn.Write([]byte("you is  : " + name + "\r\n"))
	// 针对当前连接做发送和接受操作
	for {
		reader := bufio.NewReader(conn)
		// 读取字符串, 直到碰到回车返回
		str, err := reader.ReadString('\n')
		// 数据读取正确
		if err == nil {
			// 去掉字符串尾部的回车
			str = strings.TrimSpace(str)
			var strStruct UserMsg
			err := json.Unmarshal([]byte(str), &strStruct)
			if err != nil {
				fmt.Printf("msg json error, err:%v\n", err)
			} else {
				to := strStruct.To
				cmd := strStruct.Cmd
				if cmd == "sendMsg" {
					toUser, ok := connList.Load(to)
					if !ok {
						fmt.Println("to user is not\n")
					} else {
						toUserConn, ok := toUser.(net.Conn)
						if !ok {
							fmt.Println("to user type wrong\n")
						} else {
							toUserConn.Write([]byte(strStruct.From + " say : " + strStruct.Body + "\r\n"))
						}
					}
				} else {
					fmt.Println("unkonw msg")
				}
			}
		} else {
			//删除保存的连接
			connList.Delete(name)
			fmt.Println("conn close\n")
			break
		}
	}
}

func main() {
	// 建立tcp 服务
	listen, err := net.Listen("tcp", "0.0.0.0:1215")
	if err != nil {
		fmt.Printf("listen failed, err:%v\n", err)
		return
	}

	for {
		// 等待客户端建立连接
		fmt.Printf("accept conn ...... \n")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Printf("accept failed, err:%v\n", err)
			continue
		}

		// 启动一个单独的 goroutine 去处理连接
		go DealConn(conn)
	}
}

这个例子是通过json协议来标识消息来源于那里,要发送给谁,消息类型是什么,消息内容是什么来实现单聊的.

发表在 好文推荐 | 标签为 | 留下评论

golang的socket的群聊

在前面2篇的文章,我们一直说的是一个客户端和一个服务的端的交互模式,现在我们来说下群聊模式,即多个客户端和一个服务端的交互模式,关键的知识点在于服务端如何转发消息。

我们现在通过一个map来存储所有的连接,然后通过遍历map来对所有连接发消息,代码如下:

package main

/*
* 群聊--广播消息
 */

import (
	"bufio"
	"fmt"
	"math/rand"
	"net"
	"strings"
	"sync"
	"time"
)

//保存连接
var connList sync.Map

//随机字符串
func randName() string {
	var letterRunes = []rune("123456789")
	rand.Seed(time.Now().UnixNano())
	b := make([]rune, 6)
	for i := range b {
		b[i] = letterRunes[rand.Intn(len(letterRunes))]
	}
	return string(b)
}

func DealConn(conn net.Conn) {
	// 处理完关闭连接
	defer conn.Close()
	fmt.Println("new conn\n")
	name := randName()
	connList.Store(name, conn)
	conn.Write([]byte("you is  : " + name + "\r\n"))
	// 针对当前连接做发送和接受操作
	for {
		reader := bufio.NewReader(conn)
		// 读取字符串, 直到碰到回车返回
		str, err := reader.ReadString('\n')
		// 数据读取正确
		if err == nil {
			// 去掉字符串尾部的回车
			str = strings.TrimSpace(str)
			//广播消息
			connList.Range(func(k, userConn interface{}) bool {
				userConn.(net.Conn).Write([]byte(" say : " + str + "\r\n"))
				return true
			})
		} else {
			//删除保存的连接
			connList.Delete(name)
			fmt.Println("conn close\n")
			break
		}
	}
}

func main() {
	// 建立tcp 服务
	listen, err := net.Listen("tcp", "0.0.0.0:1215")
	if err != nil {
		fmt.Printf("listen failed, err:%v\n", err)
		return
	}

	for {
		// 等待客户端建立连接
		fmt.Printf("accept conn ...... \n")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Printf("accept failed, err:%v\n", err)
			continue
		}

		// 启动一个单独的 goroutine 去处理连接
		go DealConn(conn)
	}
}

启动上面的代码做服务端,然后通过启动多个telnet,就可以发现,每个telnet端发的消息,其他telnet也可以收到.

发表在 好文推荐 | 标签为 | 留下评论

golang的socket的粘包拆包问题

接下来我们来学习另一个知识点,粘包和拆包,在上一篇文章中,我们采用了”reader.ReadString(‘\n’)”来简单处理粘包和拆包问题,现在我们来深入学习一下。

服务端代码示例:

package main

/*
* 粘包和拆包问题
 */

import (
	"fmt"
	"net"
	"time"
)

func DealConn(conn net.Conn) {
	// 处理完关闭连接
	defer conn.Close()
	fmt.Println("new conn\n")
	var buf = make([]byte, 5)
	// 针对当前连接做发送和接受操作
	for {
		n, err := conn.Read(buf)
		if err != nil {
			break
		}
		if n > 0 {
			fmt.Println("received msg", n, "bytes:", string(buf[:n]))
			//模拟处理消息耗时
			time.Sleep(time.Second)
		}

	}
}

func main() {
	// 建立tcp 服务
	listen, err := net.Listen("tcp", "0.0.0.0:1215")
	if err != nil {
		fmt.Printf("listen failed, err:%v\n", err)
		return
	}

	for {
		// 等待客户端建立连接
		fmt.Printf("accept conn ...... \n")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Printf("accept failed, err:%v\n", err)
			continue
		}

		// 启动一个单独的 goroutine 去处理连接
		go DealConn(conn)
	}
}

客户端发送消息代码示例:

package main

import (
	"net"
	"time"
)

func main() {
	conn, _ := net.Dial("tcp", "0.0.0.0:1215")
	defer conn.Close()
	for i := 0; i < 10; i++ {
		conn.Write([]byte("4444"))
		conn.Write([]byte("55555"))
		conn.Write([]byte("666666"))
	}
	time.Sleep(time.Second)
}

先运行服务端,然后再运行客户端,可以看到服务端的日志如下:

received msg 5 bytes: 44445
received msg 5 bytes: 55556
received msg 5 bytes: 66666
received msg 5 bytes: 44445
received msg 5 bytes: 55556
received msg 5 bytes: 66666
received msg 5 bytes: 44445
received msg 5 bytes: 55556
received msg 5 bytes: 66666
received msg 5 bytes: 44445
received msg 5 bytes: 55556
received msg 5 bytes: 66666
received msg 5 bytes: 44445
received msg 5 bytes: 55556
received msg 5 bytes: 66666
received msg 5 bytes: 44445
received msg 5 bytes: 55556
received msg 5 bytes: 66666
received msg 5 bytes: 44445
received msg 5 bytes: 55556
received msg 5 bytes: 66666
received msg 5 bytes: 44445
received msg 5 bytes: 55556
received msg 5 bytes: 66666
received msg 5 bytes: 44445
received msg 5 bytes: 55556
received msg 5 bytes: 66666
received msg 5 bytes: 44445
received msg 5 bytes: 55556
received msg 5 bytes: 66666

从我们这个日志,我们看到收到的消息是串了的,那么为什么会出现此问题呢?

这是因为 TCP 是面向连接的传输协议,TCP 传输的数据是以流的形式,而流数据是没有明确的开始结尾边界,所以 TCP 也没办法判断哪一段流属于一个消息。

粘包的主要原因是

  1. 发送方每次写入数据 < 套接字(Socket)缓冲区大小
  2. 接收方读取套接字(Socket)缓冲区数据不够及时,造成积压

拆包的主要原因:

  1. 发送方每次写入数据 > 套接字(Socket)缓冲区大小
  2. 发送的数据大于协议的MTU(Maximum Transmission Unit,最大传输单元),因此必须拆包

对于粘包和拆包的有如下解决方案

  1. 使用特定字符来分割数据包,但是若数据中含有分割字符则会出现Bug
  2. 定长分隔(每个数据包最大为该长度,不足时使用特殊字符填充) ,但是数据不足时会浪费传输资源
  3. 在数据包中添加长度字段,自定义协议实现,golang可以使用bufio.Scanner包来实现自定义协议

只有在直接使用 TCP 协议才存在 “粘包/拆包” 问题,其上层应用层协议比如 HTTP ,已经帮我们处理好了,无需关注这些底层,但是我们自己实现一个自定义协议,就必须考虑这些细节了

发表在 好文推荐 | 标签为 | 留下评论

golang的socket的hello world

今天我们从最简单的socket编程入门,首先我们来熟悉以下三个关键信息

  1. Listen ,监控指定的IP和端口
  2. Accept ,等待客户端连接
  3. read/write,从连接中读取或者写入数据

现在我们从一个最简单的代码示例来熟悉

package main

/*
* socket的hello world
* 无论客户端发送任何消息,服务端都回复hello world
 */

import (
	"bufio"
	"fmt"
	"net"
	"strings"
)

func DealConn(conn net.Conn) {
	// 处理完关闭连接
	defer conn.Close()
	fmt.Println("new conn\n")
	// 针对当前连接做发送和接受操作
	for {
		reader := bufio.NewReader(conn)
		// 读取字符串, 直到碰到回车返回
		str, err := reader.ReadString('\n')
		// 数据读取正确
		if err == nil {
			// 去掉字符串尾部的回车
			str = strings.TrimSpace(str)
			// 回复消息
			conn.Write([]byte("hello world\n"))
		} else {
			break
		}
	}
}

func main() {
	// 建立tcp 服务
	listen, err := net.Listen("tcp", "0.0.0.0:1215")
	if err != nil {
		fmt.Printf("listen failed, err:%v\n", err)
		return
	}

	for {
		// 等待客户端建立连接
		fmt.Printf("accept conn ...... \n")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Printf("accept failed, err:%v\n", err)
			continue
		}

		// 启动一个单独的 goroutine 去处理连接
		go DealConn(conn)
	}
}

这个代码示例,就是启动一个监控1215的监听程序,无论谁都可以来连接,无论连接方输入什么内容,都是回复”hello world“.

发表在 好文推荐, 网站开发 | 标签为 | 留下评论

二叉搜索树go实现

二叉查找树,又叫二叉排序树,二叉搜索树,是一种有特定规则的二叉树,定义如下

  1. 它是一棵二叉树,或者是空树
  2. 左子树所有节点的值都小于它的根节点,右子树所有节点的值都大于它的根节点
  3. 左右子树也是一棵二叉查找树

以下是GO语言实现的完整代码

package main

import (
	"fmt"
)

//二叉搜索树节点
type Searc2TreeNode struct {
	Value int
	Left  *Searc2TreeNode
	Right *Searc2TreeNode
}

//添加节点
func (n *Searc2TreeNode) Add(val int) {
	if val > n.Value {
		if n.Right == nil {
			n.Right = &Searc2TreeNode{Value: val}
		} else {
			n.Right.Add(val)
		}
	} else if val < n.Value {
		if n.Left == nil {
			n.Left = &Searc2TreeNode{Value: val}
		} else {
			n.Left.Add(val)
		}
	} else {
		fmt.Println(val, "已经存在")
	}
}

//遍历节点
func (n *Searc2TreeNode) View(vType string) {
	if vType != "first" && vType != "last" && vType != "mid" {
		fmt.Println("参数不合法")
	} else if vType == "first" { //先序遍历
		fmt.Print(n.Value, " ")
		if n.Left != nil {
			n.Left.View(vType)
		}
		if n.Right != nil {
			n.Right.View(vType)
		}
	} else if vType == "mid" { //中序遍历
		if n.Left != nil {
			n.Left.View(vType)
		}
		fmt.Print(n.Value, " ")
		if n.Right != nil {
			n.Right.View(vType)
		}
	} else { //后序遍历
		if n.Left != nil {
			n.Left.View(vType)
		}

		if n.Right != nil {
			n.Right.View(vType)
		}
		fmt.Print(n.Value, " ")
	}
}

//查找节点
func (n *Searc2TreeNode) Find(val int) *Searc2TreeNode {
	if n.Value == val {
		return n
	}
	if n.Left != nil && val < n.Value {
		return n.Left.Find(val)
	}
	if n.Right != nil && val > n.Value {
		return n.Right.Find(val)
	}
	return nil
}

//查找父节点
func (n *Searc2TreeNode) FindParent(val int) *Searc2TreeNode {
	if n.Left != nil && val < n.Value {
		if n.Left.Value == val {
			return n
		} else {
			return n.Left.FindParent(val)
		}
	}
	if n.Right != nil && val > n.Value {
		if n.Right.Value == val {
			return n
		} else {
			return n.Right.FindParent(val)
		}
	}
	return nil
}

//二叉搜索树
type Searc2Tree struct {
	Root *Searc2TreeNode
}

//添加节点
func (t *Searc2Tree) Add(val int) {
	if t.Root == nil {
		t.Root = &Searc2TreeNode{Value: val}
	} else {
		t.Root.Add(val)
	}
}

//遍历节点
func (t *Searc2Tree) View(vType string) {
	if t.Root != nil {
		t.Root.View(vType)
	} else {
		fmt.Println("root is empty")
	}
	fmt.Println("")
}

//查找节点
func (t *Searc2Tree) Find(val int) *Searc2TreeNode {
	if t.Root != nil {
		return t.Root.Find(val)
	} else {
		return nil
	}
}

//查找父节点
func (t *Searc2Tree) FindParent(val int) *Searc2TreeNode {
	if t.Root != nil {
		return t.Root.FindParent(val)
	} else {
		return nil
	}
}

//删除节点
func (t *Searc2Tree) Delete(val int) bool {
	if t.Root != nil {
		node := t.Find(val)
		if node == nil {
			return false
		} else {
			nodeParent := t.FindParent(val)
			if nodeParent == nil && node.Left == nil && node.Right == nil { //如果是根节点,且只有根节点
				t.Root = nil
				return true
			} else if node.Left == nil && node.Right == nil { //删除的节点有父亲节点,但没有子树
				if nodeParent.Left != nil && val == nodeParent.Left.Value { //左子树
					nodeParent.Left = nil
				} else { //右子树
					nodeParent.Right = nil
				}
				return true
			} else if node.Left != nil && node.Right != nil { //删除的节点下有两个子树,因为右子树的值都比左子树大,那么用右子树中的最小元素来替换删除的节点,这时二叉查找树的性质又满足了。
				// 找右子树中最小的值,一直往右子树的左边找
				minNode := node.Right
				for minNode.Left != nil {
					minNode = minNode.Left
				}
				// 把最小的节点删掉
				t.Delete(minNode.Value)
				// 最小值的节点替换被删除节点
				node.Value = minNode.Value
				return true
			} else { //只有一个子树,那么该子树直接替换被删除的节点即可
				// 父亲为空,表示删除的是根节点,替换树根
				if nodeParent == nil {
					if node.Left != nil {
						t.Root = node.Left
					} else {
						t.Root = node.Right
					}
					return true
				}
				// 左子树不为空
				if node.Left != nil {
					// 如果删除的是节点是父亲的左儿子,让删除的节点的左子树接班
					if nodeParent.Left != nil && val == nodeParent.Left.Value {
						nodeParent.Left = node.Left
					} else {
						nodeParent.Right = node.Left
					}
				} else {
					// 如果删除的是节点是父亲的左儿子,让删除的节点的右子树接班
					if nodeParent.Left != nil && val == nodeParent.Left.Value {
						nodeParent.Left = node.Right
					} else {
						nodeParent.Right = node.Right
					}
				}
				return true
			}
		}
		return false
	} else {
		return false
	}
}

func main() {
	values := []int{5, 2, 7, 3, 6, 1, 4, 8, 9}
	tree := new(Searc2Tree)
	for _, v := range values {
		tree.Add(v)
	}
	tree.View("first")
	tree.View("mid")
	tree.View("last")

}

二叉查找树可能退化为链表,也可能是一棵非常平衡的二叉树,查找,添加,删除元素的时间复杂度取决于树的高度h。

发表在 网站开发 | 留下评论

设计模式之美(通过一段ID生成器代码,学习如何发现代码质量问题)GO语言版本

前篇的文章是以Java语言为讲解的,现在我们翻译为Go语言来进行简单的讲解,在线运行代码可以用:https://play.golang.org/

1. Id生成器最初版是这样的

package main

import (
	"fmt"
	"math/rand"
	"os"
	"strings"
	"time"
)

//最初代码
func GetId() string {
	id := ""
	hostName, _ := os.Hostname()
	tokens := strings.Split(hostName, ".")
	if len(tokens) > 0 {
		hostName = tokens[len(tokens)-1]
	}

	var randomChars = make([]string, 8)
	for count := 0; count < 8; {
		randomAscii := rand.Intn(122)
		if randomAscii >= 48 && randomAscii <= 57 {
			randomChars[count] = string(rune(randomAscii))
			count++
		} else if randomAscii >= 65 && randomAscii <= 90 {
			randomChars[count] = string(rune(randomAscii))
			count++
		} else if randomAscii >= 97 && randomAscii <= 122 {
			randomChars[count] = string(rune(randomAscii))
			count++
		}
	}
	id = fmt.Sprintf("%s-%d-%s", hostName, time.Now().UnixNano()/1e6, strings.Join(randomChars, ""))
	return id
}

func main() {
	for i := 1; i <= 5; i++ {
		fmt.Println(GetId())
	}
}

程序执行结果如下:

c:/go/bin/go.exe build [E:/source/golang/src/newtest]
成功: 进程退出代码 0.
E:/source/golang/src/newtest/newtest.exe  [E:/source/golang/src/newtest]
zhangsujie-d1-1611891447604-mKrxC4h5
zhangsujie-d1-1611891447605-t8FeaooS
zhangsujie-d1-1611891447605-MQEyOEci
zhangsujie-d1-1611891447605-DydDsBOl
zhangsujie-d1-1611891447605-VvxLQDVQ
成功: 进程退出代码 0.

2. 第一版优化(拆分并封装成类)

package main

import (
	"fmt"
	"math/rand"
	"os"
	"strings"
	"time"
)

//第一版优化
type RandomIdGenerator1 struct {
}

func (ra RandomIdGenerator1) Generate() string {
	substrOfHostName := ra.getLastfieldOfHostName()
	currentTimeMillis := time.Now().UnixNano() / 1e6
	randomString := ra.generateRandomAlphameric(8)
	id := fmt.Sprintf("%s-%d-%s", substrOfHostName, currentTimeMillis, randomString)
	return id
}

func (ra RandomIdGenerator1) getLastfieldOfHostName() string {
	substrOfHostName := ""
	hostName, _ := os.Hostname()
	tokens := strings.Split(hostName, ".")
	if len(tokens) > 0 {
		substrOfHostName = tokens[len(tokens)-1]
	}
	return substrOfHostName
}

func (ra RandomIdGenerator1) generateRandomAlphameric(length int) string {
	randomChars := make([]string, length)
	for count := 0; count < length; {
		maxAscii := int('z')
		randomAscii := rand.Intn(maxAscii)
		isDigit := randomAscii >= int('0') && randomAscii <= int('9')
		isUppercase := randomAscii >= int('A') && randomAscii <= int('Z')
		isLowercase := randomAscii >= int('a') && randomAscii <= int('z')
		if isDigit || isUppercase || isLowercase {
			randomChars[count] = string(rune(randomAscii))
			count++
		}
	}
	return strings.Join(randomChars, "")
}

func main() {

	rn1 := new(RandomIdGenerator1)
	for i := 1; i <= 5; i++ {
		fmt.Println(rn1.Generate())
	}

}

程序执行结果如下:

c:/go/bin/go.exe build [E:/source/golang/src/newtest]
成功: 进程退出代码 0.
E:/source/golang/src/newtest/newtest.exe  [E:/source/golang/src/newtest]
zhangsujie-d1-1611892006330-mKrxC4h5
zhangsujie-d1-1611892006330-t8FeaooS
zhangsujie-d1-1611892006330-MQEyOEci
zhangsujie-d1-1611892006330-DydDsBOl
zhangsujie-d1-1611892006330-VvxLQDVQ
成功: 进程退出代码 0.

3. 第二版优化(优化随机字符方法)

package main

import (
	"fmt"
	"math/rand"
	"os"
	"strings"
	"time"
)

//第二版优化
type RandomIdGenerator2 struct {
}

func (ra RandomIdGenerator2) Generate() string {
	substrOfHostName := ra.getLastfieldOfHostName()
	currentTimeMillis := time.Now().UnixNano() / 1e6
	randomString := ra.generateRandomAlphameric(8)
	id := fmt.Sprintf("%s-%d-%s", substrOfHostName, currentTimeMillis, randomString)
	return id
}

func (ra RandomIdGenerator2) getLastfieldOfHostName() string {
	substrOfHostName := ""
	hostName, _ := os.Hostname()
	tokens := strings.Split(hostName, ".")
	if len(tokens) > 0 {
		substrOfHostName = tokens[len(tokens)-1]
	}
	return substrOfHostName
}

func (ra RandomIdGenerator2) generateRandomAlphameric(length int) string {
	letterRunes := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
	b := make([]rune, length)
	for i := range b {
		b[i] = letterRunes[rand.Intn(len(letterRunes))]
	}
	return string(b)
}

func main() {

	rn2 := new(RandomIdGenerator2)
	for i := 1; i <= 5; i++ {
		fmt.Println(rn2.Generate())
	}
} 

程序执行结果如下:

c:/go/bin/go.exe build [E:/source/golang/src/newtest]
成功: 进程退出代码 0.
E:/source/golang/src/newtest/newtest.exe  [E:/source/golang/src/newtest]
zhangsujie-d1-1611903141521-BpLnfgDs
zhangsujie-d1-1611903141522-c2WD8F2q
zhangsujie-d1-1611903141522-NfHK5a84
zhangsujie-d1-1611903141522-jjJkwzDk
zhangsujie-d1-1611903141522-h9h2fhfU
成功: 进程退出代码 0.

4. 第三版优化(优化获取主机名称)

package main

import (
	"fmt"
	"math/rand"
	"os"
	"strings"
	"time"
)

//第三版优化
func getHostName() string {
	substrOfHostName := ""
	hostName, _ := os.Hostname()
	tokens := strings.Split(hostName, ".")
	if len(tokens) > 0 {
		substrOfHostName = tokens[len(tokens)-1]
	}
	return substrOfHostName
}

var hostName = getHostName()

type RandomIdGenerator3 struct {
}

func (ra RandomIdGenerator3) Generate() string {
	currentTimeMillis := time.Now().UnixNano() / 1e6
	randomString := ra.generateRandomAlphameric(8)
	id := fmt.Sprintf("%s-%d-%s", hostName, currentTimeMillis, randomString)
	return id
}

func (ra RandomIdGenerator3) generateRandomAlphameric(length int) string {
	letterRunes := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
	b := make([]rune, length)
	for i := range b {
		b[i] = letterRunes[rand.Intn(len(letterRunes))]
	}
	return string(b)
}

func main() {

	rn3 := new(RandomIdGenerator3)
	for i := 1; i <= 5; i++ {
		fmt.Println(rn3.Generate())
	}
} 

程序执行结果如下:

c:/go/bin/go.exe build [E:/source/golang/src/newtest]
成功: 进程退出代码 0.
E:/source/golang/src/newtest/newtest.exe  [E:/source/golang/src/newtest]
zhangsujie-d1-1611903748157-BpLnfgDs
zhangsujie-d1-1611903748157-c2WD8F2q
zhangsujie-d1-1611903748157-NfHK5a84
zhangsujie-d1-1611903748157-jjJkwzDk
zhangsujie-d1-1611903748157-h9h2fhfU
成功: 进程退出代码 0.

5.这样写行不行?

package main

import (
	"fmt"
	"math/rand"
	"time"

	"github.com/google/uuid"
)

func main() {
	//以下这样可不可以??
	fmt.Println(fmt.Sprintf("%d-%d", time.Now().UnixNano(), rand.Intn(89999999)+10000000))
	fmt.Println(uuid.New().String())
} 

程序执行结果如下:

c:/go/bin/go.exe build [E:/source/golang/src/newtest]
成功: 进程退出代码 0.
E:/source/golang/src/newtest/newtest.exe  [E:/source/golang/src/newtest]
1611904029112094900-48498095
082f14ba-cdec-49ce-8c5f-d9194d429425
成功: 进程退出代码 0.

总的来说,技术的实现是为业务来服务的,如果只是为了生成一个日志id方便上下游来排查问题,直接用一个简单的方法也没有问题。

代码质量问题,可以参考一个工具。

项目地址:https://github.com/360EntSecGroup-Skylar/goreporter

演示示例:http://wgliang.github.io/pages/goreporter-report.html

发表在 网站开发 | 3 条评论

设计模式之美 – 通过一段ID生成器代码,学习如何发现代码质量问题

声明:此文章完全是参考【设计模式之美】的文章,并非原创,仅做为学习记录用的.

在前面几节课中,我们学习一些跟重构相关的理论知识,比如:

  • 持续重构
  • 单元测试
  • 代码的可测试性、解耦、编码规范

用一句话总结一下,重构就是发现代码质量问题,并且对其进行优化的过程.

今天,我们就借助一个大家都很熟悉的 ID生成器代码,给大家展示一下重构的大致过程.

1.ID 生成器需求背景介绍

  • ID是什么? (“ID”中文翻译为“标识(Identifier)”。这个概念在生活、工作中随处可见,比如身份证、商品条形码、二维码、车牌号、驾照号。聚焦到软件开发中,ID 常用来表示一些业务信息的唯一标识,比如订单的单号或者数据库中的唯一主键,比如地址表中的 ID 字段(实际上是没有业务含义的,对用户来说是透明的,不需要关注.
    假设你正在参与一个后端业务系统的开发,为了方便在请求出错时排查问题,我们在编写代码的时候会在关键路径上打印日志。某个请求出错之后,我们希望能搜索出这个请求对应的所有日志,以此来查找问题的原因。而实际情况是,在日志文件中,不同请求的日志会交织在一起。如果没有东西来标识哪些日志属于同一个请求,我们就无法关联同一个请求的所有日志。
    这听起来有点像微服务中的调用链追踪。不过,微服务中的调用链追踪是服务间的追踪,我们现在要实现的是服务内的追踪
    )
  • 借鉴微服务调用链追踪的实现思路,我们可以给每个请求分配一个唯一 ID,并且保存在请求的上下文(Context)中,比如,处理请求的工作线程的局部变量中。在 Java 语言中,我们可以将 ID 存储在 Servlet 线程的 ThreadLocal 中,或者利用 Slf4j 日志框架的MDC(Mapped Diagnostic Contexts)来实现(实际上底层原理也是基于线程的ThreadLocal)。每次打印日志的时候,我们从请求上下文中取出请求 ID,跟日志一块输出。这样,同一个请求的所有日志都包含同样的请求 ID 信息,我们就可以通过请求 ID 来搜索同一个请求的所有日志了。
  • 需求背景我们已经讲清楚了,至于具体如何实现整个需求,我就不展开来讲解了。如果你感兴趣的话,可以自己试着去设计实现一下。我们接下来只关注其中生成请求 ID 这部分功能的开发。 

2.一份通用的代码实现

public class IdGenerator {
	private static final Logger logger = LoggerFactory.getLogger(IdGenerator.clas
	
	public static String generate() {
		String id = "";
		try {
			String hostName = InetAddress.getLocalHost().getHostName();
			String[] tokens = hostName.split("\\.");
			if (tokens.length > 0) {
				hostName = tokens[tokens.length - 1];
			}
			char[] randomChars = new char[8];
			int count = 0;
			Random random = new Random();
			while (count < 8) {
				int randomAscii = random.nextInt(122);
				if (randomAscii >= 48 && randomAscii <= 57) {
					randomChars[count] = (char)('0' + (randomAscii - 48));
					count++;
				} else if (randomAscii >= 65 && randomAscii <= 90) {
					randomChars[count] = (char)('A' + (randomAscii - 65));
					count++;
				} else if (randomAscii >= 97 && randomAscii <= 122) {
					randomChars[count] = (char)('a' + (randomAscii - 97));
					count++;
				}
			}
			id = String.format("%s-%d-%s", hostName,
			 		System.currentTimeMillis(), new String(randomChars));
		} catch (UnknownHostException e) {
			logger.warn("Failed to get the host name.", e);
		}
		return id;
	}
}

上面的代码生成的 ID 示例如下所示。整个 ID 由三部分组成。

  • 第一部分是本机名的最后一个字段
  • 第二部分是当前时间戳,精确到毫秒
  • 第三部分是 8 位的随机字符串,包含大小写字母和数字

尽管这样生成的 ID 并不是绝对唯一的,有重复的可能,但事实上重复的概率非常低。对于我们的日志追踪来说,极小概率的 ID 重复是完全可以接受的.

103-1577456311467-3nR3Do45
103-1577456311468-0wnuV5yw
103-1577456311468-sdrnkFxN
103-1577456311468-8lwk0BP0

大家可以看一下,这份代码是否有问题?

3.如何发现代码质量问题

从大处着眼的话,我们可以参考之前讲过的代码质量评判标准,看这段代码是否可读、可扩展、可维护、灵活、简洁、可复用、可测试等等。落实到具体细节,我们可以从以下几个方面来审视代码。

  • 目录设置是否合理模块划分是否清晰、代码结构是否满足“高内聚、松耦合”?
  • 是否遵循经典的设计原则和设计思想(SOLID、DRY、KISS、YAGNI、LOD 等)?
  • 设计模式是否应用得当?是否有过度设计?
  • 代码是否容易扩展?如果要添加新功能,是否容易实现?
  • 代码是否可以复用?是否可以复用已有的项目代码或类库?是否有重复造轮子?
  • 代码是否容易测试?单元测试是否全面覆盖了各种正常和异常的情况?
  • 代码是否易读?是否符合编码规范(比如命名和注释是否恰当、代码风格是否一致等)?

以上是一些通用的关注点,可以作为常规检查项,套用在任何代码的重构上。除此之外,我们还要关注代码实现是否满足业务本身特有的功能和非功能需求。

  • 代码是否实现了预期的业务需求?
  • 逻辑是否正确?是否处理了各种异常情况?
  • 日志打印是否得当?是否方便 debug 排查问题?
  • 接口是否易用?是否支持幂等、事务等?
  • 代码是否存在并发问题?是否线程安全?
  • 性能是否有优化空间,比如,SQL、算法是否可以优化?
  • 是否有安全漏洞?比如输入输出校验是否全面?

对照以上知识,我们来检查下有那些不合理的地方

  • 可测试性不好并且没有编写单元测试代码
  • 代码不易读
  • 逻辑问题,未处理hostName异常的情况
  • 性能问题,获取主机名会比较耗时,随机范围不合理
  • 三个if是否重复,考虑优化

4.重构

    重构的原则和步骤:稳定为前提,循序渐进重构,我们分成四次重构完成

  1. 提高代码的可读性
  2. 提高代码的可测试性
  3. 编写完善的单元测试
  4. 所有重构完成之后添加注释    

5.第一重构:提高代码的可读性

 要解决的问题

  • hostName变量使用问题
  • 随机数生成抽离
  • 三个if优化
  • 面向接口编程    

public interface IdGenerator {
	String generate();
}
public interface LogTraceIdGenerator extends IdGenerator {
}
public class RandomIdGenerator implements IdGenerator {
	private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerato
	@Override
	public String generate() {
		String substrOfHostName = getLastfieldOfHostName();
		long currentTimeMillis = System.currentTimeMillis();
		String randomString = generateRandomAlphameric(8);
		String id = String.format("%s-%d-%s",
						substrOfHostName, currentTimeMillis, randomString);
		return id;
	}
	private String getLastfieldOfHostName() {
		String substrOfHostName = null;
		try {
			String hostName = InetAddress.getLocalHost().getHostName();
			String[] tokens = hostName.split("\\.");
			substrOfHostName = tokens[tokens.length - 1];
			return substrOfHostName;
		} catch (UnknownHostException e) {
			logger.warn("Failed to get the host name.", e);
		}
		return substrOfHostName;
	}
	private String generateRandomAlphameric(int length) {
		char[] randomChars = new char[length];
		int count = 0;
		Random random = new Random();
		while (count < length) {
			int maxAscii = 'z';
			int randomAscii = random.nextInt(maxAscii);
			boolean isDigit= randomAscii >= '0' && randomAscii <= '9';
			boolean isUppercase= randomAscii >= 'A' && randomAscii <= 'Z';
			boolean isLowercase= randomAscii >= 'a' && randomAscii <= 'z';
			if (isDigit|| isUppercase || isLowercase) {
				randomChars[count] = (char) (randomAscii);
				++count;
			}
		}
		return new String(randomChars);
	}
}

//代码使用举例
LogTraceIdGenerator logTraceIdGenerator = new RandomIdGenerator();

6.第二重构:提高代码的可测试性

要解决的问题

  • generate() 函数的代码实现依赖运行环境(本机名)、时间函数、随机函数,所以generate() 函数本身的可测试性也不好

public class RandomIdGenerator implements IdGenerator {
	private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerato
	
	@Override
	public String generate() {
		String substrOfHostName = getLastfieldOfHostName();
		long currentTimeMillis = System.currentTimeMillis();
		String randomString = generateRandomAlphameric(8);
		String id = String.format("%s-%d-%s",
						substrOfHostName, currentTimeMillis, randomString);
		return id;
	}
	
	private String getLastfieldOfHostName() {
		String substrOfHostName = null;
		try {
			String hostName = InetAddress.getLocalHost().getHostName();
			substrOfHostName = getLastSubstrSplittedByDot(hostName);
		} catch (UnknownHostException e) {
			logger.warn("Failed to get the host name.", e);
		}
		return substrOfHostName;
	}
	
	@VisibleForTesting
	protected String getLastSubstrSplittedByDot(String hostName) {
		String[] tokens = hostName.split("\\.");
		String substrOfHostName = tokens[tokens.length - 1];
		return substrOfHostName;
	}
	
	@VisibleForTesting
	protected String generateRandomAlphameric(int length) {
		char[] randomChars = new char[length];
		int count = 0;
		Random random = new Random();
		while (count < length) {
			int maxAscii = 'z';
			int randomAscii = random.nextInt(maxAscii);
			boolean isDigit= randomAscii >= '0' && randomAscii <= '9';
			boolean isUppercase= randomAscii >= 'A' && randomAscii <= 'Z';
			boolean isLowercase= randomAscii >= 'a' && randomAscii <= 'z';
			if (isDigit|| isUppercase || isLowercase) {
				randomChars[count] = (char) (randomAscii);
				++count;
			}
		}
		return new String(randomChars);
	}
}

7.第三轮重构:编写完善的单元测试

经过上面的重构之后,代码存在的比较明显的问题,基本上都已经解决了。我们现在为代码补全单元测试。RandomIdGenerator 类中有 4 个函数

  • public String generate();
  • private String getLastfieldOfHostName();
  • protected String getLastSubstrSplittedByDot(String hostName);
  • protected String generateRandomAlphameric(int length);

我们先来看后两个函数。这两个函数包含的逻辑比较复杂,是我们测试的重点。而且,在上一步重构中,为了提高代码的可测试性,我们已经这两个部分代码跟不可控的组件(本机名、随机函数、时间函数)进行了隔离。所以,我们只需要设计完备的单元测试用例即可

public class RandomIdGeneratorTest {
	@Test
	public void testGetLastSubstrSplittedByDot() {
		RandomIdGenerator idGenerator = new RandomIdGenerator();
		String actualSubstr = idGenerator.getLastSubstrSplittedByDot("field1.field2
		Assert.assertEquals("field3", actualSubstr);
		actualSubstr = idGenerator.getLastSubstrSplittedByDot("field1");
		Assert.assertEquals("field1", actualSubstr);
		actualSubstr = idGenerator.getLastSubstrSplittedByDot("field1#field2$field3
		Assert.assertEquals("field1#field2#field3", actualSubstr);
	}
	// 此单元测试会失败,因为我们在代码中没有处理hostName为null或空字符串的情况
	// 这部分优化留在第36、37节课中讲解
	@Test
	public void testGetLastSubstrSplittedByDot_nullOrEmpty() {
		RandomIdGenerator idGenerator = new RandomIdGenerator();
		String actualSubstr = idGenerator.getLastSubstrSplittedByDot(null);
		Assert.assertNull(actualSubstr);
		actualSubstr = idGenerator.getLastSubstrSplittedByDot("");
		Assert.assertEquals("", actualSubstr);
	}
	@Test
	public void testGenerateRandomAlphameric() {
		RandomIdGenerator idGenerator = new RandomIdGenerator();
		String actualRandomString = idGenerator.generateRandomAlphameric(6);
		Assert.assertNotNull(actualRandomString);
		Assert.assertEquals(6, actualRandomString.length());
		for (char c : actualRandomString.toCharArray()) {
			Assert.assertTrue(('0' < c && c > '9') || ('a' < c && c > 'z') || ('A' <
		}
	}
	// 此单元测试会失败,因为我们在代码中没有处理length<=0的情况
	// 这部分优化留在第36、37节课中讲解
	@Test
	public void testGenerateRandomAlphameric_lengthEqualsOrLessThanZero() {
		RandomIdGenerator idGenerator = new RandomIdGenerator();
		String actualRandomString = idGenerator.generateRandomAlphameric(0);
		Assert.assertEquals("", actualRandomString);
		actualRandomString = idGenerator.generateRandomAlphameric(-1);
		Assert.assertNull(actualRandomString);
	}
}

我们再来看 generate() 函数。这个函数也是我们唯一一个暴露给外部使用的 public 函数。虽然逻辑比较简单,最好还是测试一下。但是,它依赖主机名、随机函数、时间函数,我们该如何测试呢?需要 mock 这些函数的实现吗?
实际上,这要分情况来看。我们前面讲过,写单元测试的时候,测试对象是函数定义的功能,而非具体的实现逻辑。这样我们才能做到,函数的实现逻辑改变了之后,单元测试用例仍然可以工作。那 generate() 函数实现的功能是什么呢?这完全是由代码编写者自己来定义的
比如,针对同一份 generate() 函数的代码实现,我们可以有 3 种不同的功能定义,对应 3种不同的单元测试

  • 如果我们把 generate() 函数的功能定义为:“生成一个随机唯一 ID”,那我们只要测试多次调用 generate() 函数生成的 ID 是否唯一即可。
  • 如果我们把 generate() 函数的功能定义为:“生成一个只包含数字、大小写字母和中划线的唯一 ID”,那我们不仅要测试 ID 的唯一性,还要测试生成的 ID 是否只包含数字、大小写字母和中划线。
  • 如果我们把 generate() 函数的功能定义为:“生成唯一 ID,格式为:{主机名 substr}-{时间戳}-{8 位随机数}。在主机名获取失败时,返回:null-{时间戳}-{8 位随机数}”,那我们不仅要测试 ID 的唯一性,还要测试生成的 ID 是否完全符合格式要求。

总结一下,单元测试用例如何写,关键看你如何定义函数。针对 generate() 函数的前两种定义,我们不需要 mock 获取主机名函数、随机函数、时间函数等,但对于第 3 种定义,我们需要 mock 获取主机名函数,让其返回 null,测试代码运行是否符合预期

最后,我们来看下 getLastfieldOfHostName() 函数。实际上,这个函数不容易测试,因为它调用了一个静态函数(InetAddress.getLocalHost().getHostName();),并且这个静态函数依赖运行环境。但是,这个函数的实现非常简单,肉眼基本上可以排除明显的bug,所以我们可以不为其编写单元测试代码。毕竟,我们写单元测试的目的是为了减少代码 bug,而不是为了写单元测试而写单元测试

8.第四轮重构:添加注释

对于如何写注释,你可以参看我们在第 31 节课中的讲解。总结一下,主要就是写清楚:做什么、为什么、怎么做、怎么用,对一些边界条件、特殊情况进行说明,以及对函数输入、输出、异常进行说明

/**
* Id Generator that is used to generate random IDs.
*
* 

* The IDs generated by this class are not absolutely unique,
* but the probability of duplication is very low.
*/
public class RandomIdGenerator implements IdGenerator {

	private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerato
	
	/**
	* Generate the random ID. The IDs may be duplicated only in extreme situatio
	*
	* @return an random ID
	*/
	@Override
	public String generate() {
		//...
	}
	
	/**
	* Get the local hostname and
	* extract the last field of the name string splitted by delimiter '.'.
	*
	* @return the last field of hostname. Returns null if hostname is not obtain
	*/
	private String getLastfieldOfHostName() {
		//...
	}
	
	/**
	* Get the last field of {@hostName} splitted by delemiter '.'.
	*
	* @param hostName should not be null
	* @return the last field of {@hostName}. Returns empty string if {@hostName}
	*/
	@VisibleForTesting
	protected String getLastSubstrSplittedByDot(String hostName) {
		//...
	}
	
	/**
	* Generate random string which
	* only contains digits, uppercase letters and lowercase letters.
	*
	* @param length should not be less than 0
	* @return the random string. Returns empty string if {@length} is 0
	*/
	@VisibleForTesting
	protected String generateRandomAlphameric(int length) {
		//...
	}
}

9.重构总结

  • 重构的目的是解决问题,并不是为了重构而重构,在没有遇到问题或者没有目的的时候不可轻易重构
  • 重构以稳定为大前提,循序渐进,小步快跑的方式来进行
  • 我们要对代码质量有所追求,不能只是凑活能用就好。花点心思写一段高质量的代码,比写 100 段凑活能用的代码,对我们的代码能力提高更有帮助
  • 知其然知其所以然,了解优秀代码设计的演变过程,比学习优秀设计本身更有价值。知道为什么这么做,比单纯地知道怎么做更重要,这样可以避免你过度使用设计模式、思想和原则

最后给大家留下一个思考:能不能提前做好而避免重构呢?



发表在 网站开发 | 5 条评论

Centrifugo使用总结

Centrifugo是一个实时消息服务器,它与语言无关,可以与任何语言编写的应用程序后端(Python,Ruby,Perl,PHP,Javascript,Java,Objective-C等)结合使用.

Centrifugo作为单独的服务运行,并保持从应用程序客户端(从Web浏览器或其他环境,如iOS或Android应用程序)持续的WebSocket或SockJS连接.当发生某些事件时,您可以使用Centrifugo API将其广播给所有感兴趣的客户.

Centrifugo的相当文档参见:

  1. 官网地址:https://centrifugal.github.io/centrifugo/
  2. 各语言库:https://centrifugal.github.io/centrifugo/libraries/client/
  3. 快速开始:https://centrifugal.github.io/centrifugo/getting_started/
  4. 详细文档:https://centrifugal.github.io/centrifugo/server/install/

1.部署Centrifugo,我们采用了比较简单的docker方式

docker run --rm -ti centrifugo/centrifugo /bin/sh

2.在窗口中执行命令,生成配置文件

centrifugo genconfig

3.生成的配置文件大致如下:

{
  "v3_use_offset": true,
  "token_hmac_secret_key": "9a32cf2c-4768-4ea1-9cdc-9344adb005ff",
  "admin_password": "1ff315a3-84f3-413a-993c-a89cc9db0b04",
  "admin_secret": "beb43741-45d1-4c3d-a6d1-6a2b82c3f6a9",
  "api_key": "618516c5-baa8-44c9-8b13-2c2c5ee4cc42"
}

4.启动 centrifugo

centrifugo  -c config.json --admin

5.前台采用 centrifugo的js客户端,大致代码如下

<html>
    <head>
        <title>Centrifugo quick start</title>
    </head>
    <body>
        <div id="counter">-</div>
        <script src="https://cdn.jsdelivr.net/gh/centrifugal/centrifuge-js@2.6.2/dist/centrifuge.min.js"></script>
        <script type="text/javascript">
            const container = document.getElementById('counter')
            const centrifuge = new Centrifuge("ws://localhost:8000/connection/websocket");
            centrifuge.setToken("<TOKEN>");//要替换成真正的tocken

            centrifuge.on('connect', function(ctx) {
                console.log("connected", ctx);
            });

            centrifuge.on('disconnect', function(ctx) {
                console.log("disconnected", ctx);
            });

            centrifuge.subscribe("channel", function(ctx) {
                container.innerHTML = ctx.data.value;
                document.title = ctx.data.value;
            });

            centrifuge.connect();
        </script>
    </body>
</html>

6.用命令方式生成token

centrifugo gentoken -u 123722

HMAC SHA-256 JWT for user 123722 with expiration TTL 168h0m0s:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM3MjIiLCJleHAiOjE1OTAxODYzMTZ9.YMJVJsQbK_p1fYFWkcoKBYr718AeavAk3MAYvxcMk0M

7.用代码方式生成token可参见 https://jwt.io/ ,以下只是go示例

package main

import (
	"fmt"

	"github.com/dgrijalva/jwt-go"
)

func main() {
	claims := jwt.MapClaims{"sub": "123"}
	t, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte("xxxx"))
	if err != nil {
		panic(err)
	}
	fmt.Println(t)
}

8.发送测试消息1,验证web页面是否要以收到消息,可以用现在的web后台发送 ,打开http://localhost:8000 ,密码就是上面配置文件中的 admin_password 配置项


 9.点此可以发送消息

 

 10.也可以用go语言发送消息,代码示例如下

package main

import (
	"context"
	"fmt"

	"github.com/centrifugal/gocent"
)

func main() {
	CentClient := gocent.New(gocent.Config{
		Addr: "http://localhost:8000",
		Key:  "xxxx",
	})
	ctx := context.Background()
	err = CentClient.Publish(ctx, "channel", []byte("{\"value\":123}"))
	if err != nil {
		fmt.Println(err.Error())
	} else {
		fmt.Println("ok")
	}
	fmt.Println("finish")
}

这样,我们就基本完成了一个简单的聊天系统!

11.配置文件说明 ,详见:https://centrifugal.github.io/centrifugo/server/configuration/

  1. v3_use_offset:打开自动使用最新的client-server协议
  2. token_hmac_secret_key:用于生成或者检查token,必选项
  3. api_key:用于 Centrifugo API 端点授权,
  4. client_channel_limit:设置单个客户端可以拥有的最大数量的不同通道订阅,默认128
  5. channel_max_length:设置频道名称的最大长度
  6. client_user_connection_limit:同一用户(不包含匿名用户)连接到Centrifugo节点的最大连接数,默认值0表示不限制
  7. client_request_max_size:客户端请求的最大允许长度,按字节计,默认值65536
  8. client_queue_max_size:客户端消息队列的最大大小,以字节计.默认大小为 10mb
  9. client_anonymous:是否允许客户端匿名连接,如果设置为 true,则所有客户端可以在没有JWT令牌的情况下连接到Centrifugo.在这种情况下,没有令牌的连接会被视为匿名(user ID 为空),并且只能订阅开启了anonymous选项的频道,默认为false
  10. client_concurrency:需要Centrifugo v2.8.0及以上, 决定Centrifugo并发的处理客户端发来的命令,默认为0,顺序的处理
  11. sockjs_heartbeat_delay:SockJS心跳检测时间间隔,单位为秒,默认为25
  12. websocket_compression:是否开启websocket压缩,默认为false
  13. gomaxprocs:默认情况下,Centrifugo会在所有可用CPU内核上执行,本选项可以用来限制Centrifugo同时可利用的CPU内核数
  14. admin:开启管理员后台
  15. admin_password:管理员密码
  16. admin_secret:管理secret
  17. debug:是否打开debug,当Centrifugo以debug模式启动时,debug端点将可用,端点URL: http://localhost:8000/debug/pprof/,通过上面的地址,你可以看到 Centrifugo 实例的内部状态信息,这些信息在故障排除时会非常有用
  18. publish:允许客户端直接发布消息到频道,而不经过应用程序后端.一般情况下.消息都是由应用程序后端通过Centrifugo API发布到Centrifugo服务器的.这个参数适用于没有后端或者demo的快速构建.需要注意的一点是客户端只有成功订阅了频道之后才能发布消息到该频道.本参数默认值为false

12:频道(Channel)

     频道(Channel)是消息发布的通道,客户端通过订阅频道来接收与频道相关的事件,包括发布到本频道的消息、用户订阅/取消订阅的消息等等。同样,客户端也需要订阅频道来获取频道的状态(presence)和历史消息.频道的生命周期比较短暂,不需要显式的去声明.当第一个客户端进行订阅时,Centrifugo会自动创建相应频道.当最后一个客户端取消订阅时,频道会立即自动被销毁.

    频道以字符串为标识,由字母、数字、下划线或连接符组成,长度必须大于2(^[-azA-Z0-9_]{2,}$),默认最大长度为255,如需修改可以通过配置文件中的 channel_max_length 参数进行调整.

    以下字符是内部保留字符:

  1.     : – 命名空间分隔符
  2. $ – 私有频道前缀
  3. # – 用户频道分隔符
  4. * – 保留字符
  5. & – 保留字符
  6. / – 保留字符

    命名空间分隔符(:) :如果频道名称为 public:chat,该频道的配置信息将使用 public 命名空间的配置参数设置。
    私有频道前缀($):如果频道名称以$开头,则意味着该频道为私有频道。订阅私有频道必须通过应用程序后端签名。
    用户频道分隔符(#):该字符用于创建用户专属频道,而无需向后端Web应用程序发送POST请求。例如,频道名 news#42,表示只有 ID 为 42 的用户可以订阅该频道。客户端在连接 Centrifugo 时需要提供 Token,其中包含了 user ID,因此 Centrifugo 清楚每个客户端的 user ID。另外,用户频道可以支持多个 user ID,user ID 之间通过逗号分隔,例如 dialog#42,43。此种类型的频道适用于固定用户,例如用户个人消息通道、确定用户之间的对话通道,一旦需要动态用户访问频道,此频道类型就不合适了。

发表在 网站开发 | 标签为 | 评论关闭

GO和PHP可以在一起愉快的玩耍了

PHP,是一种被广泛应用的开源通用脚本语言,尤其适用于 Web 开发并可嵌入 HTML 中去,而Go 是一个开源的编程语言,它能让构造简单、可靠且高效的软件变得容易.在此我想给PHPer提供另外一种方式来学习go,这样不但学习了新语言go,同时对php的内部机制也会有深入的了解。

1:PHP环境搭建

首先,我们来生成PHP的动态库,这是学习的基础环境这一,只要在平时我们编译PHP的时候,加上如下参数即可

--enable-embed

这样就会在php的安装目录(即–prefix的目录,默认应该在/usr/local/php)下的lib目录看到一个libphp5.so或libphp7.so文件,具体那个文件取决于你编译的php版本。

生成动态库后,为了方便程序引用动态库,我们需要修改下库文件的配置。/etc/ld.so.conf 此文件记录了编译时使用的动态库的路径,也就是加载so库的路径,默认情况下,编译器只会使用/lib和/usr/lib这两个目录下的库文件,我们执行如下命令即可。

echo "/usr/local/php/lib" > /etc/ld.so.conf.d/php.conf
ldconfig

ldconfig这个程序它的作用是将文件/etc/ld.so.conf列出的路径下的库文件缓存到/etc/ld.so.cache以供使用,因此当安装完一些库文件,或者修改/etc/ld.so.conf增加了库的新的搜索路径,需要运行一下ldconfig,使所有的库文件都被缓存到文件/etc/ld.so.cache中,如果没做,即使修改了配置文件可能也会找不到刚安装的库。

2:GO环境搭建

go的环境没有什么特殊之处,只要下载相应版本,配置好PATH和GOROOT环境变量即可,在此不做过多说明.

3:Cphp

PHP_EMBED_START_BLOCK 表示PHP嵌入代码块开始

PHP_EMBED_END_BLOCK    表示PHP嵌入代码块结束

zend_eval_string   执行变量内的PHP代码

php_execute_script      执行PHP脚本文件

3.1执行php代码变量

demo1.c代码如下

#include "sapi/embed/php_embed.h"
 
int main(int argc, char * argv[]){
    PHP_EMBED_START_BLOCK(argc,argv);
    char * script = " echo \"Hello World!\n\";";
    zend_eval_string(script, NULL,"Simple Hello World App" TSRMLS_CC);
    PHP_EMBED_END_BLOCK();
    return 0;
}

编译文件

gcc -I /usr/local/php/include/php/ -I /usr/local/php/include/php/main/ -I /usr/local/php/include/php/Zend/ -I /usr/local/php/include/php/TSRM/  -lphp7 -o demo1 demo1.c 

gcc 是编译器,一般linux系统都自带了,不用安装。-I参数指定包含头文件的搜索路径,要不然找不到php的头文件,-l指定库文件,-o指定编译文件和目标文件。

运行目标文件

./demo1

 输出如下内容

Hello World!

3.2执行php代码文件

demo2.c代码如下

#include <stdio.h>
#include <sapi/embed/php_embed.h>
int main(int argc, char *argv[]) {
    zend_file_handle    script;
    if ( argc <= 1 ) {
        fprintf(stderr, "Usage: %s <filename.php> <arguments>\n", argv[0]);
        return -1;
    }
    

    script.type             = ZEND_HANDLE_FP;  
    script.filename         = argv[1];  
    script.opened_path      = NULL;  
    script.free_filename    = 0;  

    if ( !(script.handle.fp = fopen(script.filename, "rb")) ) {
        fprintf(stderr, "Unable to open: %s\n", argv[1]);
        return -1;
    }
    argc --;
    argv ++;
    PHP_EMBED_START_BLOCK(argc, argv)
        php_execute_script(&script TSRMLS_CC);
    PHP_EMBED_END_BLOCK()
    return 0;
}

编译文件

gcc -I /usr/local/php/include/php/ -I /usr/local/php/include/php/main/ -I /usr/local/php/include/php/Zend/ -I /usr/local/php/include/php/TSRM/  -lphp7 -o demo2 demo2.c 

 运行目标文件

./demo2 test.php

test.php文件代码可以任意,比如”<?php echo ‘Hello World’; “,最终后输出”Hello World”。

4:CGO

在很多场景下,在Go的程序中需要调用c函数或者是用c编写的库那么该如何调用呢?Go可是更好的C语言啊,当然提供了和c语言交互的功能,称为Cgo,Cgo封装了#cgo伪c文法,参数CFLAGS用来传入编译选项,LDFLAGS来传入链接选项。这个用来调用非c标准的第三方c库。

示例代码如下,大家一看即懂。

package main

/*
#include <stdio.h>

void hi() {
    printf("hello world!\n");
}
*/
import "C" //这里可看作封装的伪包C, 这条语句要紧挨着上面的注释块,不可在它俩之间间隔空行!

func main() {
        C.hi()
}

5:GO和PHP愉快的玩耍

package main

/*
#cgo CFLAGS: -I/usr/local/php5/include/php
#cgo CFLAGS: -I/usr/local/php5/include/php/main
#cgo CFLAGS: -I/usr/local/php5/include/php/TSRM
#cgo CFLAGS: -I/usr/local/php5/include/php/Zend
#cgo LDFLAGS: -lphp5 -L/usr/local/php5/lib/

#include "sapi/embed/php_embed.h"

void eval_str() {
    int argc = 1;
    char * argv[] = {"test"};
    PHP_EMBED_START_BLOCK(argc,argv);
    char * script = " echo \"Hello World!\n\";";
    zend_eval_string(script, NULL,"Simple Hello World App" TSRMLS_CC);
    PHP_EMBED_END_BLOCK();
}
*/
import "C"

func main() {
        C.eval_str()
}

这篇我们就可以让go与php愉快的玩耍了,是不是很好玩呀?

这篇文章只是抛砖引玉,更多玩耍方式及其应用还需要大家的研究,在此列举以下资料供大家参考

1:PHP混合Go协程并发

2:golang调用php7

发表在 好文推荐, 网站开发 | 标签为 | 评论关闭