一、前言
最近需要做一个嵌入式系统显示地图的项目,百度地图给我们留出了API接口可以调用。百度地图API网址为:https://lbsyun.baidu.com
之前已经有同事做好了地图获取的程序,但是显示的位置和实际位置大概有1km的偏差,上网查阅各种资料,试过各种经纬度转换的函数,最后得到的结果都很差。最后在百度地图常见问题的一栏中见到了以下内容:
国际经纬度坐标标准为WGS-84,国内必须至少使用国测局制定的GCJ-02,对地理位置进行首次加密。百度坐标在此基础上,进行了BD-09二次加密措施,更加保护了个人隐私。百度对外接口的坐标系并不是GPS采集的真实经纬度,需要通过坐标转换接口进行转换。”
根据以上信息可以知道,既然是百度地图官方加密,那么想要得到正确的百度地图坐标,就只能通过百度地图给出的坐标转换接口进行转换。其他任何民间的转换方法都不可靠,因为就算有人找到了转换方法,百度地图官方也可以改。
二、启动百度地图服务,获取转换过后的经纬度
点击菜单栏的“开发文档”->“服务接口”->“Web服务API”,
在左侧服务选择栏中点击“坐标转换”,如下图:
这里就是坐标转换的服务介绍。
在使用服务之前,要先完成“登录百度账号”->“申请成为百度开发者”->“获取服务秘钥(ak)”,在官方网页上有指引教程。
或者登录百度账号后,在“控制台”->“应用管理”->“我的应用”中点击“创建应用”,如下图:
在弹出的界面中应用名称名字随便取,应用类型选浏览器端,启用服务里面的”静态图”和“坐标转换”都要勾选(本次只使用坐标转换服务,以后会用到静态图),Referer白名单里写一个“*”即可,然后点击提交。
这时在应用列表中就能看到刚才创建的应用了, 并且有一个AK,如下图:
将这个AK复制下来,在网页中打开“坐标转换”的“服务文档”,这里有一个网址,如下图:
里面的参数都有介绍,但一般不用改,只要将ak填入自己的ak,并且经纬度填入自己用GPS/北斗模块获取到的经纬度即可,然后将这一串网址复制到浏览器中打开即可。浏览器会直接显示返回结果,如下:
{"status":0,"result":[{"x":108.99576850316559,"y":34.38357890574941}]}
这是一个json的数据格式,status结果为0表示转换成功,result中的x表示的就是转换后的经度,y表示的就是转换后的纬度。
有些人可能会问,为什么没有指定是东半球还是西半球,没有指定是南半球还是北半球,我只能说没必要。如果想获取境外地图,百度地图有境外地图的服务。
三、通过wireshark抓取HTTP数据包
上节只是通过浏览器得到了转换后的坐标,如果要在linux下编程实现,需要将自己“冒充”为浏览器。
首先,我们需要知道,浏览器到底给谁发送了什么东西,这个时候就要用到一个非常强大的网络抓包工具:wireshark。另外,在浏览器中输入的网址是以“https”开头的,这是加密过的,就算把包抓出来也看不出里面的内容。直接删掉“https”中的“s”,将删掉“s”的网址放到浏览器中照样能够得到转换后的经纬度,但是这样就是明文传输了,我们就可以看出里面的数据了。
打开wireshark,双击正在上网的网卡(我的是WLAN 5),如下图:
这时wireshark抓出来很多包,我们需要设置一下过滤规则,在过滤器中输入“http”后按回车,如下图:
一下子过滤掉很多数据,这时在浏览器中输入坐标转换的网址,注意要把前面的https改为http,当浏览器得到转换结果之后停止wireshark的抓包,这时wireshark抓到很多包,如下图:
从抓到的数据中很容易分辨出哪些是和百度地图相关的数据包,这样我们就知道了目标IP地址为220.181.43.101,为了再次过滤掉不必要的干扰,在过滤规则里加上ip.addr==220.181.43.101,这样就只剩和百度地图相关的数据了,如下图:
任选一条,右键->追踪流->HTTP流,如下图:
在弹出的界面中能够看到,红色区域是源(自己)向目标(百度地图)发出的数据,蓝色区域是目标(百度地图)向源(自己)返回的数据,其中就有我们想要的转换过后的数据,如下图:
按照上图中红色部分发送的请求包编写程序(因为第1段红色部分发出去之后,百度地图就返回了转换过后的经纬度,因此不需要再发送第2段红色部分的数据了,然鹅我也不知道第二段红色部分数据发出去是为了什么)。特别注意,数据中换行的地方是真的换行符,windows中的换行符为“\r\n”,并且最后一行是个空行,空行也必须发出去。发送的内容是HTTP请求包,想知道包里各部分代表什么意义可以去搜索“HTTP请求报文格式”。
编写的程序是将接收到的报文打印出来,具体代码先不放出来, 最后会放出完整版代码。程序运行结果如下:
可以看到,接收到的数据前面部分和抓包抓出来的数据一样,最后那部分,也就是包含了我们要的经纬度数据的那部分打印出来却是乱码。这是为什么呢,首先,在wireshark中能看到原始数据,如下图:
在“Server: apache\r\n”后面还有一个空行(这可以作为报文内容的起始标志),后面的数据确确实实不是在wireshark中看到的json数据。但是在前面打印出来的数据中能看到,“Content-Encoding: gzip”这一行,说明内容是用gzip编码过的。想要得到最终的数据还得用gzip解码。
四、将经纬度数据解码出来
4.1)用Ubuntu自带的gzip命令解码
通过编程,可以将数据内容保存到一个文件中,该文件是用gzip编码过的二进制文件,文件后缀必须为.gz,例如contet.txt.gz,之后输入命令,gzip -d contet.txt.gz,那么contet.txt.gz就会变成一个名为contet.txt的新文件,这个文件就是解码过后的文件。打开该文件就可以看到里面的json数据。接下来编写程序将文件中的内容读取出来再解析即可,如果有cJSON库可以用cJSON库解析,这里数据比较简单,用字符串解析也很容易。
编写程序时,调用函数system("gzip -d contet.txt.gz");就相当于在命令行输入了gzip -d contet.txt.gz。
4.2)用zlib库解码
4.1中的方法需要在嵌入式linux平台中支持gzip命令,显然一般的平台是不支持这条命令的。原本以为自己要手撕代码,但幸运的是在网上找到一篇博客,上面的代码可以直接使用,博客链接如下:https://blog.csdn.net/weixin_28607671/article/details/116988589
将这份代码移植过来之后可以直接使用,我在上面的基础上稍微进行了一些改动,原代码解码之后会少两个字节,我改动后不会少字节了。
五、完整代码
1 /** 2 * filename: bdmap_coord.c 3 * author: Suzkfly 4 * date: 2021-08-21 5 * platform: linux 6 * 将GPS/北斗模块的经纬度转换为百度地图经纬度。编译时要加-lz参数,第26行的AK要改为自己的AK 7 */ 8 #include <sys/types.h> 9 #include <sys/socket.h> 10 #include <stdio.h> 11 #include <string.h> 12 #include <netinet/ip.h> 13 #include <netinet/in.h> 14 #include <arpa/inet.h> 15 #include <stdlib.h> 16 #include <sys/stat.h> 17 #include <fcntl.h> 18 #include <unistd.h> 19 #include <zlib.h> 20 21 #define SER_PORT 80 /* HTTP请求端口固定为80 */ 22 #define SER_ADDR "220.181.43.101" /* 百度地图服务IP地址 */ 23 24 #define ORIGINAL_LON "108.9844475" /* 原始经度 */ 25 #define ORIGINAL_LAT "34.37899" /* 原始纬度 */ 26 #define AK "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" /* AK,填入自己的AK */ 27 //https://lbsyun.baidu.com/ 28 29 /** 30 * \brief gzip解压 31 * 32 * \param[in] pSrc:需要解压的数据首地址 33 * \param[in] srcSize:需要解压的数据长度 34 * \param[out]:pOutDest:存放解压后的数据的二级指针 35 * \param[out]:pOutBufSize:解压后的数据长度 36 * 37 * \retval 成功返回0,失败返回-1 38 * 39 * \note gzip和zip解压大致相同,但是他们的头数据大小不一样,这个得注意,用inflateInit2(&d_stream,47) 40 * \note pOutBufSize可以传入NULL 41 */ 42 int vidpeek_uncompressGzip (unsigned char* pSrc, unsigned int srcSize, char**pOutDest, unsigned int* pOutBufSize) 43 { 44 char* pBuf = pSrc + (srcSize - 1); 45 unsigned int len = *pBuf; 46 int uncompressResult; 47 z_stream d_stream; 48 int i = 0; 49 50 if ((pSrc == NULL ) || (pOutDest == NULL) || (*pOutDest == NULL)) { 51 return -1; 52 } 53 54 //printf("#############pSrc 0x%x 0x%x 0x%x 0x%x", pSrc[0], pSrc[1], pSrc[2], pSrc[3]); 55 //check gz file,rfc1952 P6 56 if((*pSrc !=0x1f)||(*(pSrc+1) != 0x8b)) { 57 printf("\nuncompressGzip non Gzip\n"); 58 return -1; 59 } 60 for (i = 0; i < 3; i++) { 61 pBuf--; 62 len <<= 8; 63 len += *pBuf; 64 } 65 66 //fortest 67 if((len == 0) || (len > 1000000)) { 68 printf("\nuncompressGzip,-1or gzip!\n"); 69 return -1; 70 } 71 72 //gzipdecompression start!!! 73 d_stream.zalloc =Z_NULL; 74 d_stream.zfree =Z_NULL; 75 d_stream.opaque = Z_NULL; 76 d_stream.next_in =Z_NULL; 77 d_stream.avail_in= 0; 78 uncompressResult =inflateInit2(&d_stream,47); 79 if(uncompressResult!=Z_OK) { 80 printf("\ninflateInit2 -1or:%d\n",uncompressResult); 81 return uncompressResult; 82 } 83 84 d_stream.next_in = pSrc; 85 d_stream.avail_in = srcSize; 86 d_stream.next_out = (char *)*pOutDest; 87 d_stream.avail_out = len + 2; /* Modify by Suzkfly,原本这里是不+2的,但是解析出来会少2个字符 */ 88 uncompressResult =inflate(&d_stream, Z_NO_FLUSH); 89 90 switch(uncompressResult) { 91 case Z_NEED_DICT: 92 uncompressResult = Z_DATA_ERROR; 93 case Z_DATA_ERROR: 94 case Z_MEM_ERROR: 95 (void)inflateEnd(&d_stream); 96 return uncompressResult; 97 } 98 99 //printf("outlen= %d, total_in= %d, total_out= %d, avail_out= %d@@@@@@@@@@@\n",len, d_stream.total_in, d_stream.total_out, d_stream.avail_out); 100 101 inflateEnd(&d_stream); 102 if (pOutBufSize != NULL) { 103 *pOutBufSize = len; 104 } 105 106 return 0; 107 } 108 109 /** 110 * \brief 将GPS/北斗模块的经纬度转换为百度地图经纬度 111 * 112 * \param[in]: p_lon_gps:GPS/北斗模块得到的经度 113 * \param[in]: p_lat_gps:GPS/北斗模块得到的纬度 114 * \param[out]:p_lon_bdmap:百度地图经度 115 * \param[out]:p_lat_bdmap:百度地图纬度 116 * 117 * \retval 成功返回0,内部错误返回-1,转换失败返回-2,参数错误返回-3 118 */ 119 int gps_to_bdmap (const char *p_lon_gps, const char *p_lat_gps, char *p_lon_bdmap, char *p_lat_bdmap) 120 { 121 int ret, i; 122 int sockfd; 123 struct sockaddr_in seraddr; 124 char buf[4096] = { 0 }; 125 int count = 0; /* 接收到的字节个数 */ 126 char *p_tmp = NULL; /* 定义2个临时指针 */ 127 char *p_tmp2 = NULL; 128 struct timeval tv_out; /* 设定超时时间 */ 129 unsigned int contet_size = 0; /* 包含了经纬度的数据长度 */ 130 char len_a[8] = { 0 }; 131 int fd = 0; 132 int len = 0; 133 134 135 /* 检查参数 */ 136 if ((p_lon_gps == NULL) || (p_lat_gps == NULL) || 137 (p_lon_bdmap == NULL) || (p_lat_bdmap == NULL)) { 138 return -3; 139 } 140 141 /* 创建socket套接字 */ 142 seraddr.sin_family = AF_INET; 143 seraddr.sin_port = htons(SER_PORT); 144 seraddr.sin_addr.s_addr = inet_addr(SER_ADDR); 145 sockfd = socket(AF_INET, SOCK_STREAM, 0); 146 if (-1 == sockfd) { 147 perror("fail to socket\n"); 148 return -1; 149 } 150 151 /* 建立连接 */ 152 ret = connect(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr)); 153 if (-1 == ret) { 154 perror("fail to connect\n"); 155 return -1; 156 } 157 158 /* 发送HTTP请求报文 */ 159 sprintf(buf, "GET /geoconv/v1/?coords=%s,%s&from=1&to=5&ak=%s HTTP/1.1\r\n", p_lon_gps, p_lat_gps, AK); 160 strcat(buf, "Host: api.map.baidu.com\r\n"); 161 strcat(buf, "Connection: keep-alive\r\n"); 162 strcat(buf, "Cache-Control: max-age=0\r\n"); 163 strcat(buf, "Upgrade-Insecure-Requests: 1\r\n"); 164 strcat(buf, "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36\r\n"); 165 strcat(buf, "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\n"); 166 strcat(buf, "Accept-Encoding: gzip, deflate\r\n"); 167 strcat(buf, "Accept-Language: zh-CN,zh;q=0.9\r\n"); 168 strcat(buf, "Cookie: BAIDUID=1179C0DFEA5AE34FB5EAB79461EB442B:FG=1\r\n\r\n"); 169 ret = send(sockfd, buf, strlen(buf), 0); 170 if (-1 == ret) { 171 perror("fail to send\n"); 172 return -1; 173 } 174 175 /* 设定接收数据超时时间为1S。这里要根据设备网络情况而定 */ 176 tv_out.tv_sec = 1; 177 tv_out.tv_usec = 0; 178 setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv_out, sizeof(tv_out)); 179 180 /* 接收HTTP响应报文 */ 181 p_tmp = buf; 182 count = 0; 183 memset(p_tmp, 0, strlen(p_tmp)); 184 while ((ret = recv(sockfd, p_tmp, sizeof(buf) - count, 0)) > 0) { 185 count += ret; 186 p_tmp += ret; 187 } 188 close(sockfd); /* 关闭套接字 */ 189 190 /* 打印接收到的报文。这里不按字符串打印是因为接收到的数据中可能会包含'\0' */ 191 //printf("count = %d\n", count); 192 //for (i = 0; i < count; i++) { 193 // printf("%c", buf[i]); 194 //} 195 //fflush(stdout); /* 刷新输出缓冲区 */ 196 197 /* 解析出被编码的数据长度 */ 198 p_tmp = strstr(buf, "Content-Length: "); 199 if (p_tmp == NULL) { 200 perror("fail to strstr\n"); 201 } 202 p_tmp += strlen("Content-Length: "); 203 p_tmp2 = strstr(p_tmp, "\r\n"); 204 strncpy(len_a, p_tmp, p_tmp2 - p_tmp); 205 contet_size = atoi(len_a); 206 //printf("len = %d\n", contet_size); 207 208 /* 找到数据内容起始地址 */ 209 p_tmp = strstr(buf, "\r\n\r\n"); 210 if (p_tmp == NULL) { 211 perror("fail to strstr\n"); 212 } 213 p_tmp += strlen("\r\n\r\n"); 214 215 #if 0 /* 使用系统自带的gzip命令解码 */ 216 /* 将数据内容保存到文件中 */ 217 fd = open("contet.txt.gz", O_RDWR | O_CREAT | O_TRUNC, 0666); 218 if (fd < 0) { 219 perror("fail to open\n"); 220 return -1; 221 } 222 ret = write(fd, p_tmp, contet_size); 223 if (ret != contet_size) { 224 perror("fail to write\n"); 225 return -1; 226 } 227 close(fd); 228 229 /* 用gzip解压,解压后的文件名为contet.txt */ 230 system("gzip -d contet.txt.gz"); 231 232 /* 打开解压后的文件,得到json数据 */ 233 fd = open("contet.txt", O_RDONLY); 234 if (fd < 0) { 235 perror("fail to open\n"); 236 return -1; 237 } 238 memset(buf, 0, sizeof(buf)); 239 read(fd, buf, sizeof(buf)); 240 //printf("buf = %s\n", buf); 241 close(fd); 242 243 system("rm contet.txt"); /* 删除中间文件 */ 244 #else /* 使用zlib库进行解码 */ 245 p_tmp2 = buf; 246 ret = vidpeek_uncompressGzip(p_tmp, contet_size, &p_tmp2, &len); 247 if (ret != 0) { 248 perror("fail to decode\n"); 249 return -1; 250 } 251 memset(&buf[len], 0, sizeof(buf) - len); 252 #endif 253 254 printf("buf = %s\n", buf); /* 可以将解析后的结果打印出来 */ 255 printf("len = %d\n", len); 256 /* 解析json,得到转换过后的经纬度。由于数据简单,这里就自己解析了,若需要全面解析可以用cJSON库 */ 257 p_tmp = strstr(buf, "status"); 258 259 /* 判断转换状态是否成功 */ 260 p_tmp = p_tmp + strlen("status") + 2; 261 if (*p_tmp != '0') { 262 printf("parse failed\n"); 263 return -2; 264 } 265 266 /* 得到经度 */ 267 p_tmp = strstr(buf, "\"x\""); 268 p_tmp += 4; 269 p_tmp2 = p_tmp; 270 while (*p_tmp2 != ',') { 271 p_tmp2++; 272 } 273 strncpy(p_lon_bdmap, p_tmp, p_tmp2 - p_tmp); 274 275 /* 得到纬度 */ 276 p_tmp = strstr(p_tmp2, "\"y\""); 277 p_tmp += 4; 278 p_tmp2 = p_tmp; 279 while (*p_tmp2 != '}') { 280 p_tmp2++; 281 } 282 strncpy(p_lat_bdmap, p_tmp, p_tmp2 - p_tmp); 283 284 return 0; 285 } 286 287 /** 288 * \brief example 289 */ 290 int main(int argc, const char *argv[]) 291 { 292 int ret = 0; 293 char lon[32] = { 0 }; 294 char lat[32] = { 0 }; 295 296 ret = gps_to_bdmap(ORIGINAL_LON, ORIGINAL_LAT, lon, lat); 297 if (ret < 0) { 298 printf("ret = %d\n", ret); 299 return -1; 300 } 301 302 printf("lon = %s\n", lon); 303 printf("lat = %s\n", lat); 304 305 return 0; 306 }