Thứ năm, 30/04/2020 | 00:00 GMT+7

Sử dụng bộ đệm trong Node.js

Bộ đệm là một không gian trong bộ nhớ (thường là RAM) để lưu trữ dữ liệu binary . Trong Node.js , ta có thể truy cập những không gian này của bộ nhớ bằng lớp Buffer . Cache lưu trữ một chuỗi các số nguyên, tương tự như một mảng trong JavaScript . Không giống như mảng, bạn không thể thay đổi kích thước của cache sau khi nó được tạo.

Bạn có thể đã ngầm sử dụng cache nếu bạn đã viết mã Node.js. Ví dụ: khi bạn đọc từ một file có fs.readFile() , dữ liệu được trả về lệnh gọi lại hoặc Promise là một đối tượng đệm. Ngoài ra, khi các yêu cầu HTTP được thực hiện trong Node.js, chúng sẽ trả về các stream dữ liệu được lưu trữ tạm thời trong cache nội bộ khi client không thể xử lý stream cùng một lúc.

Cache hữu ích khi bạn đang tương tác với dữ liệu binary , thường là ở cấp độ mạng thấp hơn. Họ cũng trang bị cho bạn khả năng thao tác dữ liệu chi tiết trong Node.js.

Trong hướng dẫn này, bạn sẽ sử dụng Node.js REPL để chạy qua các ví dụ khác nhau về cache , chẳng hạn như tạo cache , đọc từ cache , ghi vào và sao chép từ cache và sử dụng cache để chuyển đổi giữa dữ liệu binary và mã hóa. Đến cuối hướng dẫn, bạn sẽ học cách sử dụng lớp Buffer để làm việc với dữ liệu binary .

Yêu cầu

Bước 1 - Tạo vùng đệm

Bước đầu tiên này sẽ chỉ cho bạn hai cách chính để tạo một đối tượng đệm trong Node.js.

Để quyết định sử dụng phương pháp nào, bạn cần trả lời câu hỏi này: Bạn muốn tạo một cache mới hay extract cache từ dữ liệu hiện có? Nếu bạn định lưu trữ dữ liệu trong bộ nhớ mà bạn chưa nhận được, bạn cần tạo một cache mới. Trong Node.js, ta sử dụng hàm phân bổ alloc() của lớp Buffer để thực hiện việc này.

Hãy mở Node.js REPL để tự mình xem. Trong terminal của bạn, nhập lệnh node :

  • node

Bạn sẽ thấy dấu nhắc bắt đầu bằng > .

Hàm alloc() lấy kích thước của vùng đệm làm đối số bắt buộc đầu tiên và duy nhất của nó. Kích thước là một số nguyên đại diện cho bao nhiêu byte bộ nhớ mà đối tượng đệm sẽ sử dụng. Ví dụ: nếu ta muốn tạo cache lớn 1KB (kilobyte), tương đương với 1024 byte, ta sẽ nhập điều này vào console :

  • const firstBuf = Buffer.alloc(1024);

Để tạo một cache mới, ta đã sử dụng lớp Buffer có sẵn trên phạm vi global , có phương thức phân bổ alloc() . Bằng cách cung cấp 1024 làm đối số cho alloc() , ta đã tạo một cache lớn 1KB.

Theo mặc định, khi bạn khởi tạo một cache bằng phân bổ alloc() , cache sẽ được lấp đầy bằng các số 0 binary làm trình giữ chỗ cho dữ liệu sau này. Tuy nhiên, ta có thể thay đổi giá trị mặc định nếu ta muốn. Nếu ta muốn tạo một vùng đệm mới với 1 s thay vì 0 s, ta sẽ đặt tham số thứ hai của hàm alloc() fill .

Trong terminal của bạn, hãy tạo một cache mới tại dấu nhắc REPL chứa đầy 1 s:

  • const filledBuf = Buffer.alloc(1024, 1);

Ta vừa tạo một đối tượng đệm mới tham chiếu đến một không gian trong bộ nhớ lưu trữ 1KB của 1 s. Mặc dù ta đã nhập một số nguyên, tất cả dữ liệu được lưu trữ trong cache là dữ liệu binary .

