天天看點

用TCP穿透NAT(TCP打洞)的實作1. TCP穿透原理:2. 程式思路:3. 聲明4. 上代碼:5. 運作示例:

    我們假設在兩個不同的區域網路後面分别有2台客戶機A和 B,AB所在的區域網路都分别通過一個路由器接入網際網路。網際網路上有一台伺服器S。 

    現在AB是無法直接和對方發送資訊的,AB都不知道對方在網際網路上真正的IP和端口, AB所在的區域網路的路由器隻允許内部向外主動發送的資訊通過。對于B直接發送給A的路由器的消息,路由會認為其“不被信任”而直接丢棄。 

    要實作 AB直接的通訊,就必須進行以下3步:A首先連接配接網際網路上的伺服器S并發送一條消息(對于UDP這種無連接配接的協定其實直接初始會話發送消息即可),這樣S就擷取了A在網際網路上的實際終端(發送消息的IP和端口号)。接着 B也進行同樣的步驟,S就知道了AB在網際網路上的終端(這就是“打洞”)。接着S分别告訴A和B對方用戶端在網際網路上的實際終端,也即S告訴A客戶B的會話終端,S告訴B客戶A的會話終端。這樣,在AB都知道了對方的實際終端之後,就可以直接通過實際終端發送消息了(因為先前雙方都向外發送過消息,路由上已經有允許資料進出的消息通道)。

1:啟動伺服器,監聽端口8877

2:第一次啟動用戶端(稱為client1),連上伺服器,伺服器将傳回字元串first,辨別這個是client1,同時,伺服器将記錄下這個用戶端的(經過轉換之後的)IP和端口。

3:第二次啟動用戶端(稱為client2),連上伺服器,伺服器将向其傳回自身的發送端口(稱為port2),以及client1的(經過轉換之後的)IP和端口。

4:然後伺服器再發client1傳回client2(經過轉換之後的)IP和端口,然後斷開與這兩個用戶端的連接配接(此時,伺服器的工作已經全部完成了)

5:client2嘗試連接配接client1,這次肯定會失敗,但它會在路由器上留下記錄,以幫忙client1成功穿透,連接配接上自己,然後設定port2端口為可重用端口,并監聽端口port2。

6:client1嘗試去連接配接client2,前幾次可能會失敗,因為穿透還沒成功,如果連接配接10次都失敗,就證明穿透失敗了(可能是硬體不支援),如果成功,則每秒向client2發送一次hello, world

7:如果client1不斷出現send message: Hello, world,client2不斷出現recv message: Hello, world,則證明實驗成功了,否則就是失敗了。

1:這個程式隻是一個DEMO,是以肯定有很多不完善的地方,請大家多多見諒。

