Linux下消息队列和socket绝对速度比拼


原文链接:http://blog.csdn.net/diaoyf/article/details/4604625


【本文已迁移到“程序员文摘” http://programmerdigest.cn/category/lajp】在当今的网络时代,我们常常见到的进程间通信方式都是socket,比如Java的EJB调用,Java和C通信,Web Service服务等。socket是最常用的通讯技术,几乎所有的系统、语言都支持,socket也是面向网络的,通信的两方可以跨越IP网络进行传输。


在本地通信中(同一台机器上的进程间通讯),socket的网络特性却成了累赘,组装解析网络报头、报文确认、CRC校验等都是针对网络的,本地通信没有必要,反而会影响传输效率。本地通信的一些传统技术,如管道、FIFO、消息队列等,没有网络功能的负担,传输速度应该高于socket,那到底高多少以至于值得在应用中替换socket技术呢,今天就来一场小测试,就System V消息队列和socket之间,做一次全面的速度比拼。

比拼场地:

本人的笔记本:赛扬1.5G 内存1.5G
系统:Ubuntu8.04 Desktop (Linux 2.6.24-24-generic)
JDK:1.6

第一回合: Java测试


先说明一下,Java并不支持System V消息队列,因此特为Java提供了JNI接口,我们使用lajp_9.09提供C源码编译的so动态连接库,lajp的下载地址和文档:http://code.google.com/p/lajp/

首先上场的是System V消息队列。

发送端程序: 
001    package test;
002
003    import lajp.MsgQ;
004
005    public class TestSend
006    {
007        /** 消息队列KEY */ 
008        static final int IPC_KEY = 0x20021230;
009
010        static
011        {
012            //JNI 
013            System.loadLibrary("lajpmsgq");
014        }
015
016        public static void main(String[] args)
017        {
018            //创建或获得现有的消息队列 
019            int msqid = MsgQ.msgget(IPC_KEY);
020            //发送字节数组 
021            byte[] msg = new byte[1024];
022        
023            for (int i = 0; i < 1024 * 5000; i++)
024            {
025                //每次发送1204字节到消息队列,9527是消息类型 
026                MsgQ.msgsnd(msqid, 9527, msg, msg.length);
027            }
028    
029            System.out.println("发送结束.");
030        }
031    }
 

接收端程序: 
001    package test;
002
003    import lajp.MsgQ;
004
005    public class TestRcv
006    {
007        /** 消息队列KEY */ 
008        static final int IPC_KEY = 0x20021230;
009
010        static
011        {
012            //JNI
013            System.loadLibrary("lajpmsgq");
014        }
015
016        public static void main(String[] args)
017        {
018            //创建或获得现有的消息队列 
019            int msqid = MsgQ.msgget(IPC_KEY);
020            //接收缓冲区 
021            byte[] msg = new byte[1024];
022        
023            long start = System.currentTimeMillis(); //开始时间
024        
025            for (int i = 0; i < 1024 * 5000; i++)
026            {
027                //每次从消息队列中接收消息类型为9527的消息,接收1204字节 
028                MsgQ.msgrcv(msqid, msg, msg.length, 9527);
029            }
030
031            long end = System.currentTimeMillis(); //结束时间 
032            System.out.println("用时:" + (end - start) + "毫秒");
033        }
034    }
 

程序很简单,需要说明的是三个JNI方法调用:
    msgget()方法: System V消息队列的技术要求,含义是通过一个指定的KEY获得消息队列标识符。
    msgsnd()方法: 发送。
    msgrcv()方法: 接收。
发送方进行了(1024 * 5000)次发送,每次发送1024字节数据,接收方进行了(1024 * 5000)次接收,每次接收1024字节,共计发送接收5G数据。测试时先启动TestSend程序,再启动TestRcv程序,共进行5轮次测试,测试结果如下:


用时:29846毫秒
用时:29591毫秒
用时:29935毫秒
用时:29730毫秒
用时:29468毫秒
平均速度:29714毫秒

用top命令监控测试期间的CPU、内存的使用:

 


接下来上场的是socket。