Dữ liệu binary có thể có nhiều định dạng khác nhau. Ví dụ, ta hãy xem xét một chuỗi binary biểu diễn một byte dữ liệu: 01110110 . Nếu chuỗi binary này đại diện cho một chuỗi bằng tiếng Anh sử dụng tiêu chuẩn mã hóa ASCII , thì nó sẽ là chữ v . Tuy nhiên, nếu máy tính của ta đang xử lý một hình ảnh, chuỗi binary đó có thể chứa thông tin về màu sắc của pixel.

Máy tính biết xử lý chúng theo cách khác nhau vì các byte được mã hóa khác nhau. Mã hóa byte là định dạng của byte. Cache trong Node.js sử dụng schemas mã hóa UTF-8 theo mặc định nếu nó được khởi tạo bằng dữ liệu chuỗi. Một byte trong UTF-8 đại diện cho một số, một chữ cái (bằng tiếng Anh và các ngôn ngữ khác) hoặc một ký hiệu. UTF-8 là một tập hợp siêu của ASCII , Mã tiêu chuẩn USA về trao đổi thông tin. ASCII có thể mã hóa các byte bằng các chữ cái tiếng Anh viết hoa và viết thường, các số 0-9 và một số ký hiệu khác như dấu chấm than ( ! ) Hoặc dấu và ( & ).

Nếu ta đang viết một chương trình chỉ có thể hoạt động với các ký tự ASCII, ta có thể thay đổi mã hóa được sử dụng bởi cache của ta bằng encoding đối số thứ ba của hàm phân bổ alloc() .

Hãy tạo một cache mới dài năm byte và chỉ lưu trữ các ký tự ASCII:

  • const asciiBuf = Buffer.alloc(5, 'a', 'ascii');

Cache được khởi tạo với năm byte ký tự a , sử dụng biểu diễn ASCII.

Lưu ý : Theo mặc định, Node.js hỗ trợ các mã hóa ký tự sau:

  • ASCII , đại diện là ascii
  • UTF-8 , được đại diện là utf-8 hoặc utf8
  • UTF-16 , được đại diện là utf-16le hoặc utf16le
  • UCS-2 , được đại diện là ucs-2 hoặc ucs2
  • Base64 , được biểu diễn dưới dạng base64
  • Hệ thập lục phân , được biểu diễn dưới dạng hex
  • ISO / IEC 8859-1 , được biểu thị dưới dạng latin1 hoặc binary

Tất cả các giá trị này được dùng trong các hàm lớp đệm chấp nhận một tham số encoding . Do đó, tất cả các giá trị này đều hợp lệ cho phương thức alloc() .

Lúc này, ta đã tạo ra các cache mới với hàm phân bổ alloc() . Nhưng đôi khi ta có thể cần tạo cache từ dữ liệu đã tồn tại, như một chuỗi hoặc mảng.

Để tạo cache từ dữ liệu đã có trước, ta sử dụng phương thức from() . Ta có thể sử dụng hàm đó để tạo vùng đệm từ:

  • Một mảng các số nguyên: Các giá trị số nguyên có thể nằm trong repository ảng từ 0 đến 255 .
  • Một ArrayBuffer : Đây là một đối tượng JavaScript lưu trữ độ dài cố định của byte.
  • Một chuỗi.
  • Một cache khác.
  • Các đối tượng JavaScript khác có Symbol.toPrimitive tính Symbol.toPrimitive . Thuộc tính đó cho JavaScript biết cách chuyển đổi đối tượng sang kiểu dữ liệu nguyên thủy: boolean , null , undefined , number , string hoặc symbol . Bạn có thể đọc thêm về Biểu tượng tại tài liệu JavaScript của Mozilla.

Hãy xem cách ta có thể tạo cache từ một chuỗi. Trong dấu nhắc Node.js, hãy nhập:

  • const stringBuf = Buffer.from('My name is Paul');

Bây giờ ta có một đối tượng đệm được tạo từ chuỗi My name is Paul . Hãy tạo một cache mới từ một cache khác mà ta đã tạo trước đó:

  • const asciiCopy = Buffer.from(asciiBuf);

Bây giờ ta đã tạo một cache asciiCopy mới chứa cùng dữ liệu với asciiBuf .

