socks5 代理服务器项目

那我整体的描述一下我的socks5 项目

  • 我的项目的功能通过VPS 服务器,搭载我的项目。可以实现访问各大网站。
  • 我的项目分为五个模块。

1. Epoll 基类

  • 我的Epoll 基类模块干的事情,由于这个模块是为了我的transfer 模块和 socks 模块提供服务,因此我设置了可继承的函数,比如Start(),它是为了绑定我的套接字,并且创建我的 epoll 用来管理我的事件,同时将我的listen 套接字加入到 epoll 事件中去。当一切准备就绪后,我就进入了我的事件循环中去,于是我有一个函数 EventLoop(),处理我的Epoll 中的事件。
  • 当然我的Epoll 事件主要有三种,分别是请求连接事件、读事件、写事件。 其中处理我的连接事件和读事件的函数是纯虚函数,而我的写事件是我的Epoll 类所实现的虚函数。为什么要把请求连接事件和读事件设置为纯虚函数?是因为我的transfer 模块和 socks5 模块对于这两个函数所要实现的功能不一样。具体功能在模块中说。

2. Socks5 模块

  • 这个模块继承自Epoll 模块。并且这个模块主要是重写Epoll 类中的连接处理函数,还有Epoll 类中读事件处理的函数。
  • 对于socks5 服务来说,连接处理比较简单,只需要将我的文件描述符加到我的Epoll 事件中,并且设置好我的通道中的参数:连接的状态(身份认证状态)以及连接的客户端文件描述符。
  • 对于socks5服务器来说,读事件处理牵扯到对socks5 协议的认识。当读事件来的时候,我socks5 需要查看这个连接的状态,分别为身份认证状态、建立连接状态和转发状态。
    • 身份认证状态的包我收到之后,首先去解析这个包,检查是不是对应的socks5 协议的包。如果是,则把连接的状态改变成建立连接状态。最后给对端回一个数据包
    • 建立连接状态的包在我收到之后,这个时候我就需要和server 端建立连接了。于是乎我再解析我的数据包,数据包有三种情况,发过来的对端地址是IPv4、域名和IPv6。如果是IPv4 的话我直接拿到IP 和port 建立连接完事。当我拿到的是域名的时候,我通过 gethostbyname() 这个函数将域名解析成IP 地址,那个函数底层调用的就是DNS 服务器而已。目前我的项目没有处理IPv6 的情况。
    • 当建立好连接之后,我会将这个文件描述符加入到我的Epoll 事件中去,并且设置我这条连接的server 通道,设置 server 通道中的文件描述符,设置连接的状态为转发状态。
    • 当收到转发的包之后,这个转发的函数我是继承父类的转发函数,完成的功能也比较简单。将数据从客户端接受过来,然后发送给服务端。这块有很多问题,
      • 比如说,我接收数据,那么我一次性接收多少数据比较好?也就是说我的buff 设置多大比较好?设置小了,对端要发多次,效率低。设置大了,我转发的时候一次性可能转发不完?多次转发又要。。。。
      • 比如说,我接收数据一次性接收完了,好说。如果接收不完,又该怎么办?
      • 还需要处理如果我recv 到一个 0 ,又该怎么办?
      • 这些问题我还在下面的问题中谈。

