Golang Socket Server自定义协议的简单实现方案

在Server和Client通讯中,由于网络等原因很可能会发生数据丢包的现象。如果数据缺失,服务端接收的信息不完整,就会造成混乱。

我们需要在Server和Client之间建立一个通讯协议,通过协议中的规则,判断当前接收到的信息是否完整。根据信息的完整情况,采取不同的处理方式。

通讯协议protocol的核心就是设计一个头部。如果传来的信息不包含这个头部,就说明当前信息和之前的信息是同一条。那么就把当前信息和之前的那条信息合并成一条。

而协议主要包含的功能是封装(Enpack)和解析(Depack)。Enpack是客户端对信息进行数据封装。封装之后可以传递给服务器。Depack是服务器对信息进行数据解析。

其中有个Const部分,用于定义头部、头部长度、客户端传入信息长度。

在代码中,我们这样定义:

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

头部的内容为"Headers",长度为7。所以ConstHeaderLenth=7.

而信息传递过程中,我们会把int类型转换成byte类型。一个int的长度等于4个byte的长度。因此,我们设置ConstMLength=4.代表客户端的传来的信息大小。

自定义协议protocal的代码示例如下:

/**
* protocol
* @Author: Jian Junbo
* @Email: junbojian@qq.com
* @Create: 2017/9/14 11:49
*
* Description: 通讯协议处理
*/
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) []byte {
 length := len(buffer)

 var i int
 data := make([]byte, 32)
 for i = 0; i < length; i++ {

  if length < i + ConstHeaderLength + ConstMLength{
   break
  }
  if string(buffer[i:i+ConstHeaderLength]) == ConstHeader {
   messageLength := ByteToInt(buffer[i+ConstHeaderLength : i+ConstHeaderLength+ConstMLength])
   if length < i+ConstHeaderLength+ConstMLength+messageLength {
    break
   }
   data = buffer[i+ConstHeaderLength+ConstMLength : i+ConstHeaderLength+ConstMLength+messageLength]
  }
 }

 if i == length {
  return make([]byte, 0)
 }
 return data
}

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

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

Server端主要通过协议来解析客户端发送来的信息。建立一个函数,用来完成连接对接收信息的处理。其中建立了通道readerChannel,并把接收来的信息放在通道里。

在放入通道之前,使用protocol和Depack对信息进行解析。

//连接处理
func handleConnection(conn net.Conn) {
 //缓冲区,存储被截断的数据
 tmpBuffer := make([]byte, 0)
 //接收解包
 readerChannel := make(chan []byte, 10000)
 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 <- tmpBuffer  //接收的信息写入通道

 }
 defer conn.Close()
}

如果信息读取发生错误(包括读取到信息结束符EOF),都会打印错误信息,并跳出循环。

Log(conn.RemoteAddr().String(), "connection error: ", err)

return

由于通道内的数据是[]byte型的。需要转换成string。这个工作有专门的获取通道数据的reader(readerChannel chan []byte)来完成。

//获取通道数据
func reader(readerchannel chan []byte) {
 for{
  select {
  case data := <-readerchannel:
   Log(string(data))  //打印通道内的信息
  }
 }
}

查看Server端代码示例:

/**
* MySocketProtocalServer
* @Author: Jian Junbo
* @Email: junbojian@qq.com
* @Create: 2017/9/14 13:54
* Copyright (c) 2017 Jian Junbo All rights reserved.
*
* Description: 服务端,接收客户端传来的信息
*/
package main
import (
 "net"
 "fmt"
 "os"
 "log"
 "protocol"
)

func main() {
 netListen, err := net.Listen("tcp", "localhost:7373")
 CheckErr(err)
 defer netListen.Close()

 Log("Waiting for client ...")  //启动后,等待客户端访问。
 for{
  conn, err := netListen.Accept()  //监听客户端
  if err != nil {
   Log(conn.RemoteAddr().String(), "发了了错误:", err)
   continue
  }
  Log(conn.RemoteAddr().String(), "tcp connection success")
  go handleConnection(conn)
 }
}

