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

在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等待连接的提示:

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端要怎么判断收到的消息是否完整呢?~

唔,答案就是这篇文章的主题啦:在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之间建立一套自定义的基础通讯协议啦,让我们运行一下看下效果:

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

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

原文发表时间:2017-05-21

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Albert陈凯

2018-09-03 简单问题:VIM中 查找 “上一个” 的快捷键是什么?G 移动光标到指定行#. 例如: 5G-> 光标下反向搜索关键词 (search the word under cursor

https://blog.csdn.net/qq_31307253/article/details/78652494

1.8K10
来自专栏阮一峰的网络日志

Javascript文件加载:LABjs和RequireJS

传统上,加载Javascript文件都是使用<script>标签。 就像下面这样:   <script type="text/javascript" src=...

39640
来自专栏coder修行路

aiohttp文档翻译-server(一)

一个 request handler 必须是一个coroutine (协程), 它接受一个Request实例作为其唯一参数,并返回一个Response 实例,如...

14520
来自专栏数据小魔方

Excel多工作薄合并

今天要给大家介绍一下Excel多工作表合并的技巧! 由于Excel工作薄文件可以包含多个工作表,所以合并起来要比Word麻烦! 目前还无法单纯通过Excel界面...

32360
来自专栏程序员宝库

IntelliJ IDEA 教程设置讲解

IntelliJ在业界被公认为最好的java开发工具之一,尤其在智能代码助手、代码自动提示、重构、J2EE支持、Ant、JUnit、CVS整合、代码审查、 创新...

15740
来自专栏编程坑太多

自己总结的,苦心总结的linux命令

18040
来自专栏企鹅号快讯

使用技巧精讲

1、快速建立连接 单击顶部活动菜单栏上的“快速建立连接按钮”(Alt+Q),如图1 图1 单击按钮后,弹出图2所示的窗口 ? 图2 Protocol:默认SSH...

22250
来自专栏守望轩

Visual Studio 2008 每日提示(十八)

#171、在任务列表创建用户任务区分代码 原文链接:You can use the Task List to create User Tasks that a...

30960
来自专栏Golang语言社区

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

在golang中,网络协议已经被封装的非常完好了,想要写一个Socket的Server,我们并不用像其他语言那样需要为socket、bind、listen、re...

1K50
来自专栏贾老师の博客

一些实用技巧与备忘

13230

扫码关注云+社区

领取腾讯云代金券