3. Transfer 模块

  • 这个模块其实也算是一个服务,部署在我的brower 浏览器和socks服务器之间,说实际点就是放在我的本机上。拿到从我本机上出去的数据,然后转发给socks5 服务器。为什么要这样做呢?我不能直接连接socks5 服务器吗??
  • 这个问题好,当然可以直接访问socks5 服务器,我们的功能是通过socks5 服务器实现访问google。我的转发模块最主要的工作转发前我对我的数据进行加密,收到数据时我再解密,为什么要加这一层呢?是因为我不想让我的数据在网络上裸奔。
  • 而且我的transfer 服务放在我本机上,网络上看到的数据永远是我加密后的数据。
  • 这个模块实现起来也比较简单。就是重写我Epoll 父类中建立连接函数和读事件函数。
    • 建立连接函数实现也比较简单,当有连接来的时候,我和socks5 服务器建立连接。然后拿到client 端和 server 端的两个文件描述符。首先将文件描述符设置为非阻塞,然后将两个文件描述符添加到Epoll 事件中,并且设置通道,将连接的状态改成转发状态,还有设置通道中国文件描述符。
    • 读事件函数更加简单。事件来了之后我只负责将连接好的通道的客户端数据转发到通道的服务端即可。调用我Epoll 父类中的转发函数即可。当然转发过程也会遇到socks5 服务器转发时遇到的问题。下面会谈到这个的。

4. 加密模块

  • 这个模块做的事情,就是服务我的转发模块。将我转发出去的数据进行加密,将我接收到的数据进行解密。
  • 我用到的加密算法也比较简单,这块是项目的一个弱点。只是简单的异或了一下。
  • 后期我会用更加有效的加密算法。

5. 日志模块

  • 这个模块和项目功能没有太大的关系。但是在调试过程中真的是少不了他啊。
  • 分别有一个跟踪流程的日志和错误的日志。
  • 定义 __ FILE__, __ LINE__, __ FUNCTION__, __ VA_ARGS__,这几个宏分别来获取到那个文件、那一行、那个函数、最后一个可变参数列表。
socks5协议
  • SOCKS5 是一个代理协议,它在使用TCP/IP协议通讯的前端机器和服务器机器之间扮演一个中介角色,使得内部网中的前端机器变得能够访问Internet网中的服务器,或者使通讯更加安全。SOCKS5 服务器通过将前端发来的请求转发给真正的目标服务器, 模拟了一个前端的行为。在这里,前端和SOCKS5之间也是通过TCP/IP协议进行通讯,前端将原本要发送给真正服务器的请求发送给SOCKS5服务器,然后SOCKS5服务器将请求转发给真正的服务器。

项目的框架

在这里插入图片描述

EpollServer 类

class EpollServer
{
public:
	EpollServer(int port)
		:_port(port)
		,_listenfd(-1)
		,_eventfd(-1)
	{}

	virtual ~EpollServer()
	{
		if(_listenfd != -1)
			close(_listenfd);
	}

	void Start();
	void EventLoop();

	void SendInLoop(int fd, const char* buf, int len);
	void Forwarding(Channel* clientChannel, Channel* serverChannel, 
					bool sendencry, bool recvdecrypt);
	void RemoveConnect(int fd);

	// 多态实现的虚函数
	virtual void ConnectEventHandle(int connectfd) = 0;
	virtual void ReadEventHandle(int connectfd) = 0;
	virtual void WriteEventHandle(int connectfd);

protected:
	int _port;	   // 端口
	int _listenfd; // 监听描述符
	int _eventfd;  // 事件描述符
	map<int, Connect*> _fdConnectMap; // fd映射连接的map容器
};
  • 我在我的EpollServer 类中定义了两个纯虚函数,分别让我的TransferServer 类和 Socks5Server 类去重写,实现多态。同时可以让子类拿到我的map 容器。使通道建立起来。

TransferServer类

class TranferServer : public EpollServer
{
public:
	TranferServer(int selfport, const char* socks5ip, int socks5port)
		: EpollServer(selfport)
	{
		memset(&_socks5addr, 0, sizeof(struct sockaddr_in));
		_socks5addr.sin_family = AF_INET;
		_socks5addr.sin_port = htons(socks5port);
		_socks5addr.sin_addr.s_addr = inet_addr(socks5ip);
	}

	// 多态实现的虚函数
	virtual void ConnectEventHandle(int connectfd);
	virtual void ReadEventHandle(int connectfd);
	
protected:
	struct sockaddr_in _socks5addr;
};

Socks5Server 类