Bây giờ ta đã có kinh nghiệm tạo cache , ta có thể đi sâu vào các ví dụ về việc đọc dữ liệu của chúng.

Bước 2 - Đọc từ Buffer

Có nhiều cách để truy cập dữ liệu trong Buffer. Ta có thể truy cập một byte riêng lẻ trong cache hoặc ta có thể extract toàn bộ nội dung.

Để truy cập một byte của cache , ta chuyển index hoặc vị trí của byte mà ta muốn. Cache lưu trữ dữ liệu tuần tự như mảng. Họ cũng lập index dữ liệu của họ như mảng, bắt đầu từ 0 . Ta có thể sử dụng ký hiệu mảng trên đối tượng đệm để lấy một byte riêng lẻ.

Hãy xem điều này trông như thế nào bằng cách tạo cache từ một chuỗi trong REPL:

  • const hiBuf = Buffer.from('Hi!');

Bây giờ ta hãy đọc byte đầu tiên của cache :

  • hiBuf[0];

Khi bạn nhấn ENTER , REPL sẽ hiển thị:

Output
72

Số nguyên 72 tương ứng với đại diện UTF-8 cho chữ H

Lưu ý : Giá trị của byte có thể là số từ 0 đến 255 . Một byte là một chuỗi 8 bit. Một bit là binary và do đó chỉ có thể có một trong hai giá trị: 0 hoặc 1 . Nếu ta có một chuỗi 8 bit và hai giá trị có thể có trên mỗi bit, thì ta có tối đa 2⁸ giá trị có thể cho một byte. Điều đó hoạt động với tối đa 256 giá trị. Vì ta bắt đầu đếm từ số 0, điều đó nghĩa là số cao nhất của ta là 255.

Hãy làm tương tự cho byte thứ hai. Nhập nội dung sau vào REPL:

  • hiBuf[1];

REPL trả về 105 , đại diện cho chữ i viết thường.

Cuối cùng, hãy lấy ký tự thứ ba:

  • hiBuf[2];

Bạn sẽ thấy 33 hiển thị trong REPL, tương ứng với ! .

Hãy thử lấy một byte từ một index không hợp lệ:

  • hiBuf[3];

REPL sẽ trả về:

Output
undefined

Điều này giống như nếu ta cố gắng truy cập một phần tử trong mảng có index không chính xác.

Bây giờ ta đã thấy cách đọc từng byte riêng lẻ của cache , hãy xem các tùy chọn của ta để truy xuất tất cả dữ liệu được lưu trữ trong cache cùng một lúc. Đối tượng đệm đi kèm với các phương thức toString()toJSON() , trả về toàn bộ nội dung của cache ở hai định dạng khác nhau.

Đúng như tên gọi của nó, phương thức toString() chuyển đổi các byte của cache thành một chuỗi và trả về cho user . Nếu ta sử dụng phương thức này trên hiBuf , ta sẽ nhận được chuỗi Hi! . Hãy thử nó!

Trong dấu nhắc , hãy nhập:

  • hiBuf.toString();

REPL sẽ trả về:

Output
'Hi!'

Cache đó được tạo từ một chuỗi. Hãy xem điều gì sẽ xảy ra nếu ta sử dụng toString() trên cache không được tạo từ dữ liệu chuỗi.

Hãy tạo một cache trống mới, lớn 10 byte:

  • const tenZeroes = Buffer.alloc(10);

Bây giờ, hãy sử dụng phương thức toString() :

  • tenZeroes.toString();

Ta sẽ thấy kết quả sau:

'\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000' 

Chuỗi \u0000 là ký tự Unicode cho NULL . Nó tương ứng với số 0 . Khi dữ liệu của cache không được mã hóa dưới dạng chuỗi, phương thức toString() trả về mã hóa UTF-8 của các byte.

toString() có một tham số tùy chọn, encoding . Ta có thể sử dụng tham số này để thay đổi mã hóa của dữ liệu đệm được trả về.

Ví dụ: nếu bạn muốn mã hóa thập lục phân cho hiBuf bạn sẽ nhập thông tin sau tại dấu nhắc:

  • hiBuf.toString('hex');

Tuyên bố đó sẽ đánh giá:

