• 【转】Linux下消息队列和socket绝对速度比拼


    在当今的网络时代,我们常常见到的进程间通信方式都是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[] argsthrows 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[] argsthrows 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次 ", 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次 ", 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次 ", 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, NULLNULL);
    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次 ", 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[] argsthrows 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[] argsthrows 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通道进行多次消息传输,在这种场景基本不用计算socket建立和关闭的时间,前面的测试都是基于长连接方式;短连接一般在建立socket通道后,只进行一次通信,然后就关闭 socket通道,这种场景必须考虑socket建立和关闭的时间(socket建立连接需要三次握手,关闭连接要四次通信)。)

  • 相关阅读:
    Flutter——去除字符串中的所有空格
    Flutter——类似淘口令复制弹窗(避免踩坑)
    Flutter——TextField输入框输入内容后,光标焦点保持在最后
    Flutter——TextField输入框光标焦点的获取和释放
    Flutter——设置text的最大长度
    Unity3D获得服务器时间/网络时间/后端时间/ServerTime,适合单机游戏使用
    Cocoapods问题汇总
    scrollToItem出错
    一种消除局部自适应对比度增强算法方块效应的方法
    实时和非实时调度策略测试总结
  • 原文地址:https://www.cnblogs.com/secondtononewe/p/6668472.html
Copyright © 2020-2023  润新知