01.高性能网络通信框架需要解决哪些问题?

 2024-03-10 04:03:00  阅读 0

1.1 高性能的网络通信框架需要解决哪些问题?

一个服务程序要向外部提供服务,就必须与外部程序进行通信。 这些外部进程通常是位于不同机器上的不同进程(所谓的客户端)。 一般的通讯方式就是我们所说的网络通讯,也就是所谓的通讯。 。 因此,网络通信组件是服务器端程序的基本组件。 其设计质量直接影响其对外提供服务的能力。 不同的业务在网络通信框架的细节上可能略有不同,但大多数设计原则是通用的。 本节讨论这些常见原则及其设计细节。

1、尽量少等待的原则

目前,互联网上有很多网络通信框架,例如Boost Asio、ACE等,但它们对于网络通信的常见技术手段是相似的。 作者认为,一个好的网络通信框架至少要解决以下问题:

如何检测新的客户端连接

如何接受客户端连接?

如何检测客户端是否有数据发送

如何接收客户端发来的数据?

如何检测连接异常? 连接异常如何处理?

如何向客户端发送数据?

向客户端发送数据后如何关闭连接?

任何有一点网络知识的人都可以回答上面提到的几个问题。 例如,接收客户端连接,使用API​​函数,接收客户端数据,使用recv函数,向客户端发送数据,使用send函数,检测客户端。 无论是否有新连接、客户端是否有新数据,都可以使用IO(IO复用)技术、poll、epoll等API。 情况确实如此。 这些基本的API构成了服务器网络通信的基础。 无论网络通信框架设计得多么巧妙,它都是建立在这些基本API之上的。

但如何巧妙地组织这些基础API才是问题的关键。 通常我们说的服务器的高性能、高并发,其实只是一种技术实现所达到的效果。 无论如何,从编程的角度来看,它无非就是一个程序。 因此,只要程序能够最大程度地满足“尽可能少”的要求,“等待”就是高性能(高效)的。 这里的效率并不是指“忙则死、闲则死”。 意思是大家可以闲着,但是如果有工作要做,大家应该尽量一起做,而不是有些线程按顺序忙着做事,一、二、三、四、五。 六、七、八十九,线程的另一部分坐在那里无所事事。

这可能有点抽象,那么我们举一些例子来详细解释一下。

以上都不是高性能服务器开发的一种思考方式,因为上面的例子都不满足“尽可能少等待”的原则。 为什么我们必须等待? 有没有办法让上述过程无需等待? 最好不仅不用等待,而且这些事情完成后也能通知我。 这样,程序就可以在原本用于等待的CPU时间片内做其他事情了。

是的,就是我们接下来要讨论的IO复用技术(IO)。

2、减少无用功的时间原则

目前系统支持的IO复用技术有,IOCP(完成端口),Linux系统支持的IO复用技术有poll和epoll。 各个函数的用法在前面的章节中已经详细介绍过,这里不再介绍。 让我们讨论一些更深层次的东西。 上面列出的IO复用功能可以分为两个层次:

这种分类的依据是什么?

我们先来分析一下第一层函数。 poll函数本质上是在一定时间内主动查询一组句柄(可以是一个,也可以是多个),看是否有事件(可读事件、可写事件、或者错误事件)。 等等),这意味着我们仍然需要每隔一段时间主动做一次这些测试。 如果这段时间检测到一些事件,我们的时间就不会浪费了。 如果这期间没有发生任何事件怎么办? 我们只能做无用功,这也是对系统资源的浪费。 因为如果一个服务器有多个连接,而CPU时间片是有限的,我们花了一定的时间去检测一些事件,却发现他们没有事件,但是在这期间我们有一些事情需要进行已处理,那么我们为什么要花时间做这个测试呢? 利用这段时间去做我们需要做的事情不是更好吗? 因此,对于服务器程序来说,如果我们想要高效,我们应该尽量避免花时间去主动查询是否有事件,而是等待系统主动告诉我们什么时候有事件,然后我们再去处理。 这就是二级函数的作用。

