Java API方式调用Kafka各种协议
众所周知,Kafka自己实现了一套二进制协议(binary protocol)用于各种功能的实现,比如发送消息,获取消息,提交位移以及创建topic等。具体协议规范参见:Kafka协议 这套协议的具体使用流程为:
- 客户端创建对应协议的请求
- 客户端发送请求给对应的broker
- broker处理请求,并发送response给客户端
虽然Kafka提供的大量的脚本工具用于各种功能的实现,但很多时候我们还是希望可以把某些功能以编程的方式嵌入到另一个系统中。这时使用Java API的方式就显得异常地灵活了。本文我将尝试给出Java API底层框架的一个范例,同时也会针对“创建topic”和“查看位移”这两个主要功能给出对应的例子。 需要提前说明的是,本文给出的范例并没有考虑Kafka集群开启安全的情况。另外Kafka的KIP4应该一直在优化命令行工具以及各种管理操作,有兴趣的读者可以关注这个KIP。
本文中用到的API依赖于kafka-clients,所以如果你使用Maven构建的话,请加上:
<dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka-clients</artifactId> <version>0.10.2.0</version> </dependency>
如果是gradle,请加上:
compile group: 'org.apache.kafka', name: 'kafka-clients', version: '0.10.2.0'
底层框架
1 /** 2 * 发送请求主方法 3 * @param host 目标broker的主机名 4 * @param port 目标broker的端口 5 * @param request 请求对象 6 * @param apiKey 请求类型 7 * @return 序列化后的response 8 * @throws IOException 9 */ 10 public ByteBuffer send(String host, int port, AbstractRequest request, ApiKeys apiKey) throws IOException { 11 Socket socket = connect(host, port); 12 try { 13 return send(request, apiKey, socket); 14 } finally { 15 socket.close(); 16 } 17 } 18 19 /** 20 * 发送序列化请求并等待response返回 21 * @param socket 连向目标broker的socket 22 * @param request 序列化后的请求 23 * @return 序列化后的response 24 * @throws IOException 25 */ 26 private byte[] issueRequestAndWaitForResponse(Socket socket, byte[] request) throws IOException { 27 sendRequest(socket, request); 28 return getResponse(socket); 29 } 30 31 /** 32 * 发送序列化请求给socket 33 * @param socket 连向目标broker的socket 34 * @param request 序列化后的请求 35 * @throws IOException 36 */ 37 private void sendRequest(Socket socket, byte[] request) throws IOException { 38 DataOutputStream dos = new DataOutputStream(socket.getOutputStream()); 39 dos.writeInt(request.length); 40 dos.write(request); 41 dos.flush(); 42 } 43 44 /** 45 * 从给定socket处获取response 46 * @param socket 连向目标broker的socket 47 * @return 获取到的序列化后的response 48 * @throws IOException 49 */ 50 private byte[] getResponse(Socket socket) throws IOException { 51 DataInputStream dis = null; 52 try { 53 dis = new DataInputStream(socket.getInputStream()); 54 byte[] response = new byte[dis.readInt()]; 55 dis.readFully(response); 56 return response; 57 } finally { 58 if (dis != null) { 59 dis.close(); 60 } 61 } 62 } 63 64 /** 65 * 创建Socket连接 66 * @param hostName 目标broker主机名 67 * @param port 目标broker服务端口, 比如9092 68 * @return 创建的Socket连接 69 * @throws IOException 70 */ 71 private Socket connect(String hostName, int port) throws IOException { 72 return new Socket(hostName, port); 73 } 74 75 /** 76 * 向给定socket发送请求 77 * @param request 请求对象 78 * @param apiKey 请求类型, 即属于哪种请求 79 * @param socket 连向目标broker的socket 80 * @return 序列化后的response 81 * @throws IOException 82 */ 83 private ByteBuffer send(AbstractRequest request, ApiKeys apiKey, Socket socket) throws IOException { 84 RequestHeader header = new RequestHeader(apiKey.id, request.version(), "client-id", 0); 85 ByteBuffer buffer = ByteBuffer.allocate(header.sizeOf() + request.sizeOf()); 86 header.writeTo(buffer); 87 request.writeTo(buffer); 88 byte[] serializedRequest = buffer.array(); 89 byte[] response = issueRequestAndWaitForResponse(socket, serializedRequest); 90 ByteBuffer responseBuffer = ByteBuffer.wrap(response); 91 ResponseHeader.parse(responseBuffer); 92 return responseBuffer; 93 }
有了这些方法的铺垫,我们就可以创建具体的请求了。
创建topic
1 /** 2 * 创建topic 3 * 由于只是样例代码,有些东西就硬编码写到程序里面了(比如主机名和端口),各位看官自行修改即可 4 * @param topicName topic名 5 * @param partitions 分区数 6 * @param replicationFactor 副本数 7 * @throws IOException 8 */ 9 public void createTopics(String topicName, int partitions, short replicationFactor) throws IOException { 10 Map<String, CreateTopicsRequest.TopicDetails> topics = new HashMap<>(); 11 // 插入多个元素便可同时创建多个topic 12 topics.put(topicName, new CreateTopicsRequest.TopicDetails(partitions, replicationFactor)); 13 int creationTimeoutMs = 60000; 14 CreateTopicsRequest request = new CreateTopicsRequest.Builder(topics, creationTimeoutMs).build(); 15 ByteBuffer response = send("localhost", 9092, request, ApiKeys.CREATE_TOPICS); 16 CreateTopicsResponse.parse(response, request.version()); 17 }
查看位移
1 /** 2 * 获取某个consumer group下的某个topic分区的位移 3 * @param groupID group id 4 * @param topic topic名 5 * @param parititon 分区号 6 * @throws IOException 7 */ 8 public void getOffsetForPartition(String groupID, String topic, int parititon) throws IOException { 9 TopicPartition tp = new TopicPartition(topic, parititon); 10 OffsetFetchRequest request = new OffsetFetchRequest.Builder(groupID, singletonList(tp)) 11 .setVersion((short)2).build(); 12 ByteBuffer response = send("localhost", 9092, request, ApiKeys.OFFSET_FETCH); 13 OffsetFetchResponse resp = OffsetFetchResponse.parse(response, request.version()); 14 OffsetFetchResponse.PartitionData partitionData = resp.responseData().get(tp); 15 System.out.println(partitionData.offset); 16 }
1 /** 2 * 获取某个consumer group下所有topic分区的位移信息 3 * @param groupID group id 4 * @return (topic分区 --> 分区信息)的map 5 * @throws IOException 6 */ 7 public Map<TopicPartition, OffsetFetchResponse.PartitionData> getAllOffsetsForGroup(String groupID) throws IOException { 8 OffsetFetchRequest request = new OffsetFetchRequest.Builder(groupID, null).setVersion((short)2).build(); 9 ByteBuffer response = send("localhost", 9092, request, ApiKeys.OFFSET_FETCH); 10 OffsetFetchResponse resp = OffsetFetchResponse.parse(response, request.version()); 11 return resp.responseData(); 12 }
okay, 上面就是“创建topic”和“查看位移”的样例代码,各位看官可以参考着这两个例子构建其他类型的请求。