1. host和port的设置
众所周知, 和一个网络服务建立连接的关键参数就是服务的地址和端口号, 与redis server建立连接也不例外.
在Jedis中, 使用HostAndPortUtil
来管理和redis server的host和port. 默认情况下可以使用localhost这个host和6379这个port
源码:
public final class HostAndPortUtil {
...
other code
...
static {
// host: localhost, port: 6379
redisHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_PORT));
redisHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_PORT + 1));
redisHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_PORT + 2));
redisHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_PORT + 3));
redisHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_PORT + 4));
redisHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_PORT + 5));
redisHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_PORT + 6));
// 哨兵模式下的地址和端口
sentinelHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT));
sentinelHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT + 1));
sentinelHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT + 2));
sentinelHostAndPortList.add(new HostAndPort("localhost", Protocol.DEFAULT_SENTINEL_PORT + 3));
// 集群模式下的地址和端口
clusterHostAndPortList.add(new HostAndPort("localhost", 7379));
clusterHostAndPortList.add(new HostAndPort("localhost", 7380));
clusterHostAndPortList.add(new HostAndPort("localhost", 7381));
clusterHostAndPortList.add(new HostAndPort("localhost", 7382));
clusterHostAndPortList.add(new HostAndPort("localhost", 7383));
clusterHostAndPortList.add(new HostAndPort("localhost", 7384));
}
...
other code
...
}
当然, 如果不想使用Jedis提供的这些默认的host和port, 可以通过HostAndPortUtil.parseHosts()
这个方法来设置自己的host和port
2. 建立连接
在设置完成redis server的host和port之后, 自然而然的就是和redis server建立连接.
在阅读连接的源代码之前, 先看一下和连接相关的UML图
可以看到
BinaryJedis
中含有
Client
的引用, 而
Client
又实现了
Connection
, 而实际上与redis server建立连接的是
Connection
, 所以只要创建
Jedis
对象, 然后使用
Client
与redis server建立连接就可以了.
接下来看下源码:
- 初始化
Jedis
对象
// 创建Jedis对象, 实际上创建Jedis对象的过程中最重要的是初始化Client对象
Jedis jedis = new Jedis(hnp.getHost(), hnp.getPort(), 500);
// Jedis构造函数中调用父类的构造函数, 在父类的构造函数中创建Client对象
public BinaryJedis(final String host, final int port, final int timeout) {
client = new Client(host, port);
client.setConnectionTimeout(timeout);
client.setSoTimeout(timeout);
}
// 同样初始化Client时会调用Connection的构造函数, 初始化Connection对象, 这是最关键的一步, 因为后面会使用Connection对象与redis server真正建立连接
public Connection(final String host, final int port) {
this.host = host;
this.port = port;
}
- 与redis server建立连接
前面提到与redis server建立连接实际上是由Connection
完成的, 因此把重点放到Connection
的连接方法上.
// 通过jedis对象调用connect()
jedis.connect();
// 经过一系列的内部调用最终会调用Connection的Connection方法
public void connect() {
if (!isConnected()) {
try {
// 创建Socket对象以及设置socket的一些参数
socket = new Socket();
// ->@wjw_add
socket.setReuseAddress(true);
socket.setKeepAlive(true);
socket.setTcpNoDelay(true);
socket.setSoLinger(true, 0);
socket.connect(new InetSocketAddress(host, port), connectionTimeout);
socket.setSoTimeout(soTimeout);
...
other code
...
// 这里是关键的地方, 初始化与redis通信的input stream和output stream, 之后的将参数发送给redis server和接受redis server的命令执行结果就是通过这两个输入输出流
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);
}
}
}
3. 与redis进行交互
与redis server建立连接之后, 就可以与redis server进行交互了, 也就是可以向redis server发送指令了. 与redis server进行交互实际上可以通过Connection
这个类进行控制
为了避免不必要的干扰, 这里采用较为简单的String的getSet
命令说明与redis server的交互, 其他的命令的交互也相识.
通过之前的UML图可以知道Client
还实现了Commands
接口, 这个接口中定义了redis中支持的所有的命令, 由此可知jedis对象所有的命令调用在内部都是通过client
对象进行调用的.
// Jedis.java
@Override
public String getSet(final String key, final String value) {
...
other code
...
// 调用client的getSet命令
client.getSet(key, value);
// 获取redis server的响应
return client.getBulkReply();
}
在client的getSet
方法中, 首先会先将命令进行编码, 也就是将字符串类型的命令转成字节形式(因为和redis server的交互是通过socket的, 也就是说是通过字节的形式在网络中进行传输的, 因此要先把命令转成字节形式)
// Client.java
@Override
public void getSet(final String key, final String value) {
// 对命令进行编码(转成字节形式)
getSet(SafeEncoder.encode(key), SafeEncoder.encode(value));
}
最后会调用Connection
的sendCommand()
方法发送命令给redis server
// Connection.java
// 真正向redis-server发送请求
public Connection sendCommand(final ProtocolCommand cmd, final byte[]... args) {
try {
// 这里会再次试着连接redis server, 也是调用之前的connect方法
connect();
// 通过Protocol发送命令, 可以看见这里使用的是之前创建的outputStream
Protocol.sendCommand(outputStream, cmd, args);
return this;
} catch (JedisConnectionException ex) {
...
错误处理
...
}
}
// Protocol.java
// 实际上就是和其他的网络通信一样通过流向socket中写入数据, 这样就把数据发送给redis server了
public static void sendCommand(final RedisOutputStream os, final ProtocolCommand command,
final byte[]... args) {
sendCommand(os, command.getRaw(), args);
}
private static void sendCommand(final RedisOutputStream os, final byte[] command,
final byte[]... args) {
try {
os.write(ASTERISK_BYTE);
os.writeIntCrLf(args.length + 1);
os.write(DOLLAR_BYTE);
os.writeIntCrLf(command.length);
os.write(command);
os.writeCrLf();
for (final byte[] arg : args) {
os.write(DOLLAR_BYTE);
os.writeIntCrLf(arg.length);
os.write(arg);
os.writeCrLf();
}
} catch (IOException e) {
throw new JedisConnectionException(e);
}
}
前面就是发送命令给redis server的流程, 但是细心的同学有没有发现, outputStream
没有flush
方法. 我们都知道像RedisOutputStream
(继承了FilterOutputStream
)这种高级输出流都是有缓存的, 所以如果没有调用flush
之类的方法数据是不会发送的, 所以这里只是把命令放到了输出流的缓存中, 但是并没有发送给远程的redis server. 那具体是在什么时候将数据发送到redis server呢, 肯定是在调用flush的时候, 那什么时候调用flush
方法呢, 让我们看一下从redis server获取命令执行结果的方法.
之前的Jedis.getSet()
方法中发送完命令之后, 紧接着就调用了client.getBulkReply()
方法, 不出意外, 这个方法是从Connection
中继承的一个方法.
// Connection.java
public String getBulkReply() {
final byte[] result = getBinaryBulkReply();
if (null != result) {
return SafeEncoder.encode(result);
} else {
return null;
}
}
public byte[] getBinaryBulkReply() {
// 就是在这里调用了flush()方法
flush();
// 读入redis server的响应结果
return (byte[]) readProtocolWithCheckingBroken();
}
protected void flush() {
try {
// 发送数据给redis server
outputStream.flush();
} catch (IOException ex) {
broken = true;
throw new JedisConnectionException(ex);
}
}
上面可以看到, Jedis实际上是在读取redis server返回的结果之前才将命令发送给redis server, 至于为什么要这么做, 我想可能是与redis的multi
和pipeline
有关.
最后再来看一下如何从redis server读取响应结果的. 从上面可以看见读取redis server的数据是调用了readProtocolWithCheckingBroken
方法
protected Object readProtocolWithCheckingBroken() {
try {
// 调用Protocol的read方法, 可以看见这里使用的是之前创建的inputStream, 也就是redis server会将响应结果打到inputStream, 然后Jedis从inputStream读取数据就可以了
return Protocol.read(inputStream);
} catch (JedisConnectionException exc) {
broken = true;
throw exc;
}
}
读取响应结果的过程就是如上所述, 但是读取的一些细节这里没有指明, 可以通过阅读Protocol.read()
和Protocol.process()
方法了解.
4. 总结
可以看见Jedis底层和redis server的交互还是通过基本的socket
连接实现的, 忽略实现的细节其实Jedis和redis server就是一个网络应答系统. 所以说看一个框架表面上功能有多强大有多灵活, 其实具体实现的原理都是一些基础的东西, 所以说基础这个东西还是很重要的, 越往底层研究越重要.