Golang学习笔记
参考学习视频地址:https://www.bilibili.com/video/BV1wE411d7th?p=1
(所有截图均来自于上述视频)
本人为自学整理的个人理解文档
0.3版本
Zinx0.3版本新增两个新概念:请求和路由
V0.2版本的链接只封装了套接字,而请求是封装了链接和用户传输的数据,后续通过请求来识别具体要实现什么功能,然后通过路由来完成对应的功能处理。
V0.3实现的效果(目前是只支持单路由,所以服务器的路由属性不是集合的那种类型): 因为需要执行的操作功能封装成路由,所以新增一个添加路由的接口,路由内部是不会绑定任何东西的,是纯粹的执行某些功能,因此服务器端测试程序里包括生成服务器函数、添加路由函数和服务器工作函数,添加路由就是将一个路由对象赋给服务器的路由属性,接下来进入服务器工作函数:服务器工作函数----只有服务器启动函数,接下来我们看服务器启动函数部分:
开头依然是与客户端连接、等待客户端数据,然后收到客户端消息后,将套接字和要实现的功能路由绑定生成链接,最后是链接的工作函数:
链接工作函数里只有读取链接且执行函数:这个函数里包括读取套接字的数据,然后将数据和当前链接再封装成请求,最后执行设定的路由函数。
文件一:
myDemo-ZinxV0.3-ClientTest.go(和0.2版本的一样,没变化)
文件二:
myDemo-ZinxV0.3-ServerTest.go
package main
import (
"fmt"
"myItem/src/Zinx/ziface"
"myItem/src/Zinx/znet"
)
/*基于Zinx框架来开发的 服务器端应用程序*/
//ping test 自定义路由
type PingRouter struct {
znet.BaseRouter
}
//Test PreHandle
func (this *PingRouter) PreHandle(request ziface.IRequest) {
fmt.Println("Call Router PreHandle...")
_,err:=request.GetConnection().GetTCPConnection().Write([]byte("before ping...\n"))
if err!= nil{
fmt.Println("Call back before ping error")
}
}
//Test Handle
func (this *PingRouter) Handle(request ziface.IRequest) {
fmt.Println("Call Router Handle...")
_,err:=request.GetConnection().GetTCPConnection().Write([]byte("ping..ping..ping...\n"))
if err!= nil{
fmt.Println("Call back ping..ping..ping.. error")
}
}
//Test PostHandle
func (this *PingRouter) PostHandle(request ziface.IRequest) {
fmt.Println("Call Router PostHandle...")
_,err:=request.GetConnection().GetTCPConnection().Write([]byte("after ping...\n"))
if err!= nil{
fmt.Println("Call back after ping error")
}
}
func main() {
//创建一个server句柄,使用Zinx的api
s:=znet.NewServer("[zinx V0.3]")
//给当前zinx框架添加一个自定义router
s.AddRouter(&PingRouter{})
//启动server
s.Server_()
}
文件三:Zinx-ziface-IConnection.go(和0.2版本的一样,没变化)
文件四:Zinx-ziface-IRequest.go
package ziface
/*IRequest接口:实际上将用户端请求的链接消息和请求的数据包装成request包*/
type IRequest interface {
//得到当前链接
GetConnection() IConnection
//得到请求的消息数据
GetData() []byte
}
文件五:Zinx-ziface-IRouter.go
package ziface
/*路由的抽象接口:路由里的数据都是IRequest*/
type IRouter interface {
//出来业务之前的钩子方法Hook
PreHandle(request IRequest)
//出来业务的主方法
Handle(request IRequest)
//出来业务之后的钩子方法
PostHandle(request IRequest)
}
文件六:Zinx-ziface-IServer.go
package ziface
/*定义一个服务器接口*/
type IServer interface {
//启动服务器
Start_()
//停止服务器
Stop_()
//运行服务器
Server_()
//路由功能:给当前的服务注册一个路由方法,供用户端的链接处理使用
AddRouter(router IRouter)
}
文件七:Zinx-znet-Connection.go
package znet
import (
"fmt"
"myItem/src/Zinx/ziface"
"net"
)
/*链接模块*/
type Connection struct {
//当前连接的socket TCP套接字
Conn *net.TCPConn
//连接ID
ConnID uint32
//当前的链接状态
IsClosed bool
//告知当前链接已经退出的/停止的channel
ExitChan chan bool
//该链接处理的方法Router
Router ziface.IRouter
}
//1.1 Strat_()里完成读数据的方法
func (c *Connection) StartReader() {
fmt.Println("Reader Goroutne is running...")
defer fmt.Println("connID=",c.ConnID,"Reader is exit,remote addr is ",c.GetRemoteAddr().String())
defer c.Stop_()
for {
//读取用户端的数据到buf,最大512字节
buf:=make([]byte,512)
_,err:=c.Conn.Read(buf)
if err!= nil {
fmt.Println("Receive buf err:",err)
continue
}
/*//调用当前链接所绑定的HandleAPI
if err:=c.handleAPI(c.Conn,buf,cnt);err!=nil{
fmt.Println("ConnID:",c.ConnID,"handle is error:",err)
break
}*/
//有了Router就不用调上面的内容了
req:=Request{
conn: c,
data: buf,
}
//执行注册的路由方法
go func(request ziface.IRequest) {
c.Router.PreHandle(request)
c.Router.Handle(request)
c.Router.PostHandle(request)
}(&req)
//从路由中找到注册绑定的Conn对应的router调用
}
}
//1.启动链接,让当前的连接准备开始工作
func (c *Connection) Start_(){
fmt.Println("Connect Start...,ConnID=",c.ConnID)
//启动从当前连接的读数据的业务
go c.StartReader()
//启动从当前链接的写数据的业务
}
//停止链接,结束当前连接的工作
func (c *Connection) Stop_(){
fmt.Println("Connect Stop...,ConnID=",c.ConnID)
if c.IsClosed == true{
return
}
c.IsClosed=true
//关闭连接
c.Conn.Close()
//关闭管道,回收资源
close(c.ExitChan)
}
//获取当前连接绑定的socket conn
func (c *Connection) GetTCPConnection() *net.TCPConn{
return c.Conn
}
//获取当前连接模块的连接ID
func (c *Connection) GetConnID() uint32{
return c.ConnID
}
//获取远程用户端的TCP状态、IP、Port
func (c *Connection) GetRemoteAddr() net.Addr{
return c.Conn.RemoteAddr()
}
//发送数据,将数据发送给远程用户端
func (c *Connection) Send(data []byte) error{
return nil
}
//初始化链接模块的方法
func NewConnection(conn *net.TCPConn,connID uint32,router ziface.IRouter) *Connection{
c:=&Connection{
Conn: conn,
ConnID: connID,
Router: router,
IsClosed: false,
ExitChan: make(chan bool,1),
}
return c
}
文件八:Zinx-znet-Request.go
package znet
import "myItem/src/Zinx/ziface"
type Request struct {
//已经和用户端建立好的链接
conn ziface.IConnection
//用户端请求的数据
data []byte
}
//得到当前链接
func (r *Request) GetConnection() ziface.IConnection{
return r.conn
}
//得到请求的消息数据
func (r *Request) GetData() []byte{
return r.data
}
文件九:Zinx-znet-Router.go
package znet
import "myItem/src/Zinx/ziface"
/*实现router时,先嵌入这个BaseRouter基类,在根据需求对这个基类的方法进行重写,所以不实现基类方法*/
type BaseRouter struct {}
//出来业务之前的钩子方法Hook
func (br *BaseRouter) PreHandle(request ziface.IRequest){}
//出来业务的主方法
func (br *BaseRouter) Handle(request ziface.IRequest){}
//出来业务之后的钩子方法
func (br *BaseRouter) PostHandle(request ziface.IRequest){}
文件十:Zinx-znet-Server.go
package znet
import (
"fmt"
"myItem/src/Zinx/ziface"
"net"
)
/*IServer的接口实现,定义一个Server的服务器模块*/
type Server struct {
//服务器名称
Name string
//服务器绑定的IP版本
IPVersion string
//服务器监听的IP
IP string
//服务器监听的端口
Port int
//当前的server添加一个router,server注册的链接对应的处理业务
Router ziface.IRouter
}
func (s *Server) Start_() {
fmt.Printf("[Start]Server Listenner at IP :%s,Port %d,is starting...\n",s.IP,s.Port)
//避免阻塞,用子协程承载
go func() {
//1.创建一个服务器,获取一个TCP的Addr
addr,err:=net.ResolveTCPAddr(s.IPVersion,fmt.Sprintf("%s:%d",s.IP,s.Port))
if err!= nil{
fmt.Println("resolve tcp addr error:",err)
return
}
//2.监听服务器的地址,等待用户端输入数据
listenner,err:=net.ListenTCP(s.IPVersion,addr)
if err!=nil {
fmt.Println("listen",s.IPVersion,"error:",err)
return
}
var cid uint32
cid=0
fmt.Println("Start Zinx server successfully,",s.Name,"successfully,Listenning...")
//3.当监听到有客户端输数据给服务器,阻塞的等待客户端链接,获取该用户端与服务器端间的一个通道conn,通过conn处理客户端链接业务
for {
conn,err:=listenner.AcceptTCP()
if err!=nil {
fmt.Println("Accept err",err)
continue
}
//将处理新链接的业务方法 和conn进行绑定,得到我们的链接模块
dealConn := NewConnection(conn,cid,s.Router)
cid++
//启动当前的链接业务处理
go dealConn.Start_()
}
}()
}
func (s *Server) Stop_() {
//将一些服务器的资源、状态或者一些已经开辟的连接信息进行停止或者回收
}
func (s *Server) Server_() {
//启动server的服务功能
s.Start_()
//扩展功能
//在这里需进入阻塞状态,避免结束服务器使用时,服务器直接关闭
//注:不在Start_()里阻塞是因为在启动服务器之后还需要做一些扩展功能,不希望服务器提前阻塞了
select {
}
}
//注册路由
func (s *Server) AddRouter(router ziface.IRouter) {
s.Router = router
fmt.Println("Add Router Successfully!")
}
/*初始化Server模块的方法*/
func NewServer(name string) ziface.IServer {
s := &Server{
Name: name,
IPVersion: "tcp4",
IP: "0.0.0.0",
Port: 8999,
Router: nil,
}
return s
}
0.4版本
0.4版本主要是完成一个全局配置的功能,通过新增一个全局变量,来完成对之前版本的一些写死的服务器信息进行替换,用可自定义的全局变量来完成服务器参数的灵活设置。
**V0.4版本的实现效果:**主要对服务器端的接口那部分进行修改,对新建服务器函数的那些参数换出全局变量,对Connectio里面的接收数据的长度改成全局变量设定的长度属性
0.5版本
**V0.5版本实现效果:**实际上消息从网络进行传输时,应该序列化,不能直接丢过去,因此采用TLV封包格式设置IMessage的格式,0.5版本主要针对之前的Request中的数据进行完善封装,在之前的版本中Request中包含自定义的链接和用户传输的数据,但是那个数据没有自带长度和消息ID的概念,而到后面的版本我们可以用消息ID来识别不同的功能操作,因此这个版本新增了Message的概念,完成对消息的进一步优化封装,至此Message成为数据处理层面的一个原子结构,下面是实现的数据结构:
使用这样的格式可以解决TCP粘包问题
(粘包问题:假如客户端向服务器端发送两条数据,但底层不清楚数据数量,当服务器端接收数据时,就有可能将两条数据读取成一条数据)
而采用TLV格式后,服务器端可先读八个字节得到数据的长度和id信息,再读出前四个字节的消息长度,然后往后偏移八个字节,按消息长度读取完整数据
封包机制:创建一个存放bytes字节的缓冲(二进制文件),将数据以二进制格式存入其中,然后依次加上消息长度属性和消息ID属性,最后返回那个二进制文件(数据包)
拆包机制:创建一个读取二进制文件(数据包)的缓冲区,只解压head信息得到消息长度和消息ID,然后判断包的长度是否符合我们在全局变量设置的包长度,不符合就直接return加报错,符合就返回数据
在Zinx框架中,首先对Request的封装内容修改,由之前的数据+链接更新为消息(Message)+链接
在服务器测试程序上,还是生成服务器函数、添加路由函数和服务器工作函数,接下来进入服务器工作函数:服务器工作函数----只有服务器启动函数,接下来我们看服务器启动函数部分:
开头是与客户端连接、等待客户端数据,然后收到客户端消息后,将套接字和要实现的功能路由绑定生成链接,最后是链接的工作函数:
链接工作函数里只有读取链接且执行函数:这个函数以前的读取链接的套接字的数据,修改为先创建拆包对象,然后读取客户端发送来的数据包的消息长度和ID存放到Message中,再将数据读取出来且存放到Message,之后将Message和当前链接再封装成Request,最后执行设定的路由函数,路由函数(这里的路由函数就是回发一个消息给客户端)做出如下修改:
首先从Request读取消息ID和数据包(后续对会增加一些和数据有关的功能操作,这里就简单的回写一些自定义的字符给客户端了),然后再将一些字符封装成二进制文件(数据包),再通过链接里的套接字发送这个数据包给客户端。
0.6版本
V0.6版本实现效果:由于之前的版本只支持单路由,而我们希望能实现多种功能,因此这一版本新增了支持多路由的功能,主要是用消息管理模块来支持多路由业务api调度管理,修改的地方有服务器的属性,服务器之前的路由属性修改为消息管理模块属性,这个模块是消息ID和对应路由绑定的结果,这就用到之前拆包获得的消息ID(主要用map来绑定消息ID和对应路由的关系),测试程序的总体流程和上个版本基本一致,以前的版本是获得Request后,执行Request对应的路由函数,而这个版本是先根据Request里的消息ID去查找消息管理模块里对应的路由函数(这一步是新的,后面的和前面的处理流程一样),再执行路由函数
0.7版本
V0.7版本实现效果:之前的版本都是服务器端接收到来自客户端的数据包,然后拆包后开一个协程执行对应的路由函数,最后将执行路由后得到的数据打包成数据包反馈给客户端了,而我们希望读取数据、执行路由函数与回馈消息进行分离,所以在这个版本新增了一个媒介:channel,主要修改的地方是,服务器端首先开一个的 读协程 来完成客户端发送过来的数据包,将拆包后执行对应的路由功能得到数据,打包后发送到channel上,然后另一个 写协程 就持续从channel读数据,当channel上有数据时,写协程就获取上面的数据并发送给客户端,而且还新加另一条exit channel专门用来传输客户端与服务器端的连接消息,如果客户端与服务器端断开连接后,写协程结束,并在结束之前往exit channel传一个数据,当写函数从exit channel上读取到数据时,就将exit channel关闭,再将读写之间的channel关闭。
0.8版本
**0.8版本实现效果(考虑的是协程的个数,因为数量多的话,协程之间的切换太耗费资源了):**由于前面测试的客户端都是比较少量的,所以几个协程的切换对效率影响不大,如果考虑到有成千上万个用户来访问我们的服务器,按照之前的版本就是每个客户端和服务器端都会开几个协程,那么是否意味着成千上万个客户端就会开启成千上万个协程,这显然不是我们所希望的。
因此在这个版本我们新增工作池和消息队列机制,修改了之前收到Request后直接开goroutine处理的机制,变成默认开10个goroutine,每个协程都是一个读写之间的协程,服务器端然后先将Request发给工作池,由工作池分配到每个协程对应的消息队列(10个协程对应10条),而每个消息队列都是一个channel,而众多的Request都有其独有的链接ID,根据ID对工作池里的消息队列数量取余,即可做到对Request的均分。
主要的修改就是在读协程里拆包获得数据封装成Request,然后丢到工作池处理业务。
0.9版本
V0.9版本实现效果:服务器能开的连接都是有限的,之前的版本都是来一个客户端就允许链接,那如果有成千上万的用户来的话,服务器有可能就会因为内存不足或者其他原因导致崩溃了,这个版本新增链接管理模块,一方面设置限定数量的链接数量,不能来客户端请求就响应,另一方面在创建链接前/后可以扩展业务,可以自己增删连接,毕竟之前的版本是由于出错或者手动终止程序才会关闭连接。
1.0版本
V1.0版本的实现效果:给链接添加可以自行配置属性的功能