2:在很多網絡中,這個程式并不能打洞成功,可能是硬體的問題(畢竟不是每種路由器都支援穿透),也可能是我程式的問題,如果大家有意見或建議,歡迎留言或給我發郵件(郵箱是:[email protected]

伺服器端:

用TCP穿透NAT(TCP打洞)的實作1. TCP穿透原理:2. 程式思路:3. 聲明4. 上代碼:5. 運作示例:

/* 

檔案:server.c 

PS:第一個連接配接上伺服器的用戶端,稱為client1,第二個連接配接上伺服器的用戶端稱為client2 

這個伺服器的功能是: 

1:對于client1,它傳回"first",并在client2連接配接上之後,将client2經過轉換後的IP和port發給client1; 

2:對于client2,它傳回client1經過轉換後的IP和port和自身的port,并在随後斷開與他們的連接配接。 

*/  

#include <stdio.h>  

#include <unistd.h>  

#include <signal.h>  

#include <sys/socket.h>  

#include <fcntl.h>  

#include <stdlib.h>  

#include <errno.h>  

#include <string.h>  

#include <arpa/inet.h>  

#define MAXLINE 128  

#define SERV_PORT 8877  

//發生了緻命錯誤,退出程式  

void error_quit(const char *str)      

{      

    fprintf(stderr, "%s", str);    

    //如果設定了錯誤号,就輸入出錯原因  

    if( errno != 0 )  

        fprintf(stderr, " : %s", strerror(errno));  

    printf("\n");  

    exit(1);      

}     

int main(void)        

{            

    int i, res, cur_port;   

    int connfd, firstfd, listenfd;     

    int count = 0;  

    char str_ip[MAXLINE];    //緩存IP位址  

    char cur_inf[MAXLINE];   //目前的連接配接資訊[IP+port]  

    char first_inf[MAXLINE];    //第一個連結的資訊[IP+port]  

    char buffer[MAXLINE];    //臨時發送緩沖區  

    socklen_t clilen;        

    struct sockaddr_in cliaddr;        

    struct sockaddr_in servaddr;  

    //建立用于監聽TCP協定套接字          

    listenfd = socket(AF_INET, SOCK_STREAM, 0);        

    memset(&servaddr, 0, sizeof(servaddr));        

    servaddr.sin_family = AF_INET;        

    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);        

    servaddr.sin_port = htons(SERV_PORT);        

    //把socket和socket位址結構聯系起來         

    res = bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));      

    if( -1 == res )  

        error_quit("bind error");  

    //開始監聽端口         

    res = listen(listenfd, INADDR_ANY);      

        error_quit("listen error");  

    while( 1 )  

    {  

        //接收來自用戶端的連接配接  

        connfd = accept(listenfd,(struct sockaddr *)&cliaddr, &clilen);    

        if( -1 == connfd )  

            error_quit("accept error");  

        inet_ntop(AF_INET, (void*)&cliaddr.sin_addr, str_ip, sizeof(str_ip));  

        count++;  

        //對于第一個連結,将其的IP+port存儲到first_inf中,  

        //并和它建立長連結,然後向它發送字元串'first',  

        if( count == 1 )  

        {  

            firstfd = connfd;  

            cur_port = ntohs(cliaddr.sin_port);  

            snprintf(first_inf, MAXLINE, "%s %d", str_ip, cur_port);     

            strcpy(cur_inf, "first\n");  

            write(connfd, cur_inf, strlen(cur_inf)+1);  

        }  

        //對于第二個連結,将其的IP+port發送給第一個連結,  

        //将第一個連結的資訊和他自身的port傳回給它自己,  

        //然後斷開兩個連結,并重置計數器  

        else if( count == 2 )  

            snprintf(cur_inf, MAXLINE, "%s %d\n", str_ip, cur_port);  

            snprintf(buffer, MAXLINE, "%s %d\n", first_inf, cur_port);  

            write(connfd, buffer, strlen(buffer)+1);  

            write(firstfd, cur_inf, strlen(cur_inf)+1);   

            close(connfd);  

            close(firstfd);  

            count = 0;  

        //如果程式運作到這裡,那肯定是出錯了  

        else  

            error_quit("Bad required");  

    }  

    return 0;  

}  

 用戶端:

用TCP穿透NAT(TCP打洞)的實作1. TCP穿透原理:2. 程式思路:3. 聲明4. 上代碼:5. 運作示例:

檔案:client.c 

這個程式的功能是:先連接配接上伺服器,根據伺服器的傳回決定它是client1還是client2, 

若是client1,它就從伺服器上得到client2的IP和Port,連接配接上client2, 

若是client2,它就從伺服器上得到client1的IP和Port和自身經轉換後的port, 

在嘗試連接配接了一下client1後(這個操作會失敗),然後根據伺服器傳回的port進行監聽。 

這樣以後,就能在兩個用戶端之間進行點對點通信了。 

typedef struct  

{  

    char ip[32];  

    int port;  

}server;  

    fprintf(stderr, "%s", str);   

