mywind

mywind

TCP PORT FORWARD 总结

程序整体框架#

简介#

程序整体是一个单线程的非阻塞的程序,分为 client 和 server 两个部分,可以将内网运行在特定端口(app port 比如 80)
的基于 TCP 协议的服务(app)映射到外网服务器上的动态端口(mapping port 比如 44567)。为了实现 client 和 server
的通信,server 需要监听一个端口(c-s port, 比如 7080)来和 client 通信。由于 NAT 存在的原因,最开始需要 client 向
server 监听的端口(7080)发送一个 connect 来建立连接,并将这个连接作为 masterfd, 来管理 server 对 client 的连接发起请求。
收到 client 的请求后,server 将监听一个动态端口(比如 44567),作为内网服务映射的端口,并通过 masterfd 将端口信息发给
客户端。之后 server 每接受一个请求,就会通过 masterfd 向 client 请求一个连接,之后客户端会建立一个 app port (比如 80)到
c-s port(比如 7080)的数据流转发 tunnel, server 就会建立一个 c-s port 到 mapping port 的数据流转发 tunnel, 两者向连接
就可以完成 port forword 功能,并通过 epoll 管理多个这样的 tunnel 的行为。

数据结构#

client 和 server 内部其实都在做着相同的事情,那就是一对 sock fd, 总是一端接受数据,然后再转发到另一端。因此
可以抽象出一个对象,来完成这些操作。

// 对fd的包装
// buf:该fd用来接收数据的缓存区
// iptr, optr记录数据读写位置的指针
// fd_fin, closed,connected:记录fd状态的变量
class fd_buf{
    public:
        int fd;
        char buf[MAXLINE];
        int iptr, optr;
        bool fd_fin,closed,connected;
        // uint32_t mod;
        fd_buf() {
            iptr = optr = 0;
            fd_fin = false;
            connected = false;
            closed = false;
            // mod = EPOLLIN;
        }
        fd_buf(int fd){
            iptr = optr = 0;
            fd_fin = false;
            connected = false;
            closed = false;
            // mod = EPOLLIN;
            this->fd = fd;
        }

        bool is_buf_empty() {
            return iptr == optr;
        }

        bool is_buf_full() {
            return iptr == MAXLINE;
        }
        

};

// 用来完成数据转发的对象
// 从用户的角度看,一段为上行,一段为下载
class tunnel{
    public:
        fd_buf *fd_up, *fd_down;
        double sum_up, sum_down;
        tunnel() {
            sum_up = 0;
            sum_down = 0;
        }
        tunnel(int fd_user, int fd_app) {
            sum_up = 0;
            sum_down = 0;
            fd_up = new fd_buf(fd_user);
            fd_down = new fd_buf(fd_app);
        }
        int do_read(fd_buf* fd_buf_in, fd_buf* pair_fd_buf);

        int do_write(fd_buf* fd_buf_out, fd_buf* pair_fd_buf);

        void fd_user_read(){
            int n = do_read(fd_up, fd_down);
            sum_up += n;
        }

        void fd_app_read(){
            do_read(fd_down, fd_up);
        }

        void fd_user_write(){
            int n = do_write(fd_up, fd_down);
            sum_down += n;
        }

        void fd_app_write(){
            do_write(fd_down, fd_up);
        }
        
        bool is_user_fd(int fd){
            return fd == fd_up->fd;
        }
        ~tunnel() {
            delete fd_down;
            delete fd_up;
        }


};

非阻塞#

为什么会阻塞?因为对于单个连接而言,如果该阶段的工作没完成,下面的任务就不能正确完成,所以必须阻塞。为什么
要设置非阻塞,因为我们要管理多个连接。在单线程的程序中,如果一个连接阻塞,其它的连接就会连带阻塞住。解决的方法
一个是多线程,一个连接一个线程,就不会相互影响了。另一个就是 I/O 多路复用,通过 epoll/select 监听 sockfd, 并返回
就绪状态的 fd, 这样就不会因为单个连接影响其它连接了。在用于 TCP 通信的 socket 编程中,connect, accept,read,write
都可能会阻塞,不过我们可以通过将其设置为非阻塞的方式来提高性能和避免单个 sock fd 阻塞住其它的 sock fd.

setnonblocking 函数#

// 一个多系统兼容的setnonblocking
int setnonblocking(int fd) {
    if (fd < 0) return -1;
    int flags;
    /* If they have O_NONBLOCK, use the Posix way to do it */
    #if defined(O_NONBLOCK)
        /* Fixme: O_NONBLOCK is defined but broken on SunOS 4.1.x and AIX 3.2.5. */
        if (-1 == (flags = fcntl(fd, F_GETFL, 0)))
        flags = 0;
        return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
    #else
        /* Otherwise, use the old way of doing it */
        flags = 1;
        return ioctl(fd, FIOBIO, &flags);
    #endif
}

