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

作者:diaoyf  |  文章来源:http://programmerdigest.cn

在当今的网络时代,我们常常见到的进程间通信方式都是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消息队列。

发送端程序:

  1. package test;
  2.  
  3. import lajp.MsgQ;
  4.  
  5. public class TestSend
  6. {
  7.     /** 消息队列KEY */
  8.     static final int IPC_KEY = 0×20021230;
  9.  
  10.     static
  11.     {
  12.         //JNI
  13.         System. loadLibrary ( "lajpmsgq" );
  14.     }
  15.  
  16.     public static void main ( String [ ] args )
  17.     {
  18.         //创建或获得现有的消息队列
  19.         int msqid = MsgQ. msgget (IPC_KEY );
  20.         //发送字节数组
  21.         byte [ ] msg = new byte [ 1024 ];
  22.      
  23.         for ( int i = 0; i < 1024 * 5000; i++ )
  24.         {
  25.             //每次发送1204字节到消息队列,9527是消息类型
  26.             MsgQ. msgsnd (msqid, 9527, msg, msg. length );
  27.         }
  28.  
  29.         System. out. println ( "发送结束." );
  30.     }
  31. }

 

接收端程序:

  1. package test;
  2.  
  3. import lajp.MsgQ;
  4.  
  5. public class TestRcv
  6. {
  7.     /** 消息队列KEY */
  8.     static final int IPC_KEY = 0×20021230;
  9.  
  10.     static
  11.     {
  12.         //JNI
  13.         System. loadLibrary ( "lajpmsgq" );
  14.     }
  15.  
  16.     public static void main ( String [ ] args )
  17.     {
  18.         //创建或获得现有的消息队列
  19.         int msqid = MsgQ. msgget (IPC_KEY );
  20.         //接收缓冲区
  21.         byte [ ] msg = new byte [ 1024 ];
  22.        
  23.         long start = System. currentTimeMillis ( ); //开始时间
  24.        
  25.         for ( int i = 0; i < 1024 * 5000; i++ )
  26.         {
  27.             //每次从消息队列中接收消息类型为9527的消息,接收1204字节
  28.             MsgQ. msgrcv (msqid, msg, msg. length, 9527 );
  29.         }
  30.  
  31.         long end = System. currentTimeMillis ( ); //结束时间
  32.         System. out. println ( "用时:" + (end – start ) + "毫秒" );
  33.     }
  34. }

 

程序很简单,需要说明的是三个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、内存的使用:

java_msgq

接下来上场的是socket。

发送端程序:

  1. import java.io.IOException;
  2. import java.io.OutputStream;
  3. import java.net.Socket;
  4.    
  5. public class SocketSend
  6. {
  7.     public static void main ( String [ ] args ) throws IOException
  8.     {
  9.         //Socket
  10.         Socket socket = new Socket ( "127.0.0.1", 9527 );
  11.         //输出流
  12.         OutputStream out = socket. getOutputStream ( );
  13.         //发送字节数组
  14.         byte [ ] msg = new byte [ 1024 ];
  15.    
  16.         long start = System. currentTimeMillis ( ); //开始时间
  17.        
  18.         for ( int i = 0; i < 1024 * 5000; i++ )
  19.         {
  20.             //发送
  21.             out. write (msg );
  22.         }
  23.    
  24.         long end = System. currentTimeMillis ( ); //结束时间
  25.         System. out. println ( "用时:" + (end – start ) + "毫秒" );
  26.     }
  27. }

 

接收端程序:

  1. import java.io.IOException;
  2. import java.io.InputStream;
  3. import java.net.ServerSocket;
  4. import java.net.Socket;
  5.  
  6. public class SocketRecv
  7. {
  8.     public static void main ( String [ ] args ) throws IOException
  9.     {
  10.         //侦听9527端口
  11.         ServerSocket serverSocket = new ServerSocket ( 9527 );
  12.         //Socket
  13.         Socket socket = serverSocket. accept ( );
  14.         //输入流
  15.         InputStream in = socket. getInputStream ( );
  16.         //接收缓冲区
  17.         byte [ ] msg = new byte [ 1024 ];
  18.            
  19.         for ( int i = 0; i < 1024 * 5000; i++ )
  20.         {
  21.             //每次接收1204字节
  22.             in. read (msg );
  23.         }
  24.            
  25.         System. out. println ( "接受结束." );
  26.     }
  27. }  

 

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

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

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

java_socket

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

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

第二回合: C程序测试

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

