Jedis
在很多教材或者教程上,通常都是很简单的一个例子来演示如何使用Java进行TCP通讯.在这款广泛被使用的开源组件中,我们能够更好的学习到一个企业级的组件在TCP连接的处理上,更应该关注哪些方面.有哪些是我们应该掌握或者了解的TCP知识.TCP协议本身相当复杂,我们做应用的可以先从应用层需要用到的相关知识开始了解.
jedis中,与redis服务端建立连接的代码在Connection这个类中.
public void connect() {
if (!isConnected()) {
try {
socket = new Socket();
// ->@wjw_add
socket.setReuseAddress(true);
socket.setKeepAlive(true); // Will monitor the TCP connection is
// valid
socket.setTcpNoDelay(true); // Socket buffer Whetherclosed, to
// ensure timely delivery of data
socket.setSoLinger(true, 0); // Control calls close () method,
// the underlying socket is closed
// immediately
// <-@wjw_add
socket.connect(new InetSocketAddress(host, port), connectionTimeout);
socket.setSoTimeout(soTimeout);
if (ssl) {
if (null == sslSocketFactory) {
sslSocketFactory = (SSLSocketFactory)SSLSocketFactory.getDefault();
}
socket = (SSLSocket) sslSocketFactory.createSocket(socket, host, port, true);
if (null != sslParameters) {
((SSLSocket) socket).setSSLParameters(sslParameters);
}
if ((null != hostnameVerifier) &&
(!hostnameVerifier.verify(host, ((SSLSocket) socket).getSession()))) {
String message = String.format(
"The connection to '%s' failed ssl/tls hostname verification.", host);
throw new JedisConnectionException(message);
}
}
outputStream = new RedisOutputStream(socket.getOutputStream());
inputStream = new RedisInputStream(socket.getInputStream());
} catch (IOException ex) {
broken = true;
throw new JedisConnectionException("Failed connecting to host "
+ host + ":" + port, ex);
}
}
}
public boolean isConnected() {
return socket != null && socket.isBound() && !socket.isClosed() && socket.isConnected()
&& !socket.isInputShutdown() && !socket.isOutputShutdown();
}
通过以上代码,可以看到用到了jdk中相关的Socket对象来与redis服务进行tcp连接.重点看几个方法
setReuseAddress(true)
setKeepAlive(true)
setTcpNoDelay(true)
setSoLinger(true, 0)
setSoTimeout(soTimeout)
Socket
setReuseAddress
要解释这个方法的作用,首先要了解TCP套接字的一个状态-time-wait.
众所周知,TCP的连接建立和断开要经历三次握手和四次挥手.
如图所示,
而time-wait这个状态就是连接发起方,通常就是client,在收到服务器端的FIN包后进入的状态.那为什么这个状态需要存在呢?客户端收到服务端发送的FIN包后,会向服务端发送一个ACK包,服务端接收到ACK包后进入CLOSED状态.但是由于网络或者其他问题,导致服务端有可能没有收到ACK包,导致服务器端重发FIN包.
time-wait这个需要保持多久呢?大概是2MSL(最大分段生存期).
MSL(最大分段生存期)指明TCP报文在Internet上最长生存时间,每个具体的TCP实现都必须选择一个确定的MSL值。RFC 1122建议是2分钟,但BSD传统实现采用了30秒。
了解了这些之后,我们可以看看这个方法的内部实现
public void setReuseAddress(boolean on) throws SocketException {
if (isClosed())
throw new SocketException("Socket is closed");
getImpl().setOption(SocketOptions.SO_REUSEADDR, Boolean.valueOf(on));
}
我们看到,它是对SO_REUSEADDR这个套接字选项进行了设置.
这个套接字选项通知内核,如果端口忙,但TCP状态位于 TIME_WAIT ,可以重用端口。如果端口忙,而TCP状态位于其他状态,重用端口时依旧得到一个错误信息,指明"地址已经使用中"。如果你的服务程序停止后想立即重启,而新套接字依旧使用同一端口,此时 SO_REUSEADDR 选项非常有用。
对应linux操作系统响应的配置为
net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭
net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
setKeepAlive
心跳检测是我们用来判断后端程序是否依旧正常服务的一个常用手段.TCP协议提供keepalive机制来监测TCP连接是否已经断开.
net.ipv4.tcp_keepalive_intvl = 20 重发keepalive包间隔
net.ipv4.tcp_keepalive_probes = 3 重发次数
net.ipv4.tcp_keepalive_time = 60 超过一段时间没有进行数据传输则进行心跳监测
setTcpNoDelay
要了解这个方法的作用,首先要知道TCP协议中一个注明的算法-Nagle算法.Nagle算法的初衷是避免小包拥塞网络.比如只需要发送一个字节的数据,可是由于TCP包本身就占用了几十个字节,这样是比较浪费网络资源的.Nagle算法导致了要么是等待ACK到达或者缓冲区满,才会发送新的数据.
再介绍一下TCP-Delayed-ACK ,它是将ACK和响应数据绑在一个包中发送,降低协议开销.
如果客户端采用了Nagle算法和服务端采用了Delayed-ACK,同时客户端采用write-write-read模式,就很容易造成客户端与服务端都在等待对方数据的问题.
Delayed Ack 是有个超时机制的,而默认的超时正好就是40ms.
这里有一篇文章很好的介绍了write-write-read这种问题
setTcpNoDelay其实就是讲Nagle算法关闭,使得每次无论数据包有多大都会被立即发送.
setSoLinger
SO_LINGER选项用来控制Socket关闭时的行为,默认情况下,执行Socket的close方法,该方法会立即返回,但底层的Socket实际上并不会立即关闭,他会立即延迟一段时间,知道发送完剩余的数据,才会真正的关闭Socket,断开连接。
setSoLinger(true, 0)
执行该方法,那么执行Socket的close方法,该方法也会立即返回,但底层的Socket也会立即关闭,所有未发送完的剩余数据被丢弃.
setSoTimeOut
官方文档中解释
启用/禁用带有指定超时值的 SO_TIMEOUT,以毫秒为单位。
将此选项设为非零的超时值时,在与此 Socket 关联的 InputStream 上调用 read() 将只阻塞此时间长度。如果超过超时值,将引发java.net.SocketTimeoutException,虽然 Socket
仍旧有效。选项必须在进入阻塞操作前被启用才能生效。超时值必须是 > 0 的数。超时值为 0 被解释为无穷大超时值。
参数:
timeout - 指定的以毫秒为单位的超时值。
抛出:
SocketException -
如果底层协议出现错误,例如 TCP 错误。