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

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


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。

故初始化流程如下


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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容