浅析Go编写的Socket服务器模块解耦及基础模块的兼备,Go语言基于Socket编写服务器端与客商端通信的实例

Server的解耦—通过Router+Controller实现逻辑分发

Go语言基于Socket编写服务器端与客户端通信的实例,gosocket

在golang中,网络协议已经被封装的非常完好了,想要写一个Socket的Server,我们并不用像其他语言那样需要为socket、bind、listen、receive等一系列操作头疼,只要使用Golang中自带的net包即可很方便的完成连接等操作~
在这里,给出一个最最基础的基于Socket的Server的写法:
复制代码 代码如下:
package main 
import ( 
    "fmt" 
    "net" 
    "log" 
    "os" 

 
 
func main() { 
 
//建立socket,监听端口 
    netListen, err := net.Listen("tcp", "localhost:1024") 
    CheckError(err) 
    defer netListen.Close() 
 
    Log("Waiting for clients") 
    for { 
        conn, err := netListen.Accept() 
        if err != nil { 
            continue 
        } 
 
        Log(conn.RemoteAddr().String(), " tcp connect success") 
        handleConnection(conn) 
    } 

//处理连接 
func handleConnection(conn net.Conn) { 
 
    buffer := make([]byte, 2048) 
 
    for { 
 
        n, err := conn.Read(buffer) 
 
        if err != nil { 
            Log(conn.RemoteAddr().String(), " connection error: ",
err) 
            return 
        } 
 
 
        Log(conn.RemoteAddr().String(), "receive data string:n",
string(buffer[:n])) 
 
    } 
 

func Log(v ...interface{}) { 
    log.Println(v...) 

 
func CheckError(err error) { 
    if err != nil { 
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) 
        os.Exit(1) 
    } 

唔,抛除Go语言里面10行代码有5行error的蛋疼之处,你可以看到,Server想要建立并接受一个Socket,其核心流程就是
复制代码 代码如下:
netListen, err := net.Listen("tcp", "localhost:1024") 

conn, err := netListen.Accept() 

n, err := conn.Read(buffer) 

这三步,通过Listen、Accept
和Read,我们就成功的绑定了一个端口,并能够读取从该端口传来的内容~
Server写好之后,接下来就是Client方面啦,我手写一个HelloWorld给大家:
复制代码 代码如下:
package main 
 
import ( 
    "fmt" 
    "net" 
    "os" 

 
func sender(conn net.Conn) { 
        words := "hello world!" 
        conn.Write([]byte(words)) 
    fmt.Println("send over") 
 

 
 
 
func main() { 
    server := "127.0.0.1:1024" 
    tcpAddr, err := net.ResolveTCPAddr("tcp4", server) 
    if err != nil { 
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) 
        os.Exit(1) 
    } 
 
    conn, err := net.DialTCP("tcp", nil, tcpAddr) 
    if err != nil { 
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) 
        os.Exit(1) 
    } 
 
 
    fmt.Println("connect success") 
    sender(conn) 
 

可以看到,Client这里的关键在于
复制代码 代码如下:
tcpAddr, err := net.ResolveTCPAddr("tcp4", server) 

conn, err := net.DialTCP("tcp", nil, tcpAddr) 

这两步,主要是负责解析端口和连接~
写好Server和Client之后,让我们运行一下看看:~~
成功运行,Console出现Server等待连接的提示:

云顶娱乐棋牌游戏 1

云顶娱乐棋牌游戏 2

云顶娱乐棋牌游戏 3

Server端成功的收到了我们的Hello-World啦,至于后面的那行红字,则是断开连接的提示~

到这里,一个最基础的使用Socket的Server-Client框架就出来啦~
如果想要让Server能够响应来自不同Client的请求,我们只要在Server端的代码的main入口中,
在 handleConnection(conn net.Conn) 这句代码的前面加上一个
go,就可以让服务器并发处理不同的Client发来的请求啦

自定义通讯协议 在上面我们做出来一个最基础的demo后,已经可以初步实现Server和Client之间的信息交流了~
这一章我会介绍一下怎么在Server和Client之间实现一个简单的通讯协议,从而增强整个信息交流过程的稳定性。
       
在Server和client的交互过程中,有时候很难避免出现网络波动,而在通讯质量较差的时候,Client有可能无法将信息流一次性完整发送,最终传到Server上的信息很可能变为很多段。
       
如下图所示,本来应该是分条传输的json,结果因为一些原因连接在了一起,这时候就会出现问题啦,Server端要怎么判断收到的消息是否完整呢?~

云顶娱乐棋牌游戏 4

 唔,答案就是这篇文章的主题啦:在Server和Client交互的时候,加入一个通讯协议(protocol),让二者的交互通过这个协议进行封装,从而使Server能够判断收到的信息是否为完整的一段。(也就是解决分包的问题)
       
因为主要目的是为了让Server能判断客户端发来的信息是否完整,因此整个协议的核心思路并不是很复杂:
协议的核心就是设计一个头部(headers),在Client每次发送信息的时候将header封装进去,再让Server在每次收到信息的时候按照预定格式将消息进行解析,这样根据Client传来的数据中是否包含headers,就可以很轻松的判断收到的信息是否完整了~
       
如果信息完整,那么就将该信息发送给下一个逻辑进行处理,如果信息不完整(缺少headers),那么Server就会把这条信息与前一条信息合并继续处理。

       
下面是协议部分的代码,主要分为数据的封装(Enpack)和解析(Depack)两个部分,其中Enpack用于Client端将传给服务器的数据封装,而Depack是Server用来解析数据,其中Const部分用于定义Headers,HeaderLength则是Headers的长度,用于后面Server端的解析。这里要说一下ConstMLength,这里代表Client传入信息的长度,因为在golang中,int转为byte后会占4长度的空间,因此设定为4。每次Client向Server发送信息的时候,除了将Headers封装进去意以外,还会将传入信息的长度也封装进去,这样可以方便Server进行解析和校验。
复制代码 代码如下:
云顶娱乐棋牌游戏 ,//通讯协议处理 
package protocol 
 
import ( 
    "bytes" 
    "encoding/binary" 

const ( 
    ConstHeader         = "Headers" 
    ConstHeaderLength   = 7 
    ConstMLength = 4 

 
//封包 
func Enpack(message []byte) []byte { 
    return append(append([]byte(ConstHeader),
IntToBytes(len(message))...), message...) 

 
//解包 
func Depack(buffer []byte, readerChannel chan []byte) []byte { 
    length := len(buffer) 
 
    var i int 
    for i = 0; i < length; i = i + 1 { 
        if length < i+ConstHeaderLength+ConstMLength { 
            break 
        } 
        if string(buffer[i:i+ConstHeaderLength]) == ConstHeader { 
            messageLength := BytesToInt(buffer[i+ConstHeaderLength :
i+ConstHeaderLength+ConstMLength]) 
            if length < i+ConstHeaderLength+ConstLength+messageLength

                break 
            } 
            data := buffer[i+ConstHeaderLength+ConstMLength :
i+ConstHeaderLength+ConstMLength+messageLength] 
            readerChannel <- data 
 
        } 
    } 
 
    if i == length { 
        return make([]byte, 0) 
    } 
    return buffer[i:] 

 
//整形转换成字节 
func IntToBytes(n int) []byte { 
    x := int32(n) 
 
    bytesBuffer := bytes.NewBuffer([]byte{}) 
    binary.Write(bytesBuffer, binary.BigEndian, x) 
    return bytesBuffer.Bytes() 

 
//字节转换成整形 
func BytesToInt(b []byte) int { 
    bytesBuffer := bytes.NewBuffer(b) 
 
    var x int32 
    binary.Read(bytesBuffer, binary.BigEndian, &x) 
 
    return int(x) 

       
协议写好之后,接下来就是在Server和Client的代码中应用协议啦,下面是Server端的代码,主要负责解析Client通过协议发来的信息流:
复制代码 代码如下:
package main   
   
import (   
    "protocol"   
    "fmt"   
    "net"   
    "os"   
)   
   
func main() {   
    netListen, err := net.Listen("tcp", "localhost:6060")   
    CheckError(err)   
   
    defer netListen.Close()   
   
    Log("Waiting for clients")   
    for {   
        conn, err := netListen.Accept()   
        if err != nil {   
            continue   
        }   
   
        //timeouSec :=10   
        //conn.   
        Log(conn.RemoteAddr().String(), " tcp connect success")   
        go handleConnection(conn)   
   
    }   
}   
   
func handleConnection(conn net.Conn) {   
   
   
    // 缓冲区,存储被截断的数据   
    tmpBuffer := make([]byte, 0)   
   
    //接收解包   
    readerChannel := make(chan []byte, 16)   
    go reader(readerChannel)   
   
    buffer := make([]byte, 1024)   
    for {   
    n, err := conn.Read(buffer)   
    if err != nil {   
    Log(conn.RemoteAddr().String(), " connection error: ", err)   
    return   
    }   
   
    tmpBuffer = protocol.Depack(append(tmpBuffer, buffer[:n]...),
readerChannel)   
    }   
    defer conn.Close()   
}   
   
func reader(readerChannel chan []byte) {   
    for {   
        select {   
        case data := <-readerChannel:   
            Log(string(data))   
        }   
    }   
}   
   
func Log(v ...interface{}) {   
    fmt.Println(v...)   
}   
   
func CheckError(err error) {   
    if err != nil {   
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())   
        os.Exit(1)   
    }   
}   

       
然后是Client端的代码,这个简单多了,只要给信息封装一下就可以了~:
复制代码 代码如下:
package main   
import (   
"protocol"   
"fmt"   
"net"   
"os"   
"time"   
"strconv"   
   
)   
   
func send(conn net.Conn) {   
    for i := 0; i < 100; i++ {   
        session:=GetSession()   
        words := "{"ID":"+ strconv.Itoa(i)
+"","Session":"+session
+"2015073109532345","Meta":"golang","Content":"message"}"   
        conn.Write(protocol.Enpacket([]byte(words)))   
    }   
    fmt.Println("send over")   
    defer conn.Close()   
}   
   
func GetSession() string{   
    gs1:=time.Now().Unix()   
    gs2:=strconv.FormatInt(gs1,10)   
    return gs2   
}   
   
func main() {   
    server := "localhost:6060"   
    tcpAddr, err := net.ResolveTCPAddr("tcp4", server)   
    if err != nil {   
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())   
        os.Exit(1)   
    }   
   
    conn, err := net.DialTCP("tcp", nil, tcpAddr)   
    if err != nil {   
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())   
        os.Exit(1)   
    }   
   
   
    fmt.Println("connect success")   
    send(conn)   
   
   
   
}   

这样我们就成功实现在Server和Client之间建立一套自定义的基础通讯协议啦,让我们运行一下看下效果:

云顶娱乐棋牌游戏 5

成功识别每一条Client发来的信息啦~~

在实际的系统项目工程中中,我们在写代码的时候要尽量避免不必要的耦合,否则你以后在更新和维护代码的时候会发现如同深陷泥潭,随便改点东西整个系统都要变动的酸爽会让你深切后悔自己当初为什么非要把东西都写到一块去(我不会说我刚实习的时候就是这么干的。。。)
所以这一篇主要说说如何设计Sever的内部逻辑,将Server处理Client发送信息的这部分逻辑与Sevrer处理Socket连接的逻辑进行解耦~
这一块的实现灵感主要是在读一个HTTP开源框架:
Beego 
的源代码的时候产生的,Beego的整个架构就是高度解耦的,这里引用一下作者的介绍:
beego 是基于八大独立的模块构建的,是一个高度解耦的框架。当初设计 beego
的时候就是考虑功能模块化,用户即使不使用 beego 的 HTTP
逻辑,也依旧可以使用这些独立模块,例如:你可以使用 cache
模块来做你的缓存逻辑;使用日志模块来记录你的操作信息;使用 config
模块来解析你各种格式的文件。所以 beego 不仅可以用于 HTTP
类的应用开发,在你的 socket 游戏开发中也是很有用的模块,这也是 beego
为什么受欢迎的一个原因。大家如果玩过乐高的话,应该知道很多高级的东西都是一块一块的积木搭建出来的,而设计
beego 的时候,这些模块就是积木,高级机器人就是 beego。 
这里上一张Beego的架构图:

您可能感兴趣的文章:

  • Go语言实现socket实例
  • Go语言Echo服务器的方法
  • Go语言实现简单Web服务器的方法
  • Go语言服务器开发实现最简单HTTP的GET与POST接口
  • Go语言服务器开发之客户端向服务器发送数据并接收返回数据的方法
  • Go语言服务器开发之简易TCP客户端与服务端实现方法
  • Go语言实现简单的一个静态WEB服务器
  • Go语言实现的一个简单Web服务器

在golang中,网络协议已经被封装的非常完好了,想要写一个Socket的Server,我们并...

云顶娱乐棋牌游戏 6

 
这是一个典型的MVC框架,可以看到,当用户发送请求到beego后,Beego内部在通过路由进行参数的过滤,然后路由根据用户发来的参数判断调用哪个Controller执行相关的逻辑,并在controller里调用相关的模块实现功能。通过这种方式,Beego成功的将所有模块都独立出来,也就是astaxie所说的“乐高积木化”。
      
在这里,我们可以仿照Beego的架构,在Server内部加入一层Router,通过Router对通过Socket发来的信息进通过我们设定的规则行的判断后,调用相关的Controller进行任务的分发处理。在这个过程中不仅Controller彼此独立,匹配规则和Controller之间也是相互独立的。
      
下面给出Router的实现代码,其中Msg的结构对应的是Json字符串,当然考虑到实习公司现在也在用这个,修改了一部分,不过核心思路是一样的哦:

复制代码 代码如下:

import ( 
    "utils" 
    "fmt" 
    "encoding/json" 

 
type Msg struct { 
    Conditions   map[string]interface{} `json:"meta"` 
    Content interface{}            `json:"content"` 

 
type Controller interface { 
    Excute(message Msg) []byte 

 
var routers [][2]interface{} 
 
func Route(judge interface{} ,controller Controller) { 
    switch judge.(type) { 
    case func(entry Msg)bool:{ 
        var arr [2]interface{} 
        arr[0] = judge 
        arr[1] = controller 
        routers = append(routers,arr) 
    } 
    case map[string]interface{}:{ 
        defaultJudge:= func(entry Msg)bool{ 
            for keyjudge , valjudge := range
judge.(map[string]interface{}){ 
                val, ok := entry.Meta[keyjudge] 
                if !ok { 
                    return false 
                } 
                if val != valjudge { 
                    return false 
                } 
            } 
            return true 
        } 
        var arr [2]interface{} 
        arr[0] = defaultjudge 
        arr[1] = controller 
        routers = append(routers,arr) 
        fmt.Println(routers) 
        } 
    default: 
        fmt.Println("Something is wrong in Router") 
    } 

     
通过自定义接口Router,我们将匹配规则judge和对应的controller封装了进去,然后在Server端负责接收socket发送信息的函数handleConnection那里再实现Router内部的遍历即可:

复制代码 代码如下:

for _ ,v := range routers{ 
        pred := v[0] 
        act := v[1] 
        var message Msg 
        err := json.Unmarshal(postdata,&message) 
        if err != nil { 
            Log(err) 
        } 
        if pred.(func(entry Msg)bool)(message) { 
            result := act.(Controller).Excute(message) 
            conn.Write(result) 
            return 
        } 
    } 

      
这样Client每次发来信息,我们就可以让Router自动跟现有的规则进行匹配,最后调用对应的Controller进行逻辑的实现啦,下面给出一个controller的编写实例,这个Controll的作用是发来的json类型是mirror的时候,将Client发来的信息原样返回:

复制代码 代码如下:

type MirrorController struct  { 
 

 
func (this *MirrorController) Excute(message Msg)[]byte { 
    mirrormsg,err :=json.Marshal(message) 
    CheckError(err) 
    return mirrormsg 

 
 
func init() { 
    var mirror  
    routers = make([][2]interface{} ,0 , 20) 
    Route(func(entry Msg)bool{ 
        if entry.Meta["msgtype"]=="mirror"{ 
        return true} 
        return  false 
    },&mirror) 
}

日志模块的设计与定时任务模块模块 作为一个Server,日志(Log)功能是必不可少的,一个设计良好的日志模块,不论是开发Server时的调试,还是运行时候的维护,都是非常有帮助的。
因为这里写的是一个比较简化的Server框架,因此我选择对Golang本身的log库进行扩充,从而实现一个简单的Log模块。
在这里,我将日志的等级大致分为Debug,Operating,Error
3个等级,Debug主要用于存放调试阶段的日志信息,Operateing用于保存Server日常运行时产生的信息,Error则是保存报错信息。
模块代码如下:

复制代码 代码如下:

相关文章

Comment ()
评论是一种美德,说点什么吧,否则我会恨你的。。。