Simple implementation scheme of self defined protocol of Golang Socket Server

Posted by sps on Sat, 25 Dec 2021 19:57:22 +0100

In the communication between Server and Client, data packet loss is likely to occur due to network and other reasons. If the data is missing and the information received by the Server is incomplete, it will cause confusion.

We need to establish a communication protocol between Server and Client to judge whether the currently received information is complete through the rules in the protocol. Different processing methods shall be adopted according to the integrity of the information.

The core of communication protocol is to design a header. If the incoming message does not contain this header, it means that the current message is the same as the previous message. Then merge the current information and the previous information into one.

The main functions of the protocol are encapsulation and decomposition. Enpack is the data encapsulation of information by the client. After encapsulation, it can be passed to the server. Depack is the server's data analysis of information.

There is a Const section, which is used to define the header, the header length and the length of the client incoming information.
In the code, we define it as follows:

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

The content of the header is "Headers" and the length is 7. So ConstHeaderLenth=7

In the process of information transmission, we will convert int type to byte type. The length of an int is equal to the length of 4 bytes. Therefore, we set ConstMLength=4 Represents the size of the information sent by the client.

The code example of custom protocol is as follows:

/**
* protocol
* @Author: Jian Junbo
* @Email: junbojian@qq.com
* @Create: 2017/9/14 11:49
*
* Description: Communication protocol processing
*/
package protocol 
import (
 "bytes"
 "encoding/binary"
)
 
const (
 ConstHeader = "Headers"
 ConstHeaderLength = 7
 ConstMLength = 4
)
 
//Packet
func Enpack(message []byte) []byte {
 return append(append([]byte(ConstHeader), IntToBytes(len(message))...), message...)
}
 
//Unpack
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
}
 
//Byte to integer
func ByteToInt(n []byte) int {
 bytesbuffer := bytes.NewBuffer(n)
 var x int32
 binary.Read(bytesbuffer, binary.BigEndian, &x) 
 return int(x)
}
 
//Convert integers to bytes
func IntToBytes(n int) []byte {
 x := int32(n)
 bytesBuffer := bytes.NewBuffer([]byte{})
 binary.Write(bytesBuffer, binary.BigEndian, x)
 return bytesBuffer.Bytes()
}

The Server side mainly parses the information sent by the client through the protocol. Establish a function to complete the connection processing of the received information. The channel readerChannel is established and the received information is placed in the channel.

Before putting into the channel, the information is parsed using protocol and Depack.

//Connection processing
func handleConnection(conn net.Conn) {
 //A buffer that stores truncated data
 tmpBuffer := make([]byte, 0)
 //Receiving and unpacking
 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  //The received information is written to the channel
 
 }
 defer conn.Close()
}

If there is an error in reading the information (including reading the information end character EOF), the error message will be printed and the loop will jump out.

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

Because the data in the channel is [] byte. Need to convert to string. This work is completed by a special reader (readerchannel channel [] byte) for obtaining channel data.

//Get channel data
func reader(readerchannel chan []byte) {
 for{
  select {
  case data := <-readerchannel:
   Log(string(data))  //Print information in the channel
  }
 }
}

View the Server side code example:

/**
* MySocketProtocalServer
* @Author: Jian Junbo
* @Email: junbojian@qq.com
* @Create: 2017/9/14 13:54
* Copyright (c) 2017 Jian Junbo All rights reserved.
*
* Description: The server receives the information from the client
*/
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 ...")  //After startup, wait for client access.
 for{
  conn, err := netListen.Accept()  //Listening client
  if err != nil {
   Log(conn.RemoteAddr().String(), "An error was sent:", err)
   continue
  }
  Log(conn.RemoteAddr().String(), "tcp connection success")
  go handleConnection(conn)
 }
}
 
//Connection processing
func handleConnection(conn net.Conn) {
 //A buffer that stores truncated data
 tmpBuffer := make([]byte, 0)
 //Receiving and unpacking
 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  //The received information is written to the channel 
 }
 defer conn.Close()
}
 
//Get channel data
func reader(readerchannel chan []byte) {
 for{
  select {
  case data := <-readerchannel:
   Log(string(data))  //Print information in the channel
  }
 }
}
 
//Log processing
func Log(v ...interface{}) {
 log.Println(v...)
}
 
//error handling
func CheckErr(err error) {
 if err != nil {
  fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
  os.Exit(1)
 }
}

The client uses Enpack to encapsulate the information to be sent to the server and writes it to the connection 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"
)
 
//Send 100 requests
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)  //Print sent information
 }
 fmt.Println("send over")
 defer conn.Close()
}
//Use the current time for identification. Decimal integer of the current time
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) 
}

Supplement: golang uses socket programming to implement a simple http server from 0 to 1

Start programming

First code

package main
import (
	"fmt"
	"net"
)
func accept_request_thread(conn net.Conn) {
	defer conn.Close()
	for {
		// Create a new slice to use as a buffer for saving data
		buf := make([]byte, 1024)
		n, err := conn.Read(buf) // Read the data content sent by the client from conn
		if err != nil {
			fmt.Printf("Client exit err=%v\n", err)
			return
		}
		fmt.Printf(" Accept message %s\n", string(buf[:n]))
	}
}
func main() {
	listen, err := net.Listen("tcp", ":8888") // Create socket for listening
	if err != nil {
		fmt.Println("listen err=", err)
		return
	}
	fmt.Println("Listening socket, created successfully, The server starts listening...")
	defer listen.Close() // Close the listener before the server ends
	// Loop waiting for client to link
	for {
		fmt.Println("Blocking waiting for client to link...")
		conn, err := listen.Accept() // Create socket for user data communication
		if err != nil {
			fmt.Println("Accept() err=", err)
		} else {
			fmt.Println("Communication socket, created successfully...")
		}
		// Here, a cooperation process is prepared to serve the client
		go accept_request_thread(conn)
	}
}

The browser sends a get request:

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

The message received by the server is as follows:

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

Our next task is to parse these strings, and get the current method, request and parameters?

First define a small target and get the current method.

Handle a simple get request

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) // Read the data content sent by the client from conn
	if err != nil {
		fmt.Printf("Client exit err=%v\n", err)
		return
	}
	// Acquisition method
	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
		// Jump to the first?
		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 + "Request created\t")
		resp := execute(path, query_string)// =1324566666789876543
		fmt.Println("return", string(resp))
		header(conn, "application/json", len(resp));
		_ , err := conn.Write(resp)
		if(err != nil){
			fmt.Println(err)
		}
	}
}
//The response client must set the head header before the browser can parse it
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 Arguments as functions are passed as pointers
 When the map is modified in the function, the value of the source map will be modified at the same time, but the expected effect will not be achieved when the map is modified to 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") // Create socket for listening
	if err != nil {
		fmt.Println("listen err=", err)
		return
	}
	fmt.Println("Listening socket, created successfully, The server starts listening...")
	defer listen.Close() // Close the listener before the server ends
	// Loop waiting for client link
	for {
		fmt.Println("Blocking waiting client links...")
		conn, err := listen.Accept() // Create socket for user data communication
		if err != nil {
			panic("Accept() err= " + err.Error())
		}
		// Here, a cooperation process is prepared to serve the client
		go accept_request_thread(conn)
	}
}

Topics: Go server