Golang通过TCP与远端服务器交互

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

内容简介:C/S构架中,客户端与服务端一般通过TCP通信。建立连接后即验证身份验证,若账户密码正确,TCP连接保持,然后client和server全双工通信。在B/S构架下,若希望用户通过浏览器也能实现客户端相同的功能,我们可以开发一个中间层为webserver,用户浏览器与webserver交互,webserver再通过tcp连接与真正的server交互。首先需要明确client与server通信格式。包括如何登陆,如何实现对资源的CURD。通过Wireshark可以抓取明文消息,对于加密消息,需要查阅源代码了解加

C/S构架中,客户端与服务端一般通过TCP通信。建立连接后即验证身份验证,若账户密码正确,TCP连接保持,然后client和server全双工通信。

在B/S构架下,若希望用户通过浏览器也能实现客户端相同的功能,我们可以开发一个中间层为webserver,用户浏览器与webserver交互,webserver再通过tcp连接与真正的server交互。

首先需要明确client与server通信格式。包括如何登陆,如何实现对资源的CURD。通过Wireshark可以抓取明文消息,对于加密消息,需要查阅源代码了解加密方式。

消息体格式

业务消息采用明文,敏感登录信息采用非对称加密RSA结合对称加密DES。

TCP连接登陆需要四个字段,分别为

用户名

密码

desKey

desIV

针对以上字段内容,使用EncryptPKCS1v15(C#系统默认加密方式)加密,再使用base64编码,构建xml包。在包头使用binary.Write写入msgLength和msgType,完成封装发送给远端服务器。对于返回值,使用刚发送的desKey结合desIV进行DES解密,获得登陆结果。

功能流程

浏览器->web service->TCPServer

Golang通过TCP与远端服务器交互

image.png

在以上通信流程中,浏览器端使用人数较多,频繁建立连接。web service和TCPServer保持一个长连接,多用户共享此TCP长连接。

对于webservice与TCP server通信,利用Routine结合Channel方式协同工作。实现TCP全双工的关键Routine如下:

SendEventProcessor

监听waiting process queue,若有则构建pakage,然后通过tcp发送到服务端,然后将此Event转移的pending response队列,考虑到允许删除无响应event,pending response队列采用slice结构。

// work as runtine
//
func SendEventProcessor() { 

    var task *Event
    for {

        Event= <-EventWaiting

        sendMessage(Event.action, Event.data)
        if Event.action != "HeartBeat" {
            log.Printf("Event message was sent...%s", Event.action)
            EventPendingMutex.Lock()
            EventPending = append(EventPending,Event)
            EventPendingMutex.Unlock()
        }
    }

}

FrameDetector:由于TCP read buffer可能存在粘包、拆包、废弃包。此routine用于实时过滤tcp read buffer,提取出完整有效的数据包。

func FrameDetector() {
    buf := new(bytes.Buffer)      //滑动窗
    buf4bytes := make([]byte, 4)  //tmp var
    cRdr := bufio.NewReader(conn) //reader from connection
    frame := Frame{}
    for {
        b, err := cRdr.ReadByte()
        if err == io.EOF {
            log.Println("readFrame:connection closed,connecting...")
            RemoteConnect(targetServer)
        }
        buf.WriteByte(b)
        if buf.Len() == 8 {
            buf.Read(buf4bytes)
            frame.Length = int(binary.LittleEndian.Uint32(buf4bytes)) //little endian 低字节先发
            buf.Read(buf4bytes)
            frame.Type = int(binary.LittleEndian.Uint32(buf4bytes)) //little endian 低字节先发
            switch frame.Type {
            case 3://msgType, login response
                                ... /create frame from bytes
                FrameChan <- &frame
                buf.Reset()//clear bytes buffer after saving the frame
                break

            case 4, 5, 411: // response

                frame.Message = make([]byte, frame.Length-4)
                n, _ := cRdr.Read(frame.Message)
                if n < frame.Length-4 { //continue waiting and reading
                    for n != frame.Length-4 {
                        b, _ := cRdr.ReadByte()
                        frame.Message = append(frame.Message, b)
                        n++
                    }
                }
                FrameChan <- &frame
                buf.Reset()
                break

            default:
                buf.ReadByte()
            }
        }
    }

}

RecieveMessageProcessor:

读服务端返回的有效Frame,解析其中数据为,从pending response队列寻找目标Event,把返回结果写进去,同时标记done,从pending中移除。

// should be work as runtine
func RecieveMessageProcessor() { 
    var frame *Frame

    for {
        frame = <-FrameChan 
        log.Printf("receiveMessage:length:%d,type:%d", frame.Length, frame.Type)
        switch frame.Type {
        case 3: // 提醒事件
                         //parse xml string and save as a map
            m := parseXML(frame.Message)
            for i, event := range EventPending { //查找到工作中的任务,标记为完成
                if event.action == "Login" {
                    event.response = m
                    event.done <- true //向监听runtine发送完成信号
                    EventPendingMutex.Lock()
                    EventPending = append(EventPending[0:i], EventPending[i+1:]...) // 将任务从工作表中清除
                    EventPendingMutex.Unlock()
                    break
                }
            }
            break

    }

}

完成tcp连接后,发起登录事件,阻塞等待登陆反馈。同时为避免TCP连接掉线问题,新开routine,每隔数秒发送HeartBeat。

故初始化流程如下

Golang通过TCP与远端服务器交互

image.png

调用接口

为便于HTTP调用,编写CRUD接口函数,函数中构建Event事件、构建NewTimer定时事件,使用select channel方式判断超时。若超时,则从queue中删除此event,同时返回超时信息给http handler。

func Delete(id,name, formula string) map[string]string {

    data := map[string]string{"ID": id, "name": name, "content": formula}
    event := Event{action: "Delete", data: data, done: make(chan bool, 1)}
    EventWaiting <- &event
    timer := time.NewTimer(timeout)
    defer timer.Stop()
    select {
    case <-timer.C:
        dropTask(&event)
        event.response = map[string]string{"status": "error", "message": "time out"}
    case <-task.done:
                //do something
                break;
    }
    return task.response
}

注意点:

time.After()在触发前,即便父函数返回也不会被garbage collector回收。仅触发后或stop状态的timer,会被gc回收。

使用正则从xml字符串提取有用信息,需要先剔除字符串中特殊字符(ascii<32)

关于TCP/ip报文,以及各控制字功能,握手流程、挥手流程,CLOSE_WAIT和TIME_WAIT参考此文 https://www.cnblogs.com/myd620/p/6252135.html

关于RSA,有多种加密模式,C#默认的是EncryptPKCS1v15

关于DES padding模式,C#默认的是PKCS7Padding


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

查看所有标签

猜你喜欢:

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

Uberland

Uberland

Alex Rosenblat / University of California Press / 2018-11-19 / GBP 21.00

Silicon Valley technology is transforming the way we work, and Uber is leading the charge. An American startup that promised to deliver entrepreneurship for the masses through its technology, Uber ins......一起来看看 《Uberland》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具