用TCP穿透NAT(TCP打洞)的實現

 

1. TCP穿透原理:

    咱們假設在兩個不一樣的局域網後面分別有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都知道了對方的實際終端以後,就能夠直接經過實際終端發送消息了(由於先前雙方都向外發送過消息,路由上已經有容許數據進出的消息通道)。

2. 程序思路:

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,則證實實驗成功了,不然就是失敗了。

3. 聲明

1:這個程序只是一個DEMO,因此確定有不少不完善的地方,請你們多多見諒。
2:在不少網絡中,這個程序並不能打洞成功,多是硬件的問題(畢竟不是每種路由器都支持穿透),也多是我程序的問題,若是你們有意見或建議,歡迎留言或給我發郵件(郵箱是:aa1080711@163.com)

4. 上代碼:

服務器端:服務器

/*
文件: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);    
    if( -1 == res )
        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 )
        {
            cur_port = ntohs(cliaddr.sin_port);
            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;
}

 

客戶端:
/*
文件:client.c
PS:第一個鏈接上服務器的客戶端,稱爲client1,第二個鏈接上服務器的客戶端稱爲client2
這個程序的功能是:先鏈接上服務器,根據服務器的返回決定它是client1仍是client2,
如果client1,它就從服務器上獲得client2的IP和Port,鏈接上client2,
如果client2,它就從服務器上獲得client1的IP和Port和自身經轉換後的port,
在嘗試鏈接了一下client1後(這個操做會失敗),而後根據服務器返回的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

typedef struct
{
    char ip[32];
    int port;
}server;

//發生了致命錯誤,退出程序
void error_quit(const char *str)    
{    
    fprintf(stderr, "%s", str); 
    //若是設置了錯誤號,就輸入出錯緣由
    if( errno != 0 )
        fprintf(stderr, " : %s", strerror(errno));
    printf("\n");
    exit(1);    
}   

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);
    if( res < 0 )
        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
        while( 1 )
        {
            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); 
        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);
        //設置端口重用
        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);    
        if( -1 == res )
            error_quit("listen error");

        while( 1 )
        {
            //接收來自客戶端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);
            }
            close(connfd);
        }
    }

    return 0;
}

 

5. 運行示例:

(第一個終端)
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
send message: Hello, world
send message: Hello, world
.................


第二個終端:
qch@qch ~/program/tcode $ ./client localhost
Get: 127.0.0.1 38073 38074
connect error
recv message: Hello, world
recv message: Hello, world
recv message: Hello, world
..................

 

 

出處:http://blog.csdn.net/small_qch/article/details/8815028網絡

我的注:併發

我認爲,service的做用遠不止這些,service能夠作一些驗證連通性、數據校驗等等的事情,只有當A和B真正開始通訊了,這時才考慮斷開A、B與service的連接。socket

相關文章
相關標籤/搜索