第二级功能实际上相当于将主动查询改为被动通知。 当有事件发生时,系统会告诉我们,我们此时就会处理,不浪费任何时间。 只是二级函数通知我们的方式不同而已。 比如他们利用窗口消息队列的事件机制来通知我们我们设置的窗口过程函数。 IOCP使用tatus函数来唤醒并从挂起状态返回,而epoll是函数刚刚返回。

例如,当函数连接另一端时,如果连接设置为非阻塞,我们不需要等待连接结果,可以立即返回。 连接完成后:会返回一个事件告诉我们连接是否成功,epoll会产生一个事件来通知我们; 比如有数据读取时会产生事件,epoll会产生事件等等。

总结一下上面表达的意思:在追求高性能的网络通信设计时,尽量不要去主动查​​询各种事件,而是采取等待操作系统通知我们事件的策略。

所以基于上面的讨论,我在这里提出的第二个原则是尽量减少做无用工作的时间。

希望读者能够深刻理解这里的含义。 在服务程序资源充足的情况下,这可能不会带来任何优势,但如果有大量的任务需要处理,需要支持高并发的服务,这基本上是非常有效的。 方法。

通过上面的分析,相信读者应该明白,对于高性能的服务,同样是IO函数,为什么不使用poll函数呢?

另外,在使用IO多路复用API时,如果失效,应及时关闭并从IO多路复用功能中移除,否则可能会导致死循环或浪费CPU检测周期。

3. 检测网络事件的有效姿势

根据上面介绍的两个原则,在高性能服务器设计中,我们一般将其设置为非阻塞,并使用级别2中提到的IO复用功能来检测每个服务器上的事件(读、写、错误等) 。

当然,这并不意味着阻塞通信模式没有用。 这已在第 4 章中介绍过。

好的,现在让我们回答第一栏中提出的七个问题:

epb通信信号错误_epoll大量通信出问题_通信eutran

如何检测新的客户端连接?

如何接受客户端连接?

默认函数将阻塞在那里。 如果在侦听器上检测到事件; 或者如果检测到事件,则说明此时有新的连接到达。 如果此时调用该函数,则不会被阻塞。 第4章有详细的例子,这里不再重复代码。

如何检测客户端是否有数据发送?

如何接收客户端发来的数据?

生成的数据也应该设置为非阻塞,只有在有可读事件或者上报的情况下才采集数据,以免做无用功。

至于一次应该收集多少数据? 我们可以根据自己的实际需求来决定,甚至可以循环反复recv(或read)。 对于非阻塞模式,如果没有数据,recv或read会立即返回(返回值为-1)。 此时,错误代码(或)将表明当前没有数据。

代码示例:

bool CMySocket::Recv()
{
    int nRet = 0;
    while (true)
    {
        char buff[512];
        nRet = ::recv(m_hSocket, buff, 512, 0);
        if (nRet == SOCKET_ERROR)
        {
            //调用recv函数直到错误码是WSAEWOULDBLOCK
            if (errno == EWOULDBLOCK)
                break;
            else
                return false;
        }
        else if (nRet < 1)
            return false;

        m_strRecvBuf.append(buff, nRet);

        ::usleep(1000);
    }

    return true;
}

如何检测连接异常? 连接异常如何处理?

同样,当我们收到异常事件,比如事件时,我们就知道发生了异常,而我们对异常的处理一般就对应着。 另外,如果send/recv或read/write函数对一端进行操作时返回0,也意味着另一端已经关闭。 此时,这个连接就没有必要存在了。 我们也可以关闭本地端。

需要注意的是,由于TCP连接是一个状态机,因此无法检测到由于两个端点之间的路由错误而导致的链路问题。 这种情况需要定时器结合心跳包来检测。 定时器和心跳包将在后面的章节中介绍。

如何向客户端发送数据?

这也是一个常见的网络传播面试问题。 在面试后端开发职位时经常会问到这个问题。 是检验一个后端开发人员是否真正理解高性能网络通信框架的重要知识点。

