airx commented on 6 Jun 2019
背景需求:
应用场景类似互动聊天直播室,可能有1-8个同时说话(视频)的嘉宾,还有n个观众。服务器将嘉宾上传的udp媒体数据包转发给房间其余人。
还有一点简单信令,比如进房间,离开房间,上嘉宾位,离开嘉宾位(变成观众)等,这部分打算用TCP做。
房间之间是相互独立的,一个服务器需要同时支持很多个房间,所以对UDP的性能要求会高,tcp只是信令以及长连接维持。UDP媒体数据包头会包含房间号和用户uid。
问题如下:
1、UDP的使用参考test_udpSock.cpp即可吗,对于我们这种场景需求,zltoolkit的使用上有没什么其他的注意,比如避免内存拷贝等事项?
2、将一个包转发给房间其他人,是自己维护一个房间所有人的udp地址列表,然后遍历发送吗,zltoolkit有没什么其他更高端高效率的办法?
3、关于房间数据结构的查找问题,目前是打算用一个udp端口进行监听,udp数据包头带上房间号,解析包头得到房间号,然后通过map查找到这个房间的数据结构;另外一种方式是,一个房间使用一个udp端口进行监听,这样这个udp端口收到的数据天然就是这个房间的,避免了我们使用map查找房间。请问这两种方式,哪种房间数量多了性能会好些呢?
@xiongziliang
Owner
xiongziliang commented on 10 Jun 2019
UDP与TCP相比,udp在互联网上传输,存在mtu限制的问题,也就是说,
你一个逻辑数据包必须拆分成若干个小于mtu的udp包发送;这样会增加很多挑战,具体如下:
1、udp包乱序以及丢包问题,目前这个业界以及有比较成熟的解决方案,
譬如kcp,srt,quic等,建议直接使用这样的成熟的方案。
2、udp由于存在在应用层分包、组包、排序、重传等机制,并且udp包是非粘连的,
需要一个包一次read操作,不好批量收包(可以理解为UDP是小包,TCP是大包),
所以从性能上来讲,UDP性能相比TCP而言,CPU使用率应该是更高的。
但是UDP相对TCP来说,又有很多优势:
1、延时低,相比TCP来说,UDP的延时是可控的。
2、应对丢包率和延时比较高的网络环境 ,UDP能充分利用网络带宽,
不像TCP在丢包率上升的情况下性能急剧下降。
3、切换网络可以无缝切换。TCP是根据发送IP和端口方式确定链接上下文,
而UDP在内核没有链接的概念。用户完全可以在UDP数据包中实现逻辑端口,根据uid或ssrc等确认链
接的唯一性(而不是ip和端口),这样用户在从wifi切换到4G时可以无感无缝切换(TCP要重建链接)。
现在针对你问的3个问题依次答复:
1、zltoolkit对udp的封装只封装到了socket层面,未封装到session层面,所以需要用户绑定会话数据处理 对象以及socket对象。在数据分发时,避免内存拷贝可以通过智能指针的方式实现,具体可以参zlmediakit分发rtp包和rtmp包的实现逻辑。
2、遍历发送房间内其他所有用户的逻辑要你自己实现,但是其他用户可能存在不同的线程上,这样你就可 能需要切换线程然后在发送。zltoolkit实现一个批量线程切换的逻辑,可以参考RingBuffer对象。
3、单udp端口的方式有很多优势,首先是是部署运维简单,特别是docker这种容器部署时很多优势,但是 单udp端口时会存在单sockfd的单线程监听的问题(不过貌似新的linux内核已经支持多socketfd绑定单端口的机制),不能发挥多核cpu的性能优势。不管单端口和多端口,建议都不要把udp端口号跟你的逻辑id号绑定,也就是说,房间id或用户id还是通过udp数据包里面的字段来识别,这样后面不管是改成单端口还是多端口都很简单。推荐你采用一个线程一个udp端口的方案来实现,这样做的好处有两个,一是每个线程都能监听一个端口,能发挥多核cpu的最大性能,二是部署和运维相对纯粹的多端口要简单很多。根据udp中的id字段,然后map中查找对象这种方式很常见的,map的查找性能足够了(实测unordered_map单线程每秒3 亿次查找)。建议这个map做成每个线程都分配一个map,这样就不需要上全局锁了。
@airx
Author
airx commented on 10 Jun 2019
一个线程一个udp端口,收包用阻塞的recvfrom或者epoll或者select,性能上应该没有区别吧?
@xiongziliang
Owner
xiongziliang commented on 10 Jun 2019
一个线程一个udp端口,收包用阻塞的recvfrom或者epoll或者select,性能上应该没有区别吧?
直接阻塞式的recvfrom理论上性能更好,但是由于你在收到数据后要做socket操作或者其他各种包括定时器、线程切换等操作,所以你还是不得不用epoll或select
@airx
Author
airx commented on 10 Jun 2019
那我参考test_udpSock.cpp的写法,一个线程用一个udp端口收,然后估计会用到test_timer,delaytask定时器相关。那么会用到线程池吗?是接收线程接收解包转发一条龙搞完,还是要拆解比较好呢?udp这种服务没什么参考的,总体架构上不太清楚怎么弄比较好,问题比较多,谢谢耐心解答了
@xiongziliang
Owner
xiongziliang commented on 11 Jun 2019 •
那我参考test_udpSock.cpp的写法,一个线程用一个udp端口收,然后估计会用到test_timer,delaytask定时器相关。那么会用到线程池吗?是接收线程接收解包转发一条龙搞完,还是要拆解比较好呢?udp这种服务没什么参考的,总体架构上不太清楚怎么弄比较好,问题比较多,谢谢耐心解答了
这样快速实现一个高性能的udp回显服务器:
//收到数据回调
auto onUdpData = [](Socket *sock,const Buffer::Ptr &buf, struct sockaddr *addr_from , int addr_len){
//回显数据到发送者
sock->send(buf,addr_from,addr_from->sa_len);
};
list udp_sock_list;
//起始udp端口为9000,创建cpu核心数目相当的udp服务器
int start_port = 9000;
//遍历每条线程,每个cpu核心对应一条线程
EventPollerPool::Instance().for_each([&](TaskExecutor::Ptr &executor){
auto poller = dynamic_pointer_cast(executor);
//创建socket对象
auto sock = std::make_shared(poller);
//绑定udp端口
sock->bindUdpSock(start_port++);
Socket *sockPtr = sock.get();
//设置收到数据的回调函数
sock->setOnRead([sockPtr,onUdpData](const Buffer::Ptr &buf, struct sockaddr *addr_from){
onUdpData(sockPtr,buf,addr_from);
});
//保存对象的强引用
udp_sock_list.emplace_back(std::move(sock));
});
@xiongziliang xiongziliang closed this on 11 Jun 2019