golang手把手实现tcp内网穿透代理(2)

栏目: Go · 发布时间: 5年前

内容简介:上一篇文章明确了我们需要做的东西,接下来就是实现具体功能首先是协议的定义,服务端和客户端需要一套协议来交互协议的内容包括了,livecheck,以及发起tcp连接建立的请求,和响应的对象

上一篇文章明确了我们需要做的东西,接下来就是实现具体功能

定义数据模型

首先是协议的定义,服务端和客户端需要一套协议来交互

协议的内容包括了,livecheck,以及发起tcp连接建立的请求,和响应的对象

假设内网里面需要暴露到外网的端口是5050,那么客户端需要请求服务端在外网建立一个端口,同时服务端需要知道内网被代理的端口是多少。

是因为当外网端口接收到tcp请求的时候,服务端也是需要下发请求到客户端要求客户端建立到被代理端口的连接的。

那么使用什么文本格式呢?首先考虑使用大家都知道的比较简单的,那就是json格式了。

既然使用的文本格式是json,那么需要了解golang下怎么使用json的序列化和反序列化一个对象。

下面开始结构体的定义:

新建一个model.go文件,用来放客户端和服务端交互的数据模型

建立一个base的结构体,里面存放一个变量是type,类型是string,用来表明这个结构体是什么类型

还有一个id,类型是string,表示消息的id

消息id是用来响应请求的,可以明确的知道是响应的哪一条请求消息。

LiveCheck直接继承Base,无须特殊的变量

Response继承Base,注意Response的id是和请求的id是一致的,这样客户端可以知道是对应哪条请求的影响消息。

response包含了error信息,result信息,statusCode信息

type LiveCheck struct {
    Base
}
type Response struct {
    Base
    Error string `json:"error"`
    Result string `json:"result"`
    StatusCode int `json:"statusCode"`
}
type Base struct {
    Type   string `json:"type"`
  Id string `json:"id"`
}

增加两个工厂方法,用来构造liveCheck和response对象,暂定使用时间戳来表示消息的id

response的id需要传递,无法默认构造,因为这个是和请求相匹配的

func NewLiveCheck() *LiveCheck {
    liveCheck := LiveCheck{}
    liveCheck.Type = "lib.LiveCheck"
    liveCheck.Id = string(time.Now().Unix())
    return &liveCheck
}

func NewResponse(error string, result string, statusCode int,id string) *Response {
    response := Response{Error: error, Result: result, StatusCode: statusCode}
    response.Type = "lib.Response"
    response.Id = id
    return &response
}

增加一个TcpRequest对象,其中LocalPort表示client端需要被代理出去的端口,ServerPort表示服务器上需要开放什么端口来代理client的端口。增加对应的工厂方法

type TcpRequest struct {
    ClientPort  int32
    ServerPort int32
    Base
}
func NewTcpRequest(clientPort int32, serverPort int32) *TcpRequest {
    tcpRequest := TcpRequest{ClientPort: clientPort, ServerPort: serverPort}
    tcpRequest.Type = "lib.TcpRequest"
    tcpRequest.Id = string(time.Now().Unix())
    return &tcpRequest
}

定义好数据模型之后,后面我们可以再回过头来补充需要的信息,暂停就是这些。

定义数据的发送和接收

接下来定义数据怎么传递和发送

tcp协议可以保证我们的数据是按照顺序发送的,那么我们只需要处理粘包的情况。

报文构造使用两端数据,第一段是报文的长度,占用4个字节,第二段是报文的内容

报文长度 报文内容
使用int32,占用4个字节 json数据,占用报文长度的字节数

这样的话,我们先读取前4个字节,获取到报文长度,然后按照顺序,把剩余长度的字节读取到报文长度为止,然后再反序列化成结构体对象即可。

新增io.go文件,存放io相关的代码,

增加ReadInt32方法,从io的reader流中读取int32