向客户端发送数据比接收数据麻烦一点,也需要一些技巧。 对于epoll的水平模式(Level),首先我们不能像注册检测数据可读事件一样从一开始就注册检测数据可写事件,因为如果我们注册可写事件,一般只要对端正常接收数据即可,我们的通常是可写的,这就导致可写事件被频繁触发。 但是当可写事件被触发时,我们不一定有数据要发送。 所以正确的做法是:如果有数据要发送,就先尝试发送。 如果无法发送或者只发送了一部分,我们需要将其余部分缓存起来,然后设置为下次检测可写事件。 当发生可写事件时,继续发送。 如果还是不能完整发送,则继续设置监听可写事件,以此类推,直到发送完所有数据。 发送所有数据后,我们需要删除对可写事件的监听,以避免无用的可写事件通知。 不知道大家注意到没有,如果一次只发送出一部分数据,那么剩下的数据就应该暂时存储起来。 这时候我们就需要一个缓冲区来存储这部分数据。 该缓冲区称为“发送缓冲区”。 发送缓冲区不仅存储本次未发送的数据,还用于存储发送过程中上层需要发送的新数据。 为了保证顺序,新的数据应该追加到当前剩余数据的末尾,并且发送时应该从发送缓冲区的头部开始发送。 也就是说,先来的先送,后来的最后送。 这部分的详细示例和说明将在本章后面的章节中给出。

向客户端发送数据后如何关闭连接?

这个问题比较难处理,因为这里的“发送”并不一定意味着实际发送。 即使我们调用send或write函数并返回成功,也只能说明数据已成功写入操作系统的协议栈,而不是数据本身。 被发布到网上。 很难判断最终的数据能否以及何时能够发送,更难判断对方是否收到了。 因此,我们目前只能简单地认为send或write返回的是我们发送的数据的字节数,我们认为“数据已发送”。 当然,也可以通过调用函数来实现所谓的“半闭包”。

有一个选项可以设置关闭后剩余数据可以保留的最长时间。 如果在停留时间内数据无法发送出去,则数据就真的丢失了。

1.2 连接的被动关闭和主动关闭

在实际应用中,连接被被动关闭是因为我们检测到连接中有异常事件(例如触发事件、send/recv函数返回0、对端关闭连接)。 此时,这个连接就没有必要存在了。 我们的连接被迫关闭。 要主动关闭连接,我们主动调用close/来关闭连接。 例如,客户端向我们发送非法数据,例如某些网络攻击中的探测数据包。 此时,出于安全考虑,我们主动关闭连接。

1.3 长连接和短连接

网络通信双方根据连接状态分为长连接和短连接。 长连接长时间保持通信双方的连接状态,这实际上是相对于短连接来说的。 通常的短连接操作步骤为:

连接 => 数据传输 => 关闭连接

长连接通常是:

连接 => 数据传输 => 保持连接 => 数据传输 => 保持连接 => ... => 关闭连接

这就需要长连接在没有数据通信时定时发送数据包来维持连接状态(“保活机制与心跳包”章节会详细介绍),短连接在没有数据通信时可以直接关闭数据传输。

那么什么时候使用长连接,什么时候使用短连接呢? 长连接主要用于通信双方需要频繁通信的场景。 缺点是通信双方需要添加相应的逻辑来维护相应的连接状态信息。 另外,连接信息本身也需要一定的系统消耗。 优点是可以进行实时数据交换。

短连接一般用于数据传输完成后可以关闭连接,或者不需要通信双方实时状态信息的应用,例如Web服务器与浏览器之间的连接。 Web服务器将页面信息发送给浏览器后,连接就关闭,需要时可以再次建立。 短连接的优点是通信双方不需要长期维护连接状态信息,可以节省连接资源; 缺点是如果数据传输频率比较高,可能需要频繁建立和关闭连接。 另外,短连接不能做到非常实时。 消息推送。

这里说一下,Web服务器一般都是使用短连接来与浏览器进行通信的。 严格来说,并不准确。 在一些http通信中,通信双方可以接受http协议头中选项的建议,并在多次通信之间保持连接状态不变。 打开。

另外,虽然大多数情况下http协议都使用短链接,并且不支持消息推送,但这种现状正在改变。 最新的http2.0标准支持服务器端推送。

正文结束。

标签: 连接 通信 发送

如本站内容信息有侵犯到您的权益请联系我们删除,谢谢!!


Copyright © 2020 All Rights Reserved 京ICP5741267-1号 统计代码