携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情
C/S模型简述
我们知道,c/s模型一般是客户端向服务器发起请求,服务端响应请求的一个模型.
客户端和服务器之间连接的数量对应关系多个客户进程可以同时访问一个服务进程,一个客户进程可以同时访问多个服务器进程提供的服务。
如下图所示:
sequenceDiagram
Client->>Server: GET /blog/page HTTP/1.1
Server->>Client: HTTP/1.1 200 OK
内网穿透
首先,明确号内网和外网的定义:
内网一般指的是局域网,局域网和广域网通信现在大多会采用net转发(因为ip不够用)
外网指的是拥有共有ip的主机,可以直接进行通信
那么, 对于我一个服务,比如说我现在是一个未上线的服务,我要想别人来访问太,如果不是在内网中,这是访问不到的. 因为找不到到达你的路由.(内网可以访问外网是因为net转发)
端口映射是 NAT 的一种,它将外网主机的 IP 地址的一个端口映射到内网中一台机器,提供相应的服务。当用户访问该 IP 的这个端口时,服务器自动将请求映射到对应局域网内部的机器上。
之前提到的内网,是不能被外网直接的访问的,只能通过一些中转技术,让内网“假装”成外网。
这就平常所说的内网穿透。
内网穿透转发实现思路
找个中间人, 中间人和双方建立连接.
分别针对外部和内部实现一个 External connection 和 Internal connection 来建立两者之间的关系
sequenceDiagram
Client->>External connection: GET /blog/page HTTP/1.1
External connection->>Internal connection: send message "open connection"
Internal connection->>External connection: tcp connection
Internal connection->>Server: tcp connection
External connection->>Internal connection: forward pack
Internal connection->>Server: forward pack
Server->>Internal connection: HTTP/1.1 200 OK
Internal connection->>External connection: forward pack
External connection->>Client: forward pack
代码实现
External connection
采用了多进程的方式. 对于每一个内网穿透的映射开一个进程用来处理
每一个进程回开启一个和内部通信的端口 一个和外部通信的端口 流程如下:
sequenceDiagram
Internal connection->>External connection(main): tcp connection
External connection(main)->>Internal connection: send message "mapping port, son port"
Internal connection->>External connection(son): tcp connection
main
int main() {
int sock_fd = socket(PF_INET, SOCK_STREAM, 0);
assert(sock_fd != -1);
sockaddr_in sock;
socklen_t socklen = sizeof(sock);
sock.sin_port = htons(8089);
sock.sin_family = AF_INET;
sock.sin_addr.s_addr = INADDR_ANY;
int stat = bind(sock_fd, (sockaddr *)&sock, sizeof(sock));
assert(stat != -1);
stat = listen(sock_fd, 10);
assert(stat != -1);
stat = getsockname(sock_fd, (struct sockaddr *)&sock, &socklen);
assert(stat != -1);
printf("port in : %d\n", ntohs(sock.sin_port));
socklen_t len = sizeof(sock);
int client_fd = -1;
while (1) {
client_fd = accept(sock_fd, (sockaddr *)&sock, &len);
assert(client_fd != -1);
if (fork() == 0) break;
}
if (client_fd == -1) return 0;
process(client_fd);
return 0;
}
add_fd
void add_fd(int e_fd, int fd, bool flag = true) {
if (flag) fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
epoll_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN | EPOLLHUP | EPOLLET | EPOLLERR;
epoll_ctl(e_fd, EPOLL_CTL_ADD, fd, &ev);
assert(fcntl(fd, F_GETFL, 0) & O_NONBLOCK);
}
process
有待优化的空间: 拷贝数据
使用 哈希来存放 内部连接和外部连接对应的描述符.
用epoll来统一管理这些socketfd
每当有一个fd有EPOLLIN事件后,都把他写入对应的socketfd
void process(int client_fd) {
/**
* 注册外部端口
**/
int socket_fd = socket(PF_INET, SOCK_STREAM, 0);
assert(socket_fd != -1);
sockaddr_in sock;
socklen_t socklen = sizeof(sock);
sock.sin_port = 0;
sock.sin_family = AF_INET;
sock.sin_addr.s_addr = INADDR_ANY;
int stat = bind(socket_fd, (sockaddr *)&sock, sizeof sock);
assert(stat != -1);
stat = listen(socket_fd, 10);
assert(stat != -1);
stat = getsockname(socket_fd, (struct sockaddr *)&sock, &socklen);
assert(stat != -1);
int now_port = ntohs(sock.sin_port);
/**
* 注册内部端口
**/
int server_fd = socket(PF_INET, SOCK_STREAM, 0);
assert(server_fd != -1);
sock.sin_port = 0;
sock.sin_family = AF_INET;
sock.sin_addr.s_addr = INADDR_ANY;
stat = bind(server_fd, (sockaddr *)&sock, sizeof(sock));
assert(stat != -1);
stat = listen(server_fd, 10);
assert(stat != -1);
stat = getsockname(server_fd, (struct sockaddr *)&sock, &socklen);
assert(stat != -1);
int server_port = ntohs(sock.sin_port);
char buf[1023];
printf("%d, %d\n", now_port, server_port);
send(client_fd, &now_port, sizeof now_port, 0);
send(client_fd, &server_port, sizeof server_port, 0);
int epoll_fd = epoll_create(10);
epoll_event event[1024];
add_fd(epoll_fd, socket_fd);
std::unordered_map<int, int> mp;
auto clear = [&](int src, int dest) {
close(src);
close(dest);
mp.erase(src);
mp.erase(dest);
};
while (true) {
stat = epoll_wait(epoll_fd, event, 1023, -1);
assert(stat != -1);
for (int i = 0; i < stat; i++) {
int fd = event[i].data.fd;
if (event[i].events | EPOLLIN) {
if (fd == socket_fd) {
while (1) {
int request = accept(socket_fd, (sockaddr *)&sock, &socklen);
if (request == -1) {
if (errno == EAGAIN || errno == ECONNABORTED || errno == EPROTO ||
errno == EINTR)
break;
perror("request");
break;
}
int pack = 0;
send(client_fd, &pack, sizeof pack, 0);
int connect = accept(server_fd, (sockaddr *)&sock, &socklen);
assert(connect != -1);
add_fd(epoll_fd, request);
add_fd(epoll_fd, connect);
mp[request] = connect;
mp[connect] = request;
}
} else {
int src = fd;
int dest = mp[fd];
if (!dest) continue;
assert(fcntl(src, F_GETFL, 0) & O_NONBLOCK);
assert(fcntl(dest, F_GETFL, 0) & O_NONBLOCK);
while (1) {
stat = recv(src, buf, sizeof(buf) - 1, 0);
if (stat == 0) {
clear(src, dest);
break;
} else if (stat == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) break;
clear(src, dest);
break;
}
buf[stat] = 0;
send(dest, buf, stat, 0);
}
}
} else {
perror("events");
if (fd == socket_fd) {
close(fd);
} else {
clear(fd, mp[fd]);
}
}
}
}
}
Internal connection
main
Internal connection 和 External connection 的关系是 多对一.
即每一个新映射到External connection上的服务都会新开一个进程去单独处理
sequenceDiagram
Internal connection->External connection(son):
graph TD;
ecm["External connection(main)"]
ecs1["External connection(son 1)"]
ecs3["External connection(son 3)"]
ecs2["External connection(son 2)"]
ic1["Internal connection 1"]
ic2["Internal connection 2"]
ic3["Internal connection 3"]
ecm-->ecs1
ecm-->ecs2
ecm-->ecs3
ecs1-->ic1
ecs2-->ic2
ecs3-->ic3
char buf[65536];
int main(int argc, char** argv) {
assert(argc == 4);
char* addr = argv[1];
int _port = atoi(argv[2]);
int server_port = atoi(argv[3]); /* wait */
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
assert(socket_fd != -1);
sockaddr_in sock;
sock.sin_family = AF_INET;
sock.sin_port = htons(server_port);
inet_aton(addr, &sock.sin_addr);
int stat = connect(socket_fd, (sockaddr*)&sock, sizeof sock);
assert(stat != -1);
defer([&]() -> void { close(socket_fd); });
int mapping_port, connect_port;
recv(socket_fd, &mapping_port, sizeof mapping_port, 0);
recv(socket_fd, &connect_port, sizeof connect_port, 0);
printf("mapping port in %d\n", mapping_port);
printf("connect port in %d\n", connect_port);
std::unordered_map<int, int> mp;
auto clear = [&](int src, int dest) {
close(src);
close(dest);
mp.erase(src);
mp.erase(dest);
};
int epoll_fd = epoll_create(10);
epoll_event event[1024];
add_fd(epoll_fd, socket_fd);
while (true) {
stat = epoll_wait(epoll_fd, event, 1023, -1);
assert(stat != -1);
for (int i = 0; i < stat; i++) {
int fd = event[i].data.fd;
if (event[i].events | EPOLLIN) {
if (fd == socket_fd) {
while (1) {
int tm;
stat = recv(socket_fd, &tm, sizeof tm, 0);
if (stat == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) break;
break;
perror("recv");
}
int server_fd = socket(PF_INET, SOCK_STREAM, 0);
assert(server_fd != -1);
{
sockaddr_in sock;
inet_aton("0.0.0.0", &sock.sin_addr);
sock.sin_port = htons(_port);
sock.sin_family = AF_INET;
stat = connect(server_fd, (sockaddr*)&sock, sizeof(sock));
assert(stat != -1);
}
int connect_fd = socket(PF_INET, SOCK_STREAM, 0);
assert(connect_fd != -1);
{
sockaddr_in sock;
inet_aton(addr, &sock.sin_addr);
sock.sin_port = htons(connect_port);
sock.sin_family = AF_INET;
stat = connect(connect_fd, (sockaddr*)&sock, sizeof(sock));
assert(stat != -1);
}
add_fd(epoll_fd, connect_fd);
add_fd(epoll_fd, server_fd);
mp[connect_fd] = server_fd;
mp[server_fd] = connect_fd;
}
} else {
int src = fd;
int dest = mp[fd];
if (!dest) continue;
assert(fcntl(src, F_GETFL, 0) & O_NONBLOCK);
assert(fcntl(dest, F_GETFL, 0) & O_NONBLOCK);
while (1) {
stat = recv(src, buf, sizeof(buf) - 1, 0);
if (stat == 0) {
clear(src, dest);
break;
} else if (stat == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) break;
clear(src, dest);
break;
}
buf[stat] = 0;
send(dest, buf, stat, 0);
}
}
} else {
perror("error");
exit(0);
}
}
}
return 0;
}
写什么代码, 不如用现成的 (
当然, 我们也可以直接用现成的工具
比如下列的工具:
Ngrok
一个通过任何NAT或防火墙为您的本地主机服务器提供即时访问、安全的URL的命令。类似花生壳,分为服务端和客户端,也可以自己搭建服务端。
Frp
frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp, udp, http, https 协议。