Output
'486921'

486921 là đại diện thập lục phân cho các byte đại diện cho chuỗi Hi! . Trong Node.js, khi user muốn chuyển đổi mã hóa dữ liệu từ dạng này sang dạng khác, họ thường đặt chuỗi vào cache và gọi toString() với mã hóa mong muốn của họ.

Phương thức toJSON() hoạt động khác nhau. Dù cache có được tạo từ một chuỗi hay không, nó luôn trả về dữ liệu dưới dạng biểu diễn số nguyên của byte.

Hãy tái sử dụng hiBuftenZeroes đệm để thực hành sử dụng toJSON() . Tại dấu nhắc , hãy nhập:

  • hiBuf.toJSON();

REPL sẽ trả về:

Output
{ type: 'Buffer', data: [ 72, 105, 33 ] }

Đối tượng JSON có type tính type sẽ luôn là Buffer . Vì vậy, các chương trình có thể phân biệt đối tượng JSON này với các đối tượng JSON khác.

Thuộc tính data chứa một mảng biểu diễn số nguyên của các byte. Bạn có thể nhận thấy rằng 72 , 10533 tương ứng với các giá trị ta nhận được khi ta kéo từng byte một.

Hãy thử phương thức toJSON() với tenZeroes :

  • tenZeroes.toJSON();

Trong REPL, bạn sẽ thấy những điều sau:

Output
{ type: 'Buffer', data: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] }

type giống như đã lưu ý trước đó. Tuy nhiên, dữ liệu bây giờ là một mảng có mười số 0.

Bây giờ ta đã trình bày các cách chính để đọc từ cache , hãy xem cách ta sửa đổi nội dung của cache .

Bước 3 - Sửa đổi cache

Có nhiều cách để ta có thể sửa đổi một đối tượng đệm hiện có. Tương tự như đọc, ta có thể sửa đổi từng byte đệm bằng cách sử dụng cú pháp mảng. Ta cũng có thể ghi nội dung mới vào cache , thay thế dữ liệu hiện có.

Hãy bắt đầu bằng cách xem xét cách ta có thể thay đổi từng byte riêng lẻ của cache . Nhớ lại biến cache hiBuf của ta , chứa chuỗi Hi! . Hãy thay đổi từng byte để nó chứa Hey thay thế.

Trong REPL, trước tiên hãy thử đặt phần tử thứ hai của hiBuf thành e :

  • hiBuf[1] = 'e';

Bây giờ, hãy xem vùng đệm này như một chuỗi để xác nhận nó đang lưu trữ đúng dữ liệu. Theo dõi bằng cách gọi phương thức toString() :

  • hiBuf.toString();

Nó sẽ được đánh giá là:

Output
'H\u0000!'

Ta nhận được kết quả kỳ lạ đó vì cache chỉ có thể chấp nhận một giá trị số nguyên. Ta không thể gán nó cho chữ e ; thay vào đó, ta phải gán cho nó số có số tương đương binary đại diện cho e :

  • hiBuf[1] = 101;

Bây giờ khi ta gọi phương thức toString() :

  • hiBuf.toString();

Ta nhận được kết quả này trong REPL:

Output
'He!'

Để thay đổi ký tự cuối cùng trong cache , ta cần đặt phần tử thứ ba thành số nguyên tương ứng với byte cho y :

  • hiBuf[2] = 121;

Hãy xác nhận bằng cách sử dụng phương thức toString() :

  • hiBuf.toString();

REPL của bạn sẽ hiển thị:

Output
'Hey'

Nếu ta cố gắng ghi một byte nằm ngoài phạm vi của cache , nó sẽ bị bỏ qua và nội dung của cache sẽ không thay đổi. Ví dụ: hãy thử đặt phần tử thứ tư không tồn tại của cache thành o :

  • hiBuf[3] = 111;

Ta có thể xác nhận cache là không thay đổi với phương thức toString() :

  • hiBuf.toString();

Đầu ra vẫn là:

Output
'Hey'

Nếu ta muốn thay đổi nội dung của toàn bộ cache , ta có thể sử dụng phương thức write() . Phương thức write() chấp nhận một chuỗi sẽ thay thế nội dung của một cache .