非阻塞 connect#

阻塞的原因#

connect 函数会激发 TCP 的三次握手,向 server 发送一个 SYN ,如果对方在一定的时间内没有回复,client 会进行重试,
最长会持续几十秒。

如何设置非阻塞#

在调用 connect 函数之前,调用 setnonblocking (fd) 即可

非阻塞 connect 的处理#

在 connect 前设置 fd 为非阻塞后,如果 tcp 连接没有完成,将会先返回 EINPROGRESS 错误。在连接发生错误情况下 fd 将
即可读又可写;在连接完成的情况下,fd 将变得可写。一般通过 getsockopt () 函数检
查有没有错误返回来判断连接是否成功建立。

 int error;
socklen_t len = sizeof(error);

if(getsockopt(fd,SOL_SOCKET, SO_ERROR, &error, &len) == 0 && error == 0){
        printf("connect success\n");
}
else {
    printf("connect failed\n");
}

非阻塞 accept#

阻塞的原因#

正常的情况下,使用 epoll/select 后,不需要考虑 accept 的阻塞,因为总是监听到连接事件后才调用 accept 的。但是有一种特殊的情况
就是:当客户端发起一个连接,并且这个事件被 epoll/select 监测到,已经放到了待处理的事件列表中,但还没有马上处理,这个时候客
户端中断了这个连接,服务器已经将这个连接移除,当进入 accept 的时候,整个程序就会被阻塞住,直到来了新的连接。

处理方法#

  1. 将对于的 listenfd 设置为非阻塞;
  2. 在 accept 后忽略 EWOULDBLOCK,ECONNABORTED,EPROTO,EINTR 错误

非阻塞 read 和 write#

可读可写的判定条件#

可读

  • 有数据可读
  • 收到对端的 FIN 包
  • 有新的连接过来
  • 待处理错误

可写

  • 有用于写的空间
  • 待处理错误

非阻塞处理#

调用 setnonblocking (fd) 函数即可,然后用 epoll 监听对应的可读可写事件。监听到对应事件
后需要在错误中忽略 EAGAIN 和 EWOULDBLOCK 错误

EPOLL 事件驱动#

以 tunnel read 为例

 // 前提 fd&EPOLLIN, 对端fd_peer状态不清楚
 n = read
 if ( n < 0) {
     //由非阻塞引入的错误不需要处理
     if(errno != EAGAIN && errno != EWOULDBLOCK) {
         //先处理在当前条件下可以马上处理的
         //再根据对端状态及应用缓存区状态分类处理

         // 关闭fd后,epoll会自动移除对该fd的监听。
         close(fd)

         //在当前缓存区不为空,对端没有关闭的情况,需要监听对端的可写
         if(!fd_pair.closed && fd.buf not empty ){
             enable fd_peer EPOLLOUT
             //因为fd已经关闭,所以要关闭对端可读
             disable fd EPOLLIN
         }
     }
 }
 else if (n == 0) {
     if(fd_pair.buf not empty ) {
         enable fd EPOLLOUT
     }

     if(!fd_pair.closed && fd.buf is empty) {
         enable fd_pair EPOLLIN
     }

 }
 else {
     if(fd.buf is empty) {
         if(fd_pair not full && !fd_pair.fd_fin) {
             fd_pair EPOLLOUT|EPOLLIN
         }
         else {
             enable fd_pair EPOLLOUT
         }
     }

     if(fd.buf is full) {
         if(fd_pair.buf not empty) {
             enable fd EPOLLOUT
         }
         else{
             disenable fd EPOLLOUT
         }
     }
 }

关于 tcp 流式套接字特性的处理#

tcp 协议可以保证数据有序的,无误的传送给对端(如果出错就重试或者报错),但是并不是一次发送多少字节的消息,对端就一次接受多少个字节。在 TCP 基础上设计应用层协议的时候,要注意这个特性。

一般的解决办法有两种,一种是定长消息,即规定一定长度的字节为一个消息,当接受的时候,先将接受字节缓存下来,如果没有达到该长度,就继续接受对端的数据。这种方法的灵活性比较差,不合适传递复杂的应用层数据

另一种是设计一个数据包头部,一部分内容用来处理用户的相关协议,并且在发送的时候加一个长度字段。这样对端再收到数据后,先解析头部数据,得到数据的长度,再在收到该长度的消息后,再解析另一个头部即可。

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。