Golang学习笔记
参考学习视频地址:https://www.bilibili.com/video/BV1wE411d7th?p=1
本人为自学整理的个人理解文档
0.1版本
服务器应该有最基本的两个模块:抽象层(ziface)和网络层(znet)。抽象层是提供服务类接口给我们实现服务器端的,而网络层也可以理解有两层,一层用于支持实现服务类接口底层的函数,另一层是实现调用前面的底层函数来实现接口。而我们最后是要使用抽象层的接口来实现一个服务端(服务器端测试程序)。
一个服务器端具备的基本功能是 开机、关机和业务服务功能,这些功能实现到程序上时,在笔者这里测试时,运行服务器端测试程序且正常执行设定的启动函数后就是开机,然后程序继续往下执行设定的业务服务函数就是启动业务服务功能,直接手动终止服务器端测试程序就是关机。
该版本实现包括:
1. 启动服务器【Start_()】:(调用net库函数实现)
1.调用NewServer()创建一个服务器句柄,这里面包括服务器的名字、IP版本、IP地址和端口号
2.将上面的IP地址加端口号进行组合(这样的形式:”IP地址:端口号”),调用net.ResolveTCPAddr生成TCP地址(程序中是 addr 那个参数),就相对于这个地址代表服务器,后面客户端的就是通过这个生成的TCP地址来连接服务器端;
3.用TCP地址和调用 net.ListenTCP()来创建listenner,这个监听器是服务器端用来监听 客户端向服务器端发送连接的消息 的;之后服务器端会持续调用AcceptTCP()来阻塞等待客户端发送请求连接的消息,直到收到请求才会执行后续的业务部分。
(上述是服务器的连接准备工作,接下来补充客户端的连接部分,这部分不属于“启动服务器”:
因为肯定是先开服务器再开客户端的,所以客户端一开始直接调用net.Dial来连接前面那个TCP地址代表的服务器端,若连接到了,就返回一个服务器端和客户端之间的链接,后续的信息交互都是通过这个链接来完成的,接下来回到服务器端的后续工作准备部分)
当有客户端调用net.Dial来连接前面那个TCP地址代表的服务器端(并返回一个客户端套接字),服务器端的AcceptTCP()会响应(也返回一个服务器端套接字)
至此服务器端和客户端连接完成,后续就是实现自己想要传输的消息。
注:套接字是通信的基石,是支持TCP/IP协议的路通信的基本操作单元。**Socket(套接字)可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。**它是网络环境中进程间通信的API(应用程序编程接口),也是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连进程。通信时其中一个网络应用程序将要传输的一段信息写入它所在主机的 Socket中,该 Socket通过与网络接口卡(NIC)相连的传输介质将这段信息送到另外一台主机的 Socket中,使对方能够接收到这段信息。 Socket是由IP地址和端口结合的,提供向应用层进程传送数据包的机制。
2. 停止服务器(暂未实现)
3. 运行服务器:调用Start_()方法(即启动服务器),调用后做阻塞处理,在两者之间考虑扩展功能(扩展功能暂未实现)
4. 初始化server:给服务器设置初始的相关属性
5. V0.1版本实现效果:一个自定义参数的服务器与一个客户端完成连接,然后互相发消息和接收消息
文件一:Zinx-ziface-IServer.go
此文件用于声明接口,这里声明了三个接口:
//启动服务器 //停止服务器 //运行服务器
Start_() Stop_() Server_()
package ziface
/*定义一个服务器接口*/
type IServer interface {
//启动服务器
Start_()
//停止服务器
Stop_()
//运行服务器
Server_()
}
文件二:Zinx-znet-Server.go
此文件用于实现接口,搭建Zinx框架
package znet
import (
"fmt"
"myItem/src/Zinx/ziface"
"net"
)
/*IServer的接口实现,定义一个Server的服务器模块*/
type Server struct {
//服务器名称
Name string
//服务器绑定的IP版本
IPVersion string
//服务器监听的IP
IP string
//服务器监听的端口
Port int
}
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
}
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
}
//用子协程承载,客户端已经与服务器建立链接,完成一些业务:做一个最基本的最大512字节的回显业务
go func() {
for {
buf := make([]byte,512)
//将数据读取到buf,返回数据长度cnt
cnt,err := conn.Read(buf)
if err!= nil {
fmt.Println("receive buf err:",err)
continue
}
fmt.Printf("receive Client buf %s,cnt %d\n",buf,cnt)
//回显功能,回写到数据长度的位置(即读取的数据)
if _,err:=conn.Write(buf[:cnt]);err!=nil {
fmt.Println("write back buf err:",err)
continue
}
}
}()
}
}()
}
func (s *Server) Stop_() {
//将一些服务器的资源、状态或者一些已经开辟的连接信息进行停止或者回收
}
func (s *Server) Server_() {
//启动server的服务功能
s.Start_()
//扩展功能
//在这里需进入阻塞状态,避免结束服务器使用时,服务器直接关闭
//注:不在Start_()里阻塞是因为在启动服务器之后还需要做一些扩展功能,不希望服务器提前阻塞了
select {
}
}
/*初始化Server模块的方法*/
func NewServer(name string) ziface.IServer {
s := &Server{
Name: name,
IPVersion: "tcp4",
IP: "0.0.0.0",
Port: 8999,
}
return s
}
文件三:myDemo-ZinxV0.1-ServerTest.go
此文件使用设计的接口完成搭建服务器,完成0.1版本的接口测试
package main
import "myItem/src/Zinx/znet"
/*基于Zinx框架来开发的 服务器端应用程序*/
func main() {
//创建一个server句柄,使用Zinx的api
s:=znet.NewServer("[zinx V0.1]")
//启动server
s.Server_()
}
文件四:myDemo-ZinxV0.1-ClientTest.go
此文件用于模拟用户端
package main
import (
"fmt"
"net"
"time"
)
/*模拟客户端*/
func main() {
fmt.Println("Client starting...")
//1.直接链接远程服务器,得到一个conn链接
conn,err:=net.Dial("tcp4","127.0.0.1:8999")
if err!= nil{
fmt.Println("Client start err:",err)
return
}
//2.链接调用Write写数据
for {
_,err:=conn.Write([]byte("Hello Zinx 0.1"))
if err != nil{
fmt.Println("write conn err:",err)
return
}
buf:=make([]byte,512)
cnt,err:=conn.Read(buf)
if err!=nil {
fmt.Println("read buf err:",err)
return
}
fmt.Printf("server call back:%s,cnt=%d\n",buf,cnt)
//这里需要cpu阻塞,设置一秒发一次
time.Sleep(1*time.Second)
}
}
0.2版本
相对0.1版本,这个版本新增了链接的概念,定义链接的结构体以及对应的链接方法,每个链接都会绑定对应的操作API,而前面服务器与客户端之间的功能交互就通过链接来完成。
以前的功能实现是通过获取客户端与服务器端的套接字conn,用合适的操作完成功能的实现,但是这一部分内容和服务器端的启动服务器的接口合在一起,这是我们不希望的,我们希望应该分离这两个部分,通过将功能单独分离出来实现成一个方法,然后新增链接这一个概念将要实现的不同功能进行绑定,因为不可能将所有封装好的方法都塞到启动服务器里,我们希望在服务器和客户端连接时,本身就绑定好对应的API操作并返回到一个变量,而我们只需要在启动服务器的部分执行这个变量,然后让他自己去找对应需要的函数。
V0.2版本实现的效果:通过将之前客户端和服务器端的套接字进行封装得到 链接 的概念,
链接的构成:套接字,链接ID,当前链接的状态,与链接绑定的处理业务方法,退出链接的channel管道
链接的方法:启动链接、停止链接、获取当前链接的套接字、得到客户端链接的IP地址和端口(TCP地址)、链接所绑定的处理业务方法、给客户端发送数据。
**(开始套娃,一层层封装)**对于服务器端的测试程序而言,只保留生成服务器函数和服务器工作函数,其他接口都加入启动函数,生成服务器就是给服务器一些初始化参数,然后返回一个服务器类型的对象,服务器工作函数----只有服务器启动函数,接下来我们看服务器启动函数部分:
V0.1版本的服务器端和客户端连接的部分保留,然后到获取套接字那一步起,就用套接字和需要的业务处理函数封装成我们新定义的链接,然后调用链接的业务工作函数,接下来我们看链接工作函数部分:
目前我们先实现服务器处理客户端信息部分,所以链接工作函数只有读取链接并执行工作函数:这个函数的内部就是V0.1版本后面的那些处理业务部分,只不过是多了一层封装,例如V0.1版本有地方要用到 套接字,这里就是用 链接.套接字,然后底层处理就和前面是一样的。
这一版本实现的内容包括:
文件一:myDemo-ZinxV0.2-ClientTest.go
这部分内容不需要变化,因为0.2版本只是对服务器处理业务的方式进行改进,而用户端的读写测试是不变的,下面的服务器端的启动测试也不需要变化
package main
import (
"fmt"
"net"
"time"
)
/*模拟客户端*/
func main() {
fmt.Println("Client starting...")
//1.连接服务器,得到一个conn套接字
conn,err:=net.Dial("tcp4","127.0.0.1:8999")
if err!= nil{
fmt.Println("Client start err:",err)
return
}
//2.调用Write写数据
for {
_,err:=conn.Write([]byte("Hello Zinx 0.2"))
if err != nil{
fmt.Println("write conn err:",err)
return
}
buf:=make([]byte,512)
cnt,err:=conn.Read(buf)
if err!=nil {
fmt.Println("read buf err:",err)
return
}
fmt.Printf("server call back:%s,cnt=%d\n",buf,cnt)
//这里需要cpu阻塞,设置一秒发一次
time.Sleep(1*time.Second)
}
}
文件二:myDemo-ZinxV0.2-ServerTest.go
package main
import "myItem/src/Zinx/znet"
/*基于Zinx框架来开发的 服务器端应用程序*/
func main() {
//创建一个server句柄,使用Zinx的api
s:=znet.NewServer("[zinx V0.2]")
//启动server
s.Server_()
}
文件三:Zinx-ziface-IConnection.go
这一部分就是对新增概念:链接 定义相关的接口方法,以后实现功能就是调用这些链接进行实现
package ziface
import "net"
//定义链接模块的抽象层
type IConnection interface {
//1.启动链接,让当前的连接准备开始工作
Start_()
//停止链接,结束当前连接的工作
Stop_()
//获取当前链接绑定的socket conn
GetTCPConnection() *net.TCPConn
//获取当前链接模块的连接ID
GetConnID() uint32
//获取远程客户端的TCP地址:(IP:Port)
GetRemoteAddr() net.Addr
//发送数据,将数据发送给远程用户端
Send(data []byte) error
}
/*定义一个处理连接业务的方法:
参数分别为一个套接字conn,待处理的数据内容,数据的长度*/
type HandleFunc func(*net.TCPConn,[]byte,int) error
文件四:Zinx-ziface-IServer.go
和0.1版本的一致,不需要变化
package ziface
/*定义一个服务器接口*/
type IServer interface {
//启动服务器
Start_()
//停止服务器
Stop_()
//运行服务器
Server_()
}
文件五:Zinx-znet-Server.go
这一部分是对接口的实现,主要是对0.1版本的部分内容进行封装,使用新概念来完成功能
package znet
import (
"errors"
"fmt"
"myItem/src/Zinx/ziface"
"net"
)
/*IServer的接口实现,定义一个Server的服务器模块*/
type Server struct {
//服务器名称
Name string
//服务器绑定的IP版本
IPVersion string
//服务器监听的IP
IP string
//服务器监听的端口
Port int
}
//定义当前客户端链接所绑定的handle API(目前功能写死,以后的优化由用户自定义自己的handle)
func CallBackToClient(conn *net.TCPConn,data []byte,cnt int) error {
//回显功能的API
fmt.Println("[Conn Handle]CallBackToClient...")
if _,err :=conn.Write(data[:cnt]);err!=nil{
fmt.Println("write back buf err:",err)
return errors.New("CallBackToClient error")
}
return nil
}
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,CallBackToClient)
cid++
//启动当前的链接业务处理
go dealConn.Start_()
}
}()
}
func (s *Server) Stop_() {
//将一些服务器的资源、状态或者一些已经开辟的连接信息进行停止或者回收
}
func (s *Server) Server_() {
//启动server的服务功能
s.Start_()
//扩展功能
//在这里需进入阻塞状态,避免结束服务器使用时,服务器直接关闭
//注:不在Start_()里阻塞是因为在启动服务器之后还需要做一些扩展功能,不希望服务器提前阻塞了
select {
}
}
/*初始化Server模块的方法*/
func NewServer(name string) ziface.IServer {
s := &Server{
Name: name,
IPVersion: "tcp4",
IP: "0.0.0.0",
Port: 8999,
}
return s
}
文件六:Zinx-znet-Connection.go
这一部分是对前面定义的Connection相关操作接口进行实现
package znet
import (
"fmt"
"myItem/src/Zinx/ziface"
"net"
)
/*链接模块*/
type Connection struct {
//当前连接的socket TCP套接字
Conn *net.TCPConn
//连接ID
ConnID uint32
//当前的链接状态
IsClosed bool
//当前链接所绑定的处理业务方法API
handleAPI ziface.HandleFunc
//告知当前链接已经退出的/停止的channel
ExitChan chan bool
}
//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)
cnt,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
}
}
}
//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,callback_api ziface.HandleFunc) *Connection{
c:=&Connection{
Conn: conn,
ConnID: connID,
handleAPI: callback_api,
IsClosed: false,
ExitChan: make(chan bool,1),
}
return c
}