class Sock5Server : public EpollServer 
{
public:
    Sock5Server(int port)
        : EpollServer(port)
    {}

	//安全认证和建立连接
    int AuthHandle(int fd);
    int EstablishmentHandle(int fd);

    virtual void ConnectEventHandle(int connectfd);
    virtual void ReadEventHandle(int connectfd);
};
与Epoll服务器相关的功能性函数
	//  操作epoll 事件
   void OPEvent(int fd, int events,int op)
    {
        struct epoll_event event;
        event.events = events;
        event.data.fd = fd;
        if(epoll_ctl(_eventfd,op,fd,&event) < 0)
        {
            ErrorLog("epoll_ctl(op:%d,fd:%d)",op,fd);
        }
    }
    // 设置描述符为非阻塞
    void SetNonblocking(int sfd)
    {
        int flags, s;
        flags = fcntl(sfd,F_GETFL,0);
        if(flags == -1)
            ErrorLog("SetNonblocking:F_GETFL");

        flags |= O_NONBLOCK;
        s = fcntl(sfd,F_GETFL,flags);
        if(s == -1)
            ErrorLog("SetNonblocking:F_GETFL");
    }
    
    // 开始 epoll server
    void EpollServer::Start()
	{
	    _listenfd = socket(PF_INET,SOCK_STREAM,0);
	    if(_listenfd == -1)
	    {
	        ErrorLog("create soket");
	        return;
	    }
	
	    struct sockaddr_in addr;
	    memset(&addr,0,sizeof(addr));
	    addr.sin_family = AF_INET;
	    addr.sin_port = htons(_port);
	    addr.sin_addr.s_addr = htonl(INADDR_ANY);
	
	    if(bind(_listenfd,(struct sockaddr*)&addr,sizeof(addr)) < 0)
	    {
	        ErrorLog("bind error");
	        return;
	    }
	    
	    if(listen(_listenfd,100000) < 0)
	    {
	        ErrorLog("listen");
	        return;
	    }
	
	    TraceLog("epoll server listen on %d",_port);
	    
	    _eventfd = epoll_create(100000);
	    if(_eventfd == -1)
	    {
	        ErrorLog("epoll_create");
	        return;
	    }
	
	    //添加listenfd 到epoll,监听连接事件
	    SetNonblocking(_listenfd);
	    OPEvent(_listenfd, EPOLLIN, EPOLL_CTL_ADD);
	
	    //进入事件循环
	    EventLoop();
}

// 事件循环
void EpollServer::EventLoop()
{
    struct epoll_event events[100000];
    while(1)
    {
        //epoll_wait 最后一个参数timeout,-1代表永远阻塞,0代表调用立即返回
        int n = epoll_wait(_eventfd, events, 100000,0);
        for(int i = 0;i < n; ++i)
        {
            if(events[i].data.fd == _listenfd)
            {
                struct sockaddr clientaddr;
                socklen_t len;
                int connectfd = accept(_listenfd, &clientaddr, &len);
                if(connectfd < 0)
                    ErrorLog("accept");

                TraceLog("new cnonnect...");
                ConnectEventHandle(connectfd);
            }
            else if(events[i].events & EPOLLIN)  //读事件
                ReadEventHandle(events[i].data.fd);
            else if(events[i].events & EPOLLOUT) //写事件
                WriteEventHandle(events[i].data.fd);
            else 
                ErrorLog("event %d",events[i].data.fd);

        }
    }
}

  • 上面的是我的头文件,框架基本上是出来了。
数据传输的通道and 连接的结构
	// 通道
    struct Channel
    {
        int _fd;   //描述符
        string _buff; //写缓冲

        Channel()
            : _fd(-1)
        {}
    };
	// 连接
    struct Connect
    {
        Sock5State _state;  //连接的状态
        Channel _clientChannel; //客户端的通道
        Channel _serverChannel; //服务端的通道
        int _ref;  //引用计数控制Connect的销毁,因为通道是一条关闭,另一条再关闭的

        Connect()
            :_state(AUTH)
            ,_ref(0)
        {}
    };
