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. 上代码:

服务器端:

  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 != )
  28. fprintf(stderr, " : %s", strerror(errno));
  29. printf("\n");
  30. exit();
  31. }
  32.  
  33. int main(void)
  34. {
  35. int i, res, cur_port;
  36. int connfd, firstfd, listenfd;
  37. int count = ;
  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, );
  48. memset(&servaddr, , 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( - == res )
  56. error_quit("bind error");
  57.  
  58. //开始监听端口
  59. res = listen(listenfd, INADDR_ANY);
  60. if( - == res )
  61. error_quit("listen error");
  62.  
  63. while( )
  64. {
  65. //接收来自客户端的连接
  66. connfd = accept(listenfd,(struct sockaddr *)&cliaddr, &clilen);
  67. if( - == 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 == )
  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\n");
  80. write(connfd, cur_inf, strlen(cur_inf)+);
  81. }
  82. //对于第二个链接,将其的IP+port发送给第一个链接,
  83. //将第一个链接的信息和他自身的port返回给它自己,
  84. //然后断开两个链接,并重置计数器
  85. else if( count == )
  86. {
  87. cur_port = ntohs(cliaddr.sin_port);
  88. snprintf(cur_inf, MAXLINE, "%s %d\n", str_ip, cur_port);
  89. snprintf(buffer, MAXLINE, "%s %d\n", first_inf, cur_port);
  90. write(connfd, buffer, strlen(buffer)+);
  91. write(firstfd, cur_inf, strlen(cur_inf)+);
  92. close(connfd);
  93. close(firstfd);
  94. count = ;
  95. }
  96. //如果程序运行到这里,那肯定是出错了
  97. else
  98. error_quit("Bad required");
  99. }
  100. return ;
  101. }