Hãy sử dụng phương thức write() để thay đổi nội dung của hiBuf trở lại Hi! . Trong shell Node.js của bạn, hãy nhập lệnh sau tại dấu nhắc:

  • hiBuf.write('Hi!');

Phương thức write() trả về 3 trong REPL. Điều này là do nó đã ghi ba byte dữ liệu. Mỗi chữ cái có kích thước một byte, vì cache này sử dụng mã hóa UTF-8, sử dụng một byte cho mỗi ký tự. Nếu cache sử dụng mã hóa UTF-16, có tối thiểu hai byte cho mỗi ký tự, thì hàm write() sẽ trả về 6 .

Bây giờ xác minh nội dung của cache bằng cách sử dụng toString() :

  • hiBuf.toString();

REPL sẽ tạo ra:

Output
'Hi!'

Điều này nhanh hơn so với việc phải thay đổi từng phần tử từng byte.

Nếu bạn cố gắng ghi nhiều byte hơn kích thước của cache , đối tượng đệm sẽ chỉ chấp nhận những byte nào phù hợp. Để minh họa, hãy tạo một cache lưu trữ ba byte:

  • const petBuf = Buffer.alloc(3);

Bây giờ ta hãy thử viết Cats vào nó:

  • petBuf.write('Cats');

Khi cuộc gọi write() được đánh giá, REPL trả về 3 cho biết chỉ có ba byte được ghi vào cache . Bây giờ xác nhận cache chứa ba byte đầu tiên:

  • petBuf.toString();

REPL trả về:

Output
'Cat'

Hàm write() thêm các byte theo thứ tự tuần tự, vì vậy chỉ ba byte đầu tiên được đặt trong cache .

Ngược lại, hãy tạo một Buffer lưu trữ bốn byte:

  • const petBuf2 = Buffer.alloc(4);

Viết các nội dung tương tự vào nó:

  • petBuf2.write('Cats');

Sau đó, thêm một số nội dung mới chiếm ít dung lượng hơn nội dung root :

  • petBuf2.write('Hi');

Vì cache ghi tuần tự, bắt đầu từ 0 , nếu ta in nội dung của cache :

  • petBuf2.toString();

Ta sẽ được chào đón với:

Output
'Hits'

Hai ký tự đầu tiên được overrides , nhưng phần còn lại của cache không bị ảnh hưởng.

Đôi khi dữ liệu ta muốn trong cache có sẵn của ta không nằm trong một chuỗi mà nằm trong một đối tượng cache khác. Trong những trường hợp này, ta có thể sử dụng hàm copy() để sửa đổi những gì cache của ta đang lưu trữ.

Hãy tạo hai vùng đệm mới:

  • const wordsBuf = Buffer.from('Banana Nananana');
  • const catchphraseBuf = Buffer.from('Not sure Turtle!');

Các wordsBufcatchphraseBuf buffers cả chứa dữ liệu chuỗi. Ta muốn sửa đổi catchphraseBuf để nó lưu trữ Nananana Turtle! thay vì Not sure Turtle! . Ta sẽ sử dụng copy() để chuyển Nananana từ wordsBuf thành catchphraseBuf từ wordsBuf thành catchphraseBuf .

Để sao chép dữ liệu từ cache này sang cache khác, ta sẽ sử dụng phương thức copy() trên cache là nguồn thông tin. Do đó, vì wordsBuf có dữ liệu chuỗi mà ta muốn sao chép, ta cần sao chép như sau:

  • wordsBuf.copy(catchphraseBuf);

Tham số target trong trường hợp này là cache catchphraseBuf .

Khi ta nhập điều đó vào REPL, nó trả về 15 cho biết rằng 15 byte đã được viết. Chuỗi Nananana chỉ sử dụng 8 byte dữ liệu, vì vậy ta biết ngay rằng bản sao của ta đã không diễn ra như dự định. Sử dụng phương thức toString() để xem nội dung của catchphraseBuf :

  • catchphraseBuf.toString();

REPL trả về:

Output
'Banana Nananana!'