发送端程序: 
001    import java.io.IOException;
002    import java.io.OutputStream;
003    import java.net.Socket;
004    
005    public class SocketSend
006    {
007        public static void main(String[] args) throws IOException
008        {
009            //Socket 
010            Socket socket = new Socket("127.0.0.1", 9527);
011            //输出流 
012            OutputStream out = socket.getOutputStream();
013            //发送字节数组 
014            byte[] msg = new byte[1024];
015    
016            long start = System.currentTimeMillis(); //开始时间 
017        
018            for (int i = 0; i < 1024 * 5000; i++)
019            {
020                //发送 
021                out.write(msg);
022            }
023    
024            long end = System.currentTimeMillis(); //结束时间 
025            System.out.println("用时:" + (end - start) + "毫秒");
026        }
027    }
 

接收端程序: 
001    import java.io.IOException;
002    import java.io.InputStream;
003    import java.net.ServerSocket;
004    import java.net.Socket;
005
006    public class SocketRecv
007    {
008        public static void main(String[] args) throws IOException
009        {
010            //侦听9527端口 
011            ServerSocket serverSocket = new ServerSocket(9527);
012            //Socket 
013            Socket socket = serverSocket.accept();
014            //输入流 
015            InputStream in = socket.getInputStream();
016            //接收缓冲区 
017            byte[] msg = new byte[1024];
018            
019            for (int i = 0; i < 1024 * 5000; i++)
020            {
021                //每次接收1204字节 
022                in.read(msg);
023            }
024            
025            System.out.println("接受结束.");
026        }
027    }
 

程序同样很简单,同样发送接收了(1024 * 5000)次,同样5G数据,socket程序必须先启动服务方SocketRecv,然后启动客户方SocketSend,共进行5轮次测试,测试结果如下:

用时:33951毫秒
用时:33448毫秒
用时:33987毫秒
用时:34638毫秒
用时:33957毫秒
平均速度:33996.2毫秒

用top命令监控测试期间的CPU、内存的使用:

 

测试结果让人对消息队列有点失望,性能优势微弱大约只领先了13%,且程序复杂性要大的多(使用了JNI)。不过重新审视测试过程有一个疑问:消息队列程序调用了自定义的JNI接口,而socket是Java内嵌的功能,是否JVM对 socket有特殊的优化呢?

怀着这个疑问,进行第二场纯C程序的测试。

第二回合: C程序测试


首先上场的还是System V消息队列。

发送端程序: 
001    #include <sys/ipc.h>  
002    #include <sys/msg.h> 
003    #include <stdio.h>  
004    
005    #define IPC_KEY 0x20021230  /* 消息队列KEY */ 
006    
007    /*消息结构*/  
008    struct message  
009    {  
010        long msg_type;            /* 消息标识符 */  
011        char msg_text[1024];    /* 消息内容 */ 
012    };
013    
014    int main()
015    {
016        /* 创建或获得现有的消息队列 */ 
017        int msqid = msgget(IPC_KEY, IPC_CREAT | 0666);
018        /* 消息结构 */ 
019        struct message msgq;
020        msgq.msg_type = 9527; /* 消息类型 */ 
021        
022        int i;
023        for (i = 0; i < 1024 * 5000; i++)
024        {    
025            /* 接收 */ 
026            msgsnd(msqid, &msgq, 1024, 0);
027        }
028    
029        printf("msgq发送结束,共发送%d次/n", i);
030        return 0;
031    }
032
 

接收端程序: 
001    #include <sys/ipc.h>  
002    #include <sys/msg.h> 
003    #include <stdio.h>  
004    
005    #define IPC_KEY 0x20021230  /* 消息队列KEY */ 
006    
007    /*消息结构*/  
008    struct message  
009    {  
010        long msg_type;            /* 消息标识符 */  
011        char msg_text[1024];    /* 消息内容 */ 
012    };
013    
014    int main()
015    {
016        /* 创建或获得现有的消息队列 */ 
017        int msqid = msgget(IPC_KEY, IPC_CREAT | 0666);
018        /* 消息结构 */ 
019        struct message msgq;
020        
021        int i;
022        for (i = 0; i < 1024 * 5000; i++)
023        {    
024            /* 接收 */ 
025            msgrcv(msqid, &msgq, 1024, 9527, 0);
026        }
027    
028        printf("msgq接收结束,共接收%d次/n", i);
029        return 0;
030    }
031
 

和第一场一样,发送接收了(1024 * 5000)次,同样5G数据,先启动接收端程序msgrecv,然后以$time msgsend方式启动客户端程序,共进行5轮次测试,time的测试结果如下:

                       用户        系统        时钟
第一次:     0.992s    7.084s    18.202s
第二次:    0.888s    7.280s    18.815s
第三次:    1.060s    7.656s    19.476s
第四次:    1.048s    7.124s    20.293s
第五次:    1.008s    7.160s    18.655s