发送端程序:

  1. #include <sys/ipc.h>
  2. #include <sys/msg.h>
  3. #include <stdio.h>
  4.    
  5. #define IPC_KEY 0×20021230  /* 消息队列KEY */
  6.    
  7. /*消息结构*/
  8. struct message
  9. {
  10.     long msg_type;             /* 消息标识符 */
  11.     char msg_text [ 1024 ];     /* 消息内容 */
  12. };
  13.    
  14. int main ( )
  15. {
  16.     /* 创建或获得现有的消息队列 */
  17.     int msqid = msgget (IPC_KEY, IPC_CREAT | 0666 );
  18.     /* 消息结构 */
  19.     struct message msgq;
  20.     msgq. msg_type = 9527; /* 消息类型 */
  21.        
  22.     int i;
  23.     for (i = 0; i < 1024 * 5000; i++ )
  24.     {  
  25.         /* 接收 */
  26.         msgsnd (msqid, &msgq, 1024, 0 );
  27.     }
  28.    
  29.     printf ( "msgq发送结束,共发送%d次\n", i );
  30.     return 0;
  31. }

 

接收端程序:

  1. #include <sys/ipc.h>
  2. #include <sys/msg.h>
  3. #include <stdio.h>
  4.    
  5. #define IPC_KEY 0×20021230  /* 消息队列KEY */
  6.    
  7. /*消息结构*/
  8. struct message
  9. {
  10.     long msg_type;             /* 消息标识符 */
  11.     char msg_text [ 1024 ];     /* 消息内容 */
  12. };
  13.    
  14. int main ( )
  15. {
  16.     /* 创建或获得现有的消息队列 */
  17.     int msqid = msgget (IPC_KEY, IPC_CREAT | 0666 );
  18.     /* 消息结构 */
  19.     struct message msgq;
  20.        
  21.     int i;
  22.     for (i = 0; i < 1024 * 5000; i++ )
  23.     {  
  24.         /* 接收 */
  25.         msgrcv (msqid, &msgq, 1024, 9527, 0 );
  26.     }
  27.    
  28.     printf ( "msgq接收结束,共接收%d次\n", i );
  29.     return 0;
  30. }

 