Theo mặc định, copy() mất toàn bộ nội dung của wordsBuf và đặt nó vào catchphraseBuf . Ta cần chọn lọc hơn cho mục tiêu của bạn và chỉ sao chép Nananana . Hãy viết lại nội dung ban đầu của catchphraseBuf trước khi tiếp tục:

  • catchphraseBuf.write('Not sure Turtle!');

Hàm copy() có thêm một vài tham số cho phép ta tùy chỉnh dữ liệu nào được sao chép vào cache khác. Đây là danh sách tất cả các tham số của hàm này:

  • target - Đây là tham số bắt buộc duy nhất của copy() . Như ta đã thấy từ lần sử dụng trước, nó là cache mà ta muốn sao chép vào.
  • targetStart - Đây là index của các byte trong cache đích mà ta nên bắt đầu sao chép vào. Theo mặc định, nó là 0 , nghĩa là nó sao chép dữ liệu bắt đầu từ phần đầu của cache .
  • sourceStart - Đây là index của các byte trong cache nguồn mà ta nên sao chép từ đó.
  • sourceEnd - Đây là index của các byte trong cache nguồn mà ta nên dừng sao chép. Theo mặc định, đó là độ dài của cache .

Vì vậy, để sao chép Nananana từ wordsBuf vào catchphraseBuf , ta target nên catchphraseBuf như trước đây. Các targetStart sẽ là 0 như ta muốn Nananana xuất hiện ở phần đầu của catchphraseBuf . sourceStart phải là 7 vì đó là index mà Nananana bắt đầu trong wordsBuf . sourceEnd sẽ tiếp tục là độ dài của cache .

Tại dấu nhắc REPL, hãy sao chép nội dung của wordsBuf như sau:

  • wordsBuf.copy(catchphraseBuf, 0, 7, wordsBuf.length);

REPL xác nhận 8 byte đã được ghi. Lưu ý cách wordsBuf.length được sử dụng làm giá trị cho tham số sourceEnd . Giống như mảng, thuộc tính length cung cấp cho ta kích thước của cache .

Bây giờ ta hãy xem nội dung của catchphraseBuf :

  • catchphraseBuf.toString();

REPL trả về:

Output
'Nananana Turtle!'

Sự thành công! Ta đã có thể sửa đổi dữ liệu của catchphraseBuf bằng cách sao chép nội dung của wordsBuf .

Bạn có thể thoát khỏi Node.js REPL nếu bạn muốn làm như vậy. Lưu ý tất cả các biến đã được tạo sẽ không còn khả dụng khi bạn thực hiện:

  • .exit

Kết luận

Trong hướng dẫn này, bạn đã biết rằng cache là phân bổ có độ dài cố định trong bộ nhớ lưu trữ dữ liệu binary . Đầu tiên, bạn tạo cache bằng cách xác định kích thước của chúng trong bộ nhớ và bằng cách khởi tạo chúng bằng dữ liệu đã có từ trước. Sau đó, bạn đọc dữ liệu từ cache bằng cách kiểm tra các byte riêng lẻ của chúng và bằng cách sử dụng các phương thức toString()toJSON() . Cuối cùng, bạn đã sửa đổi dữ liệu được lưu trữ bởi cache bằng cách thay đổi từng byte riêng lẻ của nó và bằng cách sử dụng phương thức write()copy() .

Cache cung cấp cho bạn cái nhìn sâu sắc về cách dữ liệu binary được xử lý bởi Node.js. Đến đây bạn có thể tương tác với cache , bạn có thể quan sát các cách khác nhau mà mã hóa ký tự ảnh hưởng đến cách dữ liệu được lưu trữ. Ví dụ: bạn có thể tạo cache từ dữ liệu chuỗi không phải là mã UTF-8 hoặc ASCII và quan sát sự khác biệt về kích thước của chúng. Bạn cũng có thể lấy một cache với UTF-8 và sử dụng toString() để chuyển đổi nó sang các schemas mã hóa khác.

Để tìm hiểu về vùng đệm trong Node.js, bạn có thể đọc tài liệu Node.js về đối tượng Buffer . Nếu bạn muốn tiếp tục học Node.js, bạn có thể quay lại loạt bài Cách viết mã trong Node.js hoặc duyệt qua các dự án lập trình và cài đặt trên trang chủ đề Node của ta .


Tags:

Các tin liên quan