数据通道转发过程

在这里插入图片描述

browser —> transfer server
  • transfer server 接收 browser 数据,加密后转发给 socks5 server。加密数据可以躲过防火墙。
socks5 server —> google
  • socks5 server接收transfer server的数据,先解密数据。再进入如下步骤:
  1. 第一次数据过来,解析socks5协议头,进行身份认证auth,并回复客户端。
  2. 第二次数据过来,解析socks5协议头,进行服务器信息解析,和 google server建立连接Establishment,并回复客户端。
  3. 后续再发数据过来,则直接解密后,转发给google server
google server —> socks5 server
  • socks5 server 接收 google server的数据,直接加密,转发给transfer server
transfer server —> browser
  • transfer server 接收 socks5 server 的数据,先解密,再传输给 browser
通道数据的转发
void EpollServer::Forwarding(Channel* clientChanne,Channel* serverChannel,bool sendencry,bool recvdecrypt)
{
    char buf[4096]; // 我一次就recv 4k 的字节
    int rlen = recv(clientChanne->_fd,buf,4096,0);
    signal(SIGPIPE,SIG_IGN);
    if(rlen < 0)
    {
        ErrorLog("recv : %d",clientChanne->_fd);
    }
    else if(rlen == 0) // 对端进入四次挥手
    {
        //client channel 发起关闭
        // client 发送recv一个0,就说明client 不会给我的server发数据了
        // socks 服务器的读是不会关闭的,它要是关闭了如何读到两端的数据
        shutdown(serverChannel->_fd,SHUT_WR);  // 关的是我的socks 服务器的write,意思是我不会再向server端写
        RemoveConnect(clientChanne->_fd);
    }
    else 
    {
        // 万一我这边一次性发不完就会出现问题
        /*
        int slen = send(serverChannel->_fd,buf,rlen,0);
        TraceLog("recv:%d->send:%d",rlen,slen);
        */
        // 因此我们通过事件循环来发
        if(recvdecrypt)
        {
            Decrypt(buf,rlen);
        }
        if(sendencry)
        {
            Encry(buf,rlen);
        }
        buf[rlen] = '\0';
        SendInLoop(serverChannel->_fd,buf,rlen);
    }
}

void EpollServer::SendInLoop(int fd, const char* buf,int len)
{
    int slen = send(fd,buf,len,0);
    if(slen < 0)
    {
        ErrorLog("send to %d",fd);
    }
    else if(slen < len)
    {
        TraceLog("recv %d bytes,send %d bytes,left %d send in loop",len,slen,len-slen);
        map<int,Connect*>::iterator it = _fdConnectMap.find(fd);
        if(it != _fdConnectMap.end())
        {
            Connect* con = it->second;
            Channel* channel = &con->_clientChannel;
            if(fd == con->_serverChannel._fd)
            {
                channel = &con->_serverChannel;
            }
            // EPOLLONESHOT 只会通知我一次,万一还是没有写完,那么我就继续调用SendInLoop
            int events = EPOLLOUT | EPOLLIN | EPOLLONESHOT;
            OPEvent(fd,events,EPOLL_CTL_MOD);
            channel->_buff.append(buf+slen);
        }
        else 
        {
            assert(false);
        }
    }
}

  • 在数据转发的过程中,我做了一个比较巧妙的做法。我始终让发送数据的一方作为我的client 端,让我的接受数据的一方作为我的 server 端。
  • 在转发数据的过程中,需要对于recv 这个函数有一定的认识。小于0 的时候是函数自身报错。等于0 的时候需要注意,相当于是client 端向要我发起关闭连接的请求,也就是说此时我client 端不会再向server 端去写数据了。于是我socks5 服务器所应该做的操作就是关闭掉我socks5 服务器 server 端的写。
  • 问题还没有完,当我转发的时候我就一定可以将我拿到的数据全部转发出去吗? 当然是不一定。这个要看对端的缓冲够不够,我方是否应该发送这么多数据。于是乎我做了这样的操作,我先去send,如果可以全部转发当然是好事,如果不能全部转发的话,我是这样操作的,大家还记得我的通道中不管的是client 还是server 的通道,我除了维护一个文件描述符之外,我还维护了一个缓冲区,这个时候它就要出场了,将没有写完的放到我的缓冲区中,然后修改我的event 事件,让他再次触发异步事件。
