├── LICENSE ├── README.md ├── build.sh ├── codec ├── codec.go └── codec_test.go ├── doc ├── README.md └── img │ ├── devices_1.png │ ├── login_1.png │ ├── map_1.png │ ├── monitor_1.png │ └── users_1.png ├── frontend ├── .gitignore ├── README.md ├── babel.config.js ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ ├── login_bg.jpg │ │ └── logo.png │ ├── components │ │ ├── Devices.vue │ │ ├── GpsTable.vue │ │ ├── Login.vue │ │ ├── MainPage.vue │ │ ├── MapWidget.vue │ │ ├── UserAdd.vue │ │ └── UserManager.vue │ ├── js │ │ ├── axiosConfig.js │ │ ├── router.js │ │ ├── utils.js │ │ └── vuex.js │ └── main.js ├── vue.config.js └── yarn.lock ├── go.mod ├── gps.go ├── proto ├── proto.go └── proto_test.go ├── script └── config.toml ├── server.go ├── term └── term.go └── utils └── utils.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 qiuzhiqian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tsp后台服务 2 | 3 | ## 预览 4 | 登录界面: 5 |  6 | 7 | 登录默认账号: 8 | 用户名:admin 9 | 密码:admin123456 10 | 11 | 主页面包括:设备列表,数据列表以及地图导航 12 | 13 | 设备列表: 14 |  15 | 16 | 数据列表: 17 |  18 | 19 | 地图导航: 20 |  21 | 22 | 用户管理: 23 |  24 | 25 | ## 编译 26 | ```c 27 | $cd frontend 28 | 29 | $yarn build 30 | 31 | $cd .. 32 | 33 | $go build 34 | ``` 35 | 36 | ## 代码导读文档 37 | [工程设计流程](https://github.com/qiuzhiqian/etc_tsp/blob/master/doc/README.md) 38 | 39 | ## 技术栈 40 | ### 后端 41 | - golang 42 | - gin 43 | - postgresql 44 | - xorm 45 | - jwt 46 | - logrus 47 | 48 | ### 前端 49 | - vue 50 | - iview/ant designer 51 | - vue-router 52 | - axios 53 | - vue-baidu-map 54 | - vuex 55 | 56 | ## 路由 57 | 后端路由同一分配到/上面,子页面路由有前端管理 58 | 59 | ``` 60 | / 61 | 默认页,被重定向到/login 62 | 63 | /login 64 | 登录页面路由 65 | 66 | /mainpage 67 | 主页面路由 68 | ``` 69 | 70 | ## API 71 | 查询在线设备列表 72 | ```c 73 | $ curl -H "Content-Type:application/json" -X POST --data '{"page":1}' http://localhost:8080/api/list 74 | 75 | {"pagecnt":1,"pagesize":10,"pageindex":1,"data":[{"ip":"127.0.0.1:52388","imei":"865501043954677","phone":"13246607267"},{"ip":"127.0.0.1:52392","imei":"865501043897165","phone":"13246607267"}]} 76 | ``` 77 | 78 | 查询设备上报gps数据 79 | ```c 80 | $ curl -H "Content-Type:application/json" -X POST --data '{"imei":"865501043954677","starttime":1575453728,"endtime":1575453826,"page":1}' http://localhost:8080/api/data 81 | 82 | {"pagecnt":2,"pagesize":10,"pageindex":1,"data":[{"imei":"865501043954677","stamp":1575453733,"warnflag":0,"state":3,"latitude":22585469,"longitude":17222187,"altitude":17409,"speed":23040,"direction":1792},{"imei":"865501043954677","stamp":1575453737,"warnflag":0,"state":3,"latitude":22585462,"longitude":113912645,"altitude":339,"speed":0,"direction":99},{"imei":"865501043954677","stamp":1575453743,"warnflag":0,"state":3,"latitude":22585456,"longitude":113912642,"altitude":326,"speed":0,"direction":99},{"imei":"865501043954677","stamp":1575453748,"warnflag":0,"state":3,"latitude":22585456,"longitude":113912643,"altitude":331,"speed":0,"direction":99},{"imei":"865501043954677","stamp":1575453752,"warnflag":0,"state":3,"latitude":22585456,"longitude":113912643,"altitude":331,"speed":0,"direction":99},{"imei":"865501043954677","stamp":1575453757,"warnflag":0,"state":3,"latitude":22585456,"longitude":113912643,"altitude":331,"speed":0,"direction":99},{"imei":"865501043954677","stamp":1575453762,"warnflag":0,"state":3,"latitude":22585456,"longitude":113912643,"altitude":331,"speed":0,"direction":99},{"imei":"865501043954677","stamp":1575453768,"warnflag":0,"state":3,"latitude":22585456,"longitude":113912645,"altitude":337,"speed":0,"direction":88},{"imei":"865501043954677","stamp":1575453773,"warnflag":0,"state":3,"latitude":22585454,"longitude":113912643,"altitude":338,"speed":0,"direction":134},{"imei":"865501043954677","stamp":1575453778,"warnflag":0,"state":3,"latitude":22585447,"longitude":113912642,"altitude":320,"speed":0,"direction":194}]} 83 | 84 | $ curl -H "Content-Type:application/json" -X POST --data '{"imei":"865501043954677","starttime":1575453728,"endtime":1575453826,"page":2}' http://localhost:8080/api/data 85 | 86 | {"pagecnt":2,"pagesize":10,"pageindex":2,"data":[{"imei":"865501043954677","stamp":1575453783,"warnflag":0,"state":3,"latitude":22585442,"longitude":113912651,"altitude":315,"speed":0,"direction":194},{"imei":"865501043954677","stamp":1575453788,"warnflag":0,"state":3,"latitude":22585446,"longitude":113912657,"altitude":340,"speed":0,"direction":194},{"imei":"865501043954677","stamp":1575453793,"warnflag":0,"state":3,"latitude":22585447,"longitude":113912659,"altitude":343,"speed":0,"direction":194},{"imei":"865501043954677","stamp":1575453798,"warnflag":0,"state":3,"latitude":22585447,"longitude":113912659,"altitude":345,"speed":0,"direction":194},{"imei":"865501043954677","stamp":1575453803,"warnflag":0,"state":3,"latitude":22585442,"longitude":113912664,"altitude":356,"speed":0,"direction":193},{"imei":"865501043954677","stamp":1575453809,"warnflag":0,"state":3,"latitude":22585447,"longitude":113912669,"altitude":374,"speed":0,"direction":193},{"imei":"865501043954677","stamp":1575453814,"warnflag":0,"state":3,"latitude":22585448,"longitude":113912673,"altitude":366,"speed":0,"direction":149},{"imei":"865501043954677","stamp":1575453818,"warnflag":0,"state":3,"latitude":22585447,"longitude":113912676,"altitude":374,"speed":0,"direction":149},{"imei":"865501043954677","stamp":1575453823,"warnflag":0,"state":3,"latitude":22585446,"longitude":113912678,"altitude":371,"speed":0,"direction":149}]} 87 | ``` 88 | 89 | 查询最新的GPS数据,供地图定位 90 | ```c 91 | $ curl -H "Content-Type:application/json" -X POST --data '{"imei":"865501043954677"}' http://localhost:8080/api/nowgps 92 | 93 | {"imei":"865501043954677","stamp":1575861194,"warnflag":0,"state":3,"latitude":22585422,"longitude":113912639,"altitude":373,"speed":0,"direction":76} 94 | ``` 95 | 96 | 登录验证 97 | ```c 98 | curl -H "Content-Type:application/json" -X POST --data '{"user":"1234566","password":"sdfasfdadf"}' http://localhost:8080/api/login 99 | 100 | {"token":"xdfasZsdfa2DsJsfa2"} 101 | ``` -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | cd frontend 2 | yarn build 3 | cd - 4 | go build -------------------------------------------------------------------------------- /codec/codec.go: -------------------------------------------------------------------------------- 1 | package codec 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | ) 8 | 9 | func RequireLen(v interface{}) (int, error) { 10 | rv := reflect.ValueOf(v) 11 | if rv.Kind() != reflect.Ptr || rv.IsNil() { 12 | return 0, fmt.Errorf("error") 13 | } 14 | 15 | return refRequireLen(reflect.ValueOf(v), reflect.StructField{}) 16 | } 17 | 18 | func Unmarshal(data []byte, v interface{}) (int, error) { 19 | rv := reflect.ValueOf(v) 20 | if rv.Kind() != reflect.Ptr || rv.IsNil() { 21 | return 0, fmt.Errorf("error") 22 | } 23 | 24 | lens, err := RequireLen(v) 25 | if err != nil { 26 | return 0, err 27 | } 28 | 29 | if len(data) < lens { 30 | return 0, fmt.Errorf("data too short,datalen:%d,lens:%d", len(data), lens) 31 | } 32 | 33 | return refUnmarshal(data, reflect.ValueOf(v), reflect.StructField{}, len(data)-lens) 34 | } 35 | 36 | func Marshal(v interface{}) ([]byte, error) { 37 | rv := reflect.ValueOf(v) 38 | if rv.Kind() != reflect.Ptr || rv.IsNil() { 39 | return []byte{}, fmt.Errorf("error") 40 | } 41 | 42 | return refMarshal(reflect.ValueOf(v), reflect.StructField{}) 43 | } 44 | 45 | func refRequireLen(v reflect.Value, tag reflect.StructField) (int, error) { 46 | var usedLen int = 0 47 | if v.Kind() == reflect.Ptr { 48 | v = v.Elem() 49 | } 50 | switch v.Kind() { 51 | case reflect.Int8: 52 | usedLen = usedLen + 1 53 | case reflect.Uint8: 54 | usedLen = usedLen + 1 55 | case reflect.Int16: 56 | usedLen = usedLen + 2 57 | case reflect.Uint16: 58 | usedLen = usedLen + 2 59 | case reflect.Int32: 60 | usedLen = usedLen + 4 61 | case reflect.Uint32: 62 | usedLen = usedLen + 4 63 | case reflect.Int64: 64 | usedLen = usedLen + 8 65 | case reflect.Uint64: 66 | usedLen = usedLen + 8 67 | case reflect.Float32: 68 | usedLen = usedLen + 4 69 | case reflect.Float64: 70 | usedLen = usedLen + 8 71 | case reflect.String: 72 | strLen := tag.Tag.Get("len") 73 | if strLen == "" { 74 | return 0, nil 75 | } 76 | lens, err := strconv.ParseInt(strLen, 10, 0) 77 | if err != nil { 78 | return 0, err 79 | } 80 | 81 | usedLen = usedLen + int(lens) 82 | case reflect.Slice: 83 | strLen := tag.Tag.Get("len") 84 | if strLen == "" { 85 | return 0, nil 86 | } 87 | lens, err := strconv.ParseInt(strLen, 10, 0) 88 | if err != nil { 89 | return 0, err 90 | } 91 | 92 | usedLen = usedLen + int(lens) 93 | case reflect.Struct: 94 | fieldCount := v.NumField() 95 | 96 | for i := 0; i < fieldCount; i++ { 97 | l, err := refRequireLen(v.Field(i), v.Type().Field(i)) 98 | if err != nil { 99 | return 0, err 100 | } 101 | 102 | usedLen = usedLen + l 103 | } 104 | } 105 | return usedLen, nil 106 | } 107 | 108 | func refUnmarshal(data []byte, v reflect.Value, tag reflect.StructField, streLen int) (int, error) { 109 | var usedLen int = 0 110 | if v.Kind() == reflect.Ptr { 111 | v = v.Elem() 112 | } 113 | switch v.Kind() { 114 | case reflect.Int8: 115 | v.SetInt(int64(data[0])) 116 | usedLen = usedLen + 1 117 | case reflect.Uint8: 118 | v.SetUint(uint64(data[0])) 119 | usedLen = usedLen + 1 120 | case reflect.Int16: 121 | if len(data) < 2 { 122 | return 0, fmt.Errorf("data to short") 123 | } 124 | v.SetInt(int64(Bytes2Word(data))) 125 | usedLen = usedLen + 2 126 | case reflect.Uint16: 127 | if len(data) < 2 { 128 | return 0, fmt.Errorf("data to short") 129 | } 130 | v.SetUint(uint64(Bytes2Word(data))) 131 | usedLen = usedLen + 2 132 | case reflect.Int32: 133 | if len(data) < 4 { 134 | return 0, fmt.Errorf("data to short") 135 | } 136 | v.SetInt(int64(Bytes2DWord(data))) 137 | usedLen = usedLen + 4 138 | case reflect.Uint32: 139 | if len(data) < 4 { 140 | return 0, fmt.Errorf("data to short") 141 | } 142 | v.SetUint(uint64(Bytes2DWord(data))) 143 | usedLen = usedLen + 4 144 | case reflect.Int64: 145 | v.SetInt(64) 146 | usedLen = usedLen + 8 147 | case reflect.Uint64: 148 | v.SetUint(64) 149 | usedLen = usedLen + 8 150 | case reflect.Float32: 151 | v.SetFloat(32.23) 152 | usedLen = usedLen + 4 153 | case reflect.Float64: 154 | v.SetFloat(64.46) 155 | usedLen = usedLen + 8 156 | case reflect.String: 157 | strLen := tag.Tag.Get("len") 158 | var lens int = 0 159 | if strLen == "" { 160 | lens = streLen 161 | } else { 162 | lens64, err := strconv.ParseInt(strLen, 10, 0) 163 | if err != nil { 164 | return 0, err 165 | } 166 | 167 | lens = int(lens64) 168 | } 169 | 170 | if len(data) < int(lens) { 171 | return 0, fmt.Errorf("data to short") 172 | } 173 | 174 | v.SetString(string(data[:lens])) 175 | usedLen = usedLen + int(lens) 176 | 177 | case reflect.Slice: 178 | strLen := tag.Tag.Get("len") 179 | var lens int = 0 180 | if strLen == "" { 181 | lens = streLen 182 | } else { 183 | lens64, err := strconv.ParseInt(strLen, 10, 0) 184 | if err != nil { 185 | return 0, err 186 | } 187 | 188 | lens = int(lens64) 189 | } 190 | 191 | v.SetBytes(data[:lens]) 192 | usedLen = usedLen + int(lens) 193 | case reflect.Struct: 194 | fieldCount := v.NumField() 195 | 196 | for i := 0; i < fieldCount; i++ { 197 | l, err := refUnmarshal(data[usedLen:], v.Field(i), v.Type().Field(i), streLen) 198 | if err != nil { 199 | return 0, err 200 | } 201 | 202 | usedLen = usedLen + l 203 | } 204 | } 205 | return usedLen, nil 206 | } 207 | 208 | func refMarshal(v reflect.Value, tag reflect.StructField) ([]byte, error) { 209 | data := make([]byte, 0) 210 | if v.Kind() == reflect.Ptr { 211 | v = v.Elem() 212 | } 213 | switch v.Kind() { 214 | case reflect.Int8: 215 | data = append(data, byte(v.Int())) 216 | case reflect.Uint8: 217 | data = append(data, byte(v.Uint())) 218 | case reflect.Int16: 219 | temp := Word2Bytes(uint16(v.Int())) 220 | data = append(data, temp...) 221 | case reflect.Uint16: 222 | temp := Word2Bytes(uint16(v.Uint())) 223 | data = append(data, temp...) 224 | case reflect.Int32: 225 | temp := Dword2Bytes(uint32(v.Int())) 226 | data = append(data, temp...) 227 | case reflect.Uint32: 228 | temp := Dword2Bytes(uint32(v.Uint())) 229 | data = append(data, temp...) 230 | case reflect.String: 231 | strLen := tag.Tag.Get("len") 232 | var lens int = 0 233 | if strLen == "" { 234 | lens = v.Len() 235 | } else { 236 | lens64, err := strconv.ParseInt(strLen, 10, 0) 237 | if err != nil { 238 | return []byte{}, err 239 | } 240 | 241 | lens = int(lens64) 242 | } 243 | 244 | if int(lens) > v.Len() { 245 | zeroSlice := make([]byte, int(lens)-v.Len()) 246 | data = append(data, zeroSlice...) 247 | } 248 | data = append(data, v.String()...) 249 | case reflect.Slice: 250 | strLen := tag.Tag.Get("len") 251 | var lens int = 0 252 | if strLen == "" { 253 | lens = v.Len() 254 | } else { 255 | lens64, err := strconv.ParseInt(strLen, 10, 0) 256 | if err != nil { 257 | return []byte{}, err 258 | } 259 | 260 | lens = int(lens64) 261 | } 262 | 263 | if int(lens) > v.Len() { 264 | zeroSlice := make([]byte, int(lens)-v.Len()) 265 | data = append(data, zeroSlice...) 266 | } 267 | data = append(data, v.Bytes()...) 268 | case reflect.Struct: 269 | fieldCount := v.NumField() 270 | 271 | for i := 0; i < fieldCount; i++ { 272 | d, err := refMarshal(v.Field(i), v.Type().Field(i)) 273 | if err != nil { 274 | return []byte{}, err 275 | } 276 | 277 | data = append(data, d...) 278 | } 279 | } 280 | return data, nil 281 | } 282 | 283 | func Bytes2Word(data []byte) uint16 { 284 | if len(data) < 2 { 285 | return 0 286 | } 287 | return (uint16(data[0]) << 8) + uint16(data[1]) 288 | } 289 | 290 | func Word2Bytes(data uint16) []byte { 291 | buff := make([]byte, 2) 292 | buff[0] = byte(data >> 8) 293 | buff[1] = byte(data) 294 | return buff 295 | } 296 | 297 | func Bytes2DWord(data []byte) uint32 { 298 | if len(data) < 4 { 299 | return 0 300 | } 301 | return (uint32(data[0]) << 24) + (uint32(data[1]) << 16) + (uint32(data[2]) << 8) + uint32(data[3]) 302 | } 303 | 304 | func Dword2Bytes(data uint32) []byte { 305 | buff := make([]byte, 4) 306 | buff[0] = byte(data >> 24) 307 | buff[1] = byte(data >> 16) 308 | buff[2] = byte(data >> 8) 309 | buff[3] = byte(data) 310 | return buff 311 | } 312 | -------------------------------------------------------------------------------- /codec/codec_test.go: -------------------------------------------------------------------------------- 1 | package codec 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestUnmarshal(t *testing.T) { 8 | type Data struct { 9 | Size int8 10 | Size2 uint16 11 | Size3 uint32 12 | Name string `len:"5"` 13 | Message string 14 | Sec []byte `len:"3"` 15 | } 16 | 17 | type Body struct { 18 | Age1 int8 19 | Age2 int16 20 | Length int32 21 | Data1 Data 22 | } 23 | 24 | data := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0x31, 0x32, 0x33, 0x34, 0x35, 0x31, 0x30, 0x03, 0x02, 0x01} 25 | pack := Body{} 26 | i, err := Unmarshal(data, &pack) 27 | if err != nil { 28 | t.Errorf("err:%s", err.Error()) 29 | } 30 | 31 | t.Log("len:", i) 32 | t.Log("pack:", pack) 33 | } 34 | 35 | func TestMarshal(t *testing.T) { 36 | type Data struct { 37 | Size int8 38 | Size2 uint16 39 | Size3 uint32 40 | Name string `len:"5"` 41 | Message string 42 | Sec []byte `len:"3"` 43 | } 44 | 45 | type Body struct { 46 | Age1 int8 47 | Age2 int16 48 | Length int32 49 | Data1 Data 50 | } 51 | 52 | pack := Body{ 53 | Age1: 13, 54 | Age2: 1201, 55 | Length: 81321, 56 | Data1: Data{ 57 | Size: 110, 58 | Size2: 39210, 59 | Size3: 85632, 60 | Name: "ASDFG", 61 | Message: "ZXCVBN", 62 | Sec: []byte{0x01, 0x02, 0x03}, 63 | }, 64 | } 65 | data, err := Marshal(&pack) 66 | if err != nil { 67 | t.Errorf("err:%s", err.Error()) 68 | } 69 | 70 | t.Log("data:", data) 71 | t.Log("pack:", pack) 72 | } 73 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # 一个TCP长连接设备管理后台工程 2 | 3 | ## 概述 4 | 5 | 这个项目最初只是用来进行一个简单的协议测试用的,而且是一个纯粹的后端命令行工程。只是后面想着只有命令行,操作也不太方便,于是便有了添加一个ui的想法。 6 | 7 | golang项目要配ui,最佳的还是配一个前端界面。而我本人并非前端出生,js功底太差,所以就想着用vue了。而且作为一个技术人员,ui界面设计也比较差,所以就打算找一个现成的ui框架来用,尝试了ant designer和iview后,决定使用iview来实现。 8 | 9 | 这个工程采用前后端分离设计: 10 | 11 | 后端采用golang语言,web框架采用gin,数据库采用postgresql,并使用xorm来简化数据库操作。使用jwt来进行权限控制。日志库采用logrus。 12 | 13 | 前端基本就是vue的生态环境,主体采用vue,ui采用iview,路由使用vur-router,状态管理使用vuex,js请求使用axios库。token存储在localstorage中,暂时没有存储到vuex中。由于前端需要绘制地图轨迹,所以用到了百度地图api和vue的地图库vue-baidu-map 14 | 15 | 因为页面为单页面,所以页面路由统一由前端来控制,后端只提供一个根路由用来加载静态数据,然后提供若干api供前端获取数据。 16 | 17 | ### 页面 18 | 19 | 目前页面只做了5个 20 | 21 | - 登录页面 22 | 23 | - 设备管理页面 24 | - 数据页面 25 | - 地图轨迹页面 26 | - 用户管理页面 27 | 28 | 5个页面均由路由控制,网页默认加载到登录页面。 29 | 30 | ## 预览 31 | 32 | 登录界面: 33 | 34 |  35 | 36 |  37 | 38 |  39 | 40 |  41 | 42 |  43 | 44 | 45 | [项目地址](https://github.com/qiuzhiqian/etc_tsp) 46 | 47 | ## 后端模型 48 | 49 | ```mermaid 50 | graph BT 51 | A(终端A) --> TCPServer 52 | B(终端B) --> TCPServer 53 | C(终端C) --> TCPServer 54 | TCPServer --> Postgresql 55 | Postgresql --> HTTPServer 56 | HTTPServer --> D(ClientA) 57 | HTTPServer --> E(ClientB) 58 | HTTPServer --> F(ClientC) 59 | ``` 60 | 61 | 62 | 63 | 后端需要设计两个服务器,一个TCP,一个HTTP。TCP主要处理与终端的长连接交互,一个TCP连接对应一台终端设备,终端设备唯一标识使用IMEI。HTTP处理与前端的交互,前端需要获取所有可用的终端设备列表,向指定的终端发送命令。所以,为了方便从ip找到对应终端,然后从对应终端找到对应的conn,我们就需要维护一个map: 64 | 65 | ```go 66 | type Terminal struct { 67 | authkey string 68 | imei string 69 | iccid string 70 | vin string 71 | tboxver string 72 | loginTime time.Time 73 | seqNum uint16 74 | phoneNum string 75 | Conn net.Conn 76 | } 77 | 78 | var connManger map[string]*Terminal 79 | ``` 80 | 81 | 至于为什么要定义成指针的形式,是因为定义成指针后我们可以直接修改map中元素结构体中对应的变量,而不需要重新定义一个元素再赋值。 82 | 83 | ```go 84 | var connManager map[string]*Terminal 85 | connManager = make(map[string]*Terminal) 86 | connManager["127.0.0.1:11000"]=&Terminal{} 87 | connManager["127.0.0.1:11001"]=&Terminal{} 88 | 89 | ... 90 | 91 | //此处能够轻松的修改对应的phoneNum修改 92 | connManager["127.0.0.1:11001"].phoneNum = "13000000000" 93 | ``` 94 | 95 | 相反,下面的这段代码修改起来就要繁琐不少: 96 | 97 | ```go 98 | var connManager map[string]Terminal 99 | connManager = make(map[string]Terminal) 100 | connManager["127.0.0.1:11000"]=Terminal{} 101 | connManager["127.0.0.1:11001"]=Terminal{} 102 | 103 | ... 104 | //此处会报错 105 | connManager["127.0.0.1:11001"].phoneNum = "13000000000" 106 | 107 | //此处修改需要定义一个临时变量,类似于读改写的模式 108 | term,ok:=connManager["127.0.0.1:11001"] 109 | term.phoneNum = "13000000000" 110 | connManager["127.0.0.1:11001"]=term 111 | ``` 112 | 113 | 上面的代码一处会报错 114 | 115 | ```bash 116 | cannot assign to struct field connManager["127.0.0.1:11001"].phoneNum in map 117 | ``` 118 | 119 | 从上面的对比就可以看到,确实是定义成指针更加方便了。 120 | 121 | ### TCP的长连接模型 122 | 123 | TCP的长连接我们选择这样的一种方式: 124 | 125 | - 每个连接分配一个读Goroutine 126 | - 写数据按需分配 127 | 128 | 如果熟悉socket的话,就知道socket一个服务器创建的基本步骤: 129 | 130 | 1. 创建socket 131 | 2. listen 132 | 3. accept 133 | 134 | 其中accept一般需要轮循调用。golang也基本是同样的流程。 135 | 136 | 一个简单的TCP服务器示例: 137 | 138 | ```go 139 | package main 140 | 141 | import ( 142 | "fmt" 143 | "net" 144 | ) 145 | 146 | type Terminal struct { 147 | authkey string 148 | imei string 149 | iccid string 150 | vin string 151 | tboxver string 152 | phoneNum string 153 | Conn net.Conn 154 | } 155 | 156 | var connManager map[string]*Terminal 157 | 158 | func recvConnMsg(conn net.Conn) { 159 | addr := conn.RemoteAddr() 160 | 161 | var term *Terminal = &Terminal{ 162 | Conn: conn, 163 | } 164 | term.Conn = conn 165 | connManager[addr.String()] = term 166 | 167 | defer func() { 168 | delete(connManager, addr.String()) 169 | conn.Close() 170 | }() 171 | 172 | for { 173 | tempbuf := make([]byte, 1024) 174 | n, err := conn.Read(tempbuf) 175 | 176 | if err != nil { 177 | return 178 | } 179 | 180 | fmt.Println("rcv:", tempbuf[:n]) 181 | } 182 | } 183 | 184 | func TCPServer(addr string) { 185 | connManager = make(map[string]*Terminal) 186 | listenSock, err := net.Listen("tcp", addr) 187 | if err != nil { 188 | return 189 | } 190 | defer listenSock.Close() 191 | 192 | for { 193 | newConn, err := listenSock.Accept() 194 | if err != nil { 195 | continue 196 | } 197 | 198 | go recvConnMsg(newConn) 199 | } 200 | } 201 | 202 | func main() { 203 | TCPServer(":19903") 204 | } 205 | ``` 206 | 207 | 以下是用来测试的客户端代码: 208 | 209 | ```go 210 | package main 211 | 212 | import ( 213 | "fmt" 214 | "net" 215 | "time" 216 | ) 217 | 218 | func main() { 219 | conn, err := net.Dial("tcp", ":19903") 220 | if err != nil { 221 | return 222 | } 223 | 224 | defer conn.Close() 225 | 226 | var n int = 0 227 | n, err = conn.Write([]byte("123456")) 228 | if err != nil { 229 | return 230 | } 231 | 232 | fmt.Println("len:", n) 233 | 234 | for { 235 | time.Sleep(time.Second * 3) 236 | } 237 | } 238 | ``` 239 | 240 | 测试结果: 241 | 242 | ```bash 243 | $ ./server 244 | rcv: [49 50 51 52 53 54] 245 | ``` 246 | 247 | ## TCP协议整合JTT808协议 248 | 249 | 前面简单说明了基于golang的net库进行TCP通讯。现在我们需要将现有的协议整合进去。行业内车辆终端一般都是对接交通部的JTT808协议,此处我们要实现的是JTT808-2019版本。 250 | 251 | ### 消息结构 252 | 253 | |标识位|消息头|消息体|校验码|标识位| 254 | | :--: | :--: | :--: | :--: | :--: | 255 | | 0x7e | | | | 0x7e | 256 | 257 | 标识位应采用0x7e表示,若校验码、消息头以及消息体中出现0x7e及0x7d,则要进行转义处理。转义规则定义如下: 258 | 259 | - 先对0x7d进行转义,转换为固定两个字节数据:0x7d 0x01; 260 | - 再对0x7e进行转义,转换为固定两个字节数据:0x7d 0x02。 261 | 262 | 转义处理过程如下: 263 | 264 | 发送消息时:先对消息进行封装,然后计算并填充校验码,最后进行转移处理; 265 | 266 | 接收消息时:先对消息进行转义还原,然后验证校验码,最后解析消息。 267 | 268 | 示例:发送一包内容为 0x30 0x7e 0x08 0x7d 0x55 的数据包,则经过封装如下:0x7e 0x 30 0x7d 0x02 0x08 0x7d 0x01 0x55 0x7e。 269 | 270 | > 注:多字节按照大端顺序传输 271 | 272 | ### 消息头 273 | 274 | | 起始字节 | 字段 | 数据类型 | 描述及要求 | 275 | | -------- | -------------- | -------- | ------------------------------------------------------------ | 276 | | 0 | 消息ID | WORD | -- | 277 | | 2 | 消息体属性 | WORD | 消息体属性格式结构见下表 | 278 | | 4 | 协议版本号 | BYTE | 协议版本号,每次关键修订递增,初始版本为1 | 279 | | 5 | 终端手机号 | BCD[10] | 根据安装后终端自身的手机号码转换。手机号不足位的,则在前面补充数字。 | 280 | | 15 | 消息流水号 | WORD | 按发送顺序从0开始循环累加 | 281 | | 17 | 消息包封装选项 | -- | 如果消息体属性中相关标识位确定消息分包处理,则该项有内容,否则无该项 | 282 | 283 | 消息体属性格式: 284 | 285 | | 15 | 14 | 13 | 12~10 | 9~0 | 286 | | ---- | -------- | ---- | ------------ | ---------- | 287 | | 保留 | 版本标识 | 分包 | 数据加密方式 | 消息体长度 | 288 | 289 | > 注版本标识位固定为1 290 | 291 | 加密方式按照如下进行: 292 | 293 | - bit10~bit12为数据加密标识位; 294 | - 当此三位为0,标识消息体不加密; 295 | - 当第10位为1,标识消息体经过RSA算法加密; 296 | - 其它位为保留位。 297 | 298 | 消息分包按照如下要求进行处理: 299 | 300 | - 当消息体属性中第13位为1时表示消息体为长消息,进行分包发送处理,具体分包消息由消息包封包项决定; 301 | - 若第13位为0,则消息头中无消息包封装项字段。 302 | 303 | 消息包封装项内容: 304 | 305 | | 起始字节 | 字段 | 数据内容 | 描述及要求 | 306 | | -------- | ---------- | -------- | -------------------- | 307 | | 0 | 消息总包数 | WORD | 该消息分包后的总包数 | 308 | | 2 | 包序号 | WORD | 从1开始 | 309 | 310 | ### 校验码 311 | 312 | 校验码的计算规则应从消息头首字节开始,同后一字节进行异或操纵直到消息体末字节结束;校 313 | 验码长度为一字节。 314 | 315 | ### 消息体 316 | 317 | 消息体只需要实现以下几个命令即可: 318 | 319 | | 命令 | 消息ID | 说明 | 320 | | ------------ | ------ | -------------------------- | 321 | | 终端通用应答 | 0x0001 | 终端通用应答 | 322 | | 平台通用应答 | 0x8001 | 平台通用应答 | 323 | | 终端心跳 | 0x0002 | 消息体为空,应答为通用应答 | 324 | | 终端注册 | 0x0100 | | 325 | | 终端注册应答 | 0x8100 | | 326 | | 终端鉴权 | 0x0102 | 应答为通用应答 | 327 | | 位置信息 | 0x0200 | 应答为通用应答 | 328 | 329 | #### 数据格式 330 | 331 | 终端通用应答: 332 | 333 | | 起始字节 | 字段 | 数据内容 | 描述及要求 | 334 | | -------- | ---------- | -------- | ---------------------------------------- | 335 | | 0 | 应答流水号 | WORD | 该消息分包后的总包数 | 336 | | 2 | 应答ID | WORD | 对应的平台消息的ID | 337 | | 4 | 结果 | BYTE | 0:成功/确认;1:失败;2消息有误;3:不支持 | 338 | 339 | 平台通用应答: 340 | 341 | | 起始字节 | 字段 | 数据内容 | 描述及要求 | 342 | | -------- | ---------- | -------- | -------------------------------------------------------- | 343 | | 0 | 应答流水号 | WORD | 对应的终端消息流水号 | 344 | | 2 | 应答ID | WORD | 对应的终端消息的ID | 345 | | 4 | 结果 | BYTE | 0:成功/确认;1:失败;2消息有误;3:不支持;4:报警处理确认 | 346 | 347 | 终端注册: 348 | 349 | | 起始字节 | 字段 | 数据内容 | 描述及要求 | 350 | | -------- | ---------- | -------- | -------------------------------------------------------- | 351 | | 0 | 应答流水号 | WORD | 对应的终端消息流水号 | 352 | | 2 | 应答ID | WORD | 对应的终端消息的ID | 353 | | 4 | 结果 | BYTE | 0:成功/确认;1:失败;2消息有误;3:不支持;4:报警处理确认 | 354 | 355 | 终端注册应答: 356 | 357 | | 起始字节 | 字段 | 数据内容 | 描述及要求 | 358 | | -------- | ---------- | -------- | ------------------------------------------------------------ | 359 | | 0 | 应答流水号 | WORD | 对应的终端注册消息的流水号 | 360 | | 2 | 结果 | BYTE | 0:成功;1:车辆已被注册;2:数据库中无该车辆;3终端已被注册;4数据库中无该终端 | 361 | | 3 | 鉴权码 | STRING | 注册结果为成功时,才有该字段 | 362 | 363 | 鉴权: 364 | 365 | | 起始字节 | 字段 | 数据内容 | 描述及要求 | 366 | | -------- | ---------- | -------- | ----------------------------------------------------- | 367 | | 0 | 鉴权码长度 | BYTE | --- | 368 | | n | 结果 | STRING | n为鉴权码长度 | 369 | | n+1 | 终端IMEI | BYTE[15] | --- | 370 | | n+16 | 软件版本号 | BYTE[20] | 厂家自定义版本号,位数不足时,后补0x00,n为鉴权码长度 | 371 | 372 | 以上就是需要实现的808协议内容,从协议中可以看到。对于协议实现,为了后续拓展方便,我们需要将它分割成两个基本部分:协议解析和协议处理。 373 | 374 | ## 协议解析 375 | 376 | 从前面内容我们可以发现,808协议是一个很典型的协议格式: 377 | 378 | ``` 379 | 固定字段+变长字段 380 | ``` 381 | 382 | 其中固定字段用来检测一个帧格式的完整性和有效性,所以一般会包含一下内容:帧头+变长字段对应的长度+校验。由于这一段的数据格式固定,目的单一,所以处理起来比较简单。 383 | 384 | 变长字段的长度是由固定字段终端某一个子字段的值决定的,而且这部分的格式比较多变,需要灵活处理。这一字段我们通常称为Body或者Apdu。 385 | 386 | 我们首先说明变长字段的处理流程。 387 | 388 | ### Body处理 389 | 390 | 正因为Body字段格式灵活,所以为了提高代码的复用性和拓展性,我们需要对Body的处理机制进行抽象,提取出一个相对通用的接口出来。 391 | 392 | 有经验的工程师都知道,一个协议格式处理,无非就是编码和解码。编码我们称之为Marshal,解码我们称之为Unmarshal。对于不同的格式,我们只需要提供不同的Marshal和Unmarshal实现即可。 393 | 394 | 395 | 396 | 从前面分析可以知道,我们现在面对的一种格式是类似于Plain的格式,这种格式没有基本的分割符,下面我们就对这种编码来实现Marshal和Unmarshal。我们将这部分逻辑定义为一个codec包 397 | 398 | ```go 399 | package codec 400 | 401 | func Unmarshal(data []byte, v interface{}) (int, error){} 402 | func Marshal(v interface{}) ([]byte, error){} 403 | ``` 404 | 405 | 参考官方库解析json的流程,很快我们就想到了用反射来实现这两个功能。 406 | 407 | 首先我们来分析Unmarshal,我们需要按照v的类型,将data数据按照对应的长度和类型赋值。举个最简单的例子: 408 | 409 | ```go 410 | func TestSimple(t *testing.T) { 411 | type Body struct { 412 | Age1 int8 413 | Age2 int16 414 | } 415 | 416 | data := []byte{0x01, 0x02, 0x03} 417 | pack := Body{} 418 | i, err := Unmarshal(data, &pack) 419 | if err != nil { 420 | t.Errorf("err:%s", err.Error()) 421 | } 422 | 423 | t.Log("len:", i) 424 | t.Log("pack:", pack) 425 | } 426 | ``` 427 | 428 | ```bash 429 | $ go test -v server/codec -run TestSimple 430 | === RUN TestSimple 431 | --- PASS: TestSimple (0.00s) 432 | codec_test.go:20: len: 3 433 | codec_test.go:21: pack: {1 515} 434 | PASS 435 | ok server/codec 0.002s 436 | ``` 437 | 438 | 对于Body结构体,第一个字段是int8,占用一个字节,所以分配的值是0x01。第二个字段是int16,占用两个字节,分配的值是0x02,0x03,然后把这两个字节按照大端格式组合成一个int16就行了。所以结果就是Age1字段为1(0x01),Age2字段为515(0x0203) 439 | 440 | 所以处理的关键是,我们要识别出v interface{}的类型,然后计算该类型对应的大小,再将data中对应大小的数据段组合成对应类型值复制给v中的对应字段。 441 | 442 | v interface{}的类型多变,可能会涉及到结构体嵌套等,所以会存在递归处理,当然第一步我们需要获取到v的类型: 443 | 444 | ```go 445 | rv := reflect.ValueOf(v) 446 | switch rv.Kind() { 447 | case reflect.Int8: 448 | // 449 | case reflect.Uint8: 450 | // 451 | case reflect.Int16: 452 | // 453 | case reflect.Uint16: 454 | // 455 | case reflect.Int32: 456 | // 457 | case reflect.Uint32: 458 | // 459 | case reflect.Int64: 460 | // 461 | case reflect.Uint64: 462 | // 463 | case reflect.Float32: 464 | // 465 | case reflect.Float64: 466 | // 467 | case reflect.String: 468 | // 469 | case reflect.Slice: 470 | // 471 | case reflect.Struct: 472 | //需要对struct中的每个元素进行解析 473 | } 474 | ``` 475 | 476 | 其他的类型都比较好处理,需要说明的是struct类型,首先我们要能够遍历struct中的各个元素,于是我们找到了: 477 | 478 | ```go 479 | fieldCount := v.NumField() 480 | v.Field(i) 481 | ``` 482 | 483 | NumField()能够获取结构体内部元素个数,然后Field(i)通过指定index就可以获取到指定的元素了。获取到了元素后,我们就需要最这个元素进行再次的Unmarshal,也就是递归。但是此时我们通过v.Field(i)获取到的是reflect.Value类型,而不是interface{}类型了,所以递归的入参我们使用reflect.Value。另外还需要考虑的一个问题是data数据的索引问题,一次调用Unmarshal就会**消耗掉**一定字节的data数据,消耗的长度应该能够被获取到,以方便下一次调用Unmarshal时,能够对入参的data数据索引做正确的设定。因此,Unmarshal函数需要返回一个当前当用后所占用的字节长度。比如int8就是一个字节,struct就是各个字段字节之和。 484 | 485 | ```go 486 | func Unmarshal(data []byte, v interface{}) (int,error) { 487 | rv := reflect.ValueOf(v) 488 | if rv.Kind() != reflect.Ptr || rv.IsNil() { 489 | return 0,fmt.Errorf("error") 490 | } 491 | 492 | return refUnmarshal(data, reflect.ValueOf(v)) 493 | } 494 | 495 | func refUnmarshal(data []byte, v reflect.Value) (int,error) { 496 | var usedLen int = 0 497 | if v.Kind() == reflect.Ptr { 498 | v = v.Elem() 499 | } 500 | switch v.Kind() { 501 | case reflect.Int8: 502 | usedLen = usedLen + 1 503 | case reflect.Uint8: 504 | usedLen = usedLen + 1 505 | case reflect.Int16: 506 | if len(data) < 2 { 507 | return 0, fmt.Errorf("data to short") 508 | } 509 | usedLen = usedLen + 2 510 | case reflect.Uint16: 511 | if len(data) < 2 { 512 | return 0, fmt.Errorf("data to short") 513 | } 514 | usedLen = usedLen + 2 515 | case reflect.Int32: 516 | if len(data) < 4 { 517 | return 0, fmt.Errorf("data to short") 518 | } 519 | usedLen = usedLen + 4 520 | case reflect.Uint32: 521 | if len(data) < 4 { 522 | return 0, fmt.Errorf("data to short") 523 | } 524 | usedLen = usedLen + 4 525 | case reflect.Int64: 526 | usedLen = usedLen + 8 527 | case reflect.Uint64: 528 | usedLen = usedLen + 8 529 | case reflect.Float32: 530 | usedLen = usedLen + 4 531 | case reflect.Float64: 532 | usedLen = usedLen + 8 533 | case reflect.String: 534 | //待处理 535 | case reflect.Slice: 536 | //待处理 537 | case reflect.Struct: 538 | fieldCount := v.NumField() 539 | 540 | for i := 0; i < fieldCount; i++ { 541 | l, err := refUnmarshal(data[usedLen:], v.Field(i), v.Type().Field(i), streLen) 542 | if err != nil { 543 | return 0, err 544 | } 545 | 546 | usedLen = usedLen + l 547 | } 548 | } 549 | return usedLen, nil 550 | } 551 | ``` 552 | 553 | 解析到这个地方我们发现,我们又遇到了另外的一个问题:我们没有办法单纯的通过类型来获取到string和struct的长度,而且我们还必须处理这两个类型,因为这两个类型在协议处理中是很常见的。既然单纯的通过类型无法判断长度,我们就要借助tag了。我们尝试着在string和slice上设定tag来解决这个问题。但是tag是属于结构体的,只有结构体内部元素才能拥有tag,而且我们不能通过元素本身获取tag,必须通过上层的struct的type才能获取到,所以此时我们入参还要加入一个通过结构体type获取到的对应字段reflect.StructField: 554 | 555 | ```go 556 | func refUnmarshal(data []byte, v reflect.Value, tag reflect.StructField) (int, error) { 557 | var usedLen int = 0 558 | if v.Kind() == reflect.Ptr { 559 | v = v.Elem() 560 | } 561 | switch v.Kind() { 562 | case reflect.Int8: 563 | usedLen = usedLen + 1 564 | case reflect.Uint8: 565 | usedLen = usedLen + 1 566 | case reflect.Int16: 567 | usedLen = usedLen + 2 568 | case reflect.Uint16: 569 | usedLen = usedLen + 2 570 | case reflect.Int32: 571 | usedLen = usedLen + 4 572 | case reflect.Uint32: 573 | usedLen = usedLen + 4 574 | case reflect.Int64: 575 | usedLen = usedLen + 8 576 | case reflect.Uint64: 577 | usedLen = usedLen + 8 578 | case reflect.Float32: 579 | usedLen = usedLen + 4 580 | case reflect.Float64: 581 | usedLen = usedLen + 8 582 | case reflect.String: 583 | strLen := tag.Tag.Get("len") 584 | var lens int = 0 585 | if strLen == "" { 586 | // 587 | } else { 588 | lens64, err := strconv.ParseInt(strLen, 10, 0) 589 | if err != nil { 590 | return 0, err 591 | } 592 | 593 | lens = int(lens64) 594 | } 595 | usedLen = usedLen + int(lens) 596 | case reflect.Slice: 597 | strLen := tag.Tag.Get("len") 598 | var lens int = 0 599 | if strLen == "" { 600 | // 601 | } else { 602 | lens64, err := strconv.ParseInt(strLen, 10, 0) 603 | if err != nil { 604 | return 0, err 605 | } 606 | 607 | lens = int(lens64) 608 | } 609 | 610 | usedLen = usedLen + int(lens) 611 | case reflect.Struct: 612 | fieldCount := v.NumField() 613 | 614 | for i := 0; i < fieldCount; i++ { 615 | l, err := refUnmarshal(data[usedLen:], v.Field(i), v.Type().Field(i)) 616 | if err != nil { 617 | return 0, err 618 | } 619 | 620 | usedLen = usedLen + l 621 | } 622 | } 623 | return usedLen, nil 624 | } 625 | ``` 626 | 627 | 这样我们就能过获取到所有的字段对应的长度了,这个很关键。然后我们只需要根据对应的长度,从data中填充对应的数据值即可 628 | 629 | ```go 630 | func refUnmarshal(data []byte, v reflect.Value, tag reflect.StructField) (int, error) { 631 | var usedLen int = 0 632 | if v.Kind() == reflect.Ptr { 633 | v = v.Elem() 634 | } 635 | switch v.Kind() { 636 | case reflect.Int8: 637 | v.SetInt(int64(data[0])) 638 | usedLen = usedLen + 1 639 | case reflect.Uint8: 640 | v.SetUint(uint64(data[0])) 641 | usedLen = usedLen + 1 642 | case reflect.Int16: 643 | if len(data) < 2 { 644 | return 0, fmt.Errorf("data to short") 645 | } 646 | v.SetInt(int64(Bytes2Word(data))) 647 | usedLen = usedLen + 2 648 | case reflect.Uint16: 649 | if len(data) < 2 { 650 | return 0, fmt.Errorf("data to short") 651 | } 652 | v.SetUint(uint64(Bytes2Word(data))) 653 | usedLen = usedLen + 2 654 | case reflect.Int32: 655 | if len(data) < 4 { 656 | return 0, fmt.Errorf("data to short") 657 | } 658 | v.SetInt(int64(Bytes2DWord(data))) 659 | usedLen = usedLen + 4 660 | case reflect.Uint32: 661 | if len(data) < 4 { 662 | return 0, fmt.Errorf("data to short") 663 | } 664 | v.SetUint(uint64(Bytes2DWord(data))) 665 | usedLen = usedLen + 4 666 | case reflect.Int64: 667 | v.SetInt(64) 668 | usedLen = usedLen + 8 669 | case reflect.Uint64: 670 | v.SetUint(64) 671 | usedLen = usedLen + 8 672 | case reflect.Float32: 673 | v.SetFloat(32.23) 674 | usedLen = usedLen + 4 675 | case reflect.Float64: 676 | v.SetFloat(64.46) 677 | usedLen = usedLen + 8 678 | case reflect.String: 679 | strLen := tag.Tag.Get("len") 680 | var lens int = 0 681 | if strLen == "" { 682 | // 683 | } else { 684 | lens64, err := strconv.ParseInt(strLen, 10, 0) 685 | if err != nil { 686 | return 0, err 687 | } 688 | 689 | lens = int(lens64) 690 | } 691 | 692 | if len(data) < int(lens) { 693 | return 0, fmt.Errorf("data to short") 694 | } 695 | 696 | v.SetString(string(data[:lens])) 697 | usedLen = usedLen + int(lens) 698 | 699 | case reflect.Slice: 700 | strLen := tag.Tag.Get("len") 701 | var lens int = 0 702 | if strLen == "" { 703 | // 704 | } else { 705 | lens64, err := strconv.ParseInt(strLen, 10, 0) 706 | if err != nil { 707 | return 0, err 708 | } 709 | 710 | lens = int(lens64) 711 | } 712 | 713 | v.SetBytes(data[:lens]) 714 | usedLen = usedLen + int(lens) 715 | case reflect.Struct: 716 | fieldCount := v.NumField() 717 | 718 | for i := 0; i < fieldCount; i++ { 719 | l, err := refUnmarshal(data[usedLen:], v.Field(i), v.Type().Field(i)) 720 | if err != nil { 721 | return 0, err 722 | } 723 | 724 | usedLen = usedLen + l 725 | } 726 | } 727 | return usedLen, nil 728 | } 729 | ``` 730 | 731 | 一个基本的Unmarshal函数就完成了。但是这个处理是比较理想的,在实际中可能会存在这样的一种情况:在一个协议中有若干字段,其他的字段都是固定长度,只有一个字段是长度可变的,而这个可变长度的计算是由总体长度-固定长度来计算出来的。在这种情况下,我们需要提前计算出已知字段的固定长度,然后用data长度-固定长度,得到唯一的可变字段的长度。所以我现在要有一个获取这个结构的有效长度的函数。前面的Unmarshal内部已经可以获取到每个字段的长度了,我们只需要把这个函数简单改造一下就行了: 732 | 733 | ```go 734 | func RequireLen(v interface{}) (int, error) { 735 | rv := reflect.ValueOf(v) 736 | if rv.Kind() != reflect.Ptr || rv.IsNil() { 737 | return 0, fmt.Errorf("error") 738 | } 739 | 740 | return refRequireLen(reflect.ValueOf(v), reflect.StructField{}) 741 | } 742 | 743 | func refRequireLen(v reflect.Value, tag reflect.StructField) (int, error) { 744 | var usedLen int = 0 745 | if v.Kind() == reflect.Ptr { 746 | v = v.Elem() 747 | } 748 | switch v.Kind() { 749 | case reflect.Int8: 750 | usedLen = usedLen + 1 751 | case reflect.Uint8: 752 | usedLen = usedLen + 1 753 | case reflect.Int16: 754 | usedLen = usedLen + 2 755 | case reflect.Uint16: 756 | usedLen = usedLen + 2 757 | case reflect.Int32: 758 | usedLen = usedLen + 4 759 | case reflect.Uint32: 760 | usedLen = usedLen + 4 761 | case reflect.Int64: 762 | usedLen = usedLen + 8 763 | case reflect.Uint64: 764 | usedLen = usedLen + 8 765 | case reflect.Float32: 766 | usedLen = usedLen + 4 767 | case reflect.Float64: 768 | usedLen = usedLen + 8 769 | case reflect.String: 770 | strLen := tag.Tag.Get("len") 771 | if strLen == "" { 772 | return 0, nil 773 | } 774 | lens, err := strconv.ParseInt(strLen, 10, 0) 775 | if err != nil { 776 | return 0, err 777 | } 778 | 779 | usedLen = usedLen + int(lens) 780 | case reflect.Slice: 781 | strLen := tag.Tag.Get("len") 782 | if strLen == "" { 783 | return 0, nil 784 | } 785 | lens, err := strconv.ParseInt(strLen, 10, 0) 786 | if err != nil { 787 | return 0, err 788 | } 789 | 790 | usedLen = usedLen + int(lens) 791 | case reflect.Struct: 792 | fieldCount := v.NumField() 793 | 794 | for i := 0; i < fieldCount; i++ { 795 | l, err := refRequireLen(v.Field(i), v.Type().Field(i)) 796 | if err != nil { 797 | return 0, err 798 | } 799 | 800 | usedLen = usedLen + l 801 | } 802 | } 803 | return usedLen, nil 804 | } 805 | ``` 806 | 807 | 这样我们就可以实现一个完整的Unmarshal 808 | 809 | ```go 810 | func Unmarshal(data []byte, v interface{}) (int, error) { 811 | rv := reflect.ValueOf(v) 812 | if rv.Kind() != reflect.Ptr || rv.IsNil() { 813 | return 0, fmt.Errorf("error") 814 | } 815 | 816 | lens, err := RequireLen(v) 817 | if err != nil { 818 | return 0, err 819 | } 820 | 821 | if len(data) < lens { 822 | return 0, fmt.Errorf("data too short") 823 | } 824 | 825 | return refUnmarshal(data, reflect.ValueOf(v), reflect.StructField{}, len(data)-lens) 826 | } 827 | 828 | func refUnmarshal(data []byte, v reflect.Value, tag reflect.StructField, streLen int) (int, error) { 829 | var usedLen int = 0 830 | if v.Kind() == reflect.Ptr { 831 | v = v.Elem() 832 | } 833 | switch v.Kind() { 834 | case reflect.Int8: 835 | v.SetInt(int64(data[0])) 836 | usedLen = usedLen + 1 837 | case reflect.Uint8: 838 | v.SetUint(uint64(data[0])) 839 | usedLen = usedLen + 1 840 | case reflect.Int16: 841 | if len(data) < 2 { 842 | return 0, fmt.Errorf("data to short") 843 | } 844 | v.SetInt(int64(Bytes2Word(data))) 845 | usedLen = usedLen + 2 846 | case reflect.Uint16: 847 | if len(data) < 2 { 848 | return 0, fmt.Errorf("data to short") 849 | } 850 | v.SetUint(uint64(Bytes2Word(data))) 851 | usedLen = usedLen + 2 852 | case reflect.Int32: 853 | if len(data) < 4 { 854 | return 0, fmt.Errorf("data to short") 855 | } 856 | v.SetInt(int64(Bytes2DWord(data))) 857 | usedLen = usedLen + 4 858 | case reflect.Uint32: 859 | if len(data) < 4 { 860 | return 0, fmt.Errorf("data to short") 861 | } 862 | v.SetUint(uint64(Bytes2DWord(data))) 863 | usedLen = usedLen + 4 864 | case reflect.Int64: 865 | v.SetInt(64) 866 | usedLen = usedLen + 8 867 | case reflect.Uint64: 868 | v.SetUint(64) 869 | usedLen = usedLen + 8 870 | case reflect.Float32: 871 | v.SetFloat(32.23) 872 | usedLen = usedLen + 4 873 | case reflect.Float64: 874 | v.SetFloat(64.46) 875 | usedLen = usedLen + 8 876 | case reflect.String: 877 | strLen := tag.Tag.Get("len") 878 | var lens int = 0 879 | if strLen == "" { 880 | lens = streLen 881 | } else { 882 | lens64, err := strconv.ParseInt(strLen, 10, 0) 883 | if err != nil { 884 | return 0, err 885 | } 886 | 887 | lens = int(lens64) 888 | } 889 | 890 | if len(data) < int(lens) { 891 | return 0, fmt.Errorf("data to short") 892 | } 893 | 894 | v.SetString(string(data[:lens])) 895 | usedLen = usedLen + int(lens) 896 | 897 | case reflect.Slice: 898 | strLen := tag.Tag.Get("len") 899 | var lens int = 0 900 | if strLen == "" { 901 | lens = streLen 902 | } else { 903 | lens64, err := strconv.ParseInt(strLen, 10, 0) 904 | if err != nil { 905 | return 0, err 906 | } 907 | 908 | lens = int(lens64) 909 | } 910 | 911 | v.SetBytes(data[:lens]) 912 | usedLen = usedLen + int(lens) 913 | case reflect.Struct: 914 | fieldCount := v.NumField() 915 | 916 | for i := 0; i < fieldCount; i++ { 917 | l, err := refUnmarshal(data[usedLen:], v.Field(i), v.Type().Field(i), streLen) 918 | if err != nil { 919 | return 0, err 920 | } 921 | 922 | usedLen = usedLen + l 923 | } 924 | } 925 | return usedLen, nil 926 | } 927 | ``` 928 | 929 | 理解了上面的流程,Marshal就就很好写了,只是复制过程反过来就行了。这其中还有一些小的转换逻辑将字节数组转换成多字节整形:Bytes2Word、Word2Bytes、Bytes2DWord、Dword2Bytes。这类转换都使用大端格式处理。完整代码如下: 930 | 931 | ```go 932 | package codec 933 | 934 | import ( 935 | "fmt" 936 | "reflect" 937 | "strconv" 938 | ) 939 | 940 | func RequireLen(v interface{}) (int, error) { 941 | rv := reflect.ValueOf(v) 942 | if rv.Kind() != reflect.Ptr || rv.IsNil() { 943 | return 0, fmt.Errorf("error") 944 | } 945 | 946 | return refRequireLen(reflect.ValueOf(v), reflect.StructField{}) 947 | } 948 | 949 | func Unmarshal(data []byte, v interface{}) (int, error) { 950 | rv := reflect.ValueOf(v) 951 | if rv.Kind() != reflect.Ptr || rv.IsNil() { 952 | return 0, fmt.Errorf("error") 953 | } 954 | 955 | lens, err := RequireLen(v) 956 | if err != nil { 957 | return 0, err 958 | } 959 | 960 | if len(data) < lens { 961 | return 0, fmt.Errorf("data too short") 962 | } 963 | 964 | return refUnmarshal(data, reflect.ValueOf(v), reflect.StructField{}, len(data)-lens) 965 | } 966 | 967 | func Marshal(v interface{}) ([]byte, error) { 968 | rv := reflect.ValueOf(v) 969 | if rv.Kind() != reflect.Ptr || rv.IsNil() { 970 | return []byte{}, fmt.Errorf("error") 971 | } 972 | 973 | return refMarshal(reflect.ValueOf(v), reflect.StructField{}) 974 | } 975 | 976 | func refRequireLen(v reflect.Value, tag reflect.StructField) (int, error) { 977 | var usedLen int = 0 978 | if v.Kind() == reflect.Ptr { 979 | v = v.Elem() 980 | } 981 | switch v.Kind() { 982 | case reflect.Int8: 983 | usedLen = usedLen + 1 984 | case reflect.Uint8: 985 | usedLen = usedLen + 1 986 | case reflect.Int16: 987 | usedLen = usedLen + 2 988 | case reflect.Uint16: 989 | usedLen = usedLen + 2 990 | case reflect.Int32: 991 | usedLen = usedLen + 4 992 | case reflect.Uint32: 993 | usedLen = usedLen + 4 994 | case reflect.Int64: 995 | usedLen = usedLen + 8 996 | case reflect.Uint64: 997 | usedLen = usedLen + 8 998 | case reflect.Float32: 999 | usedLen = usedLen + 4 1000 | case reflect.Float64: 1001 | usedLen = usedLen + 8 1002 | case reflect.String: 1003 | strLen := tag.Tag.Get("len") 1004 | if strLen == "" { 1005 | return 0, nil 1006 | } 1007 | lens, err := strconv.ParseInt(strLen, 10, 0) 1008 | if err != nil { 1009 | return 0, err 1010 | } 1011 | 1012 | usedLen = usedLen + int(lens) 1013 | case reflect.Slice: 1014 | strLen := tag.Tag.Get("len") 1015 | if strLen == "" { 1016 | return 0, nil 1017 | } 1018 | lens, err := strconv.ParseInt(strLen, 10, 0) 1019 | if err != nil { 1020 | return 0, err 1021 | } 1022 | 1023 | usedLen = usedLen + int(lens) 1024 | case reflect.Struct: 1025 | fieldCount := v.NumField() 1026 | 1027 | for i := 0; i < fieldCount; i++ { 1028 | l, err := refRequireLen(v.Field(i), v.Type().Field(i)) 1029 | if err != nil { 1030 | return 0, err 1031 | } 1032 | 1033 | usedLen = usedLen + l 1034 | } 1035 | } 1036 | return usedLen, nil 1037 | } 1038 | 1039 | func refUnmarshal(data []byte, v reflect.Value, tag reflect.StructField, streLen int) (int, error) { 1040 | var usedLen int = 0 1041 | if v.Kind() == reflect.Ptr { 1042 | v = v.Elem() 1043 | } 1044 | switch v.Kind() { 1045 | case reflect.Int8: 1046 | v.SetInt(int64(data[0])) 1047 | usedLen = usedLen + 1 1048 | case reflect.Uint8: 1049 | v.SetUint(uint64(data[0])) 1050 | usedLen = usedLen + 1 1051 | case reflect.Int16: 1052 | if len(data) < 2 { 1053 | return 0, fmt.Errorf("data to short") 1054 | } 1055 | v.SetInt(int64(Bytes2Word(data))) 1056 | usedLen = usedLen + 2 1057 | case reflect.Uint16: 1058 | if len(data) < 2 { 1059 | return 0, fmt.Errorf("data to short") 1060 | } 1061 | v.SetUint(uint64(Bytes2Word(data))) 1062 | usedLen = usedLen + 2 1063 | case reflect.Int32: 1064 | if len(data) < 4 { 1065 | return 0, fmt.Errorf("data to short") 1066 | } 1067 | v.SetInt(int64(Bytes2DWord(data))) 1068 | usedLen = usedLen + 4 1069 | case reflect.Uint32: 1070 | if len(data) < 4 { 1071 | return 0, fmt.Errorf("data to short") 1072 | } 1073 | v.SetUint(uint64(Bytes2DWord(data))) 1074 | usedLen = usedLen + 4 1075 | case reflect.Int64: 1076 | v.SetInt(64) 1077 | usedLen = usedLen + 8 1078 | case reflect.Uint64: 1079 | v.SetUint(64) 1080 | usedLen = usedLen + 8 1081 | case reflect.Float32: 1082 | v.SetFloat(32.23) 1083 | usedLen = usedLen + 4 1084 | case reflect.Float64: 1085 | v.SetFloat(64.46) 1086 | usedLen = usedLen + 8 1087 | case reflect.String: 1088 | strLen := tag.Tag.Get("len") 1089 | var lens int = 0 1090 | if strLen == "" { 1091 | lens = streLen 1092 | } else { 1093 | lens64, err := strconv.ParseInt(strLen, 10, 0) 1094 | if err != nil { 1095 | return 0, err 1096 | } 1097 | 1098 | lens = int(lens64) 1099 | } 1100 | 1101 | if len(data) < int(lens) { 1102 | return 0, fmt.Errorf("data to short") 1103 | } 1104 | 1105 | v.SetString(string(data[:lens])) 1106 | usedLen = usedLen + int(lens) 1107 | 1108 | case reflect.Slice: 1109 | strLen := tag.Tag.Get("len") 1110 | var lens int = 0 1111 | if strLen == "" { 1112 | lens = streLen 1113 | } else { 1114 | lens64, err := strconv.ParseInt(strLen, 10, 0) 1115 | if err != nil { 1116 | return 0, err 1117 | } 1118 | 1119 | lens = int(lens64) 1120 | } 1121 | 1122 | v.SetBytes(data[:lens]) 1123 | usedLen = usedLen + int(lens) 1124 | case reflect.Struct: 1125 | fieldCount := v.NumField() 1126 | 1127 | for i := 0; i < fieldCount; i++ { 1128 | l, err := refUnmarshal(data[usedLen:], v.Field(i), v.Type().Field(i), streLen) 1129 | if err != nil { 1130 | return 0, err 1131 | } 1132 | 1133 | usedLen = usedLen + l 1134 | } 1135 | } 1136 | return usedLen, nil 1137 | } 1138 | 1139 | func refMarshal(v reflect.Value, tag reflect.StructField) ([]byte, error) { 1140 | data := make([]byte, 0) 1141 | if v.Kind() == reflect.Ptr { 1142 | v = v.Elem() 1143 | } 1144 | switch v.Kind() { 1145 | case reflect.Int8: 1146 | data = append(data, byte(v.Int())) 1147 | case reflect.Uint8: 1148 | data = append(data, byte(v.Uint())) 1149 | case reflect.Int16: 1150 | temp := Word2Bytes(uint16(v.Int())) 1151 | data = append(data, temp...) 1152 | case reflect.Uint16: 1153 | temp := Word2Bytes(uint16(v.Uint())) 1154 | data = append(data, temp...) 1155 | case reflect.Int32: 1156 | temp := Dword2Bytes(uint32(v.Int())) 1157 | data = append(data, temp...) 1158 | case reflect.Uint32: 1159 | temp := Dword2Bytes(uint32(v.Uint())) 1160 | data = append(data, temp...) 1161 | case reflect.String: 1162 | strLen := tag.Tag.Get("len") 1163 | lens, err := strconv.ParseInt(strLen, 10, 0) 1164 | if err != nil { 1165 | return []byte{}, err 1166 | } 1167 | 1168 | if int(lens) > v.Len() { 1169 | zeroSlice := make([]byte, int(lens)-v.Len()) 1170 | data = append(data, zeroSlice...) 1171 | } 1172 | data = append(data, v.String()...) 1173 | case reflect.Slice: 1174 | strLen := tag.Tag.Get("len") 1175 | lens, err := strconv.ParseInt(strLen, 10, 0) 1176 | if err != nil { 1177 | return []byte{}, err 1178 | } 1179 | 1180 | if int(lens) > v.Len() { 1181 | zeroSlice := make([]byte, int(lens)-v.Len()) 1182 | data = append(data, zeroSlice...) 1183 | } 1184 | data = append(data, v.Bytes()...) 1185 | case reflect.Struct: 1186 | fieldCount := v.NumField() 1187 | 1188 | for i := 0; i < fieldCount; i++ { 1189 | fmt.Println(v.Field(i).Type().String()) 1190 | d, err := refMarshal(v.Field(i), v.Type().Field(i)) 1191 | if err != nil { 1192 | return []byte{}, err 1193 | } 1194 | 1195 | data = append(data, d...) 1196 | } 1197 | } 1198 | return data, nil 1199 | } 1200 | 1201 | func Bytes2Word(data []byte) uint16 { 1202 | if len(data) < 2 { 1203 | return 0 1204 | } 1205 | return (uint16(data[0]) << 8) + uint16(data[1]) 1206 | } 1207 | 1208 | func Word2Bytes(data uint16) []byte { 1209 | buff := make([]byte, 2) 1210 | buff[0] = byte(data >> 8) 1211 | buff[1] = byte(data) 1212 | return buff 1213 | } 1214 | 1215 | func Bytes2DWord(data []byte) uint32 { 1216 | if len(data) < 4 { 1217 | return 0 1218 | } 1219 | return (uint32(data[0]) << 24) + (uint32(data[1]) << 16) + (uint32(data[2]) << 8) + uint32(data[3]) 1220 | } 1221 | 1222 | func Dword2Bytes(data uint32) []byte { 1223 | buff := make([]byte, 4) 1224 | buff[0] = byte(data >> 24) 1225 | buff[1] = byte(data >> 16) 1226 | buff[2] = byte(data >> 8) 1227 | buff[3] = byte(data) 1228 | return buff 1229 | } 1230 | ``` 1231 | 1232 | ## 帧过滤器 1233 | 1234 | 帧过滤器的作用就是,从接收到的buff中,过滤出有效的完整jtt808数据包。由于是tcp通讯,那么这其中不可避免的会涉及到数据包的两个常规处理:拆包和粘包。 1235 | 1236 | 拆包和粘包的简要说明: 1237 | 1238 | ``` 1239 | 假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。 1240 | 1241 | (1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包; 1242 | 1243 | (2)服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包; 1244 | 1245 | (3)服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包; 1246 | 1247 | (4)服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。 1248 | 1249 | 如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。 1250 | ``` 1251 | 1252 | 拆包与粘包的说明网上资料很多,此处不做过多说明。但是我们设计出来的过滤器,要能够正常应对拆包和粘包的情况。 1253 | 1254 | 首先,我们根据jtt808协议定义我们的数据包结构: 1255 | 1256 | ```go 1257 | type MultiField struct { 1258 | MsgSum uint16 1259 | MsgIndex uint16 1260 | } 1261 | 1262 | type Header struct { 1263 | MID uint16 1264 | Attr uint16 1265 | Version uint8 1266 | PhoneNum string 1267 | SeqNum uint16 1268 | MutilFlag MultiField 1269 | } 1270 | 1271 | type Message struct { 1272 | HEADER Header 1273 | BODY []byte 1274 | } 1275 | ``` 1276 | 1277 | 由于Attr其实是由多个位域字段组成,所以我们再定义三个函数: 1278 | 1279 | ```go 1280 | func (h *Header) IsMulti() bool { 1281 | if ((h.Attr >> 12) & 0x0001) > 0 { 1282 | return true 1283 | } 1284 | return false 1285 | } 1286 | 1287 | //BodyLen is a function for get body len 1288 | func (h *Header) BodyLen() int { 1289 | return int(h.Attr & 0x03ff) 1290 | } 1291 | 1292 | //MakeAttr is generate attr 1293 | func MakeAttr(verFlag byte, mut bool, enc byte, lens uint16) uint16 { 1294 | attr := lens & 0x03FF 1295 | 1296 | if verFlag > 0 { 1297 | attr = attr & 0x4000 1298 | } 1299 | 1300 | if mut { 1301 | attr = attr & 0x2000 1302 | } 1303 | 1304 | encMask := (uint16(enc) & 0x0007) << 10 1305 | return attr + encMask 1306 | } 1307 | ``` 1308 | 1309 | 由于要考虑拆包和粘包问题,所以我们的过滤器需要能够同时分析多包数据,但是基本单元函数分析一帧数据,所以我们先实现一帧数据的过滤器:filterSigle 1310 | 1311 | 我们想要的过滤器原型是如下的一个函数: 1312 | 1313 | ```go 1314 | func filterSigle(data []byte) (Message, int, error) 1315 | ``` 1316 | 1317 | 该函数接收一个存有从tcp端接收的数据流切片,需要返回我们解析出来的Message、解析过后消耗的字节数,错误信息。 1318 | 1319 | 很明显,这个返回的消耗字节数就是为了应对拆包和粘包用的。 1320 | 1321 | 我们知道jtt808协议是以0x7e开始和结尾的,我们定义一个常量: 1322 | 1323 | ```go 1324 | const ( 1325 | ProtoHeader byte = 0x7e 1326 | ) 1327 | ``` 1328 | 1329 | filterSigle的第一个逻辑就是要识别帧头和帧尾了: 1330 | 1331 | ```go 1332 | var usedLen int = 0 1333 | 1334 | startindex := bytes.IndexByte(data, ProtoHeader) 1335 | if startindex >= 0 { 1336 | usedLen = startindex + 1 1337 | endindex := bytes.IndexByte(data[usedLen:], ProtoHeader) 1338 | if endindex >= 0 { 1339 | endindex = endindex + usedLen 1340 | } 1341 | } 1342 | ``` 1343 | 1344 | 此处的帧头和帧尾索引均相对于data的起始字节而言。理想的情况下,在startindex和endindex之间的数据就是我们需要解析的数据,所以之后的解析都是对这部分数据进行分析。我们对这部分的逻辑单独用一个函数来处理,这个函数主要完成三个逻辑:转义、校验检查和解析 1345 | 1346 | ```go 1347 | func frameParser(data []byte) (Message, error) { 1348 | 1349 | } 1350 | ``` 1351 | 1352 | data入参从startindex到endindex,不包括startindex和endindex。 1353 | 1354 | 在转义之前,首先判断基本的长度。通过对帧头固定字段分析可以知道,消息头部分的长度为17或者19个字节,加上帧头帧尾校验位的话最小就是17+3=20个字节,鉴于BODY部分可能为空,所以整个帧最小长度应该为20字节: 1355 | 1356 | ```go 1357 | if len(data)+2 < 17+3 { 1358 | return Message{}, fmt.Errorf("header is too short") 1359 | } 1360 | ``` 1361 | 1362 | 转义比较简单,为了代码复用,定义成一个函数: 1363 | 1364 | ```go 1365 | func Escape(data, oldBytes, newBytes []byte) []byte { 1366 | buff := make([]byte, 0) 1367 | 1368 | var startindex int = 0 1369 | 1370 | for startindex < len(data) { 1371 | index := bytes.Index(data[startindex:], oldBytes) 1372 | if index >= 0 { 1373 | buff = append(buff, data[startindex:index]...) 1374 | buff = append(buff, newBytes...) 1375 | startindex = index + len(oldBytes) 1376 | } else { 1377 | buff = append(buff, data[startindex:]...) 1378 | startindex = len(data) 1379 | } 1380 | } 1381 | return buff 1382 | } 1383 | ``` 1384 | 1385 | 调用: 1386 | 1387 | ```go 1388 | //不包含帧头帧尾 1389 | frameData := Escape(data[:len(data)], []byte{0x7d, 0x02}, []byte{0x7e}) 1390 | frameData = Escape(frameData, []byte{0x7d, 0x01}, []byte{0x7d}) 1391 | ``` 1392 | 1393 | 校验就是简单的异或校验,从消息头到消息体结束,即data[:len(data)-1] 1394 | 1395 | ```go 1396 | func checkSum(data []byte) byte { 1397 | var sum byte = 0 1398 | for _, itemdata := range data { 1399 | sum ^= itemdata 1400 | } 1401 | return sum 1402 | } 1403 | ``` 1404 | 1405 | 调用: 1406 | 1407 | ```go 1408 | rawcs := checkSum(frameData[:len(frameData)-1]) 1409 | 1410 | if rawcs != frameData[len(frameData)-1] { 1411 | return Message{}, fmt.Errorf("cs is not match:%d--%d", rawcs, frameData[len(frameData)-1]) 1412 | } 1413 | ``` 1414 | 1415 | 然后就是对frameData中的具体数据进行解析了: 1416 | 1417 | ```go 1418 | var usedLen int = 0 1419 | var msg Message 1420 | msg.HEADER.MID = codec.Bytes2Word(frameData[usedLen:]) 1421 | usedLen = usedLen + 2 1422 | msg.HEADER.Attr = codec.Bytes2Word(frameData[usedLen:]) 1423 | usedLen = usedLen + 2 1424 | msg.HEADER.Version = frameData[usedLen] 1425 | usedLen = usedLen + 1 1426 | ``` 1427 | 1428 | 注意usedLen要跟着实时变化。 1429 | 1430 | 手机号固定为10个字节,不足的话前面会填充0,所以我们要把前面无效的0去掉,使用bytes.TrimLeftFunc: 1431 | 1432 | ```go 1433 | tempPhone := bytes.TrimLeftFunc(frameData[usedLen:usedLen+10], func(r rune) bool { return r == 0x00 }) 1434 | msg.HEADER.PhoneNum = string(tempPhone) 1435 | usedLen = usedLen + 10 1436 | msg.HEADER.SeqNum = codec.Bytes2Word(frameData[usedLen:]) 1437 | usedLen = usedLen + 2 1438 | ``` 1439 | 1440 | 同时还要对多帧的情况进行判断: 1441 | 1442 | ```go 1443 | if msg.HEADER.IsMulti() { 1444 | msg.HEADER.MutilFlag.MsgSum = codec.Bytes2Word(frameData[usedLen:]) 1445 | usedLen = usedLen + 2 1446 | msg.HEADER.MutilFlag.MsgIndex = codec.Bytes2Word(frameData[usedLen:]) 1447 | usedLen = usedLen + 2 1448 | } 1449 | ``` 1450 | 1451 | 再次对usedLen长度判断一下,避免超过界限: 1452 | 1453 | ```go 1454 | if len(frameData) < usedLen { 1455 | return Message{}, fmt.Errorf("flag code is too short") 1456 | } 1457 | ``` 1458 | 1459 | 处理到上面的地方后,接着的就是BODY部分了,直接copy对应的长度,长度为: 1460 | 1461 | ``` 1462 | len(frameData)-usedLen 1463 | ``` 1464 | 1465 | 逻辑如下 1466 | 1467 | ```go 1468 | msg.BODY = make([]byte, len(frameData)-usedLen) 1469 | copy(msg.BODY, frameData[usedLen:len(frameData)]) 1470 | usedLen = len(frameData) 1471 | 1472 | return msg, nil 1473 | ``` 1474 | 1475 | 到此正常的流程就走完了。 1476 | 1477 | 调用: 1478 | 1479 | ```go 1480 | msg, err := frameParser(frameData) 1481 | ``` 1482 | 1483 | 当返回错误时,返回的长度值应该为endindex,即不包括endindex处对应的0x7e。因为这个0x7e可能是后面数据的帧头。 1484 | 1485 | ```go 1486 | msg, err := frameParser(data[startindex+1 : endindex]) 1487 | if err != nil { 1488 | return Message{}, endindex, err 1489 | } 1490 | 1491 | return msg, endindex + 1, nil 1492 | ``` 1493 | 1494 | 对于 1495 | 1496 | ```go 1497 | if endindex >= 0 1498 | ``` 1499 | 1500 | 条件不符合的,说明没有找到帧尾,可以包帧头前面的去掉了,但是帧头和帧头后面的数据要保留,用来跟之后的数据流拼接。 1501 | 1502 | ```go 1503 | return Message{}, startindex, fmt.Errorf("can't find end flag") 1504 | ``` 1505 | 1506 | 对于 1507 | 1508 | ```go 1509 | if startindex >= 0 1510 | ``` 1511 | 1512 | 条件不符合的,说明没有找到帧头,那就是整个帧都是无效的: 1513 | 1514 | ```go 1515 | return Message{}, len(data), fmt.Errorf("can't find start flag") 1516 | ``` 1517 | 1518 | 这样就实现了一个单帧的过滤器。接着我们在单帧过滤器的基础上来实现多帧过滤器。 1519 | 1520 | 我们只需要对数据流进行单帧过滤,然后返回消耗的字节数。如果消耗了一定字节数后,还有剩余的字节,我们再对这些字节进行单帧过滤。依次循环,直到字节数消耗完或者发生错误。 1521 | 1522 | 所有循环结束后,我们还需要将剩余的字节数保留,用来跟下一次的数据流进行拼接。函数实现如下: 1523 | 1524 | ```go 1525 | //Filter is proto Filter api 1526 | func Filter(data []byte) ([]Message, int, error) { 1527 | var usedLen int = 0 1528 | msgList := make([]Message, 0) 1529 | var cnt int = 0 1530 | for { 1531 | cnt++ 1532 | if cnt > 10 { 1533 | return []Message{}, 0, fmt.Errorf("time too much") 1534 | } 1535 | if usedLen >= len(data) { 1536 | break 1537 | } 1538 | 1539 | msg, lens, err := filterSigle(data[usedLen:]) 1540 | if err != nil { 1541 | usedLen = usedLen + lens 1542 | fmt.Println("err:", err) 1543 | return msgList, usedLen, nil 1544 | } 1545 | usedLen = usedLen + lens 1546 | msgList = append(msgList, msg) 1547 | } 1548 | return msgList, usedLen, nil 1549 | } 1550 | ``` 1551 | 1552 | 整个过滤器完整实现: 1553 | 1554 | ```go 1555 | package proto 1556 | 1557 | import ( 1558 | "bytes" 1559 | "fmt" 1560 | "tsp/codec" 1561 | "tsp/utils" 1562 | ) 1563 | 1564 | const ( 1565 | ProtoHeader byte = 0x7e 1566 | ) 1567 | 1568 | type MultiField struct { 1569 | MsgSum uint16 1570 | MsgIndex uint16 1571 | } 1572 | 1573 | type Header struct { 1574 | MID uint16 1575 | Attr uint16 1576 | Version uint8 1577 | PhoneNum string 1578 | SeqNum uint16 1579 | MutilFlag MultiField 1580 | } 1581 | 1582 | func (h *Header) IsMulti() bool { 1583 | if ((h.Attr >> 12) & 0x0001) > 0 { 1584 | return true 1585 | } 1586 | return false 1587 | } 1588 | 1589 | //BodyLen is a function for get body len 1590 | func (h *Header) BodyLen() int { 1591 | return int(h.Attr & 0x03ff) 1592 | } 1593 | 1594 | //MakeAttr is generate attr 1595 | func MakeAttr(verFlag byte, mut bool, enc byte, lens uint16) uint16 { 1596 | attr := lens & 0x03FF 1597 | 1598 | if verFlag > 0 { 1599 | attr = attr & 0x4000 1600 | } 1601 | 1602 | if mut { 1603 | attr = attr & 0x2000 1604 | } 1605 | 1606 | encMask := (uint16(enc) & 0x0007) << 10 1607 | return attr + encMask 1608 | } 1609 | 1610 | //Message is struct for message for jtt808 1611 | type Message struct { 1612 | HEADER Header 1613 | BODY []byte 1614 | } 1615 | 1616 | func Version() string { 1617 | return "1.0.0" 1618 | } 1619 | 1620 | func Name() string { 1621 | return "jtt808" 1622 | } 1623 | 1624 | //Filter is proto Filter api 1625 | func Filter(data []byte) ([]Message, int, error) { 1626 | var usedLen int = 0 1627 | msgList := make([]Message, 0) 1628 | var cnt int = 0 1629 | for { 1630 | //添加一个计数器,防止数据异常导致死循环 1631 | cnt++ 1632 | if cnt > 10 { 1633 | cnt = 0 1634 | return []Message{}, 0, fmt.Errorf("time too much") 1635 | } 1636 | if usedLen >= len(data) { 1637 | break 1638 | } 1639 | 1640 | msg, lens, err := filterSigle(data[usedLen:]) 1641 | if err != nil { 1642 | usedLen = usedLen + lens 1643 | fmt.Println("err:", err) 1644 | return msgList, usedLen, nil 1645 | } 1646 | usedLen = usedLen + lens 1647 | msgList = append(msgList, msg) 1648 | } 1649 | return msgList, usedLen, nil 1650 | } 1651 | 1652 | func filterSigle(data []byte) (Message, int, error) { 1653 | var usedLen int = 0 1654 | 1655 | startindex := bytes.IndexByte(data, ProtoHeader) 1656 | if startindex >= 0 { 1657 | usedLen = startindex + 1 1658 | endindex := bytes.IndexByte(data[usedLen:], ProtoHeader) 1659 | if endindex >= 0 { 1660 | endindex = endindex + usedLen 1661 | 1662 | msg, err := frameParser(data[startindex+1 : endindex]) 1663 | if err != nil { 1664 | return Message{}, endindex, err 1665 | } 1666 | 1667 | return msg, endindex + 1, nil 1668 | } 1669 | 1670 | return Message{}, startindex, fmt.Errorf("can't find end flag") 1671 | } 1672 | return Message{}, len(data), fmt.Errorf("can't find start flag") 1673 | } 1674 | 1675 | func Escape(data, oldBytes, newBytes []byte) []byte { 1676 | buff := make([]byte, 0) 1677 | 1678 | var startindex int = 0 1679 | 1680 | for startindex < len(data) { 1681 | index := bytes.Index(data[startindex:], oldBytes) 1682 | if index >= 0 { 1683 | buff = append(buff, data[startindex:index]...) 1684 | buff = append(buff, newBytes...) 1685 | startindex = index + len(oldBytes) 1686 | } else { 1687 | buff = append(buff, data[startindex:]...) 1688 | startindex = len(data) 1689 | } 1690 | } 1691 | return buff 1692 | } 1693 | 1694 | func frameParser(data []byte) (Message, error) { 1695 | if len(data)+2 < 17+3 { 1696 | return Message{}, fmt.Errorf("header is too short") 1697 | } 1698 | 1699 | //不包含帧头帧尾 1700 | frameData := Escape(data[:len(data)], []byte{0x7d, 0x02}, []byte{0x7e}) 1701 | frameData = Escape(frameData, []byte{0x7d, 0x01}, []byte{0x7d}) 1702 | 1703 | //之后的操作都是基于frameData来处理 1704 | rawcs := checkSum(frameData[:len(frameData)-1]) 1705 | 1706 | if rawcs != frameData[len(frameData)-1] { 1707 | return Message{}, fmt.Errorf("cs is not match:%d--%d", rawcs, frameData[len(frameData)-1]) 1708 | } 1709 | 1710 | var usedLen int = 0 1711 | var msg Message 1712 | msg.HEADER.MID = codec.Bytes2Word(frameData[usedLen:]) 1713 | usedLen = usedLen + 2 1714 | msg.HEADER.Attr = codec.Bytes2Word(frameData[usedLen:]) 1715 | usedLen = usedLen + 2 1716 | msg.HEADER.Version = frameData[usedLen] 1717 | usedLen = usedLen + 1 1718 | 1719 | tempPhone := bytes.TrimLeftFunc(frameData[usedLen:usedLen+10], func(r rune) bool { return r == 0x00 }) 1720 | msg.HEADER.PhoneNum = string(tempPhone) 1721 | usedLen = usedLen + 10 1722 | msg.HEADER.SeqNum = codec.Bytes2Word(frameData[usedLen:]) 1723 | usedLen = usedLen + 2 1724 | 1725 | if msg.HEADER.IsMulti() { 1726 | msg.HEADER.MutilFlag.MsgSum = codec.Bytes2Word(frameData[usedLen:]) 1727 | usedLen = usedLen + 2 1728 | msg.HEADER.MutilFlag.MsgIndex = codec.Bytes2Word(frameData[usedLen:]) 1729 | usedLen = usedLen + 2 1730 | } 1731 | 1732 | if len(frameData) < usedLen { 1733 | return Message{}, fmt.Errorf("flag code is too short") 1734 | } 1735 | 1736 | msg.BODY = make([]byte, len(frameData)-usedLen) 1737 | copy(msg.BODY, frameData[usedLen:len(frameData)]) 1738 | usedLen = len(frameData) 1739 | 1740 | return msg, nil 1741 | } 1742 | ``` 1743 | 1744 | ## 封包器 1745 | 1746 | 上面介绍了过滤器,过滤器实际就是一个能够处理粘包和拆包的解析器,和封包器的作用正好相反。但是封包器会很简单,因为封包没有粘包和拆包的处理。 1747 | 1748 | 代码如下: 1749 | 1750 | ```go 1751 | //Packer is proto Packer api 1752 | func Packer(msg Message) []byte { 1753 | data := make([]byte, 0) 1754 | tempbytes := codec.Word2Bytes(msg.HEADER.MID) 1755 | data = append(data, tempbytes...) 1756 | datalen := uint16(len(msg.BODY)) & 0x03FF 1757 | datalen = datalen | 0x4000 1758 | 1759 | tempbytes = utils.Word2Bytes(datalen) 1760 | data = append(data, tempbytes...) 1761 | 1762 | data = append(data, msg.HEADER.Version) 1763 | 1764 | if len(msg.HEADER.PhoneNum) < 10 { 1765 | data = append(data, make([]byte, 10-len(msg.HEADER.PhoneNum))...) 1766 | data = append(data, msg.HEADER.PhoneNum...) 1767 | } else { 1768 | data = append(data, msg.HEADER.PhoneNum[:10]...) 1769 | } 1770 | 1771 | tempbytes = utils.Word2Bytes(msg.HEADER.SeqNum) 1772 | data = append(data, tempbytes...) 1773 | 1774 | if msg.HEADER.IsMulti() { 1775 | data = append(data, utils.Word2Bytes(msg.HEADER.MutilFlag.MsgSum)...) 1776 | data = append(data, utils.Word2Bytes(msg.HEADER.MutilFlag.MsgIndex)...) 1777 | } 1778 | 1779 | data = append(data, msg.BODY...) 1780 | 1781 | csdata := byte(checkSum(data[:])) 1782 | data = append(data, csdata) 1783 | 1784 | //添加头尾 1785 | var tmpdata []byte = []byte{0x7e} 1786 | 1787 | for _, item := range data { 1788 | if item == 0x7d { 1789 | tmpdata = append(tmpdata, 0x7d, 0x01) 1790 | } else if item == 0x7e { 1791 | tmpdata = append(tmpdata, 0x7d, 0x02) 1792 | } else { 1793 | tmpdata = append(tmpdata, item) 1794 | } 1795 | } 1796 | tmpdata = append(tmpdata, 0x7e) 1797 | 1798 | return tmpdata 1799 | } 1800 | ``` 1801 | 1802 | ## 处理器 1803 | 1804 | 处理器用来处理接收到的有效TCP数据包,它应该是比过滤器更上层的一个模块。因为我们是用来管理TCP连接的,一个tcp连接代表着一个终端设备,这个终端设备有各种属性和操作逻辑,这些东西都是依附于TCP的长连接。我们单独定义一个包来组织这部分内容: 1805 | 1806 | ```go 1807 | package term 1808 | ``` 1809 | 1810 | 而我们的处理器就存在于这个包中。由于这个模块是tcp数据的实际处理模块,所以会牵扯到许多相关连的包,比如前面的codec、proto等,还有数据库的操作。 1811 | 1812 | 这一部分我们主要只介绍处理器的逻辑。前面我们说了,我们要处理的包有: 1813 | 1814 | - 平台通用应答 1815 | - 终端通用应答 1816 | - 终端注册 1817 | - 终端注册应答 1818 | - 终端鉴权 1819 | - 心跳 1820 | - 位置上报处理 1821 | 1822 | 通过proto的filter我们得到了各个Message,并且获取了其中的帧头信息,BODY部分还没有处理。而我们的codec正是用来处理BODY部分的编/解码器。 1823 | 1824 | 所以处理器的基本流程就是根据Message中Header信息,分别处理其Body数据,然后返回处理的结果。这个处理的结果往往就是需要响应的数据流。所以我们的处理器函数的样子大概就是这样的: 1825 | 1826 | ```go 1827 | func (t *Terminal) Handler(msg proto.Message) []byte{ 1828 | 1829 | } 1830 | ``` 1831 | 1832 | 传入一个Message,入后输出需要响应的数据,如果返回nil则表明没有数据需要响应。 1833 | 1834 | 其中Terminal这个结构体我们在**后端模型**这个装接中有提及到: 1835 | 1836 | ```go 1837 | type Terminal struct { 1838 | authkey string 1839 | imei string 1840 | iccid string 1841 | vin string 1842 | tboxver string 1843 | loginTime time.Time 1844 | seqNum uint16 1845 | phoneNum []byte 1846 | Conn net.Conn 1847 | Engine *xorm.Engine 1848 | Ch chan int 1849 | } 1850 | ``` 1851 | 1852 | 同时为了使用codec的序列化和反序列化,我们还需要定义如下结构体: 1853 | 1854 | ```go 1855 | type TermAckBody struct { 1856 | AckSeqNum uint16 1857 | AckID uint16 1858 | AckResult uint8 1859 | } 1860 | 1861 | type PlatAckBody struct { 1862 | AckSeqNum uint16 1863 | AckID uint16 1864 | AckResult uint8 1865 | } 1866 | 1867 | type RegisterBody struct { 1868 | ProID uint16 1869 | CityID uint16 1870 | ManufID []byte `len:"11"` 1871 | TermType []byte `len:"30"` 1872 | TermID []byte `len:"30"` 1873 | LicPlateColor uint8 1874 | LicPlate string 1875 | } 1876 | 1877 | type RegisterAckBody struct { 1878 | AckSeqNum uint16 1879 | AckResult uint8 1880 | AuthKey string 1881 | } 1882 | 1883 | type AuthBody struct { 1884 | AuthKeyLen uint8 1885 | AuthKey string 1886 | Imei []byte `len:"15"` 1887 | Version []byte `len:"20"` 1888 | } 1889 | 1890 | type GPSInfoBody struct { 1891 | WarnFlag uint32 1892 | State uint32 1893 | Lat uint32 1894 | Lng uint32 1895 | Alt uint16 1896 | Speed uint16 1897 | Dir uint16 1898 | Time []byte `len:"6"` 1899 | } 1900 | 1901 | type CtrlBody struct { 1902 | Cmd uint8 1903 | Param string 1904 | } 1905 | ``` 1906 | 1907 | 下面就来正式讲解Handler的实现。 1908 | 1909 | 首先获取保存Header中的电话号和流水号到Terminal中: 1910 | 1911 | ```go 1912 | if t.phoneNum == nil { 1913 | t.phoneNum = make([]byte, 10) 1914 | } 1915 | 1916 | copy(t.phoneNum, []byte(msg.HEADER.PhoneNum)) 1917 | t.seqNum = msg.HEADER.SeqNum 1918 | ``` 1919 | 1920 | 然后通过switch来匹配消息id,并对其body部分做相关处理: 1921 | 1922 | ```go 1923 | switch msg.HEADER.MID { 1924 | case proto.TermAck: 1925 | // 1926 | case proto.Register: 1927 | // 1928 | case proto.Login: 1929 | // 1930 | case proto.Heartbeat: 1931 | // 1932 | case proto.Gpsinfo: 1933 | // 1934 | } 1935 | return nil 1936 | ``` 1937 | 1938 | 我们先说注册,我们使用帧头中的手机号,在数据库中查找对应的鉴权码。然后从msg中获取body部分,通过codec反序列话得到RegisterBody实例。为了简单,我们此处不做其他数据验证,直接做出数据响应即可。生成需要响应的RegisterAckBody实例,然后序列化为body切片,然后生成响应的Message,再通过封包器封包为数据流返回: 1939 | 1940 | ```go 1941 | devinfo := new(DevInfo) 1942 | 1943 | devinfo.PhoneNum = strings.TrimLeft(utils.HexBuffToString(t.phoneNum), "0") 1944 | 1945 | is, _ := t.Engine.Get(devinfo) 1946 | if !is { 1947 | return []byte{} 1948 | } 1949 | 1950 | var reg RegisterBody 1951 | _, err := codec.Unmarshal(msg.BODY, ®) 1952 | if err != nil { 1953 | fmt.Println("err:", err) 1954 | } 1955 | 1956 | var body []byte 1957 | body, err = codec.Marshal(&RegisterAckBody{ 1958 | AckSeqNum: msg.HEADER.SeqNum, 1959 | AckResult: 0, 1960 | AuthKey: devinfo.Authkey, 1961 | }) 1962 | if err != nil { 1963 | fmt.Println("err:", err) 1964 | } 1965 | 1966 | msgAck := proto.Message{ 1967 | HEADER: proto.Header{ 1968 | MID: proto.RegisterAck, 1969 | Attr: proto.MakeAttr(1, false, 0, uint16(len(body))), 1970 | Version: 1, 1971 | PhoneNum: string(t.phoneNum), 1972 | SeqNum: t.seqNum, 1973 | }, 1974 | BODY: body, 1975 | } 1976 | return proto.Packer(msgAck) 1977 | ``` 1978 | 1979 | 上面有涉及到数据库的查询操作,这部分使用了xorm,具体的参考xorm官方文档:[xorm官方文档](http://gobook.io/read/gitea.com/xorm/manual-zh-CN/) 1980 | 1981 | 上面涉及一个utils.HexBuffToString函数,这个函数会将字符串转换为16进制格式的字符串,本身是基于strconv.FormatUint(uint64(value), 16)完成的,但是这个函数会没有办法指定转换后的填充值,比如0x0A会直接转换成"A"而不是"0A",所以需要做一点特殊处理: 1982 | 1983 | ```go 1984 | func HexBuffToString(hex []byte) string { 1985 | var ret string 1986 | for _, value := range hex { 1987 | str := strconv.FormatUint(uint64(value), 16) 1988 | if len([]rune(str)) == 1 { 1989 | ret = ret + "0" + str 1990 | } else { 1991 | ret = ret + str 1992 | } 1993 | } 1994 | return ret 1995 | } 1996 | ``` 1997 | 1998 | Handler其他部分的流程大体差不多,就不做过多讲解了,完整代码: 1999 | 2000 | ```go 2001 | //Handler is proto Handler api 2002 | func (t *Terminal) Handler(msg proto.Message) []byte { 2003 | if t.phoneNum == nil { 2004 | t.phoneNum = make([]byte, 10) 2005 | } 2006 | 2007 | copy(t.phoneNum, []byte(msg.HEADER.PhoneNum)) 2008 | t.seqNum = msg.HEADER.SeqNum 2009 | 2010 | switch msg.HEADER.MID { 2011 | case proto.TermAck: 2012 | reqID := codec.Bytes2Word(msg.BODY[2:4]) 2013 | if reqID == proto.UpdateReq { 2014 | //ch <- 1 2015 | //升级命令 2016 | } 2017 | case proto.Register: 2018 | devinfo := new(DevInfo) 2019 | 2020 | devinfo.PhoneNum = strings.TrimLeft(utils.HexBuffToString(t.phoneNum), "0") 2021 | 2022 | is, _ := t.Engine.Get(devinfo) 2023 | if !is { 2024 | return []byte{} 2025 | } 2026 | 2027 | var reg RegisterBody 2028 | _, err := codec.Unmarshal(msg.BODY, ®) 2029 | if err != nil { 2030 | fmt.Println("err:", err) 2031 | } 2032 | 2033 | var body []byte 2034 | body, err = codec.Marshal(&RegisterAckBody{ 2035 | AckSeqNum: msg.HEADER.SeqNum, 2036 | AckResult: 0, 2037 | AuthKey: devinfo.Authkey, 2038 | }) 2039 | if err != nil { 2040 | fmt.Println("err:", err) 2041 | } 2042 | 2043 | msgAck := proto.Message{ 2044 | HEADER: proto.Header{ 2045 | MID: proto.RegisterAck, 2046 | Attr: proto.MakeAttr(1, false, 0, uint16(len(body))), 2047 | Version: 1, 2048 | PhoneNum: string(t.phoneNum), 2049 | SeqNum: t.seqNum, 2050 | }, 2051 | BODY: body, 2052 | } 2053 | return proto.Packer(msgAck) 2054 | case proto.Login: 2055 | var auth AuthBody 2056 | _, err := codec.Unmarshal(msg.BODY, &auth) 2057 | if err != nil { 2058 | fmt.Println("err:", err) 2059 | } 2060 | t.authkey = auth.AuthKey 2061 | t.imei = string(auth.Imei) 2062 | t.tboxver = string(auth.Version) 2063 | 2064 | var body []byte 2065 | body, err = codec.Marshal(&PlatAckBody{ 2066 | AckSeqNum: msg.HEADER.SeqNum, 2067 | AckID: msg.HEADER.MID, 2068 | AckResult: 0, 2069 | }) 2070 | if err != nil { 2071 | fmt.Println("err:", err) 2072 | } 2073 | 2074 | msgAck := proto.Message{ 2075 | HEADER: proto.Header{ 2076 | MID: proto.PlatAck, 2077 | Attr: proto.MakeAttr(1, false, 0, uint16(len(body))), 2078 | Version: 1, 2079 | PhoneNum: string(t.phoneNum), 2080 | SeqNum: t.seqNum, 2081 | }, 2082 | BODY: body, 2083 | } 2084 | return proto.Packer(msgAck) 2085 | case proto.Heartbeat: 2086 | var err error 2087 | var body []byte 2088 | body, err = codec.Marshal(&PlatAckBody{ 2089 | AckSeqNum: msg.HEADER.SeqNum, 2090 | AckID: msg.HEADER.MID, 2091 | AckResult: 0, 2092 | }) 2093 | if err != nil { 2094 | fmt.Println("err:", err) 2095 | } 2096 | 2097 | msgAck := proto.Message{ 2098 | HEADER: proto.Header{ 2099 | MID: proto.PlatAck, 2100 | Attr: proto.MakeAttr(1, false, 0, uint16(len(body))), 2101 | Version: 1, 2102 | PhoneNum: string(t.phoneNum), 2103 | SeqNum: t.seqNum, 2104 | }, 2105 | BODY: body, 2106 | } 2107 | return proto.Packer(msgAck) 2108 | case proto.Gpsinfo: 2109 | var gpsInfo GPSInfoBody 2110 | _, err := codec.Unmarshal(msg.BODY, &gpsInfo) 2111 | if err != nil { 2112 | fmt.Println("err:", err) 2113 | } 2114 | 2115 | gpsdata := new(GPSData) 2116 | gpsdata.Imei = t.imei 2117 | gpsdata.Stamp = time.Now() 2118 | gpsdata.WarnFlag = gpsInfo.WarnFlag 2119 | gpsdata.State = gpsInfo.State 2120 | gpsdata.Latitude = gpsInfo.Lat 2121 | gpsdata.Longitude = gpsInfo.Lng 2122 | 2123 | gpsdata.Altitude = gpsInfo.Alt 2124 | gpsdata.Speed = gpsInfo.Speed 2125 | gpsdata.Direction = gpsInfo.Dir 2126 | 2127 | if (gpsdata.State & 0x00000001) > 0 { 2128 | gpsdata.AccState = 1 2129 | } else { 2130 | gpsdata.AccState = 0 2131 | } 2132 | 2133 | if (gpsdata.State & 0x00000002) > 0 { 2134 | gpsdata.GpsState = 1 2135 | } else { 2136 | gpsdata.GpsState = 0 2137 | } 2138 | 2139 | _, err = t.Engine.Insert(gpsdata) 2140 | if err != nil { 2141 | fmt.Println("insert gps err:", err) 2142 | } 2143 | 2144 | var body []byte 2145 | body, err = codec.Marshal(&PlatAckBody{ 2146 | AckSeqNum: msg.HEADER.SeqNum, 2147 | AckID: msg.HEADER.MID, 2148 | AckResult: 0, 2149 | }) 2150 | if err != nil { 2151 | fmt.Println("err:", err) 2152 | } 2153 | 2154 | msgAck := proto.Message{ 2155 | HEADER: proto.Header{ 2156 | MID: proto.PlatAck, 2157 | Attr: proto.MakeAttr(1, false, 0, uint16(len(body))), 2158 | Version: 1, 2159 | PhoneNum: string(t.phoneNum), 2160 | SeqNum: t.seqNum, 2161 | }, 2162 | BODY: body, 2163 | } 2164 | return proto.Packer(msgAck) 2165 | } 2166 | 2167 | return nil 2168 | } 2169 | ``` 2170 | 2171 | ## 数据库操作 2172 | 2173 | 数据库部分我使用postgresql,为了简化上层调用,我又使用了xorm。 2174 | 2175 | 库导入 2176 | 2177 | ```go 2178 | import ( 2179 | "github.com/go-xorm/xorm" 2180 | _ "github.com/lib/pq" 2181 | ) 2182 | ``` 2183 | 2184 | 创建一个全局引擎指针 2185 | 2186 | ```go 2187 | var engine *xorm.Engine 2188 | ``` 2189 | 2190 | 定义一个初始化函数,用来初始化数据库相关的一些内容: 2191 | 2192 | ```go 2193 | func xormInit(driverName string, dataSourceName string) (*xorm.Engine, error) { 2194 | 2195 | } 2196 | ``` 2197 | 2198 | 数据库中,我们需要操作四个数据库表:users、log_frame、dev_info和gps_data。根据golang和xorm的映射关系,我们先创建对应的四个结构体。因为dev_info和gps_data是提供给term模块使用的,所以这两个结构体在term包中 2199 | 2200 | ```go 2201 | type Users struct { 2202 | Id int `xorm:"pk autoincr notnull id"` 2203 | Name string `xorm:"name"` 2204 | Password string `xorm:"password"` 2205 | IsAdmin bool `xorm:"admin"` 2206 | Stamp time.Time `xorm:"stamp"` 2207 | } 2208 | 2209 | type LogFrame struct { 2210 | Id int `xorm:"pk autoincr notnull id"` 2211 | Stamp time.Time `xorm:"DateTime notnull 'stamp'"` 2212 | Dir int `xorm:"dir"` 2213 | Frame string `xorm:"Varchar(2048) frame"` 2214 | } 2215 | ``` 2216 | 2217 | ```go 2218 | type DevInfo struct { 2219 | Authkey string `xorm:"auth_key"` 2220 | Imei string `xorm:"imei"` 2221 | Vin string `xorm:"vin"` 2222 | PhoneNum string `xorm:"pk notnull phone_num"` 2223 | ProvId uint16 `xorm:"prov_id"` 2224 | CityId uint16 `xorm:"city_id"` 2225 | Manuf string `xorm:"manuf"` 2226 | TermType string `xorm:"term_type"` 2227 | TermId string `xorm:"term_id"` 2228 | PlateColor int `xorm:"plate_color"` 2229 | PlateNum string `xorm:"plate_num"` 2230 | } 2231 | 2232 | func (d DevInfo) TableName() string { 2233 | return "dev_info" 2234 | } 2235 | 2236 | type GPSData struct { 2237 | Imei string `xorm:"pk notnull imei` 2238 | Stamp time.Time `xorm:"DateTime pk notnull stamp` 2239 | WarnFlag uint32 `xorm:"warnflag"` 2240 | State uint32 `xorm:"state"` 2241 | AccState uint8 `xorm:"accstate"` 2242 | GpsState uint8 `xorm:"gpsstate"` 2243 | Latitude uint32 `xorm:"latitude"` 2244 | Longitude uint32 `xorm:"longitude"` 2245 | Altitude uint16 `xorm:"altitude"` 2246 | Speed uint16 `xorm:"speed"` 2247 | Direction uint16 `xorm:"direction"` 2248 | } 2249 | 2250 | func (d GPSData) TableName() string { 2251 | return "gps_data" 2252 | } 2253 | ``` 2254 | 2255 | 我们可以通过结构体的TableName方法来指定数据库表名 2256 | 2257 | 初始化xorm引擎 2258 | 2259 | ```go 2260 | var err error 2261 | engine, err = xorm.NewEngine(driverName, dataSourceName) 2262 | if err != nil { 2263 | return engine, err 2264 | } 2265 | ``` 2266 | 2267 | 同步数据表结构: 2268 | 2269 | ```go 2270 | users := new(Users) 2271 | err = engine.Sync2(users) 2272 | if err != nil { 2273 | return engine, err 2274 | } 2275 | 2276 | logframe := new(LogFrame) 2277 | err = engine.Sync2(logframe) 2278 | if err != nil { 2279 | return engine, err 2280 | } 2281 | 2282 | gpsdata := new(term.GPSData) 2283 | err = engine.Sync2(gpsdata) 2284 | if err != nil { 2285 | return engine, err 2286 | } 2287 | 2288 | devinfo := new(term.DevInfo) 2289 | err = engine.Sync2(devinfo) 2290 | if err != nil { 2291 | return engine, err 2292 | } 2293 | ``` 2294 | 2295 | Sync2会同步结构体到实际的数据库表上面,如果表不存在,就会自动创建,如果结构体中新增了字段,数据库中也会增加相应字段。 2296 | 2297 | 调用: 2298 | 2299 | ```go 2300 | engine, err = xormInit("postgres", "postgres://pqgotest:pqgotest@localhost/pqgodb?sslmode=require") 2301 | ``` 2302 | 2303 | 连接一个postgres数据库,地址为localhost,用户名为pqgotest,密码为pqgotest,数据库名为pqgodb,后面的sslmode为ssl模式,具体细节网上很多教程,此处不展开。 2304 | 2305 | 数据库的插入操作主要使用xorm的Insert函数,查询使用xorm的Find和Get。查询可以配合Where等函数使用。具体参考[xorm官方文档](http://gobook.io/read/gitea.com/xorm/manual-zh-CN/) 2306 | 2307 | 2308 | ## TCP服务器 2309 | 2310 | 前面的所有内容都是为了实现一个基于jtt808协议的TCP服务器而做的工作,我们现在需要吧上面讲解的内容整合起来,让各个模块协调工作,完成一整套的服务流程。 2311 | 2312 | ### 配置加载 2313 | 2314 | 先说一下配置加载,对于一个应用程序,为了增加其灵活性,不可避免的需要使用配置文件。综合个方面考虑,我选择使用toml格式的配置文件,解析库使用"github.com/BurntSushi/toml" 2315 | 2316 | ```toml 2317 | # config 2318 | 2319 | [tcp] 2320 | ip = "" 2321 | port = 19903 2322 | 2323 | [web] 2324 | ip = "" 2325 | port = 8080 2326 | 2327 | [map] 2328 | appKey = "your baidu map key" 2329 | 2330 | [postgresql] 2331 | hostname="localhost" 2332 | tablename="pqgodb" 2333 | user="pqgotest" 2334 | password="pqgotest" 2335 | ``` 2336 | 2337 | 配置包括了整个应用的配置,定义配置对应的结构体: 2338 | 2339 | ```go 2340 | type Config struct { 2341 | TcpCfg TcpConfig `toml:"tcp"` 2342 | WebCfg WebConfig `toml:"web"` 2343 | MapCfg MapConfig `toml:"map"` 2344 | PgCfg PgConfig `toml:"postgresql"` 2345 | } 2346 | 2347 | type TcpConfig struct { 2348 | Ip string 2349 | Port int 2350 | } 2351 | 2352 | type WebConfig struct { 2353 | Ip string 2354 | Port int 2355 | } 2356 | 2357 | type MapConfig struct { 2358 | AppKey string 2359 | } 2360 | 2361 | type PgConfig struct { 2362 | Hostname string 2363 | Tablename string 2364 | User string 2365 | Password string 2366 | } 2367 | 2368 | var config Config 2369 | ``` 2370 | 2371 | 配置加载 2372 | 2373 | ```go 2374 | curpath := GetCurrentDirectory() 2375 | _, err = toml.DecodeFile(curpath+"/config.toml", &config) 2376 | if err != nil { 2377 | log.Info("config error: ", err) 2378 | return 2379 | } 2380 | ``` 2381 | 2382 | 其中GetCurrentDirectory获取当前应用的绝对路径 2383 | 2384 | ```go 2385 | func GetCurrentDirectory() string { 2386 | dir, err := filepath.Abs(filepath.Dir(os.Args[0])) 2387 | if err != nil { 2388 | log.Fatal(err) 2389 | return "" 2390 | } 2391 | return strings.Replace(dir, "\\", "/", -1) //将\替换成/ 2392 | } 2393 | ``` 2394 | 2395 | ### TCP连接管理 2396 | 2397 | 这一部分基本跟前面后端模型中的讲解一致。 2398 | 2399 | ```go 2400 | var connManger map[string]*term.Terminal 2401 | 2402 | connManger = make(map[string]*term.Terminal) 2403 | ``` 2404 | 2405 | 创建服务器还是listen和accept 2406 | 2407 | ```go 2408 | address := config.TcpCfg.Ip + ":" + strconv.FormatInt(int64(config.TcpCfg.Port), 10) 2409 | log.Info("address port ", address) 2410 | 2411 | listenSock, err := net.Listen("tcp", address) 2412 | if err != nil { 2413 | log.WithFields(logrus.Fields{"Error:": err.Error()}).Error("check") 2414 | os.Exit(1) 2415 | } 2416 | 2417 | defer listenSock.Close() 2418 | 2419 | for { 2420 | newConn, err := listenSock.Accept() 2421 | if err != nil { 2422 | continue 2423 | } 2424 | 2425 | go recvConnMsg(newConn) 2426 | } 2427 | ``` 2428 | 2429 | recvConnMsg处理TCP客户端数据接收 2430 | 2431 | ```go 2432 | func recvConnMsg(conn net.Conn){ 2433 | 2434 | } 2435 | ``` 2436 | 2437 | 首先创建一个Terminal并添加到connManager中 2438 | 2439 | ```go 2440 | buf := make([]byte, 0) 2441 | addr := conn.RemoteAddr() 2442 | log.WithFields(logrus.Fields{"network": addr.Network(), "ip": addr.String()}).Info("recv") 2443 | 2444 | var t *term.Terminal = &term.Terminal{ 2445 | Conn: conn, 2446 | Engine: engine, 2447 | Ch: make(chan int), 2448 | } 2449 | connManger[addr.String()] = t 2450 | ipaddress = addr.String() 2451 | ``` 2452 | 2453 | 如果该TCP断开的话,需要从connManager中删除对应的元素。 2454 | 2455 | ```go 2456 | defer func() { 2457 | delete(connManger, addr.String()) 2458 | conn.Close() 2459 | }() 2460 | ``` 2461 | 2462 | 之后就是循环读取数据然后处理数据了: 2463 | 2464 | ```go 2465 | for { 2466 | tempbuf := make([]byte, 1024) 2467 | } 2468 | ``` 2469 | 2470 | 读取数据,这个数据要与上次没处理完的数据进行拼接,因为可能有粘包拆包的情况 2471 | 2472 | ```go 2473 | n, err := conn.Read(tempbuf) 2474 | 2475 | if err != nil { 2476 | log.WithFields(logrus.Fields{"network": addr.Network(), "ip": addr.String()}).Info("closed") 2477 | return 2478 | } 2479 | 2480 | buf = append(buf, tempbuf[:n]...) 2481 | var outlog string 2482 | for _, val := range buf { 2483 | outlog += fmt.Sprintf("%02X", val) 2484 | } 2485 | log.WithFields(logrus.Fields{"data": outlog}).Info("<--- ") 2486 | 2487 | logframe := new(LogFrame) 2488 | logframe.Stamp = time.Now() 2489 | logframe.Dir = 0 2490 | logframe.Frame = outlog 2491 | _, err = engine.Insert(logframe) 2492 | if err != nil { 2493 | log.WithFields(logrus.Fields{"error": err.Error()}).Info("insert") 2494 | } 2495 | ``` 2496 | 2497 | 调用过滤器对接收的数据进行处理,并对已经使用的字节进行偏移: 2498 | 2499 | ```go 2500 | var msg []proto.Message 2501 | var lens int 2502 | msg, lens, err = proto.Filter(buf) 2503 | if err != nil { 2504 | // 2505 | } 2506 | 2507 | buf = buf[lens:] 2508 | ``` 2509 | 2510 | msg是一个消息切片,对这个切片中的消息进行循环处理,直到全部处理完为止: 2511 | 2512 | ```go 2513 | for len(msg) > 0 { 2514 | //处理消息 2515 | sendBuf := t.Handler(msg[0]) 2516 | 2517 | if sendBuf != nil { 2518 | outlog = "" 2519 | for _, val := range sendBuf { 2520 | outlog += fmt.Sprintf("%02X", val) 2521 | } 2522 | log.WithFields(logrus.Fields{"data": outlog}).Info("---> ") 2523 | 2524 | logframe := &LogFrame{ 2525 | Stamp: time.Now(), 2526 | Dir: 1, 2527 | Frame: outlog, 2528 | } 2529 | _, err = engine.Insert(logframe) 2530 | if err != nil { 2531 | log.WithFields(logrus.Fields{"error": err.Error()}).Info("insert") 2532 | } 2533 | 2534 | conn.Write(sendBuf) 2535 | 2536 | msg = msg[1:] 2537 | } 2538 | } 2539 | ``` 2540 | 2541 | 消息处理调用了处理器,处理器会返回需要响应的数据,将处理器返回的数据通过conn.Write发送给对应的tcp就响应成功。 2542 | 2543 | 这就是整个tcp服务器的基本模型了。 2544 | 2545 | 运行起来的实际日志情况: 2546 | 2547 | ``` 2548 | time="2019-12-31T17:53:52+08:00" level=info msg="address port :8080" 2549 | [GIN-debug] Listening and serving HTTP on :8080 2550 | time="2019-12-31T17:54:01+08:00" level=info msg=recv ip="127.0.0.1:38936" network=tcp 2551 | time="2019-12-31T17:54:01+08:00" level=info msg="<--- " data=7E0102402C018986041210187042775504B0083572747333323573383635353031303433393534363737312E313700000000000000000000000000000000FB7E 2552 | time="2019-12-31T17:54:01+08:00" level=info msg="---> " data=7E80014005018986041210187042775504B004B0010200C77E 2553 | time="2019-12-31T17:54:03+08:00" level=info msg="<--- " data=7E0200401C018986041210187042775504B100000000000000030158A08A06CA2B3B01270000003F191231175400277E 2554 | time="2019-12-31T17:54:03+08:00" level=info msg="---> " data=7E80014005018986041210187042775504B104B1020000C67E 2555 | time="2019-12-31T17:54:05+08:00" level=info msg="<--- " data=7E0200401C018986041210187042775504B200000000000000030158A08506CA2B3C012A000000151912311754050E7E 2556 | time="2019-12-31T17:54:06+08:00" level=info msg="---> " data=7E80014005018986041210187042775504B204B2020000C67E 2557 | time="2019-12-31T17:54:08+08:00" level=info msg="<--- " data=7E0200401C018986041210187042775504B300000000000000030158A08A06CA2B3B01270000003F191231175355777E 2558 | time="2019-12-31T17:54:08+08:00" level=info msg="---> " data=7E80014005018986041210187042775504B304B3020000C67E 2559 | time="2019-12-31T17:54:11+08:00" level=info msg="<--- " data=7E0200401C018986041210187042775504B400000000000000030158A08206CA2B39013100000015191231175410047E 2560 | time="2019-12-31T17:54:11+08:00" level=info msg="---> " data=7E80014005018986041210187042775504B404B4020000C67E 2561 | time="2019-12-31T17:54:13+08:00" level=info msg="<--- " data=7E00024000018986041210187042775504B5F37E 2562 | time="2019-12-31T17:54:13+08:00" level=info msg="---> " data=7E80014005018986041210187042775504B504B5000200C67E 2563 | time="2019-12-31T17:54:16+08:00" level=info msg="<--- " data=7E0200401C018986041210187042775504B600000000000000030158A07D0206CA2B3E012E00000015191231175415E77E 2564 | time="2019-12-31T17:54:16+08:00" level=info msg="---> " data=7E80014005018986041210187042775504B604B6020000C67E 2565 | time="2019-12-31T17:54:21+08:00" level=info msg="<--- " data=7E0200401C018986041210187042775504B700000000000000030158A07D0206CA2B3E012E00000015191231175420D37E 2566 | time="2019-12-31T17:54:21+08:00" level=info msg="---> " data=7E80014005018986041210187042775504B704B7020000C67E 2567 | time="2019-12-31T17:54:26+08:00" level=info msg="<--- " data=7E0200401C018986041210187042775504B800000000000000030158A07D0206CA2B3E012E00000015191231175425D97E 2568 | time="2019-12-31T17:54:26+08:00" level=info msg="---> " data=7E80014005018986041210187042775504B804B8020000C67E 2569 | time="2019-12-31T17:54:31+08:00" level=info msg="<--- " data=7E0200401C018986041210187042775504B900000000000000030158A07D0206CA2B3E013100000015191231175430D27E 2570 | time="2019-12-31T17:54:31+08:00" level=info msg="---> " data=7E80014005018986041210187042775504B904B9020000C67E 2571 | time="2019-12-31T17:54:36+08:00" level=info msg="<--- " data=7E0200401C018986041210187042775504BA00000000000000030158A07D0206CA2B3D013400000015191231175435D27E 2572 | time="2019-12-31T17:54:36+08:00" level=info msg="---> " data=7E80014005018986041210187042775504BA04BA020000C67E 2573 | time="2019-12-31T17:54:41+08:00" level=info msg="<--- " data=7E0200401C018986041210187042775504BB00000000000000030158A07D0206CA2B3D013400000015191231175440A67E 2574 | time="2019-12-31T17:54:41+08:00" level=info msg="---> " data=7E80014005018986041210187042775504BB04BB020000C67E 2575 | time="2019-12-31T17:54:42+08:00" level=info msg="<--- " data=7E00024000018986041210187042775504BCFA7E 2576 | time="2019-12-31T17:54:42+08:00" level=info msg="---> " data=7E80014005018986041210187042775504BC04BC000200C67E 2577 | ``` 2578 | 2579 | -------------------------------------------------------------------------------- /doc/img/devices_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiuzhiqian/etc_tsp/d98dd88144e701061ece9ebf72ac5e68beafaafc/doc/img/devices_1.png -------------------------------------------------------------------------------- /doc/img/login_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiuzhiqian/etc_tsp/d98dd88144e701061ece9ebf72ac5e68beafaafc/doc/img/login_1.png -------------------------------------------------------------------------------- /doc/img/map_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiuzhiqian/etc_tsp/d98dd88144e701061ece9ebf72ac5e68beafaafc/doc/img/map_1.png -------------------------------------------------------------------------------- /doc/img/monitor_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiuzhiqian/etc_tsp/d98dd88144e701061ece9ebf72ac5e68beafaafc/doc/img/monitor_1.png -------------------------------------------------------------------------------- /doc/img/users_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiuzhiqian/etc_tsp/d98dd88144e701061ece9ebf72ac5e68beafaafc/doc/img/users_1.png -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # tsp 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn run build 16 | ``` 17 | 18 | ### Run your tests 19 | ``` 20 | yarn run test 21 | ``` 22 | 23 | ### Lints and fixes files 24 | ``` 25 | yarn run lint 26 | ``` 27 | 28 | ### Customize configuration 29 | See [Configuration Reference](https://cli.vuejs.org/config/). 30 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsp", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.19.0", 12 | "core-js": "^3.4.3", 13 | "view-design": "^4.0.2", 14 | "vue": "^2.6.10", 15 | "vue-axios": "^2.1.5", 16 | "vue-baidu-map": "^0.21.22", 17 | "vue-router": "^3.1.3", 18 | "vuex": "^3.1.2" 19 | }, 20 | "devDependencies": { 21 | "@vue/cli-plugin-babel": "^4.1.0", 22 | "@vue/cli-plugin-eslint": "^4.1.0", 23 | "@vue/cli-service": "^4.1.0", 24 | "babel-eslint": "^10.0.3", 25 | "eslint": "^5.16.0", 26 | "eslint-plugin-vue": "^5.0.0", 27 | "vue-template-compiler": "^2.6.10" 28 | }, 29 | "eslintConfig": { 30 | "root": true, 31 | "env": { 32 | "node": true 33 | }, 34 | "extends": [ 35 | "plugin:vue/essential", 36 | "eslint:recommended" 37 | ], 38 | "rules": {}, 39 | "parserOptions": { 40 | "parser": "babel-eslint" 41 | } 42 | }, 43 | "browserslist": [ 44 | "> 1%", 45 | "last 2 versions" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiuzhiqian/etc_tsp/d98dd88144e701061ece9ebf72ac5e68beafaafc/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 |