用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 != )
fprintf(stderr, " : %s", strerror(errno));
printf("\n");
exit();
} int main(void)
{
int i, res, cur_port;
int connfd, firstfd, listenfd;
int count = ;
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, );
memset(&servaddr, , 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( - == res )
error_quit("bind error"); //开始监听端口
res = listen(listenfd, INADDR_ANY);
if( - == res )
error_quit("listen error"); while( )
{
//接收来自客户端的连接
connfd = accept(listenfd,(struct sockaddr *)&cliaddr, &clilen);
if( - == 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 == )
{
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)+);
}
//对于第二个链接,将其的IP+port发送给第一个链接,
//将第一个链接的信息和他自身的port返回给它自己,
//然后断开两个链接,并重置计数器
else if( count == )
{
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)+);
write(firstfd, cur_inf, strlen(cur_inf)+);
close(connfd);
close(firstfd);
count = ;
}
//如果程序运行到这里,那肯定是出错了
else
error_quit("Bad required");
}
return ;
}
客户端:
/*
文件: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[];
int port;
}server; //发生了致命错误,退出程序
void error_quit(const char *str)
{
fprintf(stderr, "%s", str);
//如果设置了错误号,就输入出错原因
if( errno != )
fprintf(stderr, " : %s", strerror(errno));
printf("\n");
exit();
} int main(int argc, char **argv)
{
int i, res, port;
int connfd, sockfd, listenfd;
unsigned int value = ;
char buffer[MAXLINE];
socklen_t clilen;
struct sockaddr_in servaddr, sockaddr, connaddr;
server other; if( argc != )
error_quit("Using: ./client <IP Address>"); //创建用于链接(主服务器)的套接字
sockfd = socket(AF_INET, SOCK_STREAM, );
memset(&sockaddr, , 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[], &sockaddr.sin_addr);
//设置端口可以被重用
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value)); //连接主服务器
res = connect(sockfd, (struct sockaddr *)&sockaddr, sizeof(sockaddr));
if( res < )
error_quit("connect error"); //从主服务器中读取出信息
res = read(sockfd, buffer, MAXLINE);
if( res < )
error_quit("read error");
printf("Get: %s", buffer); //若服务器返回的是first,则证明是第一个客户端
if( 'f' == buffer[] )
{
//从服务器中读取第二个客户端的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, );
memset(&connaddr, , 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( )
{
static int j = ;
res = connect(connfd, (struct sockaddr *)&connaddr, sizeof(connaddr));
if( res == - )
{
if( j >= )
error_quit("can't connect to the other client\n");
printf("connect error, try again. %d\n", j++);
sleep();
}
else
break;
} strcpy(buffer, "Hello, world\n");
//连接成功后,每隔一秒钟向对方(客户端2)发送一句hello, world
while( )
{
res = write(connfd, buffer, strlen(buffer)+);
if( res <= )
error_quit("write error");
printf("send message: %s", buffer);
sleep();
}
}
//第二个客户端的行为
else
{
//从主服务器返回的信息中取出客户端1的IP+port和自己公网映射后的port
sscanf(buffer, "%s %d %d", other.ip, &other.port, &port); //创建用于TCP协议的套接字
sockfd = socket(AF_INET, SOCK_STREAM, );
memset(&connaddr, , 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 < )
printf("connect error\n"); //创建用于监听的套接字
listenfd = socket(AF_INET, SOCK_STREAM, );
memset(&servaddr, , 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( - == res )
error_quit("bind error"); //开始监听端口
res = listen(listenfd, INADDR_ANY);
if( - == res )
error_quit("listen error"); while( )
{
//接收来自客户端1的连接
connfd = accept(listenfd,(struct sockaddr *)&sockaddr, &clilen);
if( - == connfd )
error_quit("accept error"); while( )
{
//循环读取来自于客户端1的信息
res = read(connfd, buffer, MAXLINE);
if( res <= )
error_quit("read error");
printf("recv message: %s", buffer);
}
close(connfd);
}
} return ;
}
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打洞)的实现的更多相关文章
- 转发 通过NAT和防火墙特性和TCP穿透的测评(翻译)
转自 http://blog.csdn.net/sjin_1314/article/details/18178329 原文:Characterization and Measurement of TC ...
- [转]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 ...
- NAT穿透(UDP打洞)
1.NAT(Network Address Translator)介绍 NAT有两大类,基本NAT和NAPT. 1.1.基本NAT 静态NAT:一个公网IP对应一个内部IP,一对一转换 动态NAT:N ...
- 【转】P2P之UDP穿透NAT的原理与实现(附源代码)
作者:shootingstars (有容乃大,无欲则刚) 日期:2004-5-25 出处:P2P中国(PPcn.net) P2P 之 UDP穿透NAT的原理与实现(附源代码)原创:shootings ...
- P2P技术详解(二):P2P中的NAT穿越(打洞)方案详解
1.内容概述 P2P即点对点通信,或称为对等联网,与传统的服务器客户端模式(如下图"P2P结构模型"所示)有着明显的区别,在即时通讯方案中应用广泛(比如IM应用中的实时音视频通信. ...
- P2P中的NAT穿越(打洞)方案详解
一.P2P(点对点技术) 点对点技术(peer-to-peer,简称P2P)又称对等互联网络技术,是一种网络新技术,依赖网络中参与者的计算能力和带宽,而不是把依赖都聚集在较少的几台服务器上. 点对点技 ...
- 【转】NAT路由器打洞原理
什么是打洞,为什么要打洞 由于Internet的快速发展 IPV4地址不够用,不能每个主机分到一个公网IP 所以使用NAT地址转换. 下面是我在网上找到的一副图 一般来说都是由私网内主机(例如上图中“ ...
- NAT路由器打洞原理
什么是打洞,为什么要打洞 由于Internet的快速发展 IPV4地址不够用,不能每个主机分到一个公网IP 所以使用NAT地址转换. 下面是我在网上找到的一副图 一般来说都是由私网内主机(例如上图中“ ...
- 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) ...
随机推荐
- $Java设计模式之——观察者模式(Observer)
(一)观察者模式简介 1.定义:定义对象间一种一对多的依赖关系,一个对象状态发生改变时,所有依赖它的对象都会接到通知并作出相应的响应. 2.应用场景: (1)GUI系统 (2)订阅-发布系统 (3)事 ...
- Django 路由、模板和模型系统
一.路由系统 URL配置(URLconf)就像Django 所支撑网站的目录.它的本质是URL模式以及要为该URL模式调用的视图函数之间的映射表:你就是以这种方式告诉Django,对于这个URL调用这 ...
- samtools+bcftools 进行SNP calling
两个软件的作用:1.samtools mpileup 主要是用于收集BAM文件中的信息,这个位点上有多少条read匹配,匹配read的碱基是什么,并将这些信息存储在BCF文件中.2.bcftools ...
- 第二节课-Data-driven approach:KNN和线性分类器分类图片
2017-08-12 1.图片分类是很多CV任务的基础: 2.图片分类要面临很多的问题,比如图片被遮挡,同一种动物有很多种颜色,形状等等,算法需要足够强壮: 3.所以很难直接写出程序来进行图片分类,常 ...
- Kafka高可用环境搭建
Apache Kafka是分布式发布-订阅消息系统,在 kafka官网上对 kafka 的定义:一个分布式发布-订阅消息传递系统. 它最初由LinkedIn公司开发,Linkedin于2010年贡献给 ...
- python机器学习——分词
使用jieba库进行分词 安装jieba就不说了,自行百度! import jieba 将标题分词,并转为list seg_list = list(jieba.cut(result.get(" ...
- sqlserver 实时同步(发布订阅)
配置发布订阅手册 不同版本须知:https://www.sqlmanager.net/en/articles/1548 向后兼容性:参考https://docs.microsoft.com/zh-cn ...
- MAC 系列 之XCode7.1 + HBuilder MUI 离线打包 ipa 上次application leader 问题:ERROR ITMS - 90534
解决方法:这个原因 网上说法是 beta 测试版本:不过的确是beta版本(7.3 beta)打包的,所以我有下载了一个正式版本 7.1版本. 再次进行测试打包!
- 国内的Git比GitHub快
GitHub的速度简直受不了! 被微软收购之后就堕落了! 用Gitee也挺好的,学习用吧!
- EXCEL中,怎样查看一个工作簿中有几个工作表?
EXCEL中,怎样查看一个工作簿中有几个工作表 有几个EXCEL文件,每个文件(工作簿)中都有上百个工作表, 怎样可以一次性查看一个簿包含几个表? 目前好像没有直接可以看到有多少张工作表的按钮,这就需 ...