用top命令监控测试期间的CPU、内存的使用:


接下来上场的是socket。

发送端程序:




001    #include <stdio.h>
002    #include <string.h>
003    #include <netdb.h>
004
005    char msg[1024]; /* 发送消息 */ 
006    
007    int main()
008    {
009        char *ip = "127.0.0.1";     /* 发送地址 */ 
010        int port = 9527;             /* 发送端口 */ 
011    
012        struct hostent *server_host = gethostbyname(ip);
013    
014        /* 客户端填充 sockaddr 结构 */ 
015        struct sockaddr_in client_addr;             /* 客户端地址结构 */ 
016        bzero(&client_addr, sizeof(client_addr));
017        client_addr.sin_family = AF_INET;             /* AF_INET:IPV4协议 */ 
018        client_addr.sin_addr.s_addr = ((struct in_addr *)(server_host->h_addr))->s_addr; /* 服务端地址 */ 
019        client_addr.sin_port = htons(port);         /* 端口 */ 
020        
021        /* 建立socket */ 
022        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
023        /* 连接 */ 
024        connect(sockfd, (struct sockaddr *)(&client_addr), sizeof(client_addr));
025            
026        int i;
027        for (i = 0; i < 1024 * 5000; i++)
028        {    
029            /* 发送 */ 
030            send(sockfd, msg, 1024, 0);
031        }
032    
033        printf("发送结束,共发送%d次/n", i);    
034        return 0;
035    }
036
 

接收端程序: 
001    #include <stdio.h>
002    #include <string.h>
003    #include <netdb.h>
004    
005    char msg[1024]; /* 接收缓冲区 */ 
006    
007    int main()
008    {
009        int listen_port = 9527;                         /* 侦听端口 */    
010        int listenfd = socket(AF_INET, SOCK_STREAM, 0); /* 建立侦听socket */ 
011    
012        /* 服务端填充 sockaddr 结构 */ 
013        struct sockaddr_in server_addr;                     /* 服务端地址结构 */ 
014        bzero(&server_addr, sizeof(server_addr));
015        server_addr.sin_family = AF_INET;                     /* AF_INET:IPV4协议 */
016        server_addr.sin_addr.s_addr = htonl(INADDR_ANY);     /* INADDR_ANY:通配地址,表示内核选择IP地址 */ 
017        server_addr.sin_port = htons(listen_port);             /* 端口 */ 
018        
019        /* 绑定端口 */ 
020        bind(listenfd, (struct sockaddr *)(&server_addr), sizeof(server_addr));
021        /* 侦听 */ 
022        listen(listenfd, 5);
023        int sockfd = accept(listenfd, NULL, NULL);
024        
025        int i;
026        for (i = 0; i < 1024 * 5000; i++)
027        {    
028            /* 接收 */ 
029            recv(sockfd, msg, 1024, 0);
030        }
031    
032        printf("接收结束,共接收%d次/n", i);    
033        return 0;
034    }
035
 

C语言中,socket程序复杂了不少。测试标准和Java相同,发送接收了(1024 * 5000)次,5G数据,先启动接收端程序,然后以time方式启动发送端,测试结果如下:

                       用户        系统        时钟
第一次:     0.524s    9.765s    20.666s
第二次:    0.492s    9.825s    20.530s
第三次:    0.468s    9.493s    21.831s
第四次:    0.512s    9.205s    20.059s
第五次:    0.440s    9.605s    21.888s

用top命令监控测试期间的CPU、内存的使用:
 

C语言的socket程序系统用时多一些,消息队列程序用户用时多一些,这和他们的实现方式相关,从时钟比较看,消息队列比socket快10%左右,和Java测试结果相似。比较Java和C,C只领先了三分之一,看来当前的Java效率已经相当高了。

还不能忙于下结论,socket的通信方式一般有两种:长连接和短连接。长连接指发送端和接收端建立连接后,可以保持socket通道进行多次消息传输,在这种场景基本不用计算socket建立和关闭的时间,前面的测试都是基于长连接方式;短连接一般在建立socket通道后,只进行一次通信,然后就关闭socket通道,这种场景必须考虑socket建立和关闭的时间(socket建立连接需要三次握手,关闭连接要四次通信)。

第三回合: Java测试(短连接)

将第一回合中的Java程序稍作修改,先看socket的:

