剖析Go编写的Socket服务器模块解耦及基础模块的设计

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

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

这是一个典型的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则是保存报错信息。 模块代码如下:

func LogErr(v ...interface{}) {  
 
    logfile := os.Stdout  
    log.Println(v...)  
    logger := log.New(logfile,"\r\n",log.Llongfile|log.Ldate|log.Ltime);  
    logger.SetPrefix("[Error]")  
    logger.Println(v...)  
    defer logfile.Close();  
}  
 
func Log(v ...interface{}) {  
 
    logfile := os.Stdout  
    log.Println(v...)  
    logger := log.New(logfile,"\r\n",log.Ldate|log.Ltime);  
    logger.SetPrefix("[Info]")  
    logger.Println(v...)  
    defer logfile.Close();  
}  
 
func LogDebug(v ...interface{}) {  
    logfile := os.Stdout  
    log.Println(v...)  
    logger := log.New(logfile,"\r\n",log.Ldate|log.Ltime);  
    logger.SetPrefix("[Debug]")  
    logger.Println(v...)  
    defer logfile.Close();  
}  
 
func CheckError(err error) {  
    if err != nil {  
        LogErr(os.Stderr, "Fatal error: %s", err.Error())  
    }  
}  

注意这里log的输出我使用的是stdout,因为这样在Server运行的时候可以直接将log重定向到指定的位置,方便整个Server的部署。不过在日常开发的时候,为了方便调试代码,我推荐将log输出到指定文件位置下,这样在调试的时候会方便很多(主要是因为golang的调试实在太麻烦,很多时候都要依靠打log的时候进行步进。便于调试的Log模块代码示意:

func Log(v ...interface{}) {  
 
    logfile := os.OpenFile("server.log",os.O_RDWR|os.O_APPEND|os.O_CREATE,0);  
    if err != nil {  
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())  
        return    }  
    log.Println(v...)  
    logger := log.New(logfile,"\r\n",log.Ldate|log.Ltime);  
    logger.SetPrefix("[Info]")  
    logger.Println(v...)  
    defer logfile.Close();  
}  

然后就是计时循环模块啦,日常运行中,Server经常要执行一些定时任务,比如隔一定时间刷新后台,隔一段时间自动刷新爬虫等等,在这里我设计了一个Task接口,通过类似于TaskList的的方式将所有定时任务注册后统一执行,代码如下:

type DoTask interface {  
    Excute()  
}  
 
var tasklist []interface{}  
 
func AddTask(controller DoTask) {  
    var arr interface{}  
    arr = controller  
    tasklist = append(tasklist,arr)  
    fmt.Println(tasklist)  
    }  

在这里以一个定时报时任务作为例子:

type Task1 struct {}  
 
func (this * Task1)Excute() {  
    timer := time.NewTicker(2 * time.Second)  
    for {  
        select {  
        case <-timer.C:  
            go func() {  
                Log(time.Now())  
            }()  
        }  
    }  
}  
 
func init() {  
    var task1 Task1  
    tasklist = make([]interface{} ,0 , 20)  
    AddTask(&task1)  
        for _, v := range tasklist {  
            v.(DoTask).Excute()  
        }  
 
}  

注意这里的定时任务要做成非阻塞的,否则整个Server都会卡在tasklist的第一个task的。。。

原文发布于微信公众号 - Golang语言社区(Golangweb)

原文发表时间:2016-08-25

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏c#开发者

打包并自动安装sql数据库

打包,并自动安装SQL数据库 应一位网友的需求,并修正了MVP李洪根".NET平台下WEB应用程序的部署(安装数据库和自动配置)"中的osql用法错误,已测试通...

33930
来自专栏java一日一条

优化 iOS 程序性能的 25 个方法

ARC(Automatic ReferenceCounting, 自动引用计数)和iOS5一起发布,它避免了最常见的也就是经常是由于我们忘记释放内存所造成的内存...

14340
来自专栏葡萄城控件技术团队

MVC5:使用Ajax和HTML5实现文件上传功能

引言 在实际编程中,经常遇到实现文件上传并显示上传进度的功能,基于此目的,本文就为大家介绍不使用flash 或任何上传文件的插件来实现带有进度显示的文件上传功能...

707100
来自专栏Golang语言社区

剖析Go编写的Socket服务器模块解耦及基础模块的设计

Server的解耦—通过Router+Controller实现逻辑分发 在实际的系统项目工程中中,我们在写代码的时候要尽量避免不必要的耦合,否则你以后在更新和维...

37960
来自专栏移动开发之家

Flutter完整开发实战详解(三、 打包与填坑篇)

作为系列文章的第三篇,继篇章一和篇章二之后,本篇将为你着重展示:Flutter开发过程的打包流程、APP包对比、细节技巧与问题处理。本篇主要描述的Flutter...

68430
来自专栏前端布道

Angular开发实践(五):深入解析变化监测

什么是变化监测 在使用 Angular 进行开发中,我们常用到 Angular 中的绑定——模型到视图的输入绑定、视图到模型的输出绑定以及视图与模型的双向绑定。...

42380
来自专栏hbbliyong

WPF命令(Command)介绍、命令和数据绑定集成应用

要开始使用命令,必须做三件事:                                               一:定义一个命令       ...

61130
来自专栏不知的专栏

反-反爬虫:用几行代码写出和人类一样的动态爬虫

本文将从 Phantomjs 动态爬虫介绍起,用3行代码傻瓜式完成基于 Casper 的动态爬虫来绕过对抗策略获取页面数据。

1.4K20
来自专栏Puppeteer学习

Headless Chrome:服务端渲染JS站点的一个方案【中篇】【翻译】防止重新渲染优化

25830
来自专栏QQ音乐技术团队的专栏

使用 Jest 进行前端单元测试

目前 Jest 已经在 Facebook 开源的 React, React Native 等前端项目中被做为标配测试框架。下面简单介绍一些 Jest 比较有用的...

1.1K90

扫码关注云+社区

领取腾讯云代金券