tcpsock for Golang
前记:本文所述的 tcpsock 库托管在 Github。
Golang 中的 net 标准库已对 TCP 网络编程作了简洁(却很不简单)的封装,基本上,可直接通过引用其提供的相关接口开发简易的网络应用程序。但我想封装一套简单并提供简洁调用接口的 TCP 网络库(即 tcpsock),以达到所谓组件化的目的,如此用户只需调用几个非常简单的接口(包括设置回调函数等)就能较直观地处理网络收发等逻辑。
一开始设计 tcpsock 时,只打算提供 TcpServer 及 TcpConn 对象而没有考虑加入 TcpClient(毕竟 Golang 多用于后台开发),但譬如网络游戏中的网关(GameGate),其既是服务器程序(服务于数计的网络客户端),又要充当客户端连接到游戏服务器(GameServer),此时即凸显 TcpClient 存在的必要了。所以重构了部分结构与代码,加入了 TcpClient 对象,而这也带来了额外的好处,具体可参考 chatroom 示例,其服务器与客户端在诸如协议解析等方面完全复用代码,大大减少了开发维护成本。
囿于个人当前的能力及精力, tcpsock 不可避免会存在一些甚至许多问题或 Bug(虽然它确实很简单),希望能在实际的项目运用及与技术同仁的探讨中一点点改进它,直至完善。
tcpsock 库共 4 个单元(tcpsock、tcpserver、tcpconn 及 tcpclient),并提供 3 个导出类型(TcpServer、TcpClient 及 TcpConn,分别实现于与其名称对应的单元),各单元代码如下:
// Copyright (C) 2017 ecofast(胡光耀). All rights reserved.
// Use of this source code is governed by a BSD-style license. // Package tcpsock provides easy to use interfaces for TCP I/O.
// Thanks to darksword(gansidui) and AlexStocks for their valuable projects
// which are gotcp(https://github.com/gansidui/gotcp)
// and getty(https://github.com/AlexStocks/getty).
package tcpsock import (
"sync"
) const (
RecvBufLenMax = 4 * 1024
SendBufLenMax = 4 * 1024 SendBufCapMax = 10
RecvBufCapMax = 10
) type tcpSock struct {
sendBufCap uint32
recvBufCap uint32
proto Protocol
exitChan chan struct{}
waitGroup *sync.WaitGroup
onConnConnect OnTcpConnCallback
onConnClose OnTcpConnCallback
} type Protocol interface {
Parse(b []byte, recvChan chan<- Packet)
Process(conn *TcpConn, p Packet)
} type Packet interface {
Marshal() []byte
}
// Copyright (C) 2017 ecofast(胡光耀). All rights reserved.
// Use of this source code is governed by a BSD-style license. package tcpsock import (
"net"
"sync"
"sync/atomic"
"time" . "github.com/ecofast/sysutils"
) type TcpServer struct {
listener *net.TCPListener
acceptTimeout int
*tcpSock
autoIncID uint32
numOfConn uint32
} func NewTcpServer(listenPort, acceptTimeout int, protocol Protocol) *TcpServer {
tcpAddr, err := net.ResolveTCPAddr("tcp", ":"+IntToStr(int(listenPort)))
CheckError(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
CheckError(err) return &TcpServer{
listener: listener,
acceptTimeout: acceptTimeout,
tcpSock: &tcpSock{
sendBufCap: SendBufCapMax,
recvBufCap: RecvBufCapMax,
proto: protocol,
exitChan: make(chan struct{}),
waitGroup: &sync.WaitGroup{},
},
}
} func (self *TcpServer) Serve() {
self.waitGroup.Add(1)
defer func() {
self.listener.Close()
self.waitGroup.Done()
}() for {
select {
case <-self.exitChan:
return default:
} self.listener.SetDeadline(time.Now().Add(time.Duration(self.acceptTimeout) * time.Second))
conn, err := self.listener.AcceptTCP()
if err != nil {
continue
} atomic.AddUint32(&self.numOfConn, 1)
self.waitGroup.Add(1)
go func() {
c := newTcpConn(atomic.AddUint32(&self.autoIncID, 1), self.tcpSock, conn, self.sendBufCap, self.recvBufCap, self.connClose)
if self.onConnConnect != nil {
self.onConnConnect(c)
}
c.run()
self.waitGroup.Done()
}()
}
} func (self *TcpServer) Close() {
close(self.exitChan)
self.waitGroup.Wait()
} func (self *TcpServer) NumOfConn() uint32 {
return atomic.LoadUint32(&self.numOfConn)
} func (self *TcpServer) connClose(conn *TcpConn) {
atomic.AddUint32(&self.numOfConn, ^uint32(0))
if self.onConnClose != nil {
self.onConnClose(conn)
}
} func (self *TcpServer) OnConnConnect(fn OnTcpConnCallback) {
self.onConnConnect = fn
} func (self *TcpServer) OnConnClose(fn OnTcpConnCallback) {
self.onConnClose = fn
}
// Copyright (C) 2017 ecofast(胡光耀). All rights reserved.
// Use of this source code is governed by a BSD-style license. package tcpsock import (
"net"
"sync"
"sync/atomic"
) type OnTcpConnCallback func(c *TcpConn) type TcpConn struct {
id uint32
owner *tcpSock
conn *net.TCPConn
sendChan chan Packet
recvChan chan Packet
closeChan chan struct{}
closeOnce sync.Once
closedFlag int32
onClose OnTcpConnCallback
} func newTcpConn(id uint32, owner *tcpSock, conn *net.TCPConn, sendCap, recvCap uint32, onClose OnTcpConnCallback) *TcpConn {
return &TcpConn{
id: id,
owner: owner,
conn: conn,
sendChan: make(chan Packet, sendCap),
recvChan: make(chan Packet, recvCap),
closeChan: make(chan struct{}),
onClose: onClose,
}
} func (self *TcpConn) ID() uint32 {
return self.id
} func (self *TcpConn) run() {
startGoroutine(self.reader, self.owner.waitGroup)
startGoroutine(self.writer, self.owner.waitGroup)
startGoroutine(self.handler, self.owner.waitGroup)
} func (self *TcpConn) Close() {
self.closeOnce.Do(func() {
atomic.StoreInt32(&self.closedFlag, 1)
close(self.sendChan)
close(self.recvChan)
close(self.closeChan)
self.conn.Close()
if self.onClose != nil {
self.onClose(self)
}
})
} func (self *TcpConn) Closed() bool {
return atomic.LoadInt32(&self.closedFlag) == 1
} func (self *TcpConn) RawConn() *net.TCPConn {
return self.conn
} func startGoroutine(fn func(), wg *sync.WaitGroup) {
wg.Add(1)
go func() {
fn()
wg.Done()
}()
} func (self *TcpConn) reader() {
defer func() {
recover()
self.Close()
}() buf := make([]byte, RecvBufLenMax)
for {
select {
case <-self.owner.exitChan:
return case <-self.closeChan:
return default:
} count, err := self.conn.Read(buf)
if err != nil {
return
}
self.owner.proto.Parse(buf[:count], self.recvChan)
}
} func (self *TcpConn) writer() {
defer func() {
recover()
self.Close()
}() for {
if self.Closed() {
return
} select {
case <-self.owner.exitChan:
return case <-self.closeChan:
return case p := <-self.sendChan:
if _, err := self.conn.Write(p.Marshal()); err != nil {
return
}
}
}
} func (self *TcpConn) handler() {
defer func() {
recover()
self.Close()
}() for {
if self.Closed() {
return
} select {
case <-self.owner.exitChan:
return case <-self.closeChan:
return case packet := <-self.recvChan:
self.owner.proto.Process(self, packet)
}
}
} func (self *TcpConn) Write(p Packet) {
if self.Closed() {
return
} defer func() {
recover()
}() self.sendChan <- p
}
// Copyright (C) 2017 ecofast(胡光耀). All rights reserved.
// Use of this source code is governed by a BSD-style license. package tcpsock import (
"net"
"sync" . "github.com/ecofast/sysutils"
) type TcpClient struct {
svrAddr *net.TCPAddr
*tcpSock
} func NewTcpClient(svrAddr string, proto Protocol) *TcpClient {
tcpAddr, err := net.ResolveTCPAddr("tcp", svrAddr)
CheckError(err)
return &TcpClient{
svrAddr: tcpAddr,
tcpSock: &tcpSock{
sendBufCap: SendBufCapMax,
recvBufCap: RecvBufCapMax,
proto: proto,
exitChan: make(chan struct{}),
waitGroup: &sync.WaitGroup{},
},
}
} func (self *TcpClient) Run() {
conn, err := net.DialTCP("tcp", nil, self.svrAddr)
CheckError(err) self.waitGroup.Add(1)
go func() {
// client sock do NOT need to identify self
c := newTcpConn( /*atomic.AddUint32(&self.autoIncID, 1)*/ 0, self.tcpSock, conn, self.sendBufCap, self.recvBufCap, self.connClose)
if self.onConnConnect != nil {
self.onConnConnect(c)
}
c.run()
self.waitGroup.Done()
}()
} func (self *TcpClient) Close() {
close(self.exitChan)
self.waitGroup.Wait()
} func (self *TcpClient) OnConnect(fn OnTcpConnCallback) {
self.onConnConnect = fn
} func (self *TcpClient) OnClose(fn OnTcpConnCallback) {
self.onConnClose = fn
} func (self *TcpClient) connClose(conn *TcpConn) {
if self.onConnClose != nil {
self.onConnClose(conn)
}
}
可以看到,tcpsock 的实现中有几处地方使用到了回调函数,或许后面会有更好的封装及处理方法(当然这绝不是说回调函数不好),暂时先这样设计吧。
而我也编写了个简单的包括服务器与客户端的聊天室程序,用以测试及验证 tcpsock,虽然这个示例挺简单,但可能比较实用,包括不限于如何使用 tcpsock,及二进制协议的设计与解析(涉及到 TCP 粘包处理)等,代码如下:
package main import (
"log"
"os"
"os/signal"
"sync"
"syscall"
"tcpsock"
. "tcpsock/samples/chatroom/protocol"
"time" . "github.com/ecofast/sysutils"
) var (
shutdown = make(chan bool, 1) mutex sync.Mutex
clients map[uint32]*tcpsock.TcpConn
) func init() {
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-signals
shutdown <- true
}()
} func onConnConnect(conn *tcpsock.TcpConn) {
// conn.Send(genChatPacket())
mutex.Lock()
defer mutex.Unlock()
clients[conn.ID()] = conn
} func onConnClose(conn *tcpsock.TcpConn) {
mutex.Lock()
defer mutex.Unlock()
delete(clients, conn.ID())
} func genChatPacket() *ChatPacket {
var head PacketHead
head.Signature = ChatSignature
head.PlayerID = 555555555
s := "current time is " + TimeToStr(time.Now())
head.BodyLen = uint32(len(s))
body := make([]byte, int(head.BodyLen))
copy(body[:], []byte(s)[:])
return NewChatPacket(head, body)
} func broadcast() {
mutex.Lock()
defer mutex.Unlock()
packet := genChatPacket()
for _, c := range clients {
c.Write(packet)
}
} func onMsg(conn *tcpsock.TcpConn, p *ChatPacket) {
mutex.Lock()
defer mutex.Unlock()
for _, c := range clients {
c.Write(p)
}
} func main() {
clients = make(map[uint32]*tcpsock.TcpConn) proto := &ChatProtocol{}
proto.OnMessage(onMsg)
server := tcpsock.NewTcpServer(9999, 2, proto)
server.OnConnConnect(onConnConnect)
server.OnConnClose(onConnClose)
log.Println("=====service start=====")
go server.Serve() ticker := time.NewTicker(10 * time.Second)
go func() {
for range ticker.C {
log.Printf("num of conn: %d\n", server.NumOfConn())
broadcast()
}
}() <-shutdown
server.Close()
log.Println("=====service end=====")
}
package protocol import (
"bytes"
"encoding/binary"
"tcpsock"
) const (
ChatSignature = 0xFFFFFFFF
PacketHeadSize = 4 + 4 + 4
) type PacketHead struct {
Signature uint32
PlayerID uint32
BodyLen uint32
} func (head *PacketHead) Bytes() []byte {
var buf bytes.Buffer
binary.Write(&buf, binary.LittleEndian, head)
return buf.Bytes()
} type ChatPacket struct {
PacketHead
Body []byte
} func NewChatPacket(head PacketHead, body []byte) *ChatPacket {
return &ChatPacket{
PacketHead: head,
Body: body,
}
} func (p *ChatPacket) Marshal() []byte {
buf := make([]byte, PacketHeadSize+len(p.Body))
copy(buf[:PacketHeadSize], p.PacketHead.Bytes()[:])
copy(buf[PacketHeadSize:], p.Body[:])
return buf
} type ChatProtocol struct {
recvBuf []byte
recvBufLen int
onMsg func(c *tcpsock.TcpConn, p *ChatPacket)
} func (self *ChatProtocol) Parse(b []byte, recvChan chan<- tcpsock.Packet) {
count := len(b)
if count+self.recvBufLen > tcpsock.RecvBufLenMax {
return
} self.recvBuf = append(self.recvBuf, b[0:count]...)
self.recvBufLen += count
offsize := 0
offset := 0
var head PacketHead
for self.recvBufLen-offsize > PacketHeadSize {
offset = 0
head.Signature = uint32(uint32(self.recvBuf[offsize+3])<<24 | uint32(self.recvBuf[offsize+2])<<16 | uint32(self.recvBuf[offsize+1])<<8 | uint32(self.recvBuf[offsize+0]))
offset += 4
head.PlayerID = uint32(uint32(self.recvBuf[offsize+offset+3])<<24 | uint32(self.recvBuf[offsize+offset+2])<<16 | uint32(self.recvBuf[offsize+offset+1])<<8 | uint32(self.recvBuf[offsize+offset+0]))
offset += 4
head.BodyLen = uint32(uint32(self.recvBuf[offsize+offset+3])<<24 | uint32(self.recvBuf[offsize+offset+2])<<16 | uint32(self.recvBuf[offsize+offset+1])<<8 | uint32(self.recvBuf[offsize+offset+0]))
offset += 4
if head.Signature == ChatSignature {
pkglen := int(PacketHeadSize + head.BodyLen)
if pkglen >= tcpsock.RecvBufLenMax {
offsize = self.recvBufLen
break
}
if offsize+pkglen > self.recvBufLen {
break
} recvChan <- NewChatPacket(head, self.recvBuf[offsize+offset:offsize+offset+int(head.BodyLen)])
offsize += pkglen
} else {
offsize++
}
} self.recvBufLen -= offsize
if self.recvBufLen > 0 {
self.recvBuf = self.recvBuf[offsize : offsize+self.recvBufLen]
} else {
self.recvBuf = nil
}
} func (self *ChatProtocol) Process(conn *tcpsock.TcpConn, p tcpsock.Packet) {
packet := p.(*ChatPacket)
self.onMsg(conn, packet)
} func (self *ChatProtocol) OnMessage(fn func(c *tcpsock.TcpConn, p *ChatPacket)) {
self.onMsg = fn
}
package main import (
"fmt"
"log"
"os"
"os/signal"
"syscall"
"tcpsock"
. "tcpsock/samples/chatroom/protocol"
) const (
ServerAddr = ":9999"
) var (
shutdown = make(chan bool, 1) tcpConn *tcpsock.TcpConn
id uint32
) func init() {
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-signals
shutdown <- true
}()
} func main() {
genID() proto := &ChatProtocol{}
proto.OnMessage(onMsg)
client := tcpsock.NewTcpClient(ServerAddr, proto)
client.OnConnect(onConnect)
client.OnClose(onClose)
go client.Run()
go input()
<-shutdown
client.Close()
} func onConnect(c *tcpsock.TcpConn) {
log.Println("successfully connect to server", c.RawConn().RemoteAddr().String())
tcpConn = c
} func onClose(c *tcpsock.TcpConn) {
log.Println("disconnect from server", c.RawConn().RemoteAddr().String())
tcpConn = nil
} func onMsg(c *tcpsock.TcpConn, p *ChatPacket) {
log.Printf("%d: %s\n", p.PlayerID, string(p.Body))
} func genID() {
fmt.Printf("pls enter your id: ")
fmt.Scan(&id)
fmt.Println("your id is:", id)
} func input() {
s := ""
for {
if n, err := fmt.Scan(&s); n == 0 || err != nil {
break
}
if tcpConn == nil {
break
}
tcpConn.Write(genPacket(s))
}
} func genPacket(s string) *ChatPacket {
var head PacketHead
head.Signature = ChatSignature
head.PlayerID = id
head.BodyLen = uint32(len(s))
body := make([]byte, head.BodyLen)
copy(body[:], []byte(s)[:])
return NewChatPacket(head, body)
}
可以看到,服务器与客户端完全共用一套协议处理单元,且各自只需对收到的网络封包作处理即可。
tcpsock for Golang的更多相关文章
- golang net之http server
golang 版本:1.12.9 简单的HTTP服务器代码: package main import ( "net/http" ) type TestHandler struct ...
- 详解golang net之netpoll
golang版本1.12.9:操作系统:readhat 7.4 golang的底层使用epoll来实现IO复用.netPoll通过pollDesc结构体将文件描述符与底层进行了绑定.netpoll实现 ...
- Golang, 以17个简短代码片段,切底弄懂 channel 基础
(原创出处为本博客:http://www.cnblogs.com/linguanh/) 前序: 因为打算自己搞个基于Golang的IM服务器,所以复习了下之前一直没怎么使用的协程.管道等高并发编程知识 ...
- 说说Golang的使用心得
13年上半年接触了Golang,对Golang十分喜爱.现在是2015年,离春节还有几天,从开始学习到现在的一年半时间里,前前后后也用Golang写了些代码,其中包括业余时间的,也有产品项目中的.一直 ...
- TODO:Golang指针使用注意事项
TODO:Golang指针使用注意事项 先来看简单的例子1: 输出: 1 1 例子2: 输出: 1 3 例子1是使用值传递,Add方法不会做任何改变:例子2是使用指针传递,会改变地址,从而改变地址. ...
- Golang 编写的图片压缩程序,质量、尺寸压缩,批量、单张压缩
目录: 前序 效果图 简介 全部代码 前序: 接触 golang 不久,一直是边学边做,边总结,深深感到这门语言的魅力,等下要跟大家分享是最近项目 服务端 用到的图片压缩程序,我单独分离了出来,做成了 ...
- golang struct扩展函数参数命名警告
今天在使用VSCode编写golang代码时,定义一个struct,扩展几个方法,如下: package storage import ( "fmt" "github.c ...
- golang语言构造函数
1.构造函数定义 构造函数 ,是一种特殊的方法.主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中.特别的一个类可以有多个构造函数 ,可根据其参数个 ...
- TODO:Golang语言TCP/UDP协议重用地址端口
TODO:Golang语言TCP/UDP协议重用地址端口 这是一个简单的包来解决重用地址的问题. go net包(据我所知)不允许设置套接字选项. 这在尝试进行TCP NAT时尤其成问题,其需要在同一 ...
随机推荐
- Ansible常见问题处理
1.ansible all -m ping报错,信息如下: [WARNING]: log file at /var/log/ansible.log is not writeable and we ca ...
- SSM知识巩固2
数据回显 1.springmvc默认对pojo数据进行回显. pojo数据传入controller方法后,springmvc自动将pojo数据放到request域,key等于pojo类型(首字母小写) ...
- 0111mysql如何选择Join的顺序
本文通过一个案例来看看MySQL优化器如何选择索引和JOIN顺序.表结构和数据准备参考本文最后部分"测试环境".这里主要介绍MySQL优化器的主要执行流程,而不是介绍一个优化器的各 ...
- 通达OA 小飞鱼工作流在线培训教程(一)HTML基础介绍
应一些刚接触工作流设计朋友的要求,这里开设一个系列教程,对通达OA工作流设计相关的内容做个介绍.方便解决一些日常经常出现的问题,希望对刚刚接触这部分工作的朋友能够有些帮助. 工作流设计须要多方面的知识 ...
- hdu2688 Rotate(树状数组)
题目链接:pid=2688">点击打开链接 题意描写叙述:对一个长度为2<=n<=3000000的数组,求数组中有序对(i<j而且F[i]<F[j])的数量?其 ...
- LCA 近期公共祖先 小结
LCA 近期公共祖先 小结 以poj 1330为例.对LCA的3种经常使用的算法进行介绍,分别为 1. 离线tarjan 2. 基于倍增法的LCA 3. 基于RMQ的LCA 1. 离线tarjan / ...
- UVA 11728 - Alternate Task 数学
Little Hasan loves to play number games with his friends. One day they were playing a game whereone ...
- Linux性能优化和监控系列(一)——top工具
解释服务器发生了什么——top工具 在检查服务器的详细工作性能状态前,系统管理员需要对当前服务器状态有总体的了解. top是检查服务器总体状态的强有力工具, 通过top可以获取CPU, Memory, ...
- [IOI 1998] Polygon
[题目链接] http://poj.org/problem?id=1179 [算法] 区间DP [代码] #include <algorithm> #include <bitset& ...
- 检测Nginx访问成功(状态码200)的IP及次数
cat access.log |awk '{print $1,$9}'|grep '200'|sort | uniq -c|awk '{print $2" "$1}'