发送端程序: 
001    import java.io.IOException;
002    import java.io.OutputStream;
003    import java.net.Socket;
004
005    public class SocketSend2
006    {
007        public static void main(String[] args) throws IOException
008        {
009            long start = System.currentTimeMillis(); //开始时间 
010            //发送字节数组 
011            byte[] msg = new byte[1024];
012
013            for (int i = 0; i < 1024 * 1000; i++)
014            {
015                //建立Socket连接 
016                Socket socket = new Socket("127.0.0.1", 9527);
017                //输出流 
018                OutputStream out = socket.getOutputStream();
019                //发送 
020                out.write(msg);    
021            
022                //关闭输出流 
023                out.close();
024                //关闭socket连接 
025                socket.close();
026            }
027
028            long end = System.currentTimeMillis(); //结束时间 
029            System.out.println("用时:" + (end - start) + "毫秒");
030        }
031    }
 
建立socket的语句放在了循环内部,这样每次发送都是新建的连接,025行的关闭语句是必须的,因为socket是系统的有限资源,支持不了这么大规模的申请。

接收端程序:




001    import java.io.IOException;
002    import java.io.InputStream;
003    import java.net.ServerSocket;
004    import java.net.Socket;
005
006    public class SocketRecv2
007    {
008        public static void main(String[] args) throws IOException
009        {
010            //侦听9527端口 
011            ServerSocket serverSocket = new ServerSocket(9527);    
012            //接收缓冲区 
013            byte[] msg = new byte[1024];        
014        
015            for (int i = 0; i < 1024 * 1000; i++)
016            {
017                //接到客户端Socket连接请求 
018                Socket socket = serverSocket.accept();
019                //输入流 
020                InputStream in = socket.getInputStream();
021                //每次接收1204字节 
022                in.read(msg);
023                
024                //关闭输入流 
025                in.close();
026                //关闭socket连接 
027                socket.close();
028            }
029            
030            System.out.println("接受结束.");
031        }
032    }
 
接收端也做了相应的改动,发送和接收次数降低到(1024 * 1000)次,测试结果:431280毫秒, 不要吃惊,没错是431.280秒,这也是书本上为什么总在强调使用数据库连接池的原因。

消息队列没有像socket那样的连接概念,为了做个参考,将第一回合中的消息队列程序也修改一下:

发送端程序: 
001    package test;
002
003    import lajp.MsgQ;
004
005    public class TestSend2
006    {
007        /** 消息队列KEY */ 
008        static final int IPC_KEY = 0x20021230;
009
010        static
011        {
012            //JNI 
013            System.loadLibrary("lajpmsgq");
014        }
015    
016        public static void main(String[] args)
017        {
018            //发送字节数组 
019            byte[] msg = new byte[1024];
020
021            for (int i = 0; i < 1024 * 1000; i++)
022            {
023                //创建或获得现有的消息队列 
024                int msqid = MsgQ.msgget(IPC_KEY);
025            
026                //每次发送1204字节 
027                MsgQ.msgsnd(msqid, 9527, msg, msg.length);
028            }
029    
030            System.out.println("发送结束.");
031        }
032    }
 
将024行的msgget()方法放在循环内部,作为和socket比较的“连接”。

接收段程序: 
001    package test;
002    
003    import lajp.MsgQ;
004    
005    public class TestRcv2
006    {
007        /** 消息队列KEY */ 
008        static final int IPC_KEY = 0x20021230;
009    
010        static
011        {
012            //JNI 
013            System.loadLibrary("lajpmsgq");
014        }
015
016        public static void main(String[] args)
017        {
018            long start = System.currentTimeMillis(); //开始时间 
019            //接收缓冲区 
020            byte[] msg = new byte[1024];
021        
022            for (int i = 0; i < 1024 * 1000; i++)
023            {
024                //创建或获得现有的消息队列 
025                int msqid = MsgQ.msgget(IPC_KEY);
026            
027                //每次接收1204字节 
028                MsgQ.msgrcv(msqid, msg, msg.length, 9527);
029            }
030
031            long end = System.currentTimeMillis(); //结束时间 
032            System.out.println("用时:" + (end - start) + "毫秒");
033        }
034    }
 

测试结果:6617毫秒。

总结:

在能够使用socket长连接的应用中,建议使用socket技术,毕竟很通用熟悉的人也多,而消息队列能够提高的效率有限;在只能使用socket短连接的应用中,特别是并发量大的场景,强烈建议使用消息队列,因为能够极大的提高通信速率。


  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值