func ReadInt32(reader io.Reader) (int32, error) {
    IntBytes := make([]byte, 4)  //构造4个字节的数组
    n, err := reader.Read(IntBytes) //读取到数组中
    if err != nil {
        return 0, err
    }
    if n != 4 {  
        return 0, errors.New(fmt.Sprintf("cannot read enough data to convert int32 , data bytes is %d ", n))
    }
    buf := bytes.NewBuffer(IntBytes)
    var IntValue int32
    err = binary.Read(buf, binary.LittleEndian, &IntValue) //使用小端,从byte转换int
    if err != nil {
        return 0, err
    }
    return IntValue, nil
}

增加WriteInt32方法,写入int32到流中

func WriteInt32(intValue int32, writer io.Writer) error {
    cache := make([]byte, 0)
    buf := bytes.NewBuffer(cache)
    _ = binary.Write(buf, binary.LittleEndian, intValue)
    _, err := writer.Write(buf.Bytes())
    if err != nil {
        return err
    }
    return nil
}

增加写入结构体的方法,在写入结构体之前,需要先json序列化,然后获取json序列化之后的内容的字节数

把这个字节数先写入到输出流中,然后再写入结构体内容。

func (base Base) WriteBaseModel(writer io.Writer) error {
    data, err := json.Marshal(&base)
    if err != nil {
        return err
    }
    dataSize := len(data)
    err = WriteInt32(int32(dataSize), writer)
    if err != nil {
        return err
    }
    _, err = writer.Write(data)
    if err != nil {
        return err
    }
    return nil
}

同样的读取也是这个逻辑,但是读取的话,需要先反序列化json到Base结构体,然后再读取type类型

然后再反序列化到正确的结构体对象

func ReadBaseModel(reader io.Reader) (base *Base, err error) {
    intValue, err := ReadInt32(reader)
    bytesData := make([]byte, intValue)
    n, err := reader.Read(bytesData)
    if err != nil {
        return (*Base)(nil), err
    }
    if int32(n) != intValue {
        return (*Base)(nil), errors.New(fmt.Sprintf("cannot read enough data to convert json , data bytes is %d ,not %d ", n, intValue))
    }
    var _base Base
    err = json.Unmarshal(bytesData, &_base)
    if err != nil {
        fmt.Println("Unmarshal failed, ", err)
        return (*Base)(nil), err
    }
    var common interface{}
    common = ModelMap[_base.Type]()
    err = json.Unmarshal(bytesData, &common)
    return common.(*Base), err
}

增加结构体type映射反射字典,字典可以根据结构体的名字来动态创建结构体对象

字典的key是一个字符串对象,value是func() interface{} 对象,也就是value是一个方法,通过调用方法

可以直接返回结构体变量!

func registerType(elem interface{}, modelMap map[string]func() interface{}) {
    typeName := reflect.TypeOf(elem).Elem()
    modelMap[typeName.Name()] = newStruct(typeName)
}

func newStruct(elem reflect.Type) func() interface{} {
    return func() interface{} {
        return reflect.New(elem).Elem().Interface()
    }
}

var ModelMap = func() map[string]func() interface{} {
    modelMap := make(map[string]func() interface{})
    registerType((*LiveCheck)(nil), modelMap)
    registerType((*Response)(nil), modelMap)
    registerType((*TcpRequest)(nil), modelMap)
    return modelMap
}()

未完待续。。。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

UX设计之道

UX设计之道

[美] 昴格尔、[美] 钱德勒 / 孙亮 / 人民邮电出版社 / 2010-4 / 35.00元

《UX设计之道:以用户体验为中心的Web设计》可以看作是用户体验设计的核心参考书。无论是Web设计领域的创业者、项目管理者还是用户体验的策划者和实施者,或是有志于Web设计领域的学生,都应该了解《UX设计之道:以用户体验为中心的Web设计》中的知识。 用户是网站的根本,网站要达到自己的商业目标,必须满足目标用户的需求——这就是以用户体验为中心的网站设计。那么,用户需求从何而来?如何将用户需......一起来看看 《UX设计之道》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

URL 编码/解码
URL 编码/解码

URL 编码/解码