socks5 协议的解析
  • socks5 服务器在运作过程中连接的状态变化。分别有三种状态
    enum Sock5State  //sock5 的三种状态
    {
        AUTH,           // 身份认证
        ESTABLISHMENT,  // 建立连接
        FORWARDING,     // 转发
    };
  • 具体的socks5 协议的解析我不多说,大家可以可行了解
身份认证阶段
// 0 表示数据没有到,继续等待
// 1 成功
// -1 失败
int Sock5Server::AuthHandle(int fd) //这块身份认证一般会给我发送3个字节
{
    char buf[260];
    int rlen = recv(fd,buf,260,MSG_PEEK); // MSG_PEEK 窥探一下并不是真的读走

    if(rlen <= 0)
    {
        return -1;
    }
    else if(rlen < 3)
    {
        return 0;
    }
    else 
    {
        recv(fd,buf,rlen,0);
        Decrypt(buf,rlen);
        if(buf[0] != 0X05)  //版本号
        {
            ErrorLog("not socks5");
            return -1;
        }
        return 1;
    }
}
  • 身份认证阶段主要是验证socks5 版本号是否一致。
建立连接阶段
// 失败 -1
// 数据没到返回 -2
// 连接成功 返回 serverfd 
int Sock5Server::EstablishmentHandle(int fd)
{
    // 当传过来是域名时,他DST.ADDR 这个字段的第一个字节保存的是域名的长度。
    // 也就是说这个域名的长度不会超过256位。
    char buf[256];
    int rlen = recv(fd,buf,256,MSG_PEEK);
    TraceLog("Establishment recv:%d",rlen);
    if(rlen <= 0)
    {
        return -1;
    }
    else if(rlen < 10)
    {
        return -2;
    }
    else 
    {
        char ip[4];
        char port[2];

        recv(fd,buf,4,0);
        Decrypt(buf,4);
        char addresstype = buf[3];
        if(addresstype == 0x01)  //ipv4
        {
            TraceLog("use ipv4");
            recv(fd,ip,4,0);
            Decrypt(ip,4);
            recv(fd,port,2,0);
            Decrypt(port,2);
        }
        else if(addresstype == 0x03) //domainname
        {
            //TraceLog("use domainname");
            char len = 0;
            //recv domainname
            recv(fd,&len,1,0);
            Decrypt(&len,1);
            recv(fd,buf,len,0); 

            buf[len] = '\0'; 
            TraceLog("encry domainname:%s",buf);  //拿到域名之后,打印出来

            Decrypt(buf,len);
            // recv port 
            recv(fd,port,2,0);
            Decrypt(port,2);

            TraceLog("decrypt domainname:%s",buf);  //拿到域名之后,打印出来
           //因此我们拿到域名后,需要ip才能connect,因此我们使用gethostbyname 这个函数
           //这个函数就是DNS服务器(底层是UDP)来请求域名服务器,得到IP
            struct hostent* hostptr = gethostbyname(buf);
            memcpy(ip,hostptr->h_addr,hostptr->h_length); // 将DNS 服务器上的第一个对应的IP地址赋值给我的IP
            TraceLog("domainname, use DNS success get ip");
        }
        else if(addresstype == 0x04) //ipv6 暂不支持ipv6
        {
            ErrorLog("not support ipv6");
            return -1;
        }
        else 
        {
            ErrorLog("invalid address type");
            return -1;
        }
        struct sockaddr_in addr;
        memset(&addr,0,sizeof(struct sockaddr_in));
        addr.sin_family = AF_INET;
        memcpy(&addr.sin_addr.s_addr,ip,4);
        addr.sin_port = *((uint16_t*)port);

        int serverfd = socket(AF_INET,SOCK_STREAM,0);
        if(serverfd < 0)
        {
            ErrorLog("server socket");
            return -1;
        }
        if(connect(serverfd,(struct sockaddr*)&addr,sizeof(addr)) < 0)
        {
            ErrorLog("connect error");
            close(serverfd);
            return -1;
        }
        TraceLog("Establishment success");
        return serverfd;
    }
}
  • 建立连接阶段,分三种情况,第一种是传过来的 IPv4 类型的 IP 地址。第二种情况是传过来的是域名。第三种情况是传过来的IPv6 类型的 IP 地址。
  • 对于IPv6 类型的 IP 地址我暂时没有处理。
  • 对于 IPv4 类型的 IP 地址处理起来比较简单,我只需要绑定好 IP 和端口就好。
  • 对于传过来是域名的情况,我调用 gethostbyname 这个函数得到 IP 地址。多说一点,这个函数其实就是底层调用 DNS 域名解析服务器,通过域名得到 IP。
  • 处理完上面的三种情况后,我们就可以向服务端建立连接了。
  • 连接建立好之后,再后来的数据报我只需要转发就好了。转发流程刚才已经介绍。
