============20191212--追加---------以下主要追究发送比较大的网络数据时多次调用 系统调用send而产生的耗时问题,不过对于实际上传输音视频实时流的使用场景,往往数据不会这么大,延迟的性能瓶颈主要不在于此========================
最近在做局域网投屏相关,发现使用vlc做推流和接受,延迟过大(1s以上),改用live555推流和接受,延迟还是过大,局域网中还打不到<200ms的延迟。于是乎研究了好一阵子live555源代码,也没有发现其代码层面故意的休眠的情况出现,苦思冥想数日,不得解,直到这个阳光明媚的周六,突然就想到一个方法,排除live555代码,有没有一个方法来直接了当地验证网络本来就性能不够呢,虽然说是局域网,似乎这个怀疑本生就不太然人觉得可靠。 周六就在家敲起了代码:
测试的代码是这样的:
一个服务端,一个客户端,tcp建立连接,服务端以每秒30帧,(每个SendFrame操作之间休眠 usleep(100*10000/30)),每帧分成1500 Byte的小帧多次发送(小帧之间的发送间隔时间,不休眠)。服务端记录下发送端每一次发送一帧前的时间点,图中Time_send,客户端记录下每一帧接受完的时间点Time_reciv. 这两个时间差,即一帧数据的传输延迟。(前提是服务端和客户端都有相同的系统时间的情况下,记录的时间才有参考价值,为保证这一点,先在一个系统上测试,这里在虚拟机上 开启两个终端,使用本地回环网络 127.0.0.1 ,一个终端作为server, 另一个终端作为client。 同时为方便比较,记录下时间的同时也把整个发送/接受到的数据一并写入了log文件。)结果如下:
可以看到,竟然达到了975231-736331=238900 微秒,也就是239毫秒。这可是本地回环网络,虽然我的环境是虚拟机ubuntu,这结果,相当不能接受。回头来看,每一次发送一帧,300Kb, 分成了205个小包发送。每一个包调用一次sendto() 发送。如果把上面的rtp分包操作去掉,不进行分包,直接调用一次 sendto()发送300Kb. 测试结果
298075-296098=1977微秒,也就是2ms. 这,才是应该有的速度。。。。。。。
如果说硬要强行解释一番的话,只能说,sendto() 系统调用,需要消耗资源,要尽量减少频繁调用,上诉的一帧数据分成 205 次sendto()发送,明显比一次调用sendto() 耗时多得多。 而mtu分包,内核的tcp/ip协议栈会自行处理,如期交给应用层自己分包,多次调用系统调用sendto,还不如一次调用sendto,分包工作就交给 tcp/ip 协议栈自行处理。
至于为什么RTP要在应用层协议自己分包,还不太明白。总之,live555也好,vlc也好,发送rtp都在应用层(rtp层)自行分了包,也就是上面的200多ms延迟的做法。
实际验证:
使用 live555 testOnDemandRTSPServer.cpp 测试h264服务, 同时使用 testRTSPClient.cpp 接受流,开启 testRTSPClient.cpp中的 #define REQUEST_STREAMING_OVER_TCP True, 用rtp-over-tcp, 然后在RTPInterface.cpp 的函数 sendDataOverTCP()的开始出添加打印信息:(也就是系统调用send()函数之前添加)
printf("send overt tcp :size %d \n",dataSize); //或者用live555里面通用的 env << 输出。
Boolean RTPInterface::sendDataOverTCP(int socketNum, u_int8_t const* data, unsigned dataSize, Boolean forceSendToSucceed) {
printf("send overt tcp :size %d \n",dataSize);或者用live555里面通用的 env << 输出
int sendResult = send(socketNum, (char const*)data, dataSize, 0/*flags*/);
....
}
以下为实际输出,可以看到server端,每次调用send()发送数据,都不会超过145Byte字节,这个数值对应在 源码MultiFramedRTPSink.cpp 宏定义#define RTP_PAYLOAD_MAX_SIZE 1456 。
不仅如此,每次发送一帧数据多次调用send(),还在每次发送之前单独发送一个 rtp-overt-tcp 需要的额外的 4字节数据。【这个是由于需要同一套上层发送接口,要同时支持rtp-over-tcp 和 rtp-over-udp的原因吧】因为这个机制,在用live555 tcp推流时,wireshark抓的网络包就不便于分析,字节的通道头和数据可能是分别在两个tcp包中的。
上述测试,在android真机上测试,晓龙625 cpu。
live555发送一第一帧 40521 byte的h264帧,在本机loop网络端接受,调用了send() 28X2 = 56次, 耗时 833496-781336=52160 微秒
52ms的传输延迟。
测试代码后续github上传。
#追加 20200506---,直接在这里贴上上述测试 demo的源了#
clinet.cpp
#include
#include
#include
#include
#include
#include
#include "com_net.h"
static FILE* fp_log = NULL;
static bool bFirst = true;
static int logIndex = 0;
//以下写log的耗时,也被记入帧传输时延 ,但占比很小可以忽略
void logRecive(char* data,int datalen)
{
if(bFirst)
{
bFirst = false;
fp_log = fopen("./get_log","w+");
if(fp_log == NULL)
{
printf("fopen erro [%d%s]!\n",__LINE__,__FUNCTION__);
}
else
{
setbuf(fp_log,0);
}
}
if(fp_log)
{
struct timeval tv;
struct tm *info;
gettimeofday(&tv,NULL);
info = gmtime(&tv.tv_sec);
fprintf(fp_log,"index:%04d datalen:%08d [%02d:%02d:%02d:%06ld]",logIndex++,datalen,info->tm_hour,info->tm_min,info->tm_sec,tv.tv_usec);
fwrite(data,1,datalen,fp_log);
}
}
int main(int argc,const char*argv[])
{
if(argc != 4)
{
printf("usage:serverIp, serverPort, frameLen\n");
return -1;
}
int connect_fd=0;
int frameLen = atoi(argv[3]);
connect_fd = tcp_net_conncet(argv[1],atoi(argv[2]));
if(connect_fd < 0)
{
printf("connect erro\n");
return -1;
}
char*buf = (char*)malloc(frameLen);
if(buf == NULL)
{
printf("malloc erro %d\n",frameLen);
return -1;
}
increaseReceiveBufferTo(connect_fd,20000);
while(1)
{
int dataRead =0;
while(dataRead
server.cpp
#include
#include
#include
#include
#include
#include
#include "com_net.h"
static bool bTCP = true;
static FILE* fp_log = NULL;
static bool bFirst = true;
static int logIndex = 0;
//以下写log的耗时,也被记入帧传输时延 ,但占比很小可以忽略
void logSend(char* data,int datalen)
{
if(bFirst)
{
bFirst = false;
fp_log = fopen("./send_log","w+");
if(fp_log == NULL)
{
printf("fopen erro [%d%s]!\n",__LINE__,__FUNCTION__);
}
else
{
setbuf(fp_log,0);
}
}
if(fp_log)
{
struct timeval tv;
struct tm *info;
gettimeofday(&tv,NULL);
info = gmtime(&tv.tv_sec);
fprintf(fp_log,"index:%04d datalen:%08d [%02d:%02d:%02d:%06ld]",logIndex++,datalen,info->tm_hour,info->tm_min,info->tm_sec,tv.tv_usec);
fwrite(data,1,datalen,fp_log);
}
}
int SendFrame(int fd, char *data, int datalen, int mtu=1500, int sendGap=0);
int SendFrame(int fd, char *data, int datalen, int mtu, int sendGap)
{
logSend(data,datalen);
if(mtu == -1)
{
int sendcount=0;
while(datalen > 0)
{
sendcount = writeSocket(fd, data, datalen);
if(sendcount < 0)
{//send over
printf("send erro \n");
break ;
}
datalen -= sendcount;
data += sendcount;
//usleep(sendGap);//微秒
}
}
else
{
int sendcount=0;
while(datalen > 0)
{
sendcount = writeSocket(fd, data, datalen>mtu?mtu:datalen);
if(sendcount < mtu)
{//send over
break ;
}
datalen -= sendcount;
data += sendcount;
usleep(sendGap);//微秒
}
}
}
int SendFileLoop(const char*file,int connect_fd,int frameLen,int fps, int sendGap)
{
if(file == NULL || connect_fd < 0 || fps <= 0 || frameLen <= 0 || sendGap<0 )
{
printf("input param erro !file[%s],connect_fd[%d],frameLen[%d],fps[%d],sendGap[%d]:",file,connect_fd,frameLen,fps,sendGap);
return -1;
}
char* buffer=(char *)malloc(frameLen);
if(buffer == NULL)
{
printf("malloc erro :%d [%d%s]\n",frameLen,__LINE__,__FUNCTION__);
return -1;
}
FILE *fp_in = fopen(file,"r");
if(fp_in == NULL)
{
printf("fopen erro: %s[%d%s]\n",file,__LINE__,__FUNCTION__);
return -1;
}
while(1)
{
if(fread(buffer,1,frameLen,fp_in)
需要的网络公关函数:
com_debug.h
#ifndef __hearder_com_debug__
#define __hearder_com_debug__
#include
#define RET_SUCESS 0
#define RET_ERR -1
#define RET_ERR_NEEDMOREDATA -2
//从 第0开始算,start
#define LEFT_CHAR_BIT_GET_RAW(pData,start,len) ( (u_int8_t)(pData)[0] & ((1<<(8-start)) - (1<<(8-start-len))) )
#define LEFT_CHAR_BIT_GET(pData,start,len) (LEFT_CHAR_BIT_GET_RAW(pData,start,len)>>(8-start-len))
#define RIGHT_CHAR_BIT_GET(pData,start,len) ( ((u_int8_t)(pData)[0]>>(start)) & ((0x01<<(len)) -1) )
#define LITTLE_MAKE_UNINT16(pData) (((u_int8_t)(pData)[0]<<8) | ((u_int8_t)(pData)[1]))
#define LITTLE_MAKE_UNINT32(pData) (((u_int8_t)(pData)[0]<<24) | ((u_int8_t)(pData)[1]<<16) | ((u_int8_t)(pData)[2]<<8) | (u_int8_t)(pData)[3])
#ifndef DEBUG_LEVE
#define DEBUG_LEVE 3
#endif
#define DEBUG_WARN(fmt, args...) do {printf("WARN![%s,%s,%d]",__FILE__,__FUNCTION__,__LINE__);printf(fmt,##args);} while(0)
#define DEBUG_ERR(fmt, args...) do {printf("ERR!!!![%s,%s,%d]",__FILE__,__FUNCTION__,__LINE__);printf(fmt,##args);} while(0)
#if DEBUG_LEVE >=3
#define DEBUG_INFO1(fmt, args...) do {printf("[%s,%s,%d]",__FILE__,__FUNCTION__,__LINE__);printf(fmt,##args);} while(0)
#define DEBUG_INFO2(fmt, args...) do {printf("[%s,%s,%d]",__FILE__,__FUNCTION__,__LINE__);printf(fmt,##args);} while(0)
#define DEBUG_INFO3(fmt, args...) do {printf("[%s,%s,%d]",__FILE__,__FUNCTION__,__LINE__);printf(fmt,##args);} while(0)
#elif DEBUG_LEVE ==2
#define DEBUG_INFO1(fmt, args...) do {printf("[%s,%s,%d]",__FILE__,__FUNCTION__,__LINE__);printf(fmt,##args);} while(0)
#define DEBUG_INFO2(fmt, args...) do {printf("[%s,%s,%d]",__FILE__,__FUNCTION__,__LINE__);printf(fmt,##args);} while(0)
#define DEBUG_INFO3(fmt, args...)
#elif DEBUG_LEVE ==1
#define DEBUG_INFO1(fmt, args...) do {printf("[%s,%s,%d]",__FILE__,__FUNCTION__,__LINE__);printf(fmt,##args);} while(0)
#define DEBUG_INFO2(fmt, args...)
#define DEBUG_INFO3(fmt, args...)
#endif
//还未实现
/*
* 最好是单例模式,互斥同步, 用宏定义重新包装 接口
*/
class CDebugInfo
{
public:
CDebugInfo(char infoLeve=-1);
~CDebugInfo();
void debug_warn();
void debug_err();
void debug_info_leve1();
void debug_info_leve2();
void debug_info_leve3();
private:
//info leve,
//-1 :all
//0 : warn and err,
//3 : info3 info2 info 1 , warn and err,
//2 : info2 info1 , warn and err,
//1 : info1, warn and err
char m_infoLeve;
};
#endif
com_debug.cpp
#include "com_debug.h"
CDebugInfo::CDebugInfo(char infoLeve)
:m_infoLeve(infoLeve)
{
}
CDebugInfo::~CDebugInfo()
{
}
void CDebugInfo::debug_warn()
{
}
void CDebugInfo::debug_err()
{
}
void CDebugInfo::debug_info_leve1()
{
}
void CDebugInfo::debug_info_leve2()
{
}
void CDebugInfo::debug_info_leve3()
{
}
com_net.h
#ifndef __header_com_net__
#define __header_com_net__
#include /* See NOTES */
#include
//typedef char int8_t;
typedef unsigned char u_int8_t;
typedef short int16_t;
typedef unsigned short u_int16_t;
typedef int int32_t;
typedef unsigned int u_int32_t;
int udp_net_setup();
int udp_connect(int socket,const char*host, int port );
int udp_get_port(int socket, int16_t *port);
int tcp_net_conncet(const char *psz_host,int i_port);
int readSocket( int socket, char* buffer, unsigned bufferSize,
struct sockaddr *fromAddress = NULL);
int writeSocket(int socket, char* buffer, unsigned bufferSize,
struct sockaddr* destAddress = NULL);
int closeSocket(int socket);
bool makeSocketNonBlocking(int sock);
bool makeSocketBlocking(int sock, unsigned writeTimeoutInMilliseconds);
unsigned getReceiveBufferSize( int socket);
unsigned increaseReceiveBufferTo(int socket, unsigned requestedSize);
int accept(const char *ip, unsigned port);
#endif
com_net.cpp
#include "com_debug.h"
#include "com_net.h"
#include
#include /* See NOTES */
#include
#include
#include
#include
#include
#include
#include
#include
#include"memwatch.h"
int udp_net_setup()
{//udp 可以直接用recevfrom 指明从哪个主机和端口接受,但是每次都要填写地址信息
//所以udp可以用connet 确定好服务端地址端口
//同样也可以先用bind绑定好自己客户端的端口地址,避免自己调用send的时候系统给客户端自己分配一个端口
int sockfd;
struct sockaddr_in cli_addr;
//close_on_exec。当父进程打开文件时,只需要应用程序设置FD_CLOSEXEC标志位,
//则当fork后exec其他程序的时候,内核自动会将其继承的父进程FD关闭,避免子进程也同时操作,造成意外
//
//vlc 和live555的socket都有 |SOCK_CLOEXEC , 解释如上
sockfd = socket(AF_INET,SOCK_DGRAM | SOCK_CLOEXEC,0);
if(sockfd < 0){
DEBUG_ERR("Fail to socket\n");
return -1;
}
//屏蔽掉SIGPIPE 消息
// setsockopt(sockfd, SOL_SOCKET, SO_NOSIGPIPE, &(int){ 1 }, sizeof (int));
int data =1;
setsockopt(sockfd, SOL_SOCKET, MSG_NOSIGNAL, &data, sizeof (int));
cli_addr.sin_family = AF_INET;
#if 1//这里使用内核自动分配的端口
cli_addr.sin_port = 0;//!!!!!!!!!!!!
#else
cli_addr.sin_port = htons(8554);//!!!!!!!!!!!!
#endif
cli_addr.sin_addr.s_addr = INADDR_ANY;
if( bind(sockfd, (struct sockaddr*)&cli_addr, sizeof(struct sockaddr_in)) < 0)
{
//当前使用默认阻塞方式。
DEBUG_ERR("Fail to bind :INADDR_ANY:0 err:%d %s\n",errno,strerror(errno));
close(sockfd);
return -1;
}
return sockfd;
}
int udp_connect(int sockfd,const char*host, int port)
{
if(sockfd <= -1)
{
return RET_ERR;
}
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
server_addr.sin_addr.s_addr = inet_addr(host);
DEBUG_INFO3("connect host:%s,port:%d\n",host,port);
if(connect(sockfd,(struct sockaddr *)&server_addr,sizeof(struct sockaddr_in)) < 0)
{
//当前使用默认阻塞方式。
DEBUG_ERR("Fail to connect :%s:%d err:%d %s\n",host,port,errno,strerror(errno));
close(sockfd);
return RET_ERR;
}
return RET_SUCESS;
}
//in net byte order
int udp_get_port(int socket, int16_t *port)
{//参考live555 GroupsockHelper.cpp getSourcePort
sockaddr_in test;
test.sin_port = 0;
socklen_t len = sizeof(test);
if (getsockname(socket, (struct sockaddr*)&test, &len) < 0)
{
DEBUG_ERR("Fail to getsockname err:%d %s\n",errno,strerror(errno));
return RET_ERR;
}
DEBUG_INFO3("GETCLIENT_UDP_PORT test.sin_port %d \n",test.sin_port);
//*port = ntohs(test.sin_port);
*port = test.sin_port;
DEBUG_INFO3("GETCLIENT_UDP_PORT *port %d \n",*port);
return RET_SUCESS;
}
//服务器端 ip,和port, 本地的port会自动分配
int tcp_net_conncet(const char *psz_host,int i_port)
{
int sockfd;
struct sockaddr_in server_addr;
//close_on_exec。当父进程打开文件时,只需要应用程序设置FD_CLOSEXEC标志位,
//则当fork后exec其他程序的时候,内核自动会将其继承的父进程FD关闭,避免子进程也同时操作,造成意外
//
//vlc 和live555的socket都有 |SOCK_CLOEXEC , 解释如上
sockfd = socket(AF_INET,SOCK_STREAM|SOCK_CLOEXEC,0);
if(sockfd < 0){
DEBUG_ERR("Fail to socket\n");
return -1;
}
//屏蔽掉SIGPIPE 消息
// setsockopt(sockfd, SOL_SOCKET, SO_NOSIGPIPE, &(int){ 1 }, sizeof (int));
int data =1;
setsockopt(sockfd, SOL_SOCKET, MSG_NOSIGNAL, &data, sizeof (int));
// int curFlags = fcntl(sockfd, F_GETFL, 0);
// fcntl(sockfd, F_SETFL, curFlags|O_NONBLOCK); //
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(i_port);
server_addr.sin_addr.s_addr = inet_addr(psz_host);
if(connect(sockfd,(struct sockaddr *)&server_addr,sizeof(struct sockaddr_in)) < 0)
{ //这里可以实现自定义超时时间,不过比较麻烦:
//https://www.cnblogs.com/alantu2018/p/8469502.html
//https://blog.csdn.net/CodeHeng/article/details/44625495
//当前使用默认阻塞方式。
DEBUG_ERR("Fail to connect :%s:%d err:%d %s\n",psz_host,i_port,errno,strerror(errno));
close(sockfd);
return -1;
}
return sockfd;
}
int readSocket( int socket, char* buffer, unsigned bufferSize,
struct sockaddr *fromAddress)
{
socklen_t addrlen = sizeof( struct sockaddr);
int bytesRead = recvfrom(socket, buffer, bufferSize, 0,
fromAddress, &addrlen);
if(bufferSize < 0)
{//erro
DEBUG_ERR("err:%d:%s\n",errno,strerror(errno));
}
return bytesRead;
}
int writeSocket(int socket,char* buffer, unsigned bufferSize,
struct sockaddr * destAddress)
{
socklen_t addrlen = sizeof( struct sockaddr);
int bytesSent = sendto(socket, (char*)buffer, bufferSize, 0,
destAddress, addrlen);
if (bytesSent != (int)bufferSize)
{//erro
DEBUG_ERR("writeSocket erro %d/%d \n",bytesSent,bufferSize);
}
return bytesSent;
}
int closeSocket(int socket)
{
return close(socket);
}
bool makeSocketNonBlocking(int sock)
{
int curFlags = fcntl(sock, F_GETFL, 0);
return fcntl(sock, F_SETFL, curFlags|O_NONBLOCK) >= 0;
}
bool makeSocketBlocking(int sock, unsigned writeTimeoutInMilliseconds)
{
bool result;
#if defined(__WIN32__) || defined(_WIN32)
unsigned long arg = 0;
result = ioctlsocket(sock, FIONBIO, &arg) == 0;
#elif defined(VXWORKS)
int arg = 0;
result = ioctl(sock, FIONBIO, (int)&arg) == 0;
#else
int curFlags = fcntl(sock, F_GETFL, 0);
result = fcntl(sock, F_SETFL, curFlags&(~O_NONBLOCK)) >= 0;
#endif
if (writeTimeoutInMilliseconds > 0) {
#ifdef SO_SNDTIMEO
#if defined(__WIN32__) || defined(_WIN32)
DWORD msto = (DWORD)writeTimeoutInMilliseconds;
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char *)&msto, sizeof(msto) );
#else
struct timeval tv;
tv.tv_sec = writeTimeoutInMilliseconds/1000;
tv.tv_usec = (writeTimeoutInMilliseconds%1000)*1000;
//send block and recv block
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char *)&tv, sizeof tv);
setsockopt(sock,SOL_SOCKET,SO_RCVTIMEO, (char *)&tv, sizeof tv);
#endif
#endif
}
return result;
}
unsigned getReceiveBufferSize( int socket)
{
unsigned curSize;
socklen_t sizeSize = sizeof(curSize);
if (getsockopt(socket, SOL_SOCKET, SO_RCVBUF,(char*)&curSize, &sizeSize) < 0)
{
DEBUG_ERR("get buffsize erro\n");
return 0;
}
return curSize;
}
unsigned increaseReceiveBufferTo(int socket, unsigned requestedSize)
{
// First, get the current buffer size. If it's already at least
// as big as what we're requesting, do nothing.
unsigned curSize = getReceiveBufferSize(socket);
// Next, try to increase the buffer to the requested size,
// or to some smaller size, if that's not possible:
while (requestedSize > curSize)
{
socklen_t sizeSize = sizeof(requestedSize);
if (setsockopt(socket, SOL_SOCKET, SO_RCVBUF,(char*)&requestedSize, sizeSize) >= 0)
{
// success
return requestedSize;
}
requestedSize = (requestedSize+curSize)/2;
}
return getReceiveBufferSize(socket);
}
int accept(const char *ip, unsigned port)
{
int ret;
pthread_t tid;
int listen_fd;
int connect_fd;
struct sockaddr_in peer_addr;
struct sockaddr_in server_addr;
socklen_t addrlen = sizeof(struct sockaddr);
listen_fd = socket(AF_INET,SOCK_STREAM,0);
if(listen_fd < 0){
perror("Fail to socket");
return -1;
}
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
server_addr.sin_addr.s_addr = inet_addr(ip);
if(bind(listen_fd,(struct sockaddr *)&server_addr,sizeof(server_addr)) < 0)
{
perror("Fail to bind");
return -1;
}
//设为监听模式
listen(listen_fd,128);
printf("Listen ....\n");
connect_fd = accept(listen_fd,(struct sockaddr *)&peer_addr,&addrlen);
if(connect_fd < 0){
perror("Fail to accept");
return -1;
}
printf("--------------------------------------\n");
printf("Ip : %s\n",inet_ntoa(peer_addr.sin_addr));
printf("Port : %d\n",ntohs(peer_addr.sin_port));
printf("--------------------------------------\n");
return connect_fd;
}
memwatch.h
这个是之前用来排除内存泄漏等问题的,现在没用,为空文件。