一、前言
最近需要做一個嵌入式系統顯示地圖的項目,百度地圖給我們留出了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 }