zoukankan      html  css  js  c++  java
  • 用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. 上代码:

    服务器端:

    Cpp代码  收藏代码
    1. /* 
    2. 文件:server.c 
    3. PS:第一个连接上服务器的客户端,称为client1,第二个连接上服务器的客户端称为client2 
    4. 这个服务器的功能是: 
    5. 1:对于client1,它返回"first",并在client2连接上之后,将client2经过转换后的IP和port发给client1; 
    6. 2:对于client2,它返回client1经过转换后的IP和port和自身的port,并在随后断开与他们的连接。 
    7. */  
    8.   
    9. #include <stdio.h>  
    10. #include <unistd.h>  
    11. #include <signal.h>  
    12. #include <sys/socket.h>  
    13. #include <fcntl.h>  
    14. #include <stdlib.h>  
    15. #include <errno.h>  
    16. #include <string.h>  
    17. #include <arpa/inet.h>  
    18.   
    19. #define MAXLINE 128  
    20. #define SERV_PORT 8877  
    21.   
    22. //发生了致命错误,退出程序  
    23. void error_quit(const char *str)      
    24. {      
    25.     fprintf(stderr, "%s", str);    
    26.     //如果设置了错误号,就输入出错原因  
    27.     if( errno != 0 )  
    28.         fprintf(stderr, " : %s", strerror(errno));  
    29.     printf(" ");  
    30.     exit(1);      
    31. }     
    32.   
    33. int main(void)        
    34. {            
    35.     int i, res, cur_port;   
    36.     int connfd, firstfd, listenfd;     
    37.     int count = 0;  
    38.     char str_ip[MAXLINE];    //缓存IP地址  
    39.     char cur_inf[MAXLINE];   //当前的连接信息[IP+port]  
    40.     char first_inf[MAXLINE];    //第一个链接的信息[IP+port]  
    41.     char buffer[MAXLINE];    //临时发送缓冲区  
    42.     socklen_t clilen;        
    43.     struct sockaddr_in cliaddr;        
    44.     struct sockaddr_in servaddr;  
    45.   
    46.     //创建用于监听TCP协议套接字          
    47.     listenfd = socket(AF_INET, SOCK_STREAM, 0);        
    48.     memset(&servaddr, 0, sizeof(servaddr));        
    49.     servaddr.sin_family = AF_INET;        
    50.     servaddr.sin_addr.s_addr = htonl(INADDR_ANY);        
    51.     servaddr.sin_port = htons(SERV_PORT);        
    52.   
    53.     //把socket和socket地址结构联系起来         
    54.     res = bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));      
    55.     if( -1 == res )  
    56.         error_quit("bind error");  
    57.   
    58.     //开始监听端口         
    59.     res = listen(listenfd, INADDR_ANY);      
    60.     if( -1 == res )  
    61.         error_quit("listen error");  
    62.   
    63.     while( 1 )  
    64.     {  
    65.         //接收来自客户端的连接  
    66.         connfd = accept(listenfd,(struct sockaddr *)&cliaddr, &clilen);    
    67.         if( -1 == connfd )  
    68.             error_quit("accept error");  
    69.         inet_ntop(AF_INET, (void*)&cliaddr.sin_addr, str_ip, sizeof(str_ip));  
    70.   
    71.         count++;  
    72.         //对于第一个链接,将其的IP+port存储到first_inf中,  
    73.         //并和它建立长链接,然后向它发送字符串'first',  
    74.         if( count == 1 )  
    75.         {  
    76.             firstfd = connfd;  
    77.             cur_port = ntohs(cliaddr.sin_port);  
    78.             snprintf(first_inf, MAXLINE, "%s %d", str_ip, cur_port);     
    79.             strcpy(cur_inf, "first ");  
    80.             write(connfd, cur_inf, strlen(cur_inf)+1);  
    81.         }  
    82.         //对于第二个链接,将其的IP+port发送给第一个链接,  
    83.         //将第一个链接的信息和他自身的port返回给它自己,  
    84.         //然后断开两个链接,并重置计数器  
    85.         else if( count == 2 )  
    86.         {  
    87.             cur_port = ntohs(cliaddr.sin_port);  
    88.             snprintf(cur_inf, MAXLINE, "%s %d ", str_ip, cur_port);  
    89.             snprintf(buffer, MAXLINE, "%s %d ", first_inf, cur_port);  
    90.             write(connfd, buffer, strlen(buffer)+1);  
    91.             write(firstfd, cur_inf, strlen(cur_inf)+1);   
    92.             close(connfd);  
    93.             close(firstfd);  
    94.             count = 0;  
    95.         }  
    96.         //如果程序运行到这里,那肯定是出错了  
    97.         else  
    98.             error_quit("Bad required");  
    99.     }  
    100.     return 0;  
    101. }  

     客户端:

    Cpp代码  收藏代码
    1. /* 
    2. 文件:client.c 
    3. PS:第一个连接上服务器的客户端,称为client1,第二个连接上服务器的客户端称为client2 
    4. 这个程序的功能是:先连接上服务器,根据服务器的返回决定它是client1还是client2, 
    5. 若是client1,它就从服务器上得到client2的IP和Port,连接上client2, 
    6. 若是client2,它就从服务器上得到client1的IP和Port和自身经转换后的port, 
    7. 在尝试连接了一下client1后(这个操作会失败),然后根据服务器返回的port进行监听。 
    8. 这样以后,就能在两个客户端之间进行点对点通信了。 
    9. */  
    10.   
    11. #include <stdio.h>  
    12. #include <unistd.h>  
    13. #include <signal.h>  
    14. #include <sys/socket.h>  
    15. #include <fcntl.h>  
    16. #include <stdlib.h>  
    17. #include <errno.h>  
    18. #include <string.h>  
    19. #include <arpa/inet.h>  
    20.   
    21. #define MAXLINE 128  
    22. #define SERV_PORT 8877  
    23.   
    24. typedef struct  
    25. {  
    26.     char ip[32];  
    27.     int port;  
    28. }server;  
    29.   
    30. //发生了致命错误,退出程序  
    31. void error_quit(const char *str)      
    32. {      
    33.     fprintf(stderr, "%s", str);   
    34.     //如果设置了错误号,就输入出错原因  
    35.     if( errno != 0 )  
    36.         fprintf(stderr, " : %s", strerror(errno));  
    37.     printf(" ");  
    38.     exit(1);      
    39. }     
    40.   
    41. int main(int argc, char **argv)       
    42. {            
    43.     int i, res, port;  
    44.     int connfd, sockfd, listenfd;   
    45.     unsigned int value = 1;  
    46.     char buffer[MAXLINE];        
    47.     socklen_t clilen;          
    48.     struct sockaddr_in servaddr, sockaddr, connaddr;    
    49.     server other;  
    50.   
    51.     if( argc != 2 )  
    52.         error_quit("Using: ./client <IP Address>");  
    53.   
    54.     //创建用于链接(主服务器)的套接字          
    55.     sockfd = socket(AF_INET, SOCK_STREAM, 0);   
    56.     memset(&sockaddr, 0, sizeof(sockaddr));        
    57.     sockaddr.sin_family = AF_INET;        
    58.     sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);        
    59.     sockaddr.sin_port = htons(SERV_PORT);        
    60.     inet_pton(AF_INET, argv[1], &sockaddr.sin_addr);  
    61.     //设置端口可以被重用  
    62.     setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value));  
    63.   
    64.     //连接主服务器  
    65.     res = connect(sockfd, (struct sockaddr *)&sockaddr, sizeof(sockaddr));   
    66.     if( res < 0 )  
    67.         error_quit("connect error");  
    68.   
    69.     //从主服务器中读取出信息  
    70.     res = read(sockfd, buffer, MAXLINE);  
    71.     if( res < 0 )  
    72.         error_quit("read error");  
    73.     printf("Get: %s", buffer);  
    74.   
    75.     //若服务器返回的是first,则证明是第一个客户端  
    76.     if( 'f' == buffer[0] )  
    77.     {  
    78.         //从服务器中读取第二个客户端的IP+port  
    79.         res = read(sockfd, buffer, MAXLINE);  
    80.         sscanf(buffer, "%s %d", other.ip, &other.port);  
    81.         printf("ff: %s %d ", other.ip, other.port);  
    82.   
    83.         //创建用于的套接字          
    84.         connfd = socket(AF_INET, SOCK_STREAM, 0);   
    85.         memset(&connaddr, 0, sizeof(connaddr));        
    86.         connaddr.sin_family = AF_INET;        
    87.         connaddr.sin_addr.s_addr = htonl(INADDR_ANY);        
    88.         connaddr.sin_port = htons(other.port);      
    89.         inet_pton(AF_INET, other.ip, &connaddr.sin_addr);  
    90.   
    91.         //尝试去连接第二个客户端,前几次可能会失败,因为穿透还没成功,  
    92.         //如果连接10次都失败,就证明穿透失败了(可能是硬件不支持)  
    93.         while( 1 )  
    94.         {  
    95.             static int j = 1;  
    96.             res = connect(connfd, (struct sockaddr *)&connaddr, sizeof(connaddr));   
    97.             if( res == -1 )  
    98.             {  
    99.                 if( j >= 10 )  
    100.                     error_quit("can't connect to the other client ");  
    101.                 printf("connect error, try again. %d ", j++);  
    102.                 sleep(1);  
    103.             }  
    104.             else   
    105.                 break;  
    106.         }  
    107.   
    108.         strcpy(buffer, "Hello, world ");  
    109.         //连接成功后,每隔一秒钟向对方(客户端2)发送一句hello, world  
    110.         while( 1 )  
    111.         {  
    112.             res = write(connfd, buffer, strlen(buffer)+1);  
    113.             if( res <= 0 )  
    114.                 error_quit("write error");  
    115.             printf("send message: %s", buffer);  
    116.             sleep(1);  
    117.         }  
    118.     }  
    119.     //第二个客户端的行为  
    120.     else  
    121.     {  
    122.         //从主服务器返回的信息中取出客户端1的IP+port和自己公网映射后的port  
    123.         sscanf(buffer, "%s %d %d", other.ip, &other.port, &port);  
    124.   
    125.         //创建用于TCP协议的套接字          
    126.         sockfd = socket(AF_INET, SOCK_STREAM, 0);   
    127.         memset(&connaddr, 0, sizeof(connaddr));        
    128.         connaddr.sin_family = AF_INET;        
    129.         connaddr.sin_addr.s_addr = htonl(INADDR_ANY);        
    130.         connaddr.sin_port = htons(other.port);        
    131.         inet_pton(AF_INET, other.ip, &connaddr.sin_addr);  
    132.         //设置端口重用  
    133.         setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value));  
    134.   
    135.         //尝试连接客户端1,肯定会失败,但它会在路由器上留下记录,  
    136.         //以帮忙客户端1成功穿透,连接上自己   
    137.         res = connect(sockfd, (struct sockaddr *)&connaddr, sizeof(connaddr));   
    138.         if( res < 0 )  
    139.             printf("connect error ");  
    140.   
    141.         //创建用于监听的套接字          
    142.         listenfd = socket(AF_INET, SOCK_STREAM, 0);   
    143.         memset(&servaddr, 0, sizeof(servaddr));        
    144.         servaddr.sin_family = AF_INET;        
    145.         servaddr.sin_addr.s_addr = htonl(INADDR_ANY);        
    146.         servaddr.sin_port = htons(port);  
    147.         //设置端口重用  
    148.         setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value));  
    149.   
    150.         //把socket和socket地址结构联系起来   
    151.         res = bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));      
    152.         if( -1 == res )  
    153.             error_quit("bind error");  
    154.   
    155.         //开始监听端口         
    156.         res = listen(listenfd, INADDR_ANY);      
    157.         if( -1 == res )  
    158.             error_quit("listen error");  
    159.   
    160.         while( 1 )  
    161.         {  
    162.             //接收来自客户端1的连接  
    163.             connfd = accept(listenfd,(struct sockaddr *)&sockaddr, &clilen);    
    164.             if( -1 == connfd )  
    165.                 error_quit("accept error");  
    166.   
    167.             while( 1 )  
    168.             {  
    169.                 //循环读取来自于客户端1的信息  
    170.                 res = read(connfd, buffer, MAXLINE);  
    171.                 if( res <= 0 )  
    172.                     error_quit("read error");  
    173.                 printf("recv message: %s", buffer);  
    174.             }  
    175.             close(connfd);  
    176.         }  
    177.     }  
    178.   
    179.     return 0;  
    180. }  

    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
    ..................

  • 相关阅读:
    将eclipse的编码设置成UTF-8
    git提交代码时报rejected
    Vue.js
    快速计算进制之间的转换
    android中canvas.drawText参数的介绍以及绘制一个文本居中的案例
    progressbar原始效果
    面试问题总结
    Android Material Design学习日志
    Android进阶之解决RecyclerView notifyItem闪屏问题
    Android TextView行间距解析
  • 原文地址:https://www.cnblogs.com/lidabo/p/7567989.html
Copyright © 2011-2022 走看看