如何编写高效的Java通信客户端

有很多机会需要编写自己的Java通信客户端,例如redis、memcache等的访问,怎么样编写一个高效的Java通信客户端呢,主要有以下几点:连接、序列化/反序列化、减少上下文切换。

1. 连接
如何高效的使用连接是网络通信中一个很关键的点,像通常用数据库我们都会用到数据库连接池,但其实数据库连接池在连接的使用上效率是不高的,数据库连接池的作用只是降低了建连接本身的消耗,但在获取/归还连接时的并发竞争还是比较严重的,在拿不到连接的时候的等待就影响更明显了。

数据库连接也许是因为各种各样的历史原因所以导致了这样的现象,但在自己可编写的其他的网络通信中,则可以采用更高效的方式,数据库连接池这种的主要问题在于连接获取/归还时的并发竞争,最好的解决方法自然是不要产生竞争。

要做到连接获取/归还不产生竞争,比较靠谱的方法就是连接不独占,也就是多个请求可以用同一个连接来同时发送(专业点说就是连接复用),要做到连接复用,需要有两点支持:NIO或AIO、request ID透传。

NIO或AIO可以实现无需像BIO一样,要占用连接来等响应,而是可以通过异步的回调来实现,这样每个请求就无需独占一个连接,而很多数据库连接目前仍然是采用bio的,so也不可能做成连接复用。

在没有request ID透传的支持下,如果多个请求共用一个连接,会导致的问题是响应回来的时候不知道是哪个请求的响应回来了,因此在连接复用的场景中,都需要增加一个request ID,在发送请求时带上这个id,响应回来的时候也把这个ID原样返回,这样客户端在接到后就可以知道是哪个请求的响应了。

在有了上面两点的支持后,就可以做到连接复用了,连接复用后如果有需要在连接上保持状态信息的,也需要做一些改造,否则仍然会导致连接无法复用。

在连接复用后,是不是还需要数据库连接池那样的创建限定个数的连接呢,通常来说是没有必要的,原因是在像mina/netty/grizzly的这些nio框架的实现下,在发送请求/接收响应的过程中,连接本身在整个处理过程中会需要独占的主要是在写发送缓冲区/接响应包上,写发送缓冲区这点本来就是串行的,所以即使多个连接也不会有什么改善,而接响应包这点上,只要包不是太大,单个连接和多个连接差距基本可以忽略,但如果包太大的话,确实会有影响,因此通常都建议在大多数的场景下采用单个长连接即可。

长连在连接是经过负载均衡设备的场景下会产生一个典型的问题,例如服务端集群有10台机器,期望的肯定是连接均衡的分散在这10台服务器上,但其实是会出现这样的现象:假设服务端其中几台重启了,这个时候客户端请求又发过来了,那么这个时候就会出现连接都建到存活的机器上了,因此最终可能会导致几台机器上有很多的连接,而另外几台完全没有连接,对于这种现象,有个龌龊的解决方法是每个连接在发送了X个请求后主动关闭掉,靠强制的这种重建连接来避免这个问题,最好的方法是连接完全不经过负载均衡设备,但这个通常只适合内部网络场景。

2. 反序列化
在第1点上提到了单个连接不可并发的地方主要是接响应包上,通常很多的通信的实现是在接响应包的动作上进行反序列化,更高效的做法是不要在接响应包的时候就去做反序列化的动作,而是改为在将响应包交给业务线程时,在业务线程里进行反序列化,这样可以尽可能的减少独占连接的时间。

另外一点不用说,就是序列化/反序列化尽可能选用高效的,例如google pb这些,不过也一定要考虑开发易用性。

3. 减少上下文切换
通常来说,在接到请求包或响应包时,由于业务端的处理可能不是纯cpu型的(例如会有io/锁等动作),因此通常都会将包交给另外的线程来处理,通常的情况下实现都会是每接到一个包,就通知下处理的线程,但在nio这类场景中,如果请求/响应包不是很大的话,很多时候接到的一个流中可能会包含多个包,如果每个包都通知一下处理的线程,就意味着一次上下文切换,优化点的做法就是如果流中有多个包,可以在把包都接好后,扔一个List给处理的线程,这在高并发的场景下会有明显的优化效果。

在编写一个高效的通信客户端时,差不多主要就是上面这3点,如果有疑问或更好的建议,欢迎发微信给我,期待!

=============================
欢迎关注微信公众号:hellojavacases

关于此微信号:
分享Java问题排查的Case、Java业界的动态和新技术、Java的一些小知识点Test,以及和大家一起讨论一些Java问题或场景,这里只有Java细节的分享,没有大道理、大架构和大框架。

公众号上发布的消息都存放在http://hellojava.info上。

《如何编写高效的Java通信客户端》有4个想法

  1. 干货,谢谢。
    有几个疑问:
    1. 当“连接复用”的两端互为客户端服务端时,可能不止需要增加一个requestID,还需要加上req/resp的标志吧?
    2. 长连接经过负载均衡设备那一段,有点疑问:当后端的几台服务器重启后,设备如F5应该可以设置成能够感知到后面的应用服务器是否ok吧?

    1. @周亚国
      1.恩,这个需要的;
      2.可以感应到,但关键的问题是既然是长连就不会断开自动去重建…

  2. 我这里有两个问题请教下:
    1、Client多个请求共用一个连接,那也涉及到对这个连接的竞争问题(其实也就相当于是客户端使用了只包含1个连接的连接池?),不然会导致多线程发送消息时,服务端收到乱序的消息(一部分消息是Client线程A,另一部分是Client线程B的)

    2、Client发送消息给Server,Server收到消息并返回通讯级的确认报文才认为这笔消息已发送到Server(如果没有这一步而只靠TCP层面的ack应该不够吧),这样Client与Server的处理应该这样吧:
    (1)Client->Server#发送请求包
    (2)Server->Client#通讯级确认包
    (3)Server->Client#应答响应包
    (4)Client->Server#通讯级确认包
    如果是这样,那么对于Client使用连接应该要等到收到(2)返回后的通讯确认包,才能释放该连接给其他线程发送其他请求使用?

    1. 1. client多个请求共用一个连接,说明连接是复用的,所以自然不存在连接竞争问题,这是由于nio带来的好处,乱序是会的,但很容易处理,例如请求的长度参数;
      2. 请仔细看看nio吧…

发表评论

电子邮件地址不会被公开。 必填项已用*标注


*