和第一场一样,发送接收了(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、内存的使用:

c_msgq

接下来上场的是socket。

发送端程序:

  1. #include <stdio.h>
  2. #include <string.h>
  3. #include <netdb.h>
  4.  
  5. char msg [ 1024 ]; /* 发送消息 */
  6.    
  7. int main ( )
  8. {
  9.     char *ip = "127.0.0.1";     /* 发送地址 */
  10.     int port = 9527;             /* 发送端口 */
  11.    
  12.     struct hostent *server_host = gethostbyname (ip );
  13.    
  14.     /* 客户端填充 sockaddr 结构 */
  15.     struct sockaddr_in client_addr;             /* 客户端地址结构 */
  16.     bzero (&client_addr, sizeof (client_addr ) );
  17.     client_addr. sin_family = AF_INET;             /* AF_INET:IPV4协议 */
  18.     client_addr. sin_addr. s_addr = ( ( struct in_addr * ) (server_host->h_addr ) )->s_addr; /* 服务端地址 */
  19.     client_addr. sin_port = htons (port );         /* 端口 */
  20.        
  21.     /* 建立socket */
  22.     int sockfd = socket (AF_INET, SOCK_STREAM, 0 );
  23.     /* 连接 */
  24.     connect (sockfd, ( struct sockaddr * ) (&client_addr ), sizeof (client_addr ) );
  25.            
  26.     int i;
  27.     for (i = 0; i < 1024 * 5000; i++ )
  28.     {  
  29.         /* 发送 */
  30.         send (sockfd, msg, 1024, 0 );
  31.     }
  32.    
  33.     printf ( "发送结束,共发送%d次\n", i );  
  34.     return 0;
  35. }

 

接收端程序:

  1. #include <stdio.h>
  2. #include <string.h>
  3. #include <netdb.h>
  4.    
  5. char msg [ 1024 ]; /* 接收缓冲区 */
  6.    
  7. int main ( )
  8. {
  9.     int listen_port = 9527;                         /* 侦听端口 */  
  10.     int listenfd = socket (AF_INET, SOCK_STREAM, 0 ); /* 建立侦听socket */
  11.    
  12.     /* 服务端填充 sockaddr 结构 */
  13.     struct sockaddr_in server_addr;                     /* 服务端地址结构 */
  14.     bzero (&server_addr, sizeof (server_addr ) );
  15.     server_addr. sin_family = AF_INET;                     /* AF_INET:IPV4协议 */
  16.     server_addr. sin_addr. s_addr = htonl (INADDR_ANY );     /* INADDR_ANY:通配地址,表示内核选择IP地址 */
  17.     server_addr. sin_port = htons (listen_port );             /* 端口 */
  18.        
  19.     /* 绑定端口 */
  20.     bind (listenfd, ( struct sockaddr * ) (&server_addr ), sizeof (server_addr ) );
  21.     /* 侦听 */
  22.     listen (listenfd, 5 );
  23.     int sockfd = accept (listenfd, NULL, NULL );
  24.        
  25.     int i;
  26.     for (i = 0; i < 1024 * 5000; i++ )
  27.     {  
  28.         /* 接收 */
  29.         recv (sockfd, msg, 1024, 0 );
  30.     }
  31.    
  32.     printf ( "接收结束,共接收%d次\n", i );  
  33.     return 0;
  34. }

 

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

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

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

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

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

发送端程序:

  1. import java.io.IOException;
  2. import java.io.OutputStream;
  3. import java.net.Socket;
  4.  
  5. public class SocketSend2
  6. {
  7.     public static void main ( String [ ] args ) throws IOException
  8.     {
  9.         long start = System. currentTimeMillis ( ); //开始时间
  10.         //发送字节数组
  11.         byte [ ] msg = new byte [ 1024 ];
  12.  
  13.         for ( int i = 0; i < 1024 * 1000; i++ )
  14.         {
  15.             //建立Socket连接
  16.             Socket socket = new Socket ( "127.0.0.1", 9527 );
  17.             //输出流
  18.             OutputStream out = socket. getOutputStream ( );
  19.             //发送
  20.             out. write (msg );  
  21.          
  22.             //关闭输出流
  23.             out. close ( );
  24.             //关闭socket连接
  25.             socket. close ( );
  26.         }
  27.  
  28.         long end = System. currentTimeMillis ( ); //结束时间
  29.         System. out. println ( "用时:" + (end – start ) + "毫秒" );
  30.     }
  31. }

 

建立socket的语句放在了循环内部,这样每次发送都是新建的连接,025行的关闭语句是必须的,因为socket是系统的有限资源,支持不了这么大规模的申请。

接收端程序:

  1. import java.io.IOException;
  2. import java.io.InputStream;
  3. import java.net.ServerSocket;
  4. import java.net.Socket;
  5.  
  6. public class SocketRecv2
  7. {
  8.     public static void main ( String [ ] args ) throws IOException
  9.     {
  10.         //侦听9527端口
  11.         ServerSocket serverSocket = new ServerSocket ( 9527 );  
  12.         //接收缓冲区
  13.         byte [ ] msg = new byte [ 1024 ];      
  14.        
  15.         for ( int i = 0; i < 1024 * 1000; i++ )
  16.         {
  17.             //接到客户端Socket连接请求
  18.             Socket socket = serverSocket. accept ( );
  19.             //输入流
  20.             InputStream in = socket. getInputStream ( );
  21.             //每次接收1204字节
  22.             in. read (msg );
  23.                
  24.             //关闭输入流
  25.             in. close ( );
  26.             //关闭socket连接
  27.             socket. close ( );
  28.         }
  29.            
  30.         System. out. println ( "接受结束." );
  31.     }
  32. }

 

接收端也做了相应的改动,发送和接收次数降低到(1024 * 1000)次,测试结果:431280毫秒,不要吃惊,没错是431.280秒,这也是书本上为什么总在强调使用数据库连接池的原因。

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

发送端程序:

  1. package test;
  2.  
  3. import lajp.MsgQ;
  4.  
  5. public class TestSend2
  6. {
  7.     /** 消息队列KEY */
  8.     static final int IPC_KEY = 0×20021230;
  9.  
  10.     static
  11.     {
  12.         //JNI
  13.         System. loadLibrary ( "lajpmsgq" );
  14.     }
  15.    
  16.     public static void main ( String [ ] args )
  17.     {
  18.         //发送字节数组
  19.         byte [ ] msg = new byte [ 1024 ];
  20.  
  21.         for ( int i = 0; i < 1024 * 1000; i++ )
  22.         {
  23.             //创建或获得现有的消息队列
  24.             int msqid = MsgQ. msgget (IPC_KEY );
  25.            
  26.             //每次发送1204字节
  27.             MsgQ. msgsnd (msqid, 9527, msg, msg. length );
  28.         }
  29.    
  30.         System. out. println ( "发送结束." );
  31.     }
  32. }

 

将024行的msgget()方法放在循环内部,作为和socket比较的“连接”。

接收段程序:

  1. package test;
  2.  
  3. import lajp.MsgQ;
  4.    
  5. public class TestRcv2
  6. {
  7.     /** 消息队列KEY */
  8.     static final int IPC_KEY = 0×20021230;
  9.    
  10.     static
  11.     {
  12.         //JNI
  13.         System. loadLibrary ( "lajpmsgq" );
  14.     }
  15.  
  16.     public static void main ( String [ ] args )
  17.     {
  18.         long start = System. currentTimeMillis ( ); //开始时间
  19.         //接收缓冲区
  20.         byte [ ] msg = new byte [ 1024 ];
  21.        
  22.         for ( int i = 0; i < 1024 * 1000; i++ )
  23.         {
  24.             //创建或获得现有的消息队列
  25.             int msqid = MsgQ. msgget (IPC_KEY );
  26.            
  27.             //每次接收1204字节
  28.             MsgQ. msgrcv (msqid, msg, msg. length, 9527 );
  29.         }
  30.  
  31.         long end = System. currentTimeMillis ( ); //结束时间
  32.         System. out. println ( "用时:" + (end – start ) + "毫秒" );
  33.     }
  34. }

 

测试结果:6617毫秒。

总结:

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

你可能感兴趣的:(socket,测试,消息列队)