客户端:

  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[];
  27. int port;
  28. }server;
  29.  
  30. //发生了致命错误,退出程序
  31. void error_quit(const char *str)
  32. {
  33. fprintf(stderr, "%s", str);
  34. //如果设置了错误号,就输入出错原因
  35. if( errno != )
  36. fprintf(stderr, " : %s", strerror(errno));
  37. printf("\n");
  38. exit();
  39. }
  40.  
  41. int main(int argc, char **argv)
  42. {
  43. int i, res, port;
  44. int connfd, sockfd, listenfd;
  45. unsigned int value = ;
  46. char buffer[MAXLINE];
  47. socklen_t clilen;
  48. struct sockaddr_in servaddr, sockaddr, connaddr;
  49. server other;
  50.  
  51. if( argc != )
  52. error_quit("Using: ./client <IP Address>");
  53.  
  54. //创建用于链接(主服务器)的套接字
  55. sockfd = socket(AF_INET, SOCK_STREAM, );
  56. memset(&sockaddr, , 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[], &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 < )
  67. error_quit("connect error");
  68.  
  69. //从主服务器中读取出信息
  70. res = read(sockfd, buffer, MAXLINE);
  71. if( res < )
  72. error_quit("read error");
  73. printf("Get: %s", buffer);
  74.  
  75. //若服务器返回的是first,则证明是第一个客户端
  76. if( 'f' == buffer[] )
  77. {
  78. //从服务器中读取第二个客户端的IP+port
  79. res = read(sockfd, buffer, MAXLINE);
  80. sscanf(buffer, "%s %d", other.ip, &other.port);
  81. printf("ff: %s %d\n", other.ip, other.port);
  82.  
  83. //创建用于的套接字
  84. connfd = socket(AF_INET, SOCK_STREAM, );
  85. memset(&connaddr, , 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( )
  94. {
  95. static int j = ;
  96. res = connect(connfd, (struct sockaddr *)&connaddr, sizeof(connaddr));
  97. if( res == - )
  98. {
  99. if( j >= )
  100. error_quit("can't connect to the other client\n");
  101. printf("connect error, try again. %d\n", j++);
  102. sleep();
  103. }
  104. else
  105. break;
  106. }
  107.  
  108. strcpy(buffer, "Hello, world\n");
  109. //连接成功后,每隔一秒钟向对方(客户端2)发送一句hello, world
  110. while( )
  111. {
  112. res = write(connfd, buffer, strlen(buffer)+);
  113. if( res <= )
  114. error_quit("write error");
  115. printf("send message: %s", buffer);
  116. sleep();
  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, );
  127. memset(&connaddr, , 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 < )
  139. printf("connect error\n");
  140.  
  141. //创建用于监听的套接字
  142. listenfd = socket(AF_INET, SOCK_STREAM, );
  143. memset(&servaddr, , 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( - == res )
  153. error_quit("bind error");
  154.  
  155. //开始监听端口
  156. res = listen(listenfd, INADDR_ANY);
  157. if( - == res )
  158. error_quit("listen error");
  159.  
  160. while( )
  161. {
  162. //接收来自客户端1的连接
  163. connfd = accept(listenfd,(struct sockaddr *)&sockaddr, &clilen);
  164. if( - == connfd )
  165. error_quit("accept error");
  166.  
  167. while( )
  168. {
  169. //循环读取来自于客户端1的信息
  170. res = read(connfd, buffer, MAXLINE);
  171. if( res <= )
  172. error_quit("read error");
  173. printf("recv message: %s", buffer);
  174. }
  175. close(connfd);
  176. }
  177. }
  178.  
  179. return ;
  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
..................

出处:http://blog.csdn.net/small_qch/article/details/8815028

个人注:

我认为,service的作用远不止这些,service可以做一些验证连通性、数据校验等等的事情,只有当A和B真正开始通信了,这时才考虑断开A、B与service的链接。

用TCP穿透NAT(TCP打洞)的实现的更多相关文章

  1. 转发 通过NAT和防火墙特性和TCP穿透的测评(翻译)

    转自 http://blog.csdn.net/sjin_1314/article/details/18178329 原文:Characterization and Measurement of TC ...

  2. [转]UDP/TCP穿越NAT的P2P通信方法研究(UDP/TCP打洞 Hole Punching)

     [转]UDP/TCP穿越NAT的P2P通信方法研究(UDP/TCP打洞 Hole Punching) http://www.360doc.com/content/12/0428/17/6187784 ...

  3. NAT穿透(UDP打洞)

    1.NAT(Network Address Translator)介绍 NAT有两大类,基本NAT和NAPT. 1.1.基本NAT 静态NAT:一个公网IP对应一个内部IP,一对一转换 动态NAT:N ...

  4. 【转】P2P之UDP穿透NAT的原理与实现(附源代码)

    作者:shootingstars (有容乃大,无欲则刚)  日期:2004-5-25 出处:P2P中国(PPcn.net) P2P 之 UDP穿透NAT的原理与实现(附源代码)原创:shootings ...

  5. P2P技术详解(二):P2P中的NAT穿越(打洞)方案详解

    1.内容概述 P2P即点对点通信,或称为对等联网,与传统的服务器客户端模式(如下图"P2P结构模型"所示)有着明显的区别,在即时通讯方案中应用广泛(比如IM应用中的实时音视频通信. ...

  6. P2P中的NAT穿越(打洞)方案详解

    一.P2P(点对点技术) 点对点技术(peer-to-peer,简称P2P)又称对等互联网络技术,是一种网络新技术,依赖网络中参与者的计算能力和带宽,而不是把依赖都聚集在较少的几台服务器上. 点对点技 ...

  7. 【转】NAT路由器打洞原理

    什么是打洞,为什么要打洞 由于Internet的快速发展 IPV4地址不够用,不能每个主机分到一个公网IP 所以使用NAT地址转换. 下面是我在网上找到的一副图 一般来说都是由私网内主机(例如上图中“ ...

  8. NAT路由器打洞原理

    什么是打洞,为什么要打洞 由于Internet的快速发展 IPV4地址不够用,不能每个主机分到一个公网IP 所以使用NAT地址转换. 下面是我在网上找到的一副图 一般来说都是由私网内主机(例如上图中“ ...

  9. OGG-01232 Receive TCP params error: TCP/IP error 104 (Connection reset by peer), endpoint:

    源端: 2015-02-05 17:45:49 INFO OGG-01815 Virtual Memory Facilities for: COM anon alloc: mmap(MAP_ANON) ...

随机推荐

  1. Java 集合系列13之 TreeMap详细介绍(源码解析)和使用示例

    转载 http://www.cnblogs.com/skywang12345/p/3310928.html https://www.jianshu.com/p/454208905619

  2. 自己动手编译Android源码(超详细)

    http://www.jianshu.com/p/367f0886e62b 在Android Studio代码调试一文中,简单的介绍了代码调试的一些技巧.现在我们来谈谈android源码编译的一些事. ...

  3. android camera jni调用

    http://www.mamicode.com/info-detail-1002139.html how to compile  library of native camera for androi ...

  4. String和StringBuilder、StringBuffer

    Java平台提供了两种类型的字符串:String和StringBuffer/StringBuilder String 只读字符串,这里的只读并不是指String类型变量无法被修改,而是指String类 ...

  5. Python 字典Dict概念和操作

    # 字典概念:无序的, 可变的键值对集合 # 定义 # 方式1 # {key: value, key: value...} # 例如 # {"name": "xin&qu ...

  6. 一言(ヒトコト)Hitokoto API

    『想要成为无论多么悲伤的时候,也能够漂亮微笑的人吧.』 Hitokoto API 更新:2014.02.22 问题/反馈:api # hitokoto.us 数据获取:[ 数据获取 ] 调用举例:[  ...

  7. java学习小笔记(三.socket通信)【转】

    三,socket通信1.http://blog.csdn.net/kongxx/article/details/7288896这个人写的关于socket通信不错,循序渐进式的讲解,用代码示例说明,运用 ...

  8. python使用笔记

    修改文件模板,支持中文. File -> Settings -> Editor -> File and Code templates -> python Scropt 在里面加 ...

  9. node操作mongdb的常用函数示例

    node操作mongdb的常用函数示例 链接数据库 var mongoose = require('mongoose'); //引用数据库模块 mongoose.connect('mongodb:// ...

  10. AppStore审核2.1被拒大礼包过审经历

    本团队的iOS端迭代至今,经历过AppStore审核的数次调整,包括审核时长.严厉程度等,尝过各种花式的拒绝理由,但从没有像2018年初这次来得猛烈和漫长.从首次提交到最后过审几乎花费一个月的时间,下 ...