我们学习反向代理知道了客户端并不知道它访问的是哪台服务器主机,有时候我们需要把请求反向到内网主机中去,这个时候就需要使用到内网穿透技术。
很多人对内网穿透技术感觉很陌生,从常规角度来说,我们都是从内网访问外部的网站,路由器的NAT技术帮我们访问到了网站的服务器,再通过NAT把服务器的响应转发到请求的客户端,如图所示
即浏览器访问访问请求,经过路由器,然后到达网站服务器,因为前提我们是指定网站的访问地址的,而内网穿透的访问流程是这样的,如图:
通过流程我们可以清晰地看到通过内网穿透技术可以轻松让外网穿透各种复杂的路由和防火墙访问到内网的设备。
目前国内使用较多的内网穿透技术有Phtunnel和WanGooe Tunnel技术。
什么是PHTunnel
PHTunnel是新一代花生壳核心组件,采用C语言实现(最小约80KB),较上一代反向代理性能全面提升。支持TCP、HTTP和HTTPS协议,端到端的TLS加密通信。
通过PHTunnel可以轻松让外网设备穿透各种复杂的路由和防火墙访问到内网的设备。
什么是WanGooe Tunnel
WanGooe Tunnel是神卓互联研发的穿透协议,采完全由 C 语言实现,单机最低支持创建6万个映射通道,百万级并发请求。支持黑名单防护验证、流量统计与限制、带宽动态调整、域名绑定、TCP和UDP加速、加密和压缩等。
除了市面上主流的花生壳和神卓互联是采用C语言实现的,主要特点是支持高并发架构,访问效率高,速度快,还有的就是清一色Go语言实现的
frp
是一个反向代理应用的开源Go实现,支持 tcp, http, https 等协议类型,并且 web 服务支持根据域名进行路由转发。分析了源码后发现由于采用的是轮询的方式,从效率上来讲单机支持的通道数是很有限的,并不支持高并发的应用场景,如果是个人使用的话私房钱又充足的情况下可以额外租一台服务器自己搭建试一下。
ngrok
ngrok是一个反向代理,通过在公共的端点和本地运行的Web服务器之间建立一个安全的通道。ngrok可捕获和分析所有通道上的流量,便于后期分析与响应。
现在的luci,自带server和client,也就是说如果有一个公网ip的话,是可以用来作为ngrok的server端的,笔者自己尝试搭建过,很容易就出现内存泄露的情况,所以没有深入对架构进行研究。
先不说架构问题,如何通过一个简单的源码Demo来演示一下内网穿透的实现呢
这里就以C语言来做个简单的非高并发的实现,如果需要高并发的具体例子,可以留言或者私下我,我看到后会发
首先是服务端,创建两个监听请求的端口,一个是给外网代理访问的,一个是与客户端通信访问的。
void ForwardSocket::init(int ctrlPort, int serverPort)
{
SOCKET ctrlsockid, serversockid, CtrlSocket = 0, AcceptSocket;
ctrlsockid = CreateSocket(INADDR_ANY, wCtrlPort);
if (ctrlsockid <= 0)
goto error2;
serversockid = CreateSocket(INADDR_ANY, wServerPort);
if (serversockid <= 0)
goto error1;
while (1)
{
printf("%d 等待接收新请求...\n",serverPort);
AcceptSocket = accept(serversockid, NULL, NULL);
if (AcceptSocket == -1)
{
printf("%d accept Error.\n",serverPort);
sleep(1);
continue;
}
make_socket_non_blocking(CtrlSocket);
}
//创建一个结构体
MyInfo.data = CtrlSocket;
MyInfo.ldata.Push(AcceptSocket);
printf("%d 接收到一个外网用户请求.\n",serverPort);
//检测通道是否是通的
if (send(CtrlSocket, (char *)&nRet, 1, 0) == -1)
{
close(CtrlSocket);
}
//单独创建一个线程进行数据读写
pthread_t tidp;
int ret = pthread_create(&tidp, 0, handleMsg, (void *)&MyInfo);
usleep(20);
CtrlSocket = 0;
}
数据中转的方法
while (true)
{
FD_ZERO(&Fd_Read); //清空集合
FD_SET(ClientSock, &Fd_Read); //将Socket 加入到集合中
FD_SET(ServerSock, &Fd_Read);
ret = select(FD_SETSIZE, &Fd_Read, NULL, NULL, NULL);
if (ret <= 0)
goto error;
if (FD_ISSET(ClientSock, &Fd_Read))
{ //检查描述符是否可以读写
nRecv = read(ClientSock, RecvBuf, sizeof(RecvBuf));
if (nRecv <= 0)
goto error;
ret = DataSend(ServerSock, RecvBuf, nRecv,0);
if (ret == 0 || ret != nRecv)
goto error;
}
if (FD_ISSET(ServerSock, &Fd_Read))
{
nRecv = read(ServerSock, RecvBuf, sizeof(RecvBuf));
if (nRecv <= 0)
goto error;
ret = DataSend(ClientSock, RecvBuf, nRecv,0);
if (ret == 0 || ret != nRecv)
goto error;
}
} //end while
这里要注意一下就是调用send发送进行发送消息的时候由于消息缓冲区大小是有限的,或者对方网络状况发生了异常,这个时候可能并不是一下子就发送成功的,所以每次发送要做检查,确保消息是已经发送成功。
int nBytesLeft = DataLen;
int nBytesSent = 0;
int ret;
while (nBytesLeft > 0)
{
ret = send(s, DataBuf + nBytesSent, nBytesLeft, 0);
if (ret <= 0)
break;
nBytesSent += ret;
nBytesLeft -= ret;
}
接下来是客户端的,主要思路是创建与服务端的socket连接和与被访问主机直接的socket连接
while (1)
{
while (!connect) {
socket= ConnectHost(dstIP, dstPort);
if (socket<= 0) {
Sleep(100);
}
else {
MyInfo.data.s = socket;
connect= true;
}
}
nRet = ioctlsocket(socket, FIONREAD, &lRecv);
if (lRecv > 0) {
nRecv = recv(socket, (char*)&ReqPort, 1, 0);
connect= false;
nCount = 0;
if (nRecv <= 0) {
closesocket(socket);
continue; //接收失败
}
nTimes++;
MyInfo.ldata.Push(socket);//传递信息的结构
hThread = CreateThread(NULL, 0, handleMsg, (LPVOID)&MyInfo, NULL, &dwThreadId);
if (hThread)
CloseHandle(hThread);
else
Sleep(100);
}
else {
nCount++;
if (nCount >= 0x1000) {
nCount = send(socket, (char*)&nCount, 1, 0);
if (nCount == SOCKET_ERROR) {
printf("send error:%d\r\n", WSAGetLastError());
closesocket(socket);
connect= false;
}
}
}
Sleep(2);
}
closesocket(socket);
其实内网穿透原理就是将外网的请求转发访问目标应用,其实里面的涉及技术还好很多的,就好比nginx,原来也很简单,但是性能要到达nginx的级别需要很深的技术功底的。
最近正在研究Nginx的检查策略,希望可以一起交流
企业场景中重启Nginx后的检测策略
在企业运维实践场中,每一个配置操作处理完毕后都应该进行快速有效的检查,这是一个合格运维人员的良好习惯。尽量使得在Nginx启动的同时,还会调用脚本通过获取header信息或模拟用户访问指定URL(wget等方式)来自动检查Nginx是否正常,最大限度的保证服务重启后,能迅速确定网站情况,而无须手工敲命令查看。这样如果配置有问题就可以迅速使用上一版本备份配置文件覆盖回来。
[root@localhost conf]# cat check_url.sh
#!/bin/bash
#--------function split--------
. /etc/rc.d/init.d/functions
function checkURL()
{
checkUrl=$1
echo 'check url start ...'
judge=($(curl -I -s --connect-timeout 2 ${checkUrl}|head -1 | tr " " "\n"))
if [[ "${judge[1]}" == '200' && "${judge[2]}" == 'OK' ]]
then
action "${checkUrl}" /bin/true
else
action "${checkUrl}" /bin/false
echo -n "retrying again...";sleep 3;
judgeagain=($(curl -I -s --connect-timeout 2 ${checkUrl}| head -1| tr "\r" "\n"))
if [[ "${judgeagain[1]}" == '200' && "${judgeagain[2]}" == 'OK' ]]
then
action "${chekcUrl}, retried again" /bin/true
else
action "${chekcUrl}, retried again" /bin/false
fi
fi
sleep 1;
}
# usage method