采用完成端口(IOCP)实现高性能网络服务器(Windows C++ 版)
Abstract Keywords Iocp C++ Iocp C++
Citation Yao Qing-sheng.采用完成端口(IOCP)实现高性能网络服务器(Windows C++ 版).FUTURE & CIVILIZATION Natural/Social Philosophy & Infomation Sciences,20240808. https://yaoqs.github.io/20240808/cai-yong-wan-cheng-duan-kou-iocp-shi-xian-gao-xing-neng-wang-luo-fu-wu-qi-windows-c-ban/
转载自 采用完成端口(IOCP)实现高性能网络服务器(Windows c++ 版)
¶ 前言
TCP\IP 已成为业界通讯标准。现在越来越多的程序需要联网。网络系统分为服务端和客户端,也就是 c\s 模式 (client \ server)。client 一般有一个或少数几个连接;server 则需要处理大量连接。大部分情况下,只有服务端才特别考虑性能问题。本文主要介绍服务端处理方法,当然也可以用于客户端。
我也发表过 c# 版网络库。其实,我最早是从事 c++ 开发,多年前就实现了对完成端口的封装。最近又把以前的代码整理一下,做了测试,也和 c# 版网络库做了粗略对比。总体上,还是 c++ 性能要好一些。c# 网络库见文章《一个高性能异步 socket 封装库的实现思路》。
Windows 平台下处理 socket 通讯有多种方式;大体可以分为阻塞模式和非阻塞模式。阻塞模式下 send 和 recv 都是阻塞的。简单讲一下这两种模式处理思路。
阻塞模式:比如调用 send 时,把要发送的数据放到网络发送缓冲区才返回。如果这时,网络发送缓冲区满了,则需要等待更久的时间。socket 的收发其实也是一种 IO,和读写硬盘数据有些类似。一般来讲,IO 处理速度总是慢的,不要和内存处理并列。对于调用 recv,至少读取一个字节数据,函数才会返回。所以对于 recv,一般用一个单独的线程处理。
非阻塞模式:send 和 recv 都是非阻塞的;比如调用 send,函数会立马返回。真正的发送结果,需要等待操作系统的再次通知。阻塞模式下一步可以完成的处理,在非阻塞模式下需要两步。就是多出的这一步,导致开发难度大大增加。高性能大并发网络服务器必须采用非阻塞模式。完成端口(IOCP)是非阻塞模式中性能最好的一种。
作者多年以前,就开始从事 winsocket 开发,最开始是采用 c++、后来采用 c#。对高性能服务器设计的体会逐步加深。人要在一定的压力下才能有所成就。最开始的一个项目是移动信令分析,所处理的消息量非常大;高峰期,每秒要处理 30 万条信令,占用带宽 500M。无论是 socket 通讯还是后面的数据处理,都必须非常优化。所以从项目的开始,我就谨小慎微,对性能特别在意。项目实施后,程序的处理性能出乎意料。一台服务器可以轻松处理一个省的信令数据(项目是 08 年开始部署,现在的硬件性能远超当时)。程序界面如下:
题外话 通过这个项目我也有些体会:1)不要怀疑 Windows 的性能,不要怀疑微软的实力。有些人遇到性能问题,或是遇到奇怪的 bug,总是把责任推给操作系统;这是不负责任的表现。应该反思自己的开发水平、设计思路。2)开发过程中,需要把业务吃透;业务是开发的基石。不了解业务,不可能开发出高性能的程序。所有的处理都有取舍,每个函数都有他的适应场合。有时候需要拿来主义,有时候需要从头开发一个函数。
¶ 目标
开发出一个完善的 IOCP 程序是非常困难的。怎么才能化繁为简?需要把 IOCP 封装;同时这个封装库要有很好的适应性,能满足各种应用场景。一个好的思路就能事半功倍。我就是围绕这两个目标展开设计。
1. 程序开发接口
socket 处理本质上可以分为:读、写、accept、socket 关闭等事件。把这些事件分为两类:a)读、accept、socket 关闭 b)写;a 类是从库中获取消息,b 类是程序主动调用函数。对于 a 类消息可以调用如下函数:
1 | //消息事件 |
对于 b 类,就是发送数据。当调用发送时,数据被放到库的发送缓冲中,函数里面返回。接口如下:
1 | enum EN_SEND_BUFFER_RESULT |
总的思路是接收时,放到接收缓冲;发送时,放到发送缓冲。外部接口只对内存中数据操作,没有任何阻塞。
2. 具有广泛的适应性
如果网络库可以用到各种场景,所处理的逻辑必须与业务无关。所以本库接收和发送的都是字节流。包协议一般有长度指示或有开始结束符。需要把字节流分成一个个完整的数据包。这就与业务逻辑有关了。所以要有分层处理思想:
¶ 库性能测试
首先对库的性能做测试,使大家对库的性能有初步印象。这些测试都不是很严格,大体能反映程序的性能。IOCP 是可扩展的,就是同时处理 10 个连接与同时处理 1000 个连接,性能上没有差别。
我的机器配置不高,cup 为酷睿 2 双核 E7500,相当于 i3 低端。
1)两台机器测试,一个发送,一个接收:带宽占用 40M,整体 cpu 占用 10%,程序占用 cpu 不超过 3%。
2)单台机器,两个程序互发:收发数据达到 30M 字节,相当于 300M 带宽,cpu 占用大概 25%。
3)采用更高性能机器测试,两个程序对发数据:cpu 为:i5-7500 CPU @ 3.40GHz
收发数据总和 80M 字节每秒,接近 1G 带宽。cpu 占用 25%。
测试程序下载地址 :《完成端口(IOCP)性能测试程序(c++ 版本 64 位程序)》。只有 exe 程序,不包括代码。
¶ 网络库设计思路
服务器要启动监听,当有客户端连接时,生成新的 socket 句柄;该 socket 句柄与完成端口关联,后续读写都通过完成端口完成。
- socket 监听(Accept 处理)
关于监听处理,参考我另一篇文章《单线程实现同时监听多个端口》。
- 数据接收
收发数据要用到类型 OVERLAPPED。需要对该类型进一步扩充,这样当从完成端口返回时,可以获取具体的数据和操作类型。这是处理完成端口一个非常重要的技巧。
1 | //完成端口操作类型 |
发送处理:overlap 包含要发送的数据。调用此函数会立马返回;当有数据到达时,会有通知。
1 | BOOL NetServer::PostRcvBuffer(SOCKET socket, PER_IO_OPERATION_DATA *overlap) |
从完成端口获取读数据事件通知:
1 | DWORD NetServer::Deal_CompletionRoutine() |
- 数据发送
数据发送时,先放到发送缓冲,再发送。向完成端口投递时,每个连接同时只能有一个正在投递的操作。
1 | BOOL NetServer::PostSendBuffer(SOCKET socket) |
¶ 总结
开发一个好的封装库必须有的好的思路。对复杂问题要学会分解,每个模块功能合理,适应性要强;要有模块化、层次化处理思路。如果网络库也处理业务逻辑,处理具体包协议,它就无法做到通用性。一个通用性好的库,才值得我们花费大气力去做好。我设计的这个库,用在了公司多个系统上;以后无论遇到任何网络协议,这个库都可以用得上,一劳永逸的解决网络库封装问题。
Address:Department of Natural/Social Philosophy & Infomation Sciences, CHINA
Biography...
转载或引用本文时请遵守许可协议,注明出处、不得用于商业用途!
Like this article? Support the author with