编写Go的TCP服务来替代PHP的Swoole

背景说明

目前公司采用微服务架构,主要开发语言为PHP,通过Swoole开启TCP服务供业务端调用。通过公司内部编写的PHP扩展封装客户端调用逻辑。

需求

暂定使用Go语言开发新的业务,并提供TCP服务。其中老的PHP项目要通过原有的客户端扩展实现无修改调用。

解决方案

通过阅读客户端扩展源码了解调用逻辑。编写简单的测试如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php

$_client = new \swoole_client(SWOOLE_SOCK_TCP | SWOOLE_KEEP);
$_client->set([
'open_length_check' => true,
'package_length_type' => 'N',
'package_length_offset' => 0,
'package_body_offset' => 4,
'package_max_length' => 24657920,
]);
if (false == $_client->connect("127.0.0.1", 8880)) {
printf("err_msg: %s err_code: %s" . PHP_EOL, var_export($_client->errMsg, true), var_export($_client->errCode, true));
}

// 随便测试个请求参数
$data = [
'api' => 'getUserInfo',
'params' => [
'user_id' => 123
]
];
$data = json_encode($data);
$data = gzcompress($data, 9);
$_client->send(pack("N", strlen($data)) . $data);

$res = $_client->recv();
$end = getTime();

$data = json_decode($res, true);

其中前4个字节是head,表示body长度,采用二进制大端字节序编码。body先进行json编码再进行了zlib压缩。这都是编写Go的TCP服务时需要处理的。

写个简单的Go TCP服务试试,先不考虑过多的错误边界处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package main

import (
"bytes"
"compress/zlib"
"encoding/binary"
"fmt"
"io"
"net"
)

func main() {
ln, err := net.Listen("tcp", ":8880")
if err != nil {
fmt.Printf("%s", err)
}
for {
conn, err := ln.Accept()
if err != nil {
fmt.Printf("accept err:%s", err)
}
go handleConnection(conn)
}
}

func handleConnection(conn net.Conn) {
fmt.Println("on conn")

var err error
headLen := 4
head := make([]byte, headLen)
if _, err = conn.Read(head); err != nil {
fmt.Println(err.Error())
return
}

// 解码大端字节序获取body长度
bodyLen := binary.BigEndian.Uint32(head)

allBody := make([]byte, 0)
readLen := 0
for bodyLen > 0 {
body := make([]byte, bodyLen)
readLen, err = conn.Read(body)
if err != nil {
fmt.Println(err.Error())
return
}

bodyLen = bodyLen - uint32(readLen)
allBody = append(allBody, body[:readLen]...)
}

// 解压zlib压缩的数据 RFC 1950
b := bytes.NewReader(allBody)
uncompressRead, err := zlib.NewReader(b)
if err != nil {
fmt.Printf("uncompress data err:%s", err)
}
var uncompressData bytes.Buffer
io.Copy(&uncompressData, uncompressRead)

// 解出的json字符串
fmt.Printf("Received:%s", uncompressData.Bytes())

// 路由调用实际业务逻辑处理 ...
// conn.Write()

conn.Close()
return
}

运行Go的TCP服务,跑一个PHP请求测试。

1
2
on conn
Received:{"api":"getUserInfo","params":{"user_id":123}}

经过多次修改测试终于成功。