int main(int argc, char **argv)       

    int i, res, port;  

    int connfd, sockfd, listenfd;   

    unsigned int value = 1;  

    char buffer[MAXLINE];        

    socklen_t clilen;          

    struct sockaddr_in servaddr, sockaddr, connaddr;    

    server other;  

    if( argc != 2 )  

        error_quit("Using: ./client <IP Address>");  

    //建立用于連結(主伺服器)的套接字          

    sockfd = socket(AF_INET, SOCK_STREAM, 0);   

    memset(&sockaddr, 0, sizeof(sockaddr));        

    sockaddr.sin_family = AF_INET;        

    sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);        

    sockaddr.sin_port = htons(SERV_PORT);        

    inet_pton(AF_INET, argv[1], &sockaddr.sin_addr);  

    //設定端口可以被重用  

    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value));  

    //連接配接主伺服器  

    res = connect(sockfd, (struct sockaddr *)&sockaddr, sizeof(sockaddr));   

    if( res < 0 )  

        error_quit("connect error");  

    //從主伺服器中讀取出資訊  

    res = read(sockfd, buffer, MAXLINE);  

        error_quit("read error");  

    printf("Get: %s", buffer);  

    //若伺服器傳回的是first,則證明是第一個用戶端  

    if( 'f' == buffer[0] )  

        //從伺服器中讀取第二個用戶端的IP+port  

        res = read(sockfd, buffer, MAXLINE);  

        sscanf(buffer, "%s %d", other.ip, &other.port);  

        printf("ff: %s %d\n", other.ip, other.port);  

        //建立用于的套接字          

        connfd = socket(AF_INET, SOCK_STREAM, 0);   

        memset(&connaddr, 0, sizeof(connaddr));        

        connaddr.sin_family = AF_INET;        

        connaddr.sin_addr.s_addr = htonl(INADDR_ANY);        

        connaddr.sin_port = htons(other.port);      

        inet_pton(AF_INET, other.ip, &connaddr.sin_addr);  

        //嘗試去連接配接第二個用戶端,前幾次可能會失敗,因為穿透還沒成功,  

        //如果連接配接10次都失敗,就證明穿透失敗了(可能是硬體不支援)  

        while( 1 )  

            static int j = 1;  

            res = connect(connfd, (struct sockaddr *)&connaddr, sizeof(connaddr));   

            if( res == -1 )  

            {  

                if( j >= 10 )  

                    error_quit("can't connect to the other client\n");  

                printf("connect error, try again. %d\n", j++);  

                sleep(1);  

            }  

            else   

                break;  

        strcpy(buffer, "Hello, world\n");  

        //連接配接成功後,每隔一秒鐘向對方(用戶端2)發送一句hello, world  

            res = write(connfd, buffer, strlen(buffer)+1);  

            if( res <= 0 )  

                error_quit("write error");  

            printf("send message: %s", buffer);  

            sleep(1);  

    //第二個用戶端的行為  

    else  

        //從主伺服器傳回的資訊中取出用戶端1的IP+port和自己公網映射後的port  

        sscanf(buffer, "%s %d %d", other.ip, &other.port, &port);  

        //建立用于TCP協定的套接字          

        sockfd = socket(AF_INET, SOCK_STREAM, 0);   

        connaddr.sin_port = htons(other.port);        

        //設定端口重用  

        setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value));  

        //嘗試連接配接用戶端1,肯定會失敗,但它會在路由器上留下記錄,  

        //以幫忙用戶端1成功穿透,連接配接上自己   

        res = connect(sockfd, (struct sockaddr *)&connaddr, sizeof(connaddr));   

        if( res < 0 )  

            printf("connect error\n");  

        //建立用于監聽的套接字          

        listenfd = socket(AF_INET, SOCK_STREAM, 0);   

        memset(&servaddr, 0, sizeof(servaddr));        

        servaddr.sin_family = AF_INET;        

        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);        

        servaddr.sin_port = htons(port);  

        setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value));  

        //把socket和socket位址結構聯系起來   

        res = bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));      

        if( -1 == res )  

            error_quit("bind error");  

        //開始監聽端口         

        res = listen(listenfd, INADDR_ANY);      

            error_quit("listen error");  

            //接收來自用戶端1的連接配接  

            connfd = accept(listenfd,(struct sockaddr *)&sockaddr, &clilen);    

            if( -1 == connfd )  

                error_quit("accept error");  

            while( 1 )  

                //循環讀取來自于用戶端1的資訊  

                res = read(connfd, buffer, MAXLINE);  

                if( res <= 0 )  

                    error_quit("read error");  

                printf("recv message: %s", buffer);  

(第一個終端)

qch@qch ~/program/tcode $ gcc server.c -o server

qch@qch ~/program/tcode $ ./server &

[1] 4688

qch@qch ~/program/tcode $ gcc client.c -o client

qch@qch ~/program/tcode $ ./client localhost

Get: first

ff: 127.0.0.1 38052

send message: Hello, world

.................

第二個終端:

Get: 127.0.0.1 38073 38074

connect error

recv message: Hello, world

..................

繼續閱讀