之前有一篇文章《华为AnyOffice eSDK建立TCP端口转发隧道》介绍了如何使用Java建立一条TCP端口隧道。实现一个将远程端口反向代理到本地的效果,类似于Nginx。比如你有一个远程Web服务器1.2.3.4:80,你建立起TCP转发隧道后,访问本地的127.0.0.1:8080端口就等于访问远程服务器的80端口。那么反向代理的轮子很多,比如Nginx,Haproxy,NodeJS,Caddy,Apache等等,为什么要自己写一个呢。我当初写这个主要目的还是为了在Android和iPhone手机端使用反向代理功能,并基于此兼容华为AnyOffice提供的eSDK,通过建立隧道来实现4层TCP协议的VPN通讯(注:华为的eSDK只提供了4层的HTTP协议的API,对4层协议没有很好的支持)。
所以这里,就介绍一下如何在IOS上实现一条TCP反向代理隧道。虽然说是用Objc开发的,但主要还是使用C语言实现的,好在IOS和MAC OS都可以在.m文件中混用C/C++和Objc代码,使用C语言的原因是华为的eSDK是用的C语言接口,他们重写了recv()和send(),变成了svn_recv()和svn_send()方法,所以为了实现最好的兼容,我们的隧道也采用C书写。Objc也有很大的任务,那就是使用GCD开启线程,使得整个代码与JAVA代码非常相似,简直如同JAVA代码一句一句翻译过来的一样,所有线程的调度也是一样的,所以在这里我就只贴代码,原理可以去我之前的博客查看。
下面就是在IOS和MAC OS中建立TCP隧道的代码了,下面代码使用的是原生socket,可以跑在任意一台iPhone和MAC主机上,对于MAC,直接在xcode中新建一个命令行应用然后执行main函数即可。对于iOS,需要把main函数放在一条子线程中异步执行。如果要和华为eSDK对接,只需把某些recv()/send()调用换成svn_recv(),svn_send()调用即可。并把下面的remote_socket初始化成svn_socket。并引入华为eSDK的头文件。具体需要更换的地方我都写在注释中了。
源代码中有两处IP地址的赋值,分别是0.0.0.0:8080 和 1.2.3.4:80,其中
0.0.0.0:8080是隧道的入口地址,你使用其他程序(比如浏览器)访问127.0.0.1:8080就等同与访问1.2.3.4:80
1.2.3.4:80是隧道出口地址,也是被反向代理的地址,使用过Nginx的攻城狮们应该不陌生。
#import
#include
#include
#include
#include
#include
//int startTunnel();
int main(int argc, const char * argv[]) {
int result = -8;
while(result == -8){//在断网的时候,accept函数会失效然后退出,然后不断的尝试重建隧道
result = startTunnel();
sleep(1);
}
}
int startTunnel() {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
struct sockaddr_in server_addr;
server_addr.sin_len = sizeof(struct sockaddr_in);
server_addr.sin_family = AF_INET;//Address families AF_INET互联网地址簇
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = inet_addr("0.0.0.0");
bzero(&(server_addr.sin_zero),8);
//创建socket
int server_socket = socket(AF_INET, SOCK_STREAM, 0);//SOCK_STREAM 有连接
int haha=1;
setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &haha, sizeof(haha));//程序退出后可以解除端口占用
if (server_socket == -1) {
perror("socket error");
return 1;
}
//绑定socket:将创建的socket绑定到本地的IP地址和端口,此socket是半相关的,只是负责侦听客户端的连接请求,并不能用于和客户端通信
int bind_result = bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (bind_result == -1) {
perror("bind error");
return -9;
}
//listen侦听 第一个参数是套接字,第二个参数为等待接受的连接的队列的大小,在connect请求过来的时候,完成三次握手后先将连接放到这个队列中,直到被accept处理。如果这个队列满>了,且有新的连接的时候,对方可能会收到出错信息。
if (listen(server_socket, 5) == -1) {
perror("listen error");
return 1;
}
while(1){
struct sockaddr_in client_address;
socklen_t address_len;
memset(&client_address,0,sizeof(client_address));
address_len = sizeof(client_address);
int client_socket = accept(server_socket, (struct sockaddr *)&client_address, &address_len);
printf("\n\n\nclient socket is %d ip address is %s:%d\n",client_socket,inet_ntoa(client_address.sin_addr),ntohs(client_address.sin_port));//打印客户端的请求地址
if(client_socket == -1){
printf("what the fuck\n");
shutdown(client_socket, SHUT_RDWR);
return -8;
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//在这里应该新开一个线程执行下面的操作
printf("接收到了新的连接\n");
//返回的client_socket为一个全相关的socket,其中包含client的地址和端口信息,通过client_socket可以和客户端进行通信。
if (client_socket == -1) {
perror("accept error");
close(client_socket);
return;
}
//-----------初始化发送到真实服务器的svn_socket--------
struct sockaddr_in remote_addr;
remote_addr.sin_len = sizeof(struct sockaddr_in);
remote_addr.sin_family = AF_INET;
remote_addr.sin_port = htons(80);//emessage服务器的端口
remote_addr.sin_addr.s_addr = inet_addr("1.2.3.4");//emessage服务器的IP地址
bzero(&(remote_addr.sin_zero),8);
int remote_socket = socket(AF_INET, SOCK_STREAM, 0);//这个socket应该是华为的svn_socket,用于和真实服务器通信
if (remote_socket == -1) {
perror("socket error");
close(client_socket);
close(remote_socket);//此处应该为svn_close
return;
}
if (connect(remote_socket, (struct sockaddr *)&remote_addr, sizeof(struct sockaddr_in))==0) {//此处应该是svn_connect
printf("destination is %d ip address is %s:%d\n",remote_socket,inet_ntoa(remote_addr.sin_addr),ntohs(remote_addr.sin_port));
//connect 成功之后,其实系统将你创建的socket绑定到一个系统分配的端口上,且其为全相关,包含服务器端的信息,可以用来和服务器端进行通信。
printf("远程服务器连接成功\n");
} else {
close(server_socket);
return;
//此处应终止,不要再往下走了
}
//----------下面是原生socket接收请求---------
//下面代码是接收手机APP发出的请求,然后转发到VPN里面
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//下面这个while循环应该单独跑在一个线程里,是把客户端转发到服务器
long byte_num = 10000;
char recv_msg[2048];//xmpp向外发送的消息
while (byte_num>0) {
bzero(recv_msg, 2048);
byte_num=0;
byte_num = recv(client_socket,recv_msg,2048,0);//接收到了xmpp向外发送的数据
if(byte_num < 1)break;
printf("length==%ld\n",byte_num);
//把接收到的字节再发到真实服务器
if (send(remote_socket, recv_msg, byte_num, 0) == -1) {//此处应该是svn_send
perror("send error");
close(remote_socket);//此处应该换成svn_socket
close(client_socket);
}else{
printf("向远程服务器发送数据成功\n");
}
}
close(client_socket);
close(remote_socket);
printf("全部请求都发送完毕\n");
});
//下面的代码是接收服务器发来的消息,然后再发给手机APP
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//下面这个while也要单独跑在一个线程里,是把真实服务器转发给客户端
long reply_num = 10000;
char server_msg[2048];//服务器响应的消息
while (reply_num>0) {
reply_num=0;
bzero(server_msg, 2048);
reply_num = recv(remote_socket, server_msg, sizeof(server_msg), 0);//接收到了真实服务器响应的消息,此处应为svn_recv
printf("length==%ld\n",reply_num);
if(reply_num < 1)break;
if (send(client_socket, server_msg, reply_num, 0) == -1) {//把服务器响应的信息发送给xmpp
perror("send error");
close(remote_socket);//此处应该换成svn_socket
close(client_socket);
} else {
//server_msg[reply_num]='\0';
//printf("向客户端发送数据成功%s\n",server_msg);
}
}
printf("全部数据都发送完毕\n");
close(remote_socket);//此处应该换成svn_socket
close(client_socket);
});
printf("全部的线程都退出了\n");
});
}
}
return 0;
}