//连接处理
func handleConnection(conn net.Conn) {
 //缓冲区,存储被截断的数据
 tmpBuffer := make([]byte, 0)
 //接收解包
 readerChannel := make(chan []byte, 10000)
 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 <- tmpBuffer  //接收的信息写入通道
 }
 defer conn.Close()
}

//获取通道数据
func reader(readerchannel chan []byte) {
 for{
  select {
  case data := <-readerchannel:
   Log(string(data))  //打印通道内的信息
  }
 }
}

//日志处理
func Log(v ...interface{}) {
 log.Println(v...)
}

//错误处理
func CheckErr(err error) {
 if err != nil {
  fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
  os.Exit(1)
 }
}

客户端使用Enpack封装要发送到服务端的信息后,写入连接conn中。

/**
* MySocketProtocalClient
* @Author: Jian Junbo
* @Email: junbojian@qq.com
* @Create: 2017/9/14 15:23
* Copyright (c) 2017 Jian Junbo All rights reserved.
*
* Description:
*/
package main
import (
 "net"
 "time"
 "strconv"
 "protocol"
 "fmt"
 "os"
)

//发送100次请求
func send(conn net.Conn) {
 for i := 0; i < 100; i++ {
  session := GetSession()
  words := "{\"ID\":\""+strconv.Itoa(i)+"\",\"Session\":\""+session+"20170914165908\",\"Meta\":\"golang\",\"Content\":\"message\"}"
  conn.Write(protocol.Enpack([]byte(words)))
  fmt.Println(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:7373"
 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)
}

补充:golang从0到1利用socket编程实现一个简单的http服务器

开始编程

第一份代码

package main
import (
	"fmt"
	"net"
)
func accept_request_thread(conn net.Conn) {
	defer conn.Close()
	for {
		// 创建一个新切片, 用作保存数据的缓冲区
		buf := make([]byte, 1024)
		n, err := conn.Read(buf) // 从conn中读取客户端发送的数据内容
		if err != nil {
			fmt.Printf("客户端退出 err=%v\n", err)
			return
		}
		fmt.Printf(" 接受消息 %s\n", string(buf[:n]))
	}
}
func main() {
	listen, err := net.Listen("tcp", ":8888") // 创建用于监听的 socket
	if err != nil {
		fmt.Println("listen err=", err)
		return
	}
	fmt.Println("监听套接字,创建成功, 服务器开始监听。。。")
	defer listen.Close() // 服务器结束前关闭 listener
	// 循环等待客户端来链接
	for {
		fmt.Println("阻塞等待客户端来链接...")
		conn, err := listen.Accept() // 创建用户数据通信的socket
		if err != nil {
			fmt.Println("Accept() err=", err)
		} else {
			fmt.Println("通信套接字,创建成功。。。")
		}
		// 这里准备起一个协程,为客户端服务
		go accept_request_thread(conn)
	}
}

浏览器发送一个get请求:

http://192.168.0.20:8888/api/camera/get_ptz?camera_id=1324566666789876543

服务端接受到的消息如下:

http://192.168.0.20:8888/api/camera/get_ptz?camera_id=1324566666789876543

我们接下来的任务就是 解析这些字符串,从中获取 当前是什么方法,什么请求,参数是什么?

先定义一个小目标,获取当前是什么方法。

处理一个简单的get请求

package main
import (
	"encoding/json"
	"fmt"
	"log"
	"net"
	"strings"
)
func unimplemented(conn net.Conn){
	var buf string
	buf = "HTTP/1.0 501 Method Not Implemented\r\n"
	_, _ = conn.Write([]byte(buf))
	buf = "Server: httpd/0.1.0\r\n"
	_, _ = conn.Write([]byte(buf))
	buf = "Content-Type: text/html\r\n"
	_, _ = conn.Write([]byte(buf))
	buf = "\r\n"
	_, _ = conn.Write([]byte(buf))
	buf = "<HTML><HEAD><TITLE>Method Not Implemented\r\n"
	_, _ = conn.Write([]byte(buf))
	buf = "</TITLE></HEAD>\r\n"
	_, _ = conn.Write([]byte(buf))
	buf = "<BODY><P>HTTP request method not supported.\r\n"
	_, _ = conn.Write([]byte(buf))
	buf = "</BODY></HTML>\r\n"
	_, _ = conn.Write([]byte(buf))
}
func accept_request_thread(conn net.Conn) {
	defer conn.Close()
	var i int
	buf := make([]byte, 1024)
	n, err := conn.Read(buf) // 从conn中读取客户端发送的数据内容
	if err != nil {
		fmt.Printf("客户端退出 err=%v\n", err)
		return
	}
	// 获取方法
	i = 0
	var method_bt strings.Builder
	for(i < n && buf[i] != ' '){
		method_bt.WriteByte(buf[i])
		i++;
	}
	method := method_bt.String()
	if(method != "GET"){
		unimplemented(conn)
		return
	}
	for(i < n && buf[i] == ' '){
		i++
	}
	//api/camera/get_ptz?camera_id=1324566666789876543
	var url_bt strings.Builder
	for(i < n && buf[i] != ' '){
		url_bt.WriteByte(buf[i])
		i++;
	}
	url := url_bt.String()
	if(method == "GET"){
		//url ---> /api/camera/get_ptz?camera_id=1324566666789876543
		// 跳到第一个?
		var path, query_string string
		j := strings.IndexAny(url, "?")
		if(j != -1){
			path = url[:j]
			if(j + 1 < len(url)){
				query_string = url[j+1:]
			}
		}else{
			path = url
		}
		fmt.Print(path + "请求已经创建\t")
		resp := execute(path, query_string)// =1324566666789876543
		fmt.Println("返回", string(resp))
		header(conn, "application/json", len(resp));
		_ , err := conn.Write(resp)
		if(err != nil){
			fmt.Println(err)
		}
	}
}
//回应客户端必须先设置好head头,浏览器才能解析
func header(conn net.Conn, content_type string , length int ) {
	var buf string
	buf = "HTTP/1.0 200 OK\r\n"
	_, _ = conn.Write([]byte(buf))
	buf = "Server: httpd/0.1.0\r\n"
	_, _ = conn.Write([]byte(buf))
	buf = "Content-Type: " + content_type + "\r\n"
	_, _ = conn.Write([]byte(buf))
	_, _ = fmt.Sscanf(buf, "Content-Length: %d\r\n", length)
	buf = "Content-Type: " + content_type + "\r\n"
	_, _ = conn.Write([]byte(buf))
	buf = "\r\n"
	_, _ = conn.Write([]byte(buf))
}
func execute(path string, query_string string) ([]byte) {
	query_params := make(map[string]string)
	parse_query_params(query_string, query_params)
	if("/api/camera/get_ptz" == path){
		/*
		 * do something
		 */
		camera_id := query_params["camera_id"]
		resp := make(map[string]interface{})
		resp["camera_id"] = camera_id
		resp["code"] = 200
		resp["msg"] = "ok"
		rs, err := json.Marshal(resp)
		if err != nil{
			log.Fatalln(err)
		}
		return rs
	}else if("get_abc" == path){
		/*
		 * do something
		 */
		return []byte("abcdcvfdswa")
	}
	return []byte("do't match")
}
/*map作为函数入参是作为指针进行传递的
函数里面对map进行修改时,会同时修改源map的值,但是将map修改为nil时,则达不到预期效果。*/
// camera_id=1324566666789876543&tt=%E5%88%9B%E5%BB%BA%E6%88%90%E5%8A%9F
func parse_query_params(query_string string, query_params map[string]string) {
	kvs := strings.Split(query_string, "&")
	if(len(kvs) == 0){
		return
	}
	for _, kv := range kvs {
		kv := strings.Split(kv, "=")
		if(len(kv) != 2){
			continue
		}
		query_params[kv[0]] = kv[1]
	}
}
func main() {
	listen, err := net.Listen("tcp", ":8888") // 创建用于监听的 socket
	if err != nil {
		fmt.Println("listen err=", err)
		return
	}
	fmt.Println("监听套接字,创建成功, 服务器开始监听。。。")
	defer listen.Close() // 服务器结束前关闭 listener
	// 循环等待客户端链接
	for {
		fmt.Println("阻塞等待客户端链接...")
		conn, err := listen.Accept() // 创建用户数据通信的socket
		if err != nil {
			panic("Accept() err= " + err.Error())
		}
		// 这里准备起一个协程,为客户端服务
		go accept_request_thread(conn)
	}
}

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。如有错误或未考虑完全的地方,望不吝赐教。

(0)

相关推荐

  • 深入理解Golang之http server的实现

    前言 对于Golang来说,实现一个简单的 http server 非常容易,只需要短短几行代码.同时有了协程的加持,Go实现的 http server 能够取得非常优秀的性能.这篇文章将会对go标准库 net/http 实现http服务的原理进行较为深入的探究,以此来学习了解网络编程的常见范式以及设计思路. HTTP服务 基于HTTP构建的网络应用包括两个端,即客户端( Client )和服务端( Server ).两个端的交互行为包括从客户端发出 request .服务端接受 request

  • golang 实现tcp server端和client端,并计算RTT时间操作

    server端代码: package main import ( "fmt" "net" "log" "os" ) func main() { addr := "0.0.0.0:8787" tcpAddr, err := net.ResolveTCPAddr("tcp",addr) if err != nil { log.Fatalf("net.ResovleTCPAddr f

  • golang实现微信小程序商城后台系统(moshopserver)

    golang和c/c++比起来是一门新的语言,一直想学,网上搜集了一些资料,有些人说很容易上手,确实是这样,和C/C++比起来,少了很多乱七八糟的语法.学一门新的语言,最好的方法就是动手写一些东西,最近小程序也比较火,也想学一下,网络上搜索的一些开源项目,基本上没有golang实现的,大部分都是nodejs和java写的,那么我就来实现一个golang版的吧,一石二鸟. 开发小程序前后端都需要开发,自己的前端经验很少,搜索了一些开源代码,有一个小程序项目让人眼前一亮,Nideshop,界面做的不

  • golang实现http server提供文件下载功能

    简介 Go(又称Golang)是Google开发的一种静态强类型.编译型.并发型,并具有垃圾回收功能的编程语言. 罗伯特·格瑞史莫(Robert Griesemer),罗勃·派克(Rob Pike)及肯·汤普逊(Ken Thompson)于2007年9月开始设计Go,稍后Ian Lance Taylor.Russ Cox加入项目.Go是基于Inferno操作系统所开发的.Go于2009年11月正式宣布推出,成为开放源代码项目,并在Linux及Mac OS X平台上进行了实现,后来追加了Windo

  • golang的httpserver优雅重启方法详解

    前言 去年在做golangserver的时候,内部比较头疼的就是在线服务发布的时候,大量用户的请求在发布时候会被重连,在那时候也想了n多的方法,最后还是落在一个github上的项目,facebook的一个golang项目grace,那时候简单研究测试了一下可以就直接在内部使用了起来,这段时间突然想起来,又想仔细研究一下这个项目了. 从原理上来说是这样一个过程: 1)发布新的bin文件去覆盖老的bin文件 2)发送一个信号量,告诉正在运行的进程,进行重启 3)正在运行的进程收到信号后,会以子进程的

  • Golang Socket Server自定义协议的简单实现方案

    在Server和Client通讯中,由于网络等原因很可能会发生数据丢包的现象.如果数据缺失,服务端接收的信息不完整,就会造成混乱. 我们需要在Server和Client之间建立一个通讯协议,通过协议中的规则,判断当前接收到的信息是否完整.根据信息的完整情况,采取不同的处理方式. 通讯协议protocol的核心就是设计一个头部.如果传来的信息不包含这个头部,就说明当前信息和之前的信息是同一条.那么就把当前信息和之前的那条信息合并成一条. 而协议主要包含的功能是封装(Enpack)和解析(Depac

  • JAVA实现基于Tcp协议的简单Socket通信实例

    好久没写博客了,前段时间忙于做项目,耽误了些时间,今天开始继续写起~ 今天来讲下关于Socket通信的简单应用,关于什么是Socket以及一些网络编程的基础,这里就不提了,只记录最简单易懂实用的东西.  1.首先先来看下基于TCP协议Socket服务端和客户端的通信模型: Socket通信步骤:(简单分为4步) 1.建立服务端ServerSocket和客户端Socket 2.打开连接到Socket的输出输入流 3.按照协议进行读写操作 4.关闭相对应的资源 2.相关联的API: 1.首先先来看下

  • python实现简单的socket server实例

    本文实例讲述了python实现简单的socket server的方法.分享给大家供大家参考.具体如下: import socket host = '' port = 55555 myServerSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) myServerSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1) myServerSocket.bind((host,

  • Golang通过包长协议处理TCP粘包的问题解决

    tcp粘包产生的原因这里就不说了,因为大家能搜索TCP粘包的处理方法,想必大概对TCP粘包有了一定了解,所以我们直接从处理思路开始讲起 tcp粘包现象代码重现 首先,我们来重现一下TCP粘包,然后再此基础之上解决粘包的问题,这里给出了client和server的示例代码如下 /* 文件名:client.go client客户端的示例代码(未处理粘包问题) 通过无限循环无时间间隔发送数据给server服务器 server将会不间断的出现TCP粘包问题 */ package main import

  • Golang中基于HTTP协议的网络服务

    目录 一.HTTP协议的网络服务 1.1 使用http.Get函数访问HTTP协议的网络服务 1.2 使用缺省客户端DefaultClient(类型为*http.Client ) 1.3 使用http.Client访问HTTP协议的网络服务 二.http.Client中的Transport字段 (1)http.Transport类型中的DialContext字段 (2)http.Transport类型中的其它字段 三.为什么会出现空闲的连接 3.1 空闲连接的产生 3.2 杜绝空闲连接的产生 四

  • Java自定义协议报文封装 添加Crc32校验的实例

    刚做完的demo,直接进入主题了,开启两个线程,模拟Socket服务端和客户端通信,将数据封装为指定格式报文发送 代码: import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.Unkno

  • Netty实现自定义协议编解码器

    目录 为什么要自定义协议 自定义协议设计 请求头 请求体 自定义协议实现 1:创建一个Maven项目,引入Netty依赖,完整的依赖如下 2:实现我们的协议请求头 3:实现我们的协议请求体 4:实现我们的协议请求类 5:实现自定义编码器 6:实现自定义解码器 7:Netty Server端 8:Netty Server端处理器 9:Netty Client端处理器 10:Netty Client端 11:测试 完整代码 为什么要自定义协议 Netty自带了一些编解码器没,比如 StringDec

  • python3实现TCP协议的简单服务器和客户端案例(分享)

    利用python3来实现TCP协议,和UDP类似.UDP应用于及时通信,而TCP协议用来传送文件.命令等操作,因为这些数据不允许丢失,否则会造成文件错误或命令混乱.下面代码就是模拟客户端通过命令行操作服务器.客户端输入命令,服务器执行并且返回结果. TCP(Transmission Control Protocol 传输控制协议):是一种面向连接的.可靠的.基于字节流的传输层通信协议,由IETF的RFC 793定义. TCP客户端 from socket import * host = '192

  • nodejs socket服务端和客户端简单通信功能

    本文实例讲述了通过node.js的net模块实现nodejs socket服务端和客户端简单通信功能,可以用作客户端对服务端的端口监听以及事件回执. server端代码 var net = require('net'); //模块引入 var listenPort = 8080;//监听端口 var server = net.createServer(function(socket){ // 创建socket服务端 console.log('connect: ' + socket.remoteA

  • golang socket断点续传大文件的实现方法

    在日常编程中,我们肯定会遇到用socket传送文件内容,如果是大文件的,总不能传送到一半因某原因断掉了,又从新传送文件内容吧.对,我们需要续传,也就是接着上次传送的位置继续发送文件内容. 续传的话,其实并不难,我理解的思路大概如下: 客户端发送消息询问服务端,你上次接收到的文件内容位置 服务端告诉客户端上次接收到的文件内容位置 客户端就从上次断点的位置继续发送文件内容 客户端发送文件内容完毕后通知服务端,然后断开连接 下面我们看看代码的实现 服务端 // file name: server.go

随机推荐