2021年3月9日 星期二

TCP穿透NAT範例 in C

 1. TCP穿透原理:

    我們假設在兩個不同的前面後面分別有2個台站A和B,AB位於的初始化都分別通過一個路由器接入互聯網。互聯網上一個台服務器S。

    現在AB是無法直接和對方發送信息的,AB都不知道對方在互聯網上真正的IP和端口,AB所在的轉換器的路由器僅允許內部並行主動發送的信息通過。對於B直接發送給A的路由器的消息,路由會認為其“不被信任”而直接替換。

    要實現AB直接的通訊,就必須進行以下3步:首先連接互聯網上的服務器S並發送一條消息(對於UDP這種無連接的協議實際上直接初始化會話發送消息即可),這樣S就獲取了然後B也進行同樣的步驟,S就知道了AB在互聯網上的終端(這就是“打洞”)。接下來分別告訴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發送一次你好,世界

7.如果client1持續出現發送消息:Hello,world,client2持續出現recv消息:Hello,world,則證明實驗成功了,否則就是失敗了。


3.聲明

1.這個程序只是一個DEMO,所以肯定有很多不完善的地方。

2.在很多網絡中,這個程序並不能打洞成功,可能是硬件的問題(畢竟不是路由器路由器支持穿透),也可能是這個程式的問題。


4.範例程式碼:

Server端:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
/*
文件: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端:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
/*
文件: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 gccserver.c−oserverqch@qch /program/tcode ./server &

[1] 4688

qch@qch ~/program/tcode gccclient.c−oclientqch@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

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

 

From:http://blog.csdn.net/small_qch/article/details/8815028

service的作用遠不止這些,service可以做一些驗證連通性,數據校正等等的事情,只有當A和B真正開始通信了,這時才考慮替代A,B與service的鏈接。

REF

https://www.cnblogs.com/mq0036/p/6589811.html

沒有留言:

張貼留言