Golang轻量级TCP服务器框架(二)—— 基础client链接封装以及业务绑定
原作者视频地址:zinx-Golang轻量级TCP服务器框架
本人为自学整理的文档,梳理思考开发框架的基本思路,方法,以及视频中不理解的地方。
若想学习,强烈建议直接观看原作视频即可。
可在下方留言交流。
1.思路
我们打算将conn这个链接进行封装管理,有两个原因:首先,该包并不打算将链接直接暴露给用户;其次,我们要对conn实现读写分离操作;最后,方便之后的多客户端链接管理。
2.iconnection接口的设计
思路框图:
type IConnection interface {
//启动链接 ,让当前链接开始工作
Start()
//停止链接, 结束当前链接
Stop()
//获取当前链接的套接字
GetTCPConnection() *net.TCPConn
//获取当前链接的链接ID
GetConnID() uint32
//获取远程客户端的TCP状态 IP Port
GetRemoteAddr() net.Addr
//发送数据,将数据发送给client
Send(data []byte) error
}
//定义一个绑定处理链接业务的方法
type HandleFunc func(*net.TCPConn, []byte, int) error
这里可以根据自己的需要在添加相应的方法
3.connection接口的实现
这里我们需要在对接口进行实现。
代码如下:
//属性
type connection struct {
//当前链接的socket TCP套接字
conn *net.TCPConn
//当前链接ID
connID uint32
//当前链接是否关闭
isClosed bool
//当前链接所绑定的API
handleFunc ziface.HandleFunc
//通知当前链接已经退出/停止的channel
exitChan chan bool
}
//方法
func NewConnection(conn *net.TCPConn, connID uint32, callbackHandle ziface.HandleFunc) *connection{}
//启动链接 ,让当前链接开始工作
func (c *connection) Start(){}
//停止链接, 结束当前链接
func (c *connection) Stop(){}
//获取当前链接的套接字
func (c *connection) GetTCPConnection() *net.TCPConn{}
//获取当前链接的链接ID
func (c *connection) GetConnID() uint32{}
//获取远程客户端的TCP状态 IP Port
func (c *connection) GetRemoteAddr() net.Addr{}
//发送数据,将数据发送给client
func (c *connection) Send(data []byte) error{}
这里我们可以看到又有一个NewConnection函数,类似上一篇中的newserver时候的方法,为什么这样做上一篇已经说明了,这里不再赘述。
这里我们主要关注前两个方法。后面的几个都是直接返回conn的相关信息,直接return即可。send暂时现忽略。
NewConnection:它的第三个参数是一个ziface.HandleFunc类型的。而且,我们的结构体里面也有一个这样类型的函数。它的定义信息如下:
type HandleFunc func(*net.TCPConn, []byte, int) error
也就是说,它表示该种类型的函数。我们只需要给这个函数传入该种类型函数,并将其赋值给相应的结构体成员即可。目的就是为了可以将某个自定义函数绑定到该链接。当然,这个功能只是为了暂时展示,后续会更改此项功能。
Start:该函数作为链接启动函数,其实也就类似server的start方法的功能。因为我们要设计一个读写分离的结构,所以在start中还要有以下结构:
func (c *connection) Start() {
//启动当前链接的读数据的业务
go c.StartReader()
//TODO:启动当前链接的写数据的业务
}
也就是将conn的读方法和写方法,作为两个单独的协程去运行。但是此处,仅仅实现了读协程(直接在StartReader实现了写),后续会将写协程分离出来。StartReader()的伪代码如下:
func (c *connection) StartReader() {
for {
buf := make([]byte, 512)
cnt , err := c.conn.Read(buf) //读协程阻塞读
//当用当前链接绑定的handel
err = c.handleFunc(c.conn, buf, cnt) //这里执行的就是为其绑定的那个函数
}
}
4.server中如何修改的呢
废话不多说,我们直接上对比代码:
//3.阻塞的等待客户端连接,处理客户端业务
*1 var cid uint32
*2 cid = 0
for {
conn, err := listenner.AcceptTCP()
if err != nil {
fmt.Println("[ERROR] Accept client conn is error :", err)
continue
}
*3 dealConn := NewConnection(conn, cid, callbackHandle)
*4 cid++
*5 dealConn.Start()
已经与client建立连接,执行一些业务:做一个简单的字节回显任务
//go func(){
// for {
// buf := make([]byte, 512)
// n, err := conn.Read(buf)
// if err != nil {
// fmt.Println("[ERROR] client conn read is error :", err)
// continue
// }
// fmt.Printf("server receive : %s, length is %d\n", buf, n)
// //回显示
// if _, err := conn.Write(buf[:n]); err != nil {
// fmt.Println("[ERROR] return client conn write is error :", err)
// continue
// }
// }
//
//}()
}
看过第一篇的同学,应该能看出来,这是server.Start中的一段代码。前面带*的那几行,都是后来加入的,注释掉的代码是之前的处理逻辑。
标记的星1、2、4的cid就是为了统计conn的技术。
星3就是为了封装conn链接以及绑定业务函数。
星5表示启动该链接,start中又包含读协程,在读协程中实现的阻塞读以及callbackHandle(作用:回显)。其实就是实现了我们屏蔽掉的地方。
5.本篇问题
主要是学习这个思路,暂时没问题。
6.方法代码实现
6.1zinx包
iconnection.go
package ziface
import "net"
//定义链接模块的抽象层
type IConnection interface {
//启动链接 ,让当前链接开始工作
Start()
//停止链接, 结束当前链接
Stop()
//获取当前链接的套接字
GetTCPConnection() *net.TCPConn
//获取当前链接的链接ID
GetConnID() uint32
//获取远程客户端的TCP状态 IP Port
GetRemoteAddr() net.Addr
//发送数据,将数据发送给client
Send(data []byte) error
}
//定义一个绑定处理链接业务的方法
type HandleFunc func(*net.TCPConn, []byte, int) error
conection.go
package znet
import (
"fmt"
"net"
"zinx/ziface"
)
type connection struct {
//当前链接的socket TCP套接字
conn *net.TCPConn
//当前链接ID
connID uint32
//当前链接是否关闭
isClosed bool
//当前链接所绑定的API
handleFunc ziface.HandleFunc
//通知当前链接已经退出/停止的channel
exitChan chan bool
}
//初始化链接模块
func NewConnection(conn *net.TCPConn, connID uint32, callbackHandle ziface.HandleFunc) *connection{
c := &connection{
conn: conn,
connID: connID,
isClosed: false,
handleFunc: callbackHandle,
exitChan: make(chan bool, 1),
}
return c
}
func (c *connection) StartReader() {
fmt.Println("connID = ", c.connID,"Reader goroutine si running...")
defer fmt.Println("connID = ", c.connID,"Reader is exit, remote addr is ", c.GetRemoteAddr().String())
defer c.Stop()
for {
buf := make([]byte, 512)
cnt , err := c.conn.Read(buf)
if err != nil {
fmt.Println("connID = ", c.connID,"recv is err :",err)
continue
}
//当用当前链接绑定的handel
err = c.handleFunc(c.conn, buf, cnt)
if err != nil {
fmt.Println("connID = ", c.connID,"handle is err :",err)
break //这里的话,直接break,或者contine都是可以的,根据自己的实际业务编写
}
}
}
//启动链接 ,让当前链接开始工作
func (c *connection) Start() {
fmt.Println("[CONN START] client connection is ", c.GetRemoteAddr().String())
//启动当前链接的读数据的业务
go c.StartReader()
//TODO:启动当前链接的写数据的业务
}
//停止链接, 结束当前链接
func (c *connection) Stop() {
fmt.Println("[CONN CLOSE] client connection is ", c.conn)
if c.isClosed {
return
}
c.isClosed = true
_ = c.conn.Close()
close(c.exitChan)
}
//获取当前链接的套接字
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()
}
//发送数据,将数据发送给client
func (c *connection) Send(data []byte) error {
return nil
}
server.go(有改动)
package znet
import (
"errors"
"fmt"
"net"
"zinx/ziface"
)
//iserver接口的实现,实现server的服务模块
type server struct {
//服务器的名称
name string
//ip的版本
ipVersion string
//ip地址
ip string
//ip监听端口
port int
}
func callbackHandle(conn *net.TCPConn, buf []byte, cnt int) error{
fmt.Println("[Conn Handle] Running...")
if _, err := conn.Write(buf); err != nil {
fmt.Println("write back buf err :", err)
return errors.New("CallBack error")
}
return nil
}
func (s *server) Start() {
go func(){
//1.获取一个tcp的addr
addr , err := net.ResolveTCPAddr(s.ipVersion, fmt.Sprintf("%s:%d", s.ip, s.port))
if err != nil {
fmt.Println("[ERROR] Resolve tcp addr is error :", err)
return
}
//2.监听服务器的地址
listenner, err := net.ListenTCP(s.ipVersion, addr)
if err != nil {
fmt.Println("[ERROR] ListenTCP tcp addr is error :", err)
return
}
fmt.Println("[ZINX] zinx start is success!!!")
var cid uint32
cid = 0
//3.阻塞的等待客户端连接,处理客户端业务
for {
conn, err := listenner.AcceptTCP()
if err != nil {
fmt.Println("[ERROR] Accept client conn is error :", err)
continue
}
dealConn := NewConnection(conn, cid, callbackHandle)
cid++
dealConn.Start()
已经与client建立连接,执行一些业务:做一个简单的字节回显任务
//go func(){
// for {
// buf := make([]byte, 512)
// n, err := conn.Read(buf)
// if err != nil {
// fmt.Println("[ERROR] client conn read is error :", err)
// continue
// }
// fmt.Printf("server receive : %s, length is %d\n", buf, n)
// //回显示
// if _, err := conn.Write(buf[:n]); err != nil {
// fmt.Println("[ERROR] return client conn write is error :", err)
// continue
// }
// }
//
//}()
}
}()
}
func (s *server) Server() {
fmt.Printf("[START] Server Listenner at IP :%s, Port :%d, is starting\n",s.ip, s.port)
s.Start()
select {}
}
func (s *server) Stop() {
}
func NewServer(name string) ziface.IServer{
s := &server{
name: name,
ipVersion: "tcp4",
ip: "0.0.0.0",
port: 8999,
}
return s
}
6.2用户调用测试程序
毫无变化
7.总结
这节没有不理解的地方,关键还是封装思路以及注册函数的这个想法,实现活用。