SOCKET通信中多线程编程的性能优化问题

背景:本文作为本人的一点心得,同时也供遇到类似问题的读者以供参考。
所需知识:简单的SOCKET编程、多线程编程知识

SOCKET编程作为当前网络编程的主要方式,在各种项目、科研、实验中多有使用,其通过套接字的方式完成端到端通信,大大方便了编程人员的网络搭建。

网络建立的模式多种多样,client-sever(用户-服务器)模式、peer-2-peer(对等)模式等等。而无论哪一种模式,在点到点链路建立后,不可避免的可能会出现多应用同时通信的情景。所以在SOCKET通信中,自然地,在多数情况下都会采用多线程通信。例如,在C-V(用户-服务器模式缩写)模式中,服务器大多不可能单一地为一个用户提供服务,所以在用户A连接建立时,服务器会自动的分配一个线程给该用户,而预留自身的监听状态,以保证后续的用户可以正常建立连接。

然而,只要实现过任何一个最基本的C-V通信的读者都会知道,SOCKET编程中有许多函数都有决定函数自身的执行状态的参数,比如接收数据函数:

ssize_t recvfrom(int sockfd,void *buf,size_t len,unsigned int flags, struct sockaddr *from,socket_t *fromlen);

中的flag字段可以是MSG_NOWAIT、0等情况。再比如

int sendto ( socket s , const void * msg, int len, unsigned int flags, conststruct sockaddr * to , int tolen ) ;

中的flag字段。

flag字段的含义定义如下:
MSG_DONTWAIT:操作不会被阻塞。
MSG_ERRQUEUE: 指示应该从套接字的错误队列上接收错误值,依据不同的协议,错误值以某种辅佐性消息的方式传递进来,使用者应该提供足够大的缓冲区。导致错误的原封包通过msg_iovec作为一般的数据来传递。导致错误的数据报原目标地址作为msg_name被提供。
MSG_PEEK:指示数据接收后,在接收队列中保留原数据,不将其删除,随后的读操作还可以接收相同的数据。
MSG_TRUNC:返回封包的实际长度,即使它比所提供的缓冲区更长, 只对packet套接字有效。
MSG_WAITALL:要求阻塞操作,直到请求得到完整的满足。然而,如果捕捉到信号,错误或者连接断开发生,或者下次被接收的数据类型不同,仍会返回少于请求量的数据。
MSG_EOR:指示记录的结束,返回的数据完成一个记录。
MSG_TRUNC:指明数据报尾部数据已被丢弃,因为它比所提供的缓冲区需要更多的空间。
MSG_CTRUNC:指明由于缓冲区空间不足,一些控制数据已被丢弃。
MSG_OOB:指示接收到out-of-band数据(即需要优先处理的数据)。
MSG_ERRQUEUE:指示除了来自套接字错误队列的错误外,没有接收到其它数据。

而如何选择合适的flag直接影响了程序的性能。

比如在代码段中:

int UDPRcv(struct tunnel_T *tunnel)
{
    int BufLength;  
    unsigned char msg[PLMsgMaxLength];
    struct sockaddr_in clientaddr;
    socklen_t len = (socklen_t) sizeof(clientaddr);
    struct tunnel_T tunnelcsma;
    int i =0,num;

    while(1)
    {           
#ifndef DISABLE_ALL     



----------


if((BufLength = recvfrom(tunnel->socket_descriptor, msg, PLMsgMaxLength, 0, (struct sockaddr *)&clientaddr, &len)) > 0)


----------


#else
        if((BufLength = uart_recv_frame((u8 *)msg, PLMsgMaxLength)) >0)
#endif
        {           
            full_ip_r += BufLength + 1;
#ifdef DEBUG        
                printf("udp received!\n");  
                for(;i" %x",msg[i]);
                i=0;
                printf("\n");           
                printf("length is %d\n",BufLength);         
#endif                  
                switch((int)msg[0])
                {           
                    case NOMAC:
                        ...
                    case FDMAC:
                        ...
                    case CSMA:
                        ...
                }           
        }   
        else 
            fprintf(stderr, "udpRcv :%s\a\n", strerror(errno));
    }
    return 0;
}

这段代码一直在不停的接收数据,如果flag参数为0,则代码运行到这里时会阻塞,直到有数据包到达时才会继续运行;而如果参数flag是MSG_NOWAIT的话,代码运行到recvfrom处发现没有数据包执行,则会打印else的语句,并继续执行。如果该UDPRcv函数是一个接收线程的话,那么flag为MSG_NOWAIT时,该线程则会不停地无效的占用资源,而如果flag为0时,该线程就会阻塞,知道有数据包到达为止。

与此类似的,在多线程编程中,不同线程间进行数据交换时所用的消息队列也有类似的机制。例如函数:

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

中都有msgflg参数,该参数同样有阻塞、非阻塞等不同的情况,其功能与recvfrom和sendto类似,在此不赘述了。

下图展示了本人仅修改了多线程SOCKET通信代码中*一处***flag参数后,端到端ping业务时延的结果。
SOCKET通信中多线程编程的性能优化问题_第1张图片


从图中可以看到,性能表现是十分明显的。

总结一下:参数选择是阻塞,还是不阻塞,需要根据代码实现的功能需求进行调整,合理设计才行。


你可能感兴趣的:(socket编程)