不得不說,在國內IP緊缺的現狀下,NAT發揮了無比巨大的做用:它以把IP和端口從新分配的方式,知足了廣大人民羣衆上網的強烈需求。可是對於我的服務器以及在內網中基於網絡的嵌入式設備,倒是個比較尷尬的事情:由於它把端口和IP進行了從新分配,外網客戶端訪問的時候很難知道server端的IP和監聽端口,尤爲是監聽端口。這個時候,子網穿透技術就應運而生了。
這兩天看了一些簡單的子網穿透的基本原理,大多數都是UDP的。可是看完後以爲TCP實現起來更爲簡單,所以簡單的寫了一個測試程序實驗了一下,結果還真行。下面是實現原理:
首先是搭建了一下網絡模型,一個是在子網中的須要穿透的Server A,一個是在公網上的輔助穿透的Server S,另外一個是須要鏈接Server A的ClientB,因爲是客戶端,在不在子網倒無所謂了。下面是簡圖:服務器
其次簡單介紹一下實現過程:首先是Server S創建一個監聽socket,用於輔助打洞。而後在Server A上創建一個socket去鏈接ServerS的監聽端口,而後這個時候NAT A就會給此鏈接分配一個端口和IP,而Server S是知道此IP和端口的,而後Server S將此信息記錄下來。 接着Server A要在剛纔那個鏈接ServerS上的端口上再創建一個監聽socket。而後Client B先去和Server S創建聯繫,獲取Server A的端口和IP信息,而後就能夠去鏈接ServerA了。
其中有兩個地方要注意,其中ServerA上的兩個socket要設置SO_REUSEADDR的屬性;其次是建議ServerA在鏈接ServerS前也先綁定一下端口。
最後是上代碼,固然其中我爲了省事兒,沒有搭建NAT B,也沒有去實現框圖中的Step2,而是採起Server S打印的方式,而後配合手動修改ClientB的鏈接端口實現的……而後個人Server S的IP是10.10.10.66;而後client B的IP是10.10.10.88;NAT A用的是一個TP-LINK的家用路由器,WAN口的IP是10.10.10.77,LAN口是192.168.1.1;Server A的IP是192.168.1.99,網關是192.168.1.1。
首先是ServerS上的代碼,爲了省事兒,請忽視句柄泄露等問題……網絡
#include<stdio.h> #include<stdlib.h> #include<sys/socket.h> #include <netinet/in.h> #include <errno.h> #include <string.h> #include <time.h> int main() { int fd_socket; int fd_connect; struct sockaddr_in serv_addr; struct sockaddr_in client_addr; socklen_t cli_len; fd_socket= socket(AF_INET, SOCK_STREAM, 0); if(fd_socket < 0) { printf("Init socket failed!\n"); return -1; } int iOption_value = 1; int iLength = sizeof(int); if(setsockopt(fd_socket,SOL_SOCKET,SO_REUSEADDR,&iOption_value, iLength)<0) { printf("setsockopt error\n"); return -1; } serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(1234); serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(fd_socket, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr_in)) < 0) { perror("bind"); printf("Bind failed!\n"); return -2; } if(listen(fd_socket, 4) < 0) { printf("Listen failed!\n"); } printf("start to accept!\n"); while(1) { fd_connect = accept(fd_socket, (struct sockaddr *)&client_addr, &cli_len); printf("the ServerA ip is %s, port is %d\n",inet_ntoa(client_addr.sin_addr),htons(client_addr.sin_port)); usleep(0); } printf("over\n"); return 0; }
而後是ClientB的代碼,其中IP(10.10.10.77)和端口(1043)是在ServerS打印以後才寫進去進行編譯運行的。socket
#include<stdio.h> #include<stdlib.h> #include<sys/socket.h> #include <netinet/in.h> #include <errno.h> #include <string.h> #include <time.h> int main() { int fd_socket; struct sockaddr_in serv_addr; fd_socket= socket(AF_INET, SOCK_STREAM, 0); if(fd_socket < 0) { printf("Init socket failed!\n"); return -1; } memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(1043); serv_addr.sin_addr.s_addr = inet_addr("10.10.10.77"); if(connect(fd_socket, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { printf("connect error\n"); return -1; } printf("connect success\n"); sleep(20); return 0; }
最後是ServerA的代碼:測試
#include<stdio.h> #include<stdlib.h> #include<sys/socket.h> #include <netinet/in.h> #include <errno.h> #include <string.h> #include <time.h> int main() { int fd_socket; int fd_connect; int fd_server; struct sockaddr_in serv_addr; struct sockaddr_in client_addr; fd_socket= socket(AF_INET, SOCK_STREAM, 0); if(fd_socket < 0) { printf("Init socket failed!\n"); return -1; } int iOption_value = 1; int iLength = sizeof(int); if(setsockopt(fd_socket,SOL_SOCKET,SO_REUSEADDR,&iOption_value, iLength)<0) { printf("setsockopt error\n"); return -1; } memset(&client_addr, 0, sizeof(client_addr)); client_addr.sin_family = AF_INET; client_addr.sin_port = htons(6666); client_addr.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(fd_socket,(struct sockaddr *)&client_addr, sizeof(client_addr)) < 0) { perror("bind"); return -1; } memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(1234); serv_addr.sin_addr.s_addr = inet_addr("10.10.10.66"); if(connect(fd_socket, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { printf("connect error\n"); return -1; } memset(&client_addr, 0, sizeof(client_addr)); int iAddrLen = sizeof(client_addr); if(getsockname(fd_socket, (struct sockaddr *)&client_addr, &iAddrLen) < 0) { printf("getsockname\n"); return -1; } printf("the port is %d\n", htons(client_addr.sin_port)); fd_server = socket(AF_INET, SOCK_STREAM, 0); if(fd_server < 0) { printf("Init socket failed!\n"); return -1; } if(setsockopt(fd_server,SOL_SOCKET,SO_REUSEADDR,&iOption_value, iLength)<0) { printf("setsockopt error\n"); return -1; } memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = client_addr.sin_port; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(fd_server,(struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { perror("bind"); return -1; } if(listen(fd_server, 12) < 0) { perror("listen"); } printf("listen success\n"); sleep(20); return 0; }
根據打印消息以及抓包數據分析來看,clientB是能夠和ServerA創建鏈接的。
雖然這個實驗是成功了,可是當我把這種方式應用到實際項目中卻出現了兩個問題:
一是以這種模式創建的通信模式,帶寬很不穩定,有時候數據會阻塞好久才能發過去,應該是中間有丟包。
二是一旦ServerA斷掉ClientB的鏈接,ClientB就沒法再與ServerA創建鏈接了。
問題二我以爲還比較好理解,由於通常NAT創建TCP的端口映射是根據SYS和FIN包爲起始終止標識來創建的,當ServerA向ClientB發FIN的時候,就會被NAT檢測到,而後關掉這種端口映射關係。
可是問題一我就想不明白了,由於當我把路由器的DMZ打開後,數據是很流暢的,能夠證實物理線路是能保證足夠帶寬的,只有在這種應用下才會出現帶寬降低問題,但願有哪位高手能夠幫忙解答一下……spa