日志模块
  • 这是个重点的模块啊,虽然说和这个服务没有关系,但是在我写项目的时候的各种调试都靠它了。出什么错误,基本上需要日志来完成。
static string GetFileName(const string& path)
{
    char ch='/';
#ifdef _WIN32
    ch='\\';
#endif
    size_t pos = path.rfind(ch);
    if(pos==string::npos)
        return path;
    else
        return path.substr(pos+ 1);
}


inline static void __TraceDebug(const char* filename,int line, const char* function, const char* format, ...)
{
#ifdef __TRACE__
    //输出调用函数的信息
    fprintf(stdout,"[TRACE][%s:%d:%s]:",GetFileName(filename).c_str(), line, function);
    //输出用户打的trace信息
    va_list args;
    va_start(args,format);
    vfprintf(stdout,format, args);
    va_end(args);
    fprintf(stdout,"\n");
#endif
}

inline static void __ErrorDebug(const char* filename,int line, const char* function, const char* format, ...)
{
#ifdef __DEBUG__
    //输出调用函数的信息
    fprintf(stdout,"[ERROR][%s:%d:%s]:",GetFileName(filename).c_str(), line, function);
    //输出用户打的trace信息
    va_list args;
    va_start(args,format);
    vfprintf(stdout,format, args);
    va_end(args);
    fprintf(stdout," errmsg:%s, errno:%d\n", strerror(errno), errno);
#endif
}

#define TraceLog(...) \
    __TraceDebug(__FILE__,__LINE__,__FUNCTION__, __VA_ARGS__);
#define ErrorLog(...) \
    __ErrorDebug(__FILE__,__LINE__,__FUNCTION__, __VA_ARGS__);
加密解密模快
  • 在我的数据从浏览器上出去的时候,我的数据必须是加密的,加密的数据才有更大的几率躲过防火墙。如果不加密相当于我的数据在网络上裸奔,本来我要完成的是躲过防火墙,不加密相当于我向穿墙访问google,还给墙看看我要访问的是google,那么墙肯定要拦截啊。
  • 但是我的加密算法可谓是最最最最最最简单的算法了吧,哈哈,但是往往简单的算法不一定能破解掉。
  • 当然我后面会使用更有效的加密算法
static inline char* XOR(char* buf, size_t len)
{
    for (size_t i = 0; i < len; ++i)
    {
        buf[i] ^= 1;
    }
}

static inline void Decrypt(char* buf, size_t len)
{
    XOR(buf, len);
}
static inline void Encry(char* buf, size_t len)
{
    XOR(buf, len);
}
  • 哈哈,加密算法简单吗?不过我的框架已经搭建好了,增加新的有效的加密算法也比较容易。

提出几个问题

问题一:

在我测试我的项目时,我总会遇到一个问题,我的服务器跑着跑着就挂了?

  • 我一直百思不得其接,不过还是我的日志功能起了大作用?我发现了一个问题。。
  • TCP是全双工的信道, 可以看作两条单工信道, TCP连接两端的两个端点各负责一条. 当对端调用close时, 虽然本意是关闭整个两条信道, 但本端只是收到FIN包. 按照TCP协议的语义, 表示对端只是关闭了其所负责的那一条单工信道, 仍然可以继续接收数据. 也就是说, 因为TCP协议的限制, 一个端点无法获知对端的socket是调用了close还是shutdown.
  • 这句不太准确,虽然本端无法知道对端是close还是shutdown(S,SHUT_WR),但是如果对端是close并且socket描述符的使用计数减为0的话,那么实际对端是关闭了两个信道。原因,实际下面已经讲了,对端如果close,相当于shutdow(S,SHUT_BOTH),本端如果再write的话,就会收到连接 RST 的。
  • 对一个已经收到FIN包的socket调用read方法, 如果接收缓冲已空, 则返回0, 这就是常说的表示连接关闭. 但第一次对其调用write方法时, 如果发送缓冲没问题, 会返回正确写入(发送). 但发送的报文会导致对端发送RST报文, 因为对端的socket已经调用了close, 完全关闭, 既不发送, 也不接收数据. 所以, 第二次调用write方法(假设在收到RST之后), 会生成SIGPIPE信号, 导致进程退出.
  • 为了避免进程退出, 可以捕获SIGPIPE信号, 或者忽略它, 给它设置SIG_IGN信号处理函数 signal(SIGPIPE, SIG_IGN);
  • 这样, 第二次调用write方法时, 会返回-1, 同时errno置为SIGPIPE. 程序便能知道对端已经关闭。实际这个函数的目的就是防止程序收到 SIGPIPE 后自动退出。
问题二

我为什么要使用 EPOLL 来实现呢?

  • 我来回答一下,首先我们假设我们使用同步的方式来进行,如果使用串行的方式,那么请听我的分析,我们都知道,比如你百度一下,那么就只会发过来一条连接吗?肯定不是的,在我们的Web 端,请求一个网页的时候,会发过去很多请求,因为一个网页上比如有些图片比较大,那么会单独起一条连接。
  • 因此如果使用串行的话,访问一个页面,比如说我发起了5个连接,当我处理第一个连接的时候,其余的四个连接就只能等着。加入我前面的连接处理的比较慢的话,那么等到我有时间去处理第五个连接时,由于时间太久,这条连接会被重置。
  • 这时有人说了,串行的不行,那么我们改用并行啊,好啊,当然可以啊,那么你封装线程池啊,进程池啊。不过我提醒各位啊,你访问个百度,比如说要来10个请求,这还是往少了的算。那么你开10个网页,那么至少100个请求,一次性来的哦。停!这时是我一个人去请求我的socks 服务器的。比如说我100个人呢,一次性10000个请求,emmmmmm,况且这时常态话的哦,我还需要维持连接的哦。这时有的杠精说了,我一台服务器干不了,我再来一台。好!我无话可说。有钱。
  • 于是乎,我使用异步的行为,有人就说了,你异步咋了,来10000个连接你就可以同时处理吗?啊,不好意思,不能。但是这些链接我可以收的下。同时还有一点,使用线程池的话,多线程可能会引出更多的问题,比如处理同一条连接的时候要加锁,如果要加锁,那么就有可能出现死锁,emmmm。
  • 那么最好的解决方案是啥呢,我们可以在使用异步的过程中再加上线程池多线程的处理,会更加高效一点。
  • 而且我们确定一下,不一定使用epoll 就是出于性能的缘故,有时候我们需要根据具体情况来具体决定。
