Module NIO của Java cung cấp ByteBuffer làm bộ chứa byte, nhưng lớp này khá phức tạp trong sử dụng, vì vậy Netty đã triển khai ByteBuf để thay thế lớp ByteBuffer của NIO. ByteBuf có các đặc điểm sau:
- Tùy chỉnh loại vùng đệm người dùng
- Thực hiện sao chép nông và sâu của vùng byte
- Dung lượng có thể tăng theo nhu cầu
- Không cần gọi flip() để chuyển đổi giữa chế độ đọc và ghi như trong ByteBuffer của JDK
- Sử dụng các chỉ số khác nhau cho đọc và ghi, tức là readIndex và writeIndex
- Hỗ trợ gọi chuỗi phương thức
- Hỗ trợ đếm tham chiếu và thao tác pool
Dưới đây chúng ta sẽ tìm hiểu sâu về ByteBuf, một trong những thành phần cốt lõi của Netty.
- Chỉ số đọc và ghi
ByteBuf có ba dấu hiệu chính là readIndex, writeIndex và capacity, lần lượt dùng để đánh dấu chỉ số đọc, chỉ số ghi và dung lượng bộ đệm. Cấu trúc dữ liệu cơ bản như hình dưới:
Khi xây dựng một ByteBuf mới không phải là Wrap hoặc Copy, readIndex = writeIndex = 0
Trong các thao tác đọc sau này, chủ yếu được chia thành hai loại: thao tác tương đối và thao tác tuyệt đối
- Thao tác tương đối: Khi đọc hoặc ghi, di chuyển readIndex hoặc writeIndex, chủ yếu là các phương thức read*() hoặc skip*() hoặc write*(), các thao tác này sẽ di chuyển hai chỉ số này khi được sử dụng.
- Thao tác tuyệt đối: Thao tác tuyệt đối chủ yếu là đọc ngẫu nhiên thông qua chỉ số (chỉ số byte, không phải readIndex và writeIndex), loại thao tác này không di chuyển readIndex hoặc writeIndex, ví dụ như getByte(int index), setByte(int index,int value), v.v.
Nếu readIndex > write hoặc write > capacity, sẽ ném ra IndexOutOfBoundException
- Loại bộ đệm
ByteBuf có ba loại triển khai chính:
- Bộ đệm heap
- Bộ đệm trực tiếp (non-heap)
- Bộ đệm composite
Bộ đệm heap được quản lý bởi JVM, do đó việc tạo và giải phóng rất thuận tiện, nhưng khi trao đổi dữ liệu, cần sao chép từ bộ nhớ JVM sang bộ nhớ trực tiếp của hệ điều hành, hiệu suất thấp hơn một chút. Tương tự, đối với bộ đệm trực tiếp, việc đọc và ghi dữ liệu rất thuận tiện, không cần sao chép dữ liệu thêm, nhưng JVM quản lý nó (tạo và giải phóng) không dễ dàng như bộ nhớ heap của JVM.
Vì vậy, tổng thể, đối với các tác vụ xử lý dữ liệu phía sau, nên sử dụng HeapByteBuf, đối với các thao tác tương tác luồng IO, nên sử dụng DirectByteBuf.
2.1 Bộ đệm heap
Bộ đệm heap là mô hình triển khai phổ biến nhất của ByteBuf, lưu trữ dữ liệu trong bộ nhớ heap của JVM, triển khai nội bộ là mảng array, vì vậy mô hình này còn được gọi là array-backed. Có thể xác định xem việc triển khai nội bộ có phải là array-backed hay không bằng cách sử dụng phương thức hasArray(), nếu có, chúng ta có thể sử dụng an toàn phương thức arrayOffset() để lấy độ lệch kết hợp với readableBytes() để lấy triển khai底层, tức là lấy byte[], ví dụ:
Charset charset = Charset.forName("utf-8");
ByteBuf stringByteBuf = Unpooled.copiedBuffer("hello world", charset);
if (stringByteBuf.hasArray()) {
byte[] array = stringByteBuf.array();
// như vậy, arrayOffset === 0
int startIndex = stringByteBuf.arrayOffset() + stringByteBuf.readerIndex();
// return value = writeIndex - readIndex
int length = stringByteBuf.readableBytes();
byte[] newBytes = Arrays.copyOfRange(array, startIndex, length);
System.out.println("Chuyển đổi array-backed:" + new String(newBytes,charset));
}
Nếu không phải là triển khai array-backed, việc gọi phương thức
arrayOffset()sẽ ném ra UnsupportOperationException.
2.2 Bộ đệm trực tiếp
Bộ đệm trực tiếp là một mô hình triển khai khác của ByteBuf, nhưng việc phân bổ bộ nhớ được thực hiện bởi hệ điều hành, và bộ nhớ không nằm trên bộ nhớ heap. Tài liệu JavaDoc chỉ ra rằng
Nội dung của bộ đệm trực tiếp sẽ tồn tại ngoài bộ nhớ heap thông thường bị thu gom rác
Điều này giải thích tại sao bộ đệm trực tiếp là phương thức lý tưởng nhất cho truyền dữ liệu mạng, nhưng so với bộ đệm heap, việc phân bổ và giải phóng bộ đệm trực tiếp khá tốn kém. Nếu cần thao tác byte dữ liệu trên bộ đệm trực tiếp, trước tiên bạn cần thực hiện thao tác sao chép dữ liệu, đoạn mã dưới đây dựa trên thao tác đọc bộ đệm trực tiếp:
Charset charset = Charset.forName("utf-8");
ByteBuf stringByteBuf = Unpooled.copiedBuffer("hello world", charset);
if(!stringByteBuf.hasArray()){
int length = stringByteBuf.readableBytes();
byte[] bytes = new byte[length];
stringByteBuf.getBytes(stringByteBuf.readerIndex(),bytes);
System.out.println(new String(bytes,charset));
}
2.3 Bộ đệm composite
Bộ đệm composite khá phức tạp, chủ yếu được triển khai bởi CompositeByteBuf, sẽ có một file riêng để tìm hiểu triển khai này sau.
- Thao tác đọc và ghi byte
ByteBuf là một lớp trừu tượng, không thể sử dụng từ khóa new để tạo, chúng ta có thể sử dụng Unpooled để tạo, như sau:
// Tạo ByteBuf, đặt dung lượng là 10
ByteBuf byteBuf = Unpooled.buffer(10);
3.1 Thao tác ghi
ByteBuf cung cấp nhiều phương thức writeByte() được nạp chồng, chúng ta có thể sử dụng nó để ghi dữ liệu. Việc sử dụng phương thức writeByte() để ghi dữ liệu sẽ tự động di chuyển writeIndex, như sau:
ByteBuf buffer = Unpooled.buffer(10);
System.out.println("Trước khi ghi, writeIndex của ByteBuf là " + buffer.writerIndex());
for (int i = 0; i < buffer.capacity(); i++) {
buffer.writeByte(i);
}
System.out.println("Sau khi ghi, writeIndex của ByteBuf là " + buffer.writerIndex());
Đồng thời, ByteBuf cũng cung cấp phương thức set(int index,int value) để ghi theo chỉ số, cung cấp chỉ số và dữ liệu, ghi theo chỉ số, như sau:
ByteBuf buffer = Unpooled.buffer(10);
System.out.println("Trước khi ghi, writeIndex của ByteBuf là " + buffer.writerIndex());
for (int i = 0; i < buffer.capacity(); i++) {
buffer.setByte(i,i);
}
System.out.println("Sau khi ghi, writeIndex của ByteBuf là " + buffer.writerIndex());
Khi ghi, nếu writeIndex > capacity, sẽ mở rộng theo cấp số mũ
3.2 Thao tác đọc
Tương tự, thao tác đọc cũng có hai cách: một là đọc theo thứ tự, một là đọc theo chỉ số. Đọc theo thứ tự thông qua readByte(), readByte đọc một byte mỗi lần, đồng thời readIndex sẽ tự động di chuyển, đọc theo chỉ số, thì không di chuyển readIndex.
3.2.1 Đọc theo thứ tự
ByteBuf có thể đọc qua phương thức readByte(), nếu byte có thể đọc cạn kiệt, sẽ ném ra ngoại lệ IndexOutOfBoundException, vì vậy khi mỗi lần readByte() cần判断 xem có thể đọc được không, đoạn mã ví dụ như sau:
ByteBuf buffer = Unpooled.buffer(10);
// Ghi dữ liệu
for (int i = 0; i < buffer.capacity(); i++) {
buffer.setByte(i,i);
}
// Lặp lại, tiếp tục đọc nếu có dữ liệu
while (buffer.isReadable()) {
System.out.print(buffer.readByte());
}
3.2.2 Đọc theo chỉ số
Sử dụng getByte(int index) có thể lấy dữ liệu, nhưng readIndex không di chuyển, nhưng yêu cầu readIndex nhỏ hơn writeIndex.
ByteBuf buffer = Unpooled.buffer(10);
for (int i = 0; i < buffer.capacity(); i++) {
buffer.setByte(i,i);
}
for (int i = 0; i < buffer.capacity(); i++) {
System.out.print(buffer.getByte(i));
}
3.3 Thao tác tìm kiếm
Phương thức tìm kiếm đơn giản nhất của ByteBuf là phương thức indexOf(), khi indexOf() không tồn tại, chỉ số trả về là -1.
ByteBuf buffer = Unpooled.buffer(6);
for (int i = 0; i < buffer.capacity(); i++) {
buffer.setByte(i,i);
}
int index = buffer.indexOf(0,buffer.capacity(),Byte.valueOf("5"));
System.out.println("Vị trí chỉ số tìm thấy = " + index);
index = buffer.indexOf(0,buffer.capacity(),Byte.valueOf("9"));
System.out.println("Vị trí chỉ số tìm thấy = " + index);
3.4 Quản lý chỉ số
ByteBuf cung cấp các phương thức mark() và reset() để đánh dấu và đặt lại readIndex và writeIndex, cụ thể là:
- markReadIndex()
- markWriteIndex()
- resetReadIndex()
- resetWriteIndex()
Để đặt lại readIndex và writeIndex, có thể sử dụng phương thức clean(), phương thức clean() được triển khai là readIndex = writeIndex = 0, nhưng không xóa nội dung, đoạn mã kiểm tra như sau:
// Yêu cầu không gian
ByteBuf byteBuf = Unpooled.buffer(10);
// Ghi 7 dữ liệu
for (int i = 0; i < 7; i++) {
byteBuf.writeByte(i);
}
// Xuất dữ liệu
while (byteBuf.isReadable()) {
System.out.print(byteBuf.readByte());
System.out.print(",");
}
System.out.println();
// In vị trí chỉ số hiện tại
int writeIndex = byteBuf.writerIndex();
// Đặt lại chỉ số
byteBuf.clear();
// Ghi 2 dữ liệu
byteBuf.writeByte(10).writeByte(10);
// Đặt chỉ số writeIndex về {writeIndex}
byteBuf.writerIndex(writeIndex);
// Xuất dữ liệu, có thể thấy 0 và 1 bị ghi đè, các dữ liệu khác vẫn giữ nguyên
while (byteBuf.isReadable()) {
System.out.print(byteBuf.readByte());
System.out.print(",");
}
3.5 Thao tác sao chép
Trong một số trường hợp, chúng ta cần sao chép một ByteBuf, ByteBuf cung cấp hai cơ chế sao chép: sao chép nông và sao chép sâu.
- Sao chép nông thường sử dụng phương thức duplicate(), phương thức này trả về một instance ByteBuf mới, nó có readIndex, writeIndex và chỉ số đánh dấu của riêng mình. Nhưng nội dung lưu trữ được chia sẻ với đối tượng nguồn, vì vậy nếu sửa đổi instance sao chép, instance gốc cũng sẽ bị sửa đổi, cần chú ý.
- Để tạo instance bản sao hoàn toàn độc lập, có thể sử dụng phương thức copy() hoặc copy(int,int).