问题三
  • 来解决一下发送数据或者接受数据会出现的问题,其实也就是在考察那几个 IO 函数的具体用法。

接收数据时我一次接收多少数据比较好?发送数据时一次发不完怎么办?recv 接收到 0 是什么情况?

  • 接收数据的时候如果buff 太小的话,可能会出现多次接收的情况。如果多次接收的话会导致效率太低。但是如果buff 太大的话,拿到这么多数据之后转发出去的时候,可能一次发不完,那么这样的话发送就会效率变慢。
  • 发送数据的时候一次发完最好,一次发不完的处理办法,我是这样解决的,我在我的通道中不仅有文件描述符还有我的缓冲区。如果一次写不完,那么我就把我没有写完的数据放到我的缓冲区中去并且将这个文件描述符继续添加到Epoll 事件中去。
  • 如果说 recv 接收到一个 0 。也就是说我的client 端不会再给我的server 端发送数据了,于是乎我们应该做的操作就应该是关闭服务器向 server 端套接字的写。并且将客户端的套接字从通道中移除掉。
问题四:
socks代理和HTTP代理的异同
  • 提到socks 代理,那么就需要知道他和HTTP代理有什么异同了。然后,我讲两个故事来分享一下。
SOCKS:Bill希望通过互联网与Jane沟通,但他们的网络之间存在一个防火墙,Bill不能直接与Jane沟通。所以,Bill连接到他的网络上的SOCKS代理,告知它他想要与Jane创建连接;SOCKS代理打开一个能穿过防火墙的连接,并促进Bill和Jane之间的通信。
HTTP:Bill希望从Jane的Web服务器下载一个网页。Bill不能直接连接到Jane的服务器,因为在他的网络上设置了防火墙。为了与该服务器通信,Bill连接到其网络的HTTP代理。他的网页浏览器与代理通信的方式与他直接连接Jane的服务器的方式相同;也就是说,网页浏览器会发送一个标准的HTTP请求头。HTTP代理连接到Jane的服务器,然后将Jane的服务器返回的任何数据传回Bill。
  • 总结一下:SOCKS工作在比HTTP代理更低的层次:SOCKS使用握手协议来通知代理软件其客户端试图进行的连接SOCKS,然后尽可能透明地进行操作,而常规代理可能会解释和重写报头(例如,使用另一种底层协议,例如FTP;然而,HTTP代理只是将HTTP请求转发到所需的HTTP服务器)。虽然HTTP代理有不同的使用式,CONNECT方法允许转发TCP连接;然而,SOCKS代理还可以转发UDP流量和反向代理,而HTTP代理不能。HTTP代理通常更了解HTTP协议,执行更高层次的过滤(虽然通常只用于GET和POST方法,而不用于CONNECT方法)。

在我测试的过程中,还有问题的话我会及时提出。。

项目源码地址:https://github.com/zhangyi-13572252156/OverTheWallProxyServer

第一次改动,加入线程池
  • 在我的连接来的时候我让主线程去处理,但是当读事件或者写事件来临的时候,我让我的线程池去处理。这种模式就是Reactor模式。
  • 也就是说,主线程只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知工作线程。除此之外,主线程不做任何实质性的工作。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。
  • 工作线程从请求队列中取出事件后,将根据事件的类型来决定如何处理他。对于可读事件,执行读数据和处理请求的操作;对于可写事件,执行写数据的操作。
  • 因此,从而使太多的连接到来的时候,服务器能够及时处理。
第二次改进,加入数据库
  • 加入MySQL 数据库,将日志保存到数据库中,使得以后查看系统的问题时,更好的调整。保证了数据的持久性。
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值