“Tôi muốn một dịch vụ thường trú và giao diện cài đặt trao đổi lệnh.” “Tôi muốn cô lập chỉ phần việc cần đặc quyền quản trị vào một tiến trình riêng.” “Tôi muốn các công cụ trên cùng một PC truyền dữ liệu cho nhau.” — Khi loại giao tiếp giữa các tiến trình (IPC) này trở nên cần thiết trên Windows, chuẩn bạn nên cân nhắc trước tiên là named pipe.
Bài viết về việc chọn cơ chế giao tiếp giữa các tiến trình trên Windows đã đặt named pipe ở vị trí “ứng viên đầu tiên cho IPC trên cùng một máy”. Bài này là phần xử lý chi tiết. Vì sao chúng là ứng viên đầu tiên, cách chọn chế độ và hình dạng máy chủ, và những gì bạn phải bảo vệ khi một dịch vụ đặc quyền dùng chúng — hướng tới các nhà phát triển viết ứng dụng nghiệp vụ và dịch vụ trên Windows, bài viết sắp xếp tư liệu cho những phán đoán thiết kế đó từ nguồn gốc.
1. Kết luận trước
- Named pipe là kênh hai chiều giữa các tiến trình, với không gian tên dạng
\\.\pipe\name. Bạn có thể tạo nhiều thực thể dưới cùng một tên và chấp nhận vài client cùng lúc.1 - Lý do chúng là ứng viên đầu tiên cho IPC trên cùng một máy là mô hình bảo mật. Bạn kiểm soát ai được kết nối bằng ACL, và máy chủ có thể xem xét rồi mượn (mạo danh) tài khoản Windows của client. TCP localhost không có cả hai.2
- Nếu muốn coi “một lần ghi = một thông điệp”, dùng chế độ thông điệp; nếu đã có framing riêng, dùng chế độ byte. Ngay cả trong chế độ thông điệp bạn vẫn phải xử lý lần đọc bị tách khi bộ đệm ngắn (ERROR_MORE_DATA).3
- Xử lý nhiều client bằng “nhiều thực thể + I/O chồng lấp” hoặc “.NET async/await”. Mẫu chính thức cho thấy hình dạng xử lý nhiều thực thể trên một luồng duy nhất.4
- Mức tối thiểu về bảo mật gồm bốn điểm: từ chối từ xa (PIPE_REJECT_REMOTE_CLIENTS), làm ACL rõ ràng, phát hiện chiếm đoạt bằng FILE_FLAG_FIRST_PIPE_INSTANCE, và giảm mức mạo danh ở phía client xuống mức cần thiết.56
- Với
ImpersonateNamedPipeClient, kiểm tra giá trị trả về là dây cứu sinh. Bỏ qua thất bại thì xử lý tiếp tục với đặc quyền của máy chủ.6
2. Named pipe là gì — Không gian tên, thực thể, và cách kết nối hoạt động
Named pipe là kênh được nhận diện bằng một tên như \\.\pipe\MyCompany.MyApp.Control. Máy chủ tạo nó bằng CreateNamedPipe, và client mở cùng tên đó bằng CreateFile. Một khi đã mở, cả hai phía đọc và ghi bằng ReadFile / WriteFile — điểm đặc trưng là bạn dùng nó theo cùng hình dạng với I/O tệp.1
Khái niệm quan trọng là thực thể (instance). Bạn có thể tạo nhiều thực thể của một pipe cùng tên, và một thực thể là một kênh với một client. Lần gọi CreateNamedPipe đầu tiên quyết định số thực thể tối đa (hoặc không giới hạn).3
Kết nối phía client có một công thức chuẩn. Khi mọi thực thể đều đang dùng, CreateFile thất bại với ERROR_PIPE_BUSY, nên bạn chờ một thực thể trống bằng WaitNamedPipe rồi thử lại. Ngoài ra, quyền truy cập bạn chỉ định khi mở phải khớp hướng mà máy chủ đã tạo — một pipe hai chiều có thể mở với đọc hoặc ghi, nhưng một pipe outbound mà máy chủ chỉ ghi phải được mở chỉ đọc, và một pipe inbound mà máy chủ chỉ đọc phải được mở chỉ ghi, nếu không CreateFile thất bại.7
flowchart TB
accTitle: Hướng của pipe và quyền truy cập phía client
accDescr: Client có thể mở pipe hai chiều với đọc hoặc ghi, nhưng phải mở pipe outbound mà máy chủ chỉ ghi ở chế độ chỉ đọc, và pipe inbound mà máy chủ chỉ đọc ở chế độ chỉ ghi
q{"Hướng máy chủ đã tạo?"} -->|"Hai chiều"| dc["Đọc hoặc ghi đều được"]
q -->|"Outbound"| oc["Mở chỉ đọc"]
q -->|"Inbound"| ic["Mở chỉ ghi"]
Hình 1: Lệch giữa hướng và quyền truy cập trở thành lỗi CreateFile. Khi điều tra lỗi kết nối, hãy kiểm tra chỗ này trước.
flowchart TB
accTitle: Cấu trúc cơ bản của named pipe
accDescr: Máy chủ tạo nhiều thực thể pipe cùng tên và chờ kết nối bằng ConnectNamedPipe; mỗi client mở tên đó bằng CreateFile và có kênh hai chiều một-một với một thực thể
s["Máy chủ"] --> i1["Thực thể 1"]
s --> i2["Thực thể 2"]
s --> i3["Thực thể 3"]
c1["Client A"] <--> i1
c2["Client B"] <--> i2
c3["Client C"] <--> i3
Hình 2: Bằng cách giữ vài thực thể cùng tên, một máy chủ có thể nói chuyện một-một với vài client cùng lúc.
Named pipe cũng có thể được mở từ xa qua SMB (\\server\pipe\name), nhưng trong thiết kế hiện đại gần như không có lý do để chủ động dùng cách đó; vấn đề đúng hơn là đừng để nó mở khi bạn không dùng (Chương 5).
3. Chế độ byte và chế độ thông điệp
Một pipe có hai chế độ truyền.3
- Chế độ byte (PIPE_TYPE_BYTE): một “luồng byte không ngắt” giống TCP. Bạn tự quyết định thông điệp kết thúc ở đâu (bạn thiết kế framing như tiền tố độ dài).
- Chế độ thông điệp (PIPE_TYPE_MESSAGE + PIPE_READMODE_MESSAGE): một lần ghi được coi là một thông điệp, và bên đọc nhận theo đơn vị đó. Cách này dễ hơn cho trao đổi yêu cầu/phản hồi.
Chế độ thông điệp còn có một bạn đồng hành tiện lợi, TransactNamedPipe, gửi yêu cầu và nhận phản hồi trong một lần gọi.8 Tuy nhiên có một cái bẫy. Nếu bộ đệm nhận nhỏ hơn toàn bộ thông điệp, lần đọc trả về ERROR_MORE_DATA và trở thành lần đọc bị tách. Đừng giả định rằng chế độ thông điệp nghĩa là “một Read luôn mang về toàn bộ”; bạn vẫn phải viết vòng lặp đọc phần còn lại. Lưu ý rằng chế độ đọc là thiết lập theo từng handle, và CreateNamedPipe chỉ quyết định nó ở phía máy chủ. Client chỉ định nó bằng SetNamedPipeHandleState sau CreateFile (trong .NET, ReadMode sau khi kết nối).7
flowchart TB
accTitle: Vòng lặp đọc phần còn lại ở chế độ thông điệp
accDescr: Nếu ReadFile thành công thì thông điệp đã đủ; nếu trả về ERROR_MORE_DATA bạn đọc phần còn lại không vừa bộ đệm rồi nối vào; mọi lỗi khác được coi là ngắt kết nối
read["Đọc bằng ReadFile"] --> r{"Kết quả?"}
r -->|"Thành công"| done["Thông điệp đủ"]
r -->|"ERROR_MORE_DATA"| more["Đọc phần còn lại rồi nối"]
more --> read
r -->|"Mọi lỗi khác"| dis["Coi là ngắt kết nối"]
Hình 3: Ngay cả ở chế độ thông điệp bạn vẫn cần “vòng lặp đọc phần còn lại”; thiếu nó thì chỉ các thông điệp lớn bị hỏng.
flowchart TB
accTitle: Khác biệt giữa chế độ byte và chế độ thông điệp
accDescr: Ở chế độ byte ba lần ghi trở thành một luồng byte không ngắt và bên nhận phải tự tách; ở chế độ thông điệp đơn vị mỗi lần ghi được giữ nguyên và tới bên nhận như vậy
bw["Chế độ byte: ghi AAA, BB, CCCC"] --> br["Nhận thành luồng byte AAABBCCCC"]
br --> bf["Bạn tự thiết kế framing"]
mw["Chế độ thông điệp: cùng ba lần ghi"] --> mr["Nhận thành ba thông điệp: AAA, BB, CCCC"]
mr --> mf["Đơn vị ghi được giữ nguyên"]
Hình 4: Chế độ thông điệp giữ “đơn vị của một lần ghi” và chuyển giao nó. Thiết kế framing trở nên không cần; chỉ đừng quên xử lý lần đọc bị tách.
Quy tắc thực tiễn để chọn rất đơn giản. Nếu trao đổi có hình dạng “yêu cầu và phản hồi”, chế độ thông điệp. Nếu bạn đang mang một dạng đã có framing sẵn (dữ liệu tuần tự hóa có tiền tố độ dài hoặc truyền luồng), dùng chế độ byte. Trong .NET, chỉ định PipeTransmissionMode.Message tương ứng với cách trước.9
4. Thiết kế máy chủ — Một luồng mỗi client, hay chồng lấp?
Thao tác cơ bản của máy chủ là vòng lặp “tạo thực thể → chờ client bằng ConnectNamedPipe → đọc và ghi → ngắt kết nối rồi sang client tiếp”. Có hai hình dạng để nói chuyện với vài client cùng lúc.
Đồng bộ, một luồng mỗi thực thể. Bạn gán một luồng cho mỗi thực thể, và mỗi luồng nói chuyện với client của mình bằng I/O đồng bộ. Mã thẳng thắn, nhưng bạn tiêu thụ một luồng mỗi client, và cũng cần cách thoát khỏi I/O chặn khi tắt toàn bộ hệ thống.
Chồng lấp (bất đồng bộ). Bạn tạo thực thể với FILE_FLAG_OVERLAPPED, phát ConnectNamedPipe / ReadFile / WriteFile bất đồng bộ, và để một số ít luồng xử lý hoàn tất cho mọi thực thể. Mẫu chính thức của Microsoft cho thấy một máy chủ chờ trên mảng sự kiện bằng WaitForMultipleObjects và xử lý nhiều thực thể trên một luồng duy nhất.4 Phần trình bày chung về I/O bất đồng bộ như đã giải thích trong bài loạt về I/O, và ở quy mô lớn hơn bạn cũng có thể gắn IOCP hoặc I/O của thread pool.
flowchart TB
accTitle: Cấu trúc máy chủ chồng lấp
accDescr: Hoàn tất các thao tác bất đồng bộ của từng thực thể được nhận trên mảng sự kiện, và một số ít luồng chờ bằng WaitForMultipleObjects rồi đẩy thực thể đã hoàn tất, tách số luồng khỏi số client
i1["Thao tác bất đồng bộ thực thể 1"] --> ev["Mảng sự kiện"]
i2["Thao tác bất đồng bộ thực thể 2"] --> ev
i3["Thao tác bất đồng bộ thực thể 3"] --> ev
ev --> wait["Chờ hoàn tất bằng WaitForMultipleObjects"]
wait --> proc["Đẩy thực thể đã hoàn tất"]
proc --> wait
Hình 5: Hình dạng chồng lấp tách số luồng khỏi số client. Mẫu chính thức chạy vòng này trên một luồng duy nhất.
.NET gần như xóa lựa chọn này. Dùng WaitForConnectionAsync / ReadAsync / WriteAsync của NamedPipeServerStream cùng async/await, bạn có hiệu quả chồng lấp trong mã thẳng thắn như hình dạng đồng bộ.9
// C#: skeleton of a server that accepts multiple clients
while (!token.IsCancellationRequested)
{
var server = new NamedPipeServerStream(
"MyCompany.MyApp.Control",
PipeDirection.InOut,
NamedPipeServerStream.MaxAllowedServerInstances,
PipeTransmissionMode.Message,
PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
try
{
await server.WaitForConnectionAsync(token);
}
catch
{
await server.DisposeAsync(); // dispose yourself when leaving before a connection
throw;
}
_ = HandleClientAsync(server, token); // ownership after connect goes to the handler
}
PipeOptions.CurrentUserOnly là chỉ định “chỉ cho phép kết nối từ các tiến trình của cùng người dùng”, một mặc định tiện và an toàn giúp bạn khỏi phải tự viết ACL.10 Nó không dùng được trong cấu hình xuyên người dùng (một dịch vụ ↔ một ứng dụng trong phiên người dùng, và tương tự), nên khi đó bạn chuyển sang thiết kế ACL ở chương tiếp theo.
flowchart TB
accTitle: Vòng chấp nhận của máy chủ bất đồng bộ .NET
accDescr: Vòng chấp nhận tạo NamedPipeServerStream, chờ kết nối bằng WaitForConnectionAsync, và khi tới thì tách xử lý client bất đồng bộ rồi ngay lập tức trở lại lần chấp nhận tiếp, nên các kết nối đồng thời được xử lý bằng mã thẳng thắn
mk["Tạo luồng máy chủ"] --> wc["Chờ bằng WaitForConnectionAsync"]
wc --> got["Kết nối tới"]
got --> hd["Tách xử lý client bất đồng bộ"]
hd --> mk
Hình 6: Vòng chấp nhận bám chu kỳ “chờ → tách → tiếp”, và xử lý của từng client tiến hành song song.
5. Bảo mật — Bốn việc bắt buộc khi dịch vụ đặc quyền dùng pipe
Lý do lớn nhất khiến named pipe là ứng viên đầu tiên cho IPC trên cùng một máy là mô hình bảo mật, nhưng điều đó chỉ đúng nếu bạn cấu hình đúng. Đặc biệt trong thiết kế broker kiểu “dịch vụ đặc quyền quản trị + ứng dụng UI đặc quyền thấp”, pipe chính là ranh giới đặc quyền. Có bốn điểm cần chốt.
(1) Từ chối từ xa. Một pipe bạn định dùng làm IPC cục bộ mà lại mở được từ mạng, bản thân đã là bề mặt tấn công. Chỉ định PIPE_REJECT_REMOTE_CLIENTS trên CreateNamedPipe thì kết nối client từ xa bị từ chối tự động.5
(2) Làm ACL rõ ràng. Truyền security descriptor trong SECURITY_ATTRIBUTES và thu hẹp người dùng cùng nhóm được phép kết nối. Đừng cấp GENERIC_WRITE cho client — quyền FILE_CREATE_PIPE_INSTANCE nằm trong đó sẽ để một client đã được phép tự tạo thực thể máy chủ cùng tên và cướp các kết nối sau. Cấp đọc và ghi như các quyền riêng, và đừng truyền quyền tạo thực thể.11
(3) Ngăn chiếm tên. Tên pipe theo kiểu ai đến trước được trước. Nếu một tiến trình độc hại tạo pipe cùng tên trước rồi chờ, client kết nối tới máy chủ giả. Máy chủ chỉ định FILE_FLAG_FIRST_PIPE_INSTANCE khi tạo thực thể đầu tiên, bảo đảm “tôi là người đầu”, và nếu việc đó thất bại thì nghi chiếm đoạt rồi dừng. Cờ này chỉ dành cho thực thể đầu tiên tuyên bố tên; gắn nó vào thực thể thứ hai trở đi làm việc tạo thất bại.3
| (4) Client giảm mức mạo danh xuống mức cần thiết. Đây là sự chuẩn bị cho trường hợp đối phương là máy chủ giả. Nếu client chỉ định **SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION** trên CreateFile, máy chủ có thể nhận diện client nhưng không thể mượn những đặc quyền đó rồi hành động.2 Đây là sự đánh đổi với luồng làm việc mạo danh — trong thiết kế broker, nơi máy chủ thực hiện truy cập thật dưới đặc quyền của client, mức nhận diện không đủ để mạo danh thành công, và bạn cần cho phép SECURITY_IMPERSONATION. Việc cho phép đó có điều kiện là chắc chắn bạn đang kết nối tới máy chủ thật. Chống chiếm đoạt phía máy chủ chỉ là cơ chế nhận ra qua lần khởi động thất bại; nếu dịch vụ thật vắng mặt và kẻ tấn công tạo pipe cùng tên trước, client vẫn có thể kết nối tới máy chủ giả. Chỉ cho phép khi bạn xác nhận đối phương qua khởi động dịch vụ được bảo đảm hoặc xác thực hai chiều sau khi kết nối. |
Việc kiểm tra danh tính và mượn đặc quyền phía máy chủ là ImpersonateNamedPipeClient. Gọi hàm này sau khi đọc một yêu cầu từ pipe thì luồng gọi bắt đầu chạy trong ngữ cảnh bảo mật của người gửi thông điệp cuối cùng đã đọc. Mở một tệp với đặc quyền của client thì việc kiểm tra truy cập được thực hiện đối với client — cơ chế để một dịch vụ đặc quyền thực thi “thao tác được yêu cầu, với đặc quyền của người yêu cầu”.6 Điều kiện tuyệt đối khi dùng nó là kiểm tra giá trị trả về. Tiếp tục sau khi mạo danh thất bại thì các thao tác sau chạy với đặc quyền cao của chính máy chủ. Tài liệu chính thức nêu rõ rằng “khi thất bại bạn không được thực thi yêu cầu của client”. Cùng với RevertToSelf sau khi xong việc, các thực hành trong bài viết về token mạo danh áp dụng nguyên như vậy.
sequenceDiagram
accTitle: Luồng xử lý yêu cầu dùng mạo danh
accDescr: Máy chủ đọc yêu cầu từ pipe, xác nhận ImpersonateNamedPipeClient thành công, rồi thực hiện thao tác với đặc quyền của client và trở về ngữ cảnh riêng bằng RevertToSelf. Nếu mạo danh thất bại thì từ chối yêu cầu mà không thực thi
participant C as Client
participant S as Máy chủ
C->>S: Gửi yêu cầu
S->>S: Đọc yêu cầu
S->>S: ImpersonateNamedPipeClient
Note over S: Khi thất bại, từ chối mà không thực thi yêu cầu
S->>S: Thực hiện thao tác với đặc quyền của client
S->>S: RevertToSelf để khôi phục ngữ cảnh gốc
S->>C: Trả lời kết quả
Hình 7: Xác nhận mạo danh thành công và RevertToSelf đáng tin cậy đi thành một gói. Tiếp tục khi thất bại thì chạy với đặc quyền của máy chủ.
flowchart TB
accTitle: Bốn điểm bảo vệ pipe của dịch vụ đặc quyền
accDescr: Phía máy chủ củng cố cửa vào bằng từ chối từ xa, ACL rõ ràng và bảo đảm thực thể đầu; phía client chỉ định mức mạo danh tối thiểu cần thiết để máy chủ giả không mượn được đặc quyền(thu hẹp về mức nhận diện nếu thiết kế không để máy chủ mượn đặc quyền)
subgraph sv["Phía máy chủ"]
r1["PIPE_REJECT_REMOTE_CLIENTS"]
r2["Hạn chế người kết nối bằng ACL"]
r3["FIRST_PIPE_INSTANCE(chỉ thực thể đầu)"]
end
subgraph cl["Phía client"]
r4["Chỉ định mức mạo danh tối thiểu"]
end
sv --> safe["Pipe như ranh giới đặc quyền"]
cl --> safe
Hình 8: Trong thiết kế mà pipe là ranh giới đặc quyền, hãy triển khai ba điểm phía máy chủ cộng một điểm phía client như một bộ.
6. Những cái bẫy thực tiễn
Cuộc đua thứ tự khởi động. Nếu client tới kết nối trước khi máy chủ tạo pipe, bạn gặp lỗi “pipe không tồn tại”. Phía client xây sẵn “không tồn tại → chờ một chút rồi thử lại”. Ngược lại, nguyên tắc phía máy chủ là bắt đầu lắng nghe bằng ConnectNamedPipe trước khi client khởi động.8
flowchart TB
accTitle: Luồng thử lại kết nối phía client
accDescr: Mở pipe bằng CreateFile; nếu pipe không tồn tại thì chờ ngắn rồi thử lại; nếu mọi thực thể đều đang dùng(ERROR_PIPE_BUSY) thì chờ một thực thể trống bằng WaitNamedPipe rồi thử lại; khi thành công thì vào giao tiếp
cf["Mở bằng CreateFile"] --> ok{"Kết quả?"}
ok -->|"Thành công"| go["Bắt đầu giao tiếp"]
ok -->|"Pipe không tồn tại"| wait1["Chờ ngắn(máy chủ chưa chạy)"]
ok -->|"ERROR_PIPE_BUSY"| wnp["Chờ thực thể trống bằng WaitNamedPipe"]
wait1 --> cf
wnp --> cf
Hình 9: Xử lý kết nối phía client phân biệt hai loại thất bại, “không tồn tại” và “đầy”, rồi đưa cả hai trở lại lần thử lại.
Phát hiện ngắt kết nối. Khi đối phương thoát, Read/Write thất bại với ERROR_BROKEN_PIPE và tương tự. Đó không phải bất thường; đó là giao tiếp hàng ngày. Máy chủ phát hiện ngắt kết nối, DisconnectNamedPipe thực thể, và chuẩn bị cho kết nối tiếp; client kết nối lại — ý tưởng “kết nối lại idempotent” được mô tả trong bài viết về ngủ/thức cũng áp dụng ở đây.
Giả định về kích thước thông điệp. Bên cạnh các lần đọc bị tách của chế độ thông điệp (Chương 3), nếu bạn không quyết định như một phần của giao thức “một thông điệp tối đa bao nhiêu byte”, một đối phương độc hại (hoặc có lỗi) có thể lãng phí bộ nhớ của bạn bằng một thông điệp khổng lồ. Quyết định một giới hạn trên, và ngắt kết nối nếu vượt; đó là cách tiếp cận an toàn.
Hoàn tất ghi và việc đối phương nhận là hai việc khác nhau. Thành công của WriteFile không có nghĩa ứng dụng phía bên kia đã xử lý dữ liệu. Các thao tác cần chắc chắn được bảo đảm bằng những thiết kế như xác nhận bằng thông điệp phản hồi, và đưa sự tương ứng giữa yêu cầu và phản hồi vào giao thức.
7. Tóm tắt
- Named pipe là ứng viên đầu tiên cho IPC trên cùng một máy. Lý do là sự dễ dùng giống I/O tệp, và sự gắn kết với mô hình bảo mật Windows gồm ACL và mạo danh.
- Chọn chế độ là “chế độ thông điệp cho yêu cầu và phản hồi, chế độ byte nếu bạn đã có framing riêng”. Ngay cả ở chế độ thông điệp bạn vẫn phải xử lý lần đọc bị tách (ERROR_MORE_DATA).
- Nhiều client là nhiều thực thể + chồng lấp, hoặc .NET async/await. Với việc mới, hình dạng bất đồng bộ của .NET là cách thẳng thắn.
- Trên một pipe là ranh giới đặc quyền, hãy lấy từ chối từ xa, ACL rõ ràng, FIRST_PIPE_INSTANCE (chỉ thực thể đầu), và giảm mức mạo danh phía client như một bộ.
- Với
ImpersonateNamedPipeClient, kiểm tra giá trị trả về vàRevertToSelflà dây cứu sinh. - Đan “ngày thường của giao tiếp” — thứ tự khởi động, ngắt kết nối, giới hạn trên của thông điệp, xác nhận phản hồi — vào thiết kế giao thức.
Named pipe là API cũ, nhưng với mục đích “để các tiến trình nói chuyện với nhau trên cùng một máy trong khi tôn trọng ranh giới tài khoản Windows”, chúng vẫn là công cụ tự nhiên nhất cho công việc. Các điểm phán đoán thiết kế gần như đã hết trong phạm vi bài này. Sau đó, hãy viết giao thức của riêng bạn ra một tờ giấy trước khi bắt đầu triển khai.
Bài viết liên quan
- Chọn cơ chế giao tiếp giữa các tiến trình trên Windows ── Bảng quyết định cho Named pipe / TCP / gRPC / Bộ nhớ dùng chung / COM
- Cách cô lập cụ thể “chỉ những thao tác cần đặc quyền quản trị” trong ứng dụng Windows
- Xử lý token mạo danh Windows cho đúng — Mượn đặc quyền theo luồng và hoàn nguyên an toàn
- Chiều sâu I/O của Windows (Phần 2) — I/O đồng bộ và bất đồng bộ: OVERLAPPED thực sự nghĩa là gì
- Những cái bẫy của bộ nhớ dùng chung và thực hành tốt nhất
Lĩnh vực tư vấn liên quan
KomuraSoft LLC đảm nhận thiết kế và triển khai liên quan giao tiếp giữa các tiến trình — tách dịch vụ khỏi ứng dụng UI, cô lập đặc quyền quản trị, và tương tự — thay thế IPC hiện có (bộ nhớ dùng chung, socket tự viết, COM, v.v.) bằng named pipe, và rà soát bảo mật giao tiếp pipe của dịch vụ đặc quyền. Tư vấn bắt đầu từ việc cùng soi thiết kế giao thức đều được chào đón.
- Phát triển ứng dụng Windows
- Tư vấn kỹ thuật và rà soát thiết kế
- Điều tra lỗi và phân tích nguyên nhân gốc rễ
- Liên hệ với chúng tôi
Liên kết tham khảo
-
Microsoft Learn, Named Pipes. Về việc named pipe là kênh một chiều hoặc hai chiều giữa máy chủ pipe và một hoặc nhiều client pipe; về việc mọi thực thể chia sẻ cùng tên trong khi có bộ đệm và handle độc lập; và về việc dùng được từ tiến trình cục bộ lẫn từ xa. ↩ ↩2
-
Microsoft Learn, Impersonating a Named Pipe Client. Về việc mạo danh để luồng máy chủ vận hành trong đặc quyền của client; về mức mạo danh mặc định là SecurityImpersonation; và về việc client có thể kiểm soát mức mạo danh bằng cờ SECURITY_SQOS_PRESENT tại thời điểm CreateFile (SECURITY_IDENTIFICATION chỉ cho phép nhận diện). ↩ ↩2
-
Microsoft Learn, CreateNamedPipeW function (namedpipeapi.h). Về hướng pipe (inbound, outbound, hai chiều), kiểu byte và kiểu thông điệp (PIPE_TYPE_BYTE / PIPE_TYPE_MESSAGE) cùng chế độ đọc (PIPE_READMODE_MESSAGE), số thực thể tối đa (PIPE_UNLIMITED_INSTANCES), chế độ bất đồng bộ qua FILE_FLAG_OVERLAPPED, bảo đảm thực thể đầu qua FILE_FLAG_FIRST_PIPE_INSTANCE, và thời gian chờ mặc định cho WaitNamedPipe. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Named Pipe Server Using Overlapped I/O. Về mẫu chính thức của máy chủ một luồng xử lý kết nối đồng thời với nhiều client qua thao tác chồng lấp. Về hình dạng chờ trên cấu trúc OVERLAPPED và sự kiện của từng thực thể bằng WaitForMultipleObjects rồi đẩy máy trạng thái của thực thể đã hoàn tất, và về xác nhận hoàn tất I/O đang chờ bằng GetOverlappedResult. ↩ ↩2
-
Microsoft Learn, CreateNamedPipeW function (namedpipeapi.h). Về hai chế độ client từ xa, PIPE_ACCEPT_REMOTE_CLIENTS (chấp nhận kết nối từ xa và kiểm tra chúng đối với security descriptor) và PIPE_REJECT_REMOTE_CLIENTS (tự động từ chối kết nối client từ xa). ↩ ↩2
-
Microsoft Learn, ImpersonateNamedPipeClient function (namedpipeapi.h). Về việc một luồng phía máy chủ bắt đầu mạo danh trong ngữ cảnh bảo mật của client của thông điệp cuối cùng đọc từ pipe; về việc trở về bằng RevertToSelf sau khi hoàn tất; và về việc tiếp tục sau khi mạo danh thất bại khiến thực thi trong ngữ cảnh (đặc quyền) của chính tiến trình máy chủ, nên luôn phải kiểm tra giá trị trả về và khi thất bại không được thực thi yêu cầu của client. ↩ ↩2 ↩3
-
Microsoft Learn, Named Pipe Client. Về việc client mở pipe bằng CreateFile; về ERROR_PIPE_BUSY khi mọi thực thể đang dùng, chờ một thực thể trống bằng WaitNamedPipe; và về handle đã mở mặc định là đọc byte, chặn, và không chồng lấp, với SetNamedPipeHandleState có thể đổi sang chế độ đọc thông điệp. ↩ ↩2
-
Microsoft Learn, Named Pipe Operations. Về thao tác chồng lấp qua ReadFileEx / WriteFileEx, lần đọc không tiêu thụ qua PeekNamedPipe, TransactNamedPipe thực hiện gửi yêu cầu và nhận phản hồi trong một lần gọi trên pipe hai chiều kiểu thông điệp, và lần đọc chặn trước khi client khởi động có thể gây cuộc đua. ↩ ↩2
-
Microsoft Learn, How to: Use Named Pipes for Network Interprocess Communication (.NET). Về kết nối và đọc/ghi bằng NamedPipeServerStream / NamedPipeClientStream, truyền theo đơn vị thông điệp qua PipeTransmissionMode.Message, và xử lý nhiều client bằng phương thức bất đồng bộ. ↩ ↩2
-
Microsoft Learn, PipeOptions Enum (System.IO.Pipes). Về việc bật I/O bất đồng bộ bằng Asynchronous, và về CurrentUserOnly có thể chỉ cho phép kết nối với các tiến trình của cùng người dùng (và cùng mức nâng quyền). ↩
-
Microsoft Learn, Named Pipe Security and Access Rights. Về thành phần các quyền truy cập named pipe; về việc GENERIC_WRITE bao gồm FILE_CREATE_PIPE_INSTANCE, nên cấp generic write cho client cũng cho phép tạo thực thể máy chủ; và về việc đọc và ghi dữ liệu được cấp như các quyền truy cập riêng. ↩
Bài viết liên quan
Các bài viết gần đây có cùng thẻ để tìm hiểu sâu hơn những chủ đề lân cận.
API thread pool Win32 — Đồng thời mà không tạo luồng, với CreateThreadpoolWork
Bạn đang rải các lời gọi CreateThread khắp mã native? Bài viết này giải thích API thread pool Win32 được thiết kế lại từ Vista — bốn đối ...
Bên trong ảo hóa Windows (Phần 2) — Bộ nhớ ngay cả kernel cũng không thấy: VBS, HVCI và Credential Guard
Khi cài sạch trên phần cứng tương thích, VBS được bật mặc định và dùng hypervisor cùng SLAT để tạo cô lập mạnh hơn kernel. Bài viết này g...
Bên trong ảo hóa Windows (Phần 3) — Máy ảo khởi động trong vài giây: Vì sao WSL2, Windows Sandbox và container lại nhẹ
Vì sao WSL2 và Windows Sandbox khởi động trong vài giây và cảm giác nhẹ đến vậy? Bài viết này giải thích các cơ chế, từ ảnh cơ sở động và...
Bên trong ảo hóa Windows (Phần 1) — Windows của bạn thực sự chạy ở đâu? Hypervisor và phân vùng
Khi bạn bật Hyper-V, chính Windows máy chủ chạy trên hypervisor như phân vùng gốc. Bài viết này giải thích nền tảng ảo hóa qua vai trò củ...
Giúp website xuất hiện khi khách tìm theo tên khu vực — Hướng dẫn Local SEO thực tiễn cho doanh nghiệp vừa và nhỏ (trang khu vực và Google Business Profile)
Dành cho các doanh nghiệp vừa và nhỏ có website không xuất hiện khi ai đó tìm kiếm "tên khu vực + ngành nghề". Bài viết này trình bày thứ...
Chủ đề liên quan
Các trang này đặt chủ đề trong bối cảnh rộng hơn của dịch vụ và quyết định.
Chủ đề kỹ thuật Windows
Cổng vào phát triển Windows, điều tra lỗi và khai thác tài sản hiện có.
Dịch vụ liên quan đến chủ đề này
Bài viết liên quan trực tiếp đến các dịch vụ sau.
Phát triển ứng dụng Windows
Ứng dụng nghiệp vụ, tích hợp thiết bị và công cụ liên lạc, từ yêu cầu đến phát triển.
Câu hỏi thường gặp
Các câu hỏi thường gặp khi tư vấn về chủ đề của bài viết.
- Nên chọn named pipe hay TCP (socket localhost) như thế nào?
- Với giao tiếp giữa các tiến trình trên cùng một máy, named pipe là ứng viên đầu tiên. Lý do nằm ở mô hình bảo mật. Một pipe có thể kiểm soát "ai được kết nối" ở cấp hệ điều hành bằng security descriptor của Windows (ACL), và máy chủ có thể xem xét rồi mượn tài khoản Windows của bên kết nối bằng ImpersonateNamedPipeClient. Điều đó khác với cổng TCP localhost, nơi bất kỳ ai cũng có thể kết nối nên bạn phải tự xác thực đối phương. Mặt khác, các lựa chọn dựa trên TCP có lợi khi khả năng giao tiếp từ xa sẽ xuất hiện sau này, khi bạn cũng nói chuyện với tiến trình trên hệ điều hành khác, hoặc khi muốn tái sử dụng một giao thức sẵn có như gRPC. Cách đánh giá này cũng được trình bày trong bài viết về việc chọn cơ chế giao tiếp giữa các tiến trình trên Windows.
- Nên dùng chế độ byte hay chế độ thông điệp?
- Nếu muốn coi "một lần ghi = một đơn vị ý nghĩa", chế độ thông điệp (PIPE_TYPE_MESSAGE + PIPE_READMODE_MESSAGE) rất tiện. Bên nhận đọc theo đúng đơn vị mà bên gửi đã ghi, nên bạn không phải tự quản lý ranh giới. Chế độ byte là một "luồng byte không ngắt" giống TCP, và bạn phải tự thiết kế khung (framing) — ví dụ tiền tố độ dài. Nếu bạn đang mang một giao thức đã có framing (ví dụ dạng tuần tự hóa có tiền tố độ dài), chế độ byte là ổn. Một lưu ý: ngay cả trong chế độ thông điệp, nếu bộ đệm nhận nhỏ hơn thông điệp thì bạn nhận được lần đọc bị tách (ERROR_MORE_DATA), nên vẫn phải xử lý. Ngoài ra, chế độ đọc là thiết lập theo từng handle, và CreateNamedPipe chỉ đặt nó ở phía máy chủ. Client phải chỉ định PIPE_READMODE_MESSAGE bằng SetNamedPipeHandleState sau CreateFile. Trong .NET, máy chủ chỉ định PipeTransmissionMode.Message, và client gán NamedPipeClientStream.ReadMode thành Message sau khi kết nối.
- Làm sao xây máy chủ nói chuyện với nhiều client cùng lúc?
- Một named pipe có thể tạo nhiều thực thể dưới cùng một tên, và một thực thể phục vụ một client. Có hai hình dạng. Một là thiết kế đồng bộ, gán một luồng cho mỗi client; triển khai thẳng thắn, nhưng tiêu thụ một luồng mỗi client. Cách kia là dùng I/O bất đồng bộ với FILE_FLAG_OVERLAPPED và để một số ít luồng xử lý ConnectNamedPipe, ReadFile và WriteFile cho mọi thực thể; mẫu chính thức của Microsoft cũng cho thấy một triển khai xử lý nhiều thực thể trên một luồng duy nhất. Trong .NET bạn có thể viết hình dạng bất đồng bộ gần như thẳng thắn như hình dạng đồng bộ, bằng NamedPipeServerStream.WaitForConnectionAsync và async/await. Trừ khi có lý do đặc biệt, hình dạng bất đồng bộ của .NET là thứ tôi khuyến nghị cho triển khai mới.
- Mức tối thiểu tôi nên làm cho bảo mật named pipe là gì?
- Bốn điểm. Thứ nhất, nếu không cần kết nối từ xa, chỉ định PIPE_REJECT_REMOTE_CLIENTS và từ chối rõ ràng kết nối qua mạng. Thứ hai, đặt ACL thích hợp bằng SECURITY_ATTRIBUTES và thu hẹp người dùng cùng nhóm được phép kết nối (ACL mặc định quá lỏng với một số mục đích). Thứ ba, chỉ định FILE_FLAG_FIRST_PIPE_INSTANCE khi tạo thực thể đầu tiên, để phát hiện "chiếm tên" — một pipe cùng tên được tạo trước (đừng gắn cờ này vào thực thể thứ hai trở đi). Thứ tư, client chỉ muốn máy chủ nhận diện mình nên chỉ định SECURITY_SQOS_PRESENT|SECURITY_IDENTIFICATION trên CreateFile, để một máy chủ giả không thể mượn (mạo danh) đặc quyền của nó. Trong thiết kế broker, nơi máy chủ thực hiện truy cập thật dưới đặc quyền của client, việc mạo danh phải được phép, nên bạn dùng hoặc không dùng hạn chế này tùy thiết kế có để máy chủ mượn đặc quyền hay không.
- Có lưu ý nào khi dùng ImpersonateNamedPipeClient?
- Quan trọng nhất là kiểm tra giá trị trả về. Nếu bạn tiếp tục sau khi mạo danh thất bại, các thao tác sau chạy với đặc quyền riêng của tiến trình máy chủ (thường là cao), và những thao tác lẽ ra không được phép với client vẫn được thông qua. Tài liệu chính thức cũng nêu rõ rằng khi thất bại bạn không được thực thi yêu cầu của client. Bạn cũng cần chỉ gọi nó sau khi đã đọc gì đó — việc mạo danh được thực hiện trong ngữ cảnh "thông điệp cuối cùng đọc từ pipe" — và trở về đáng tin cậy ngữ cảnh gốc bằng RevertToSelf khi xong việc. Cơ chế quanh mạo danh (token, mức mạo danh, SeImpersonatePrivilege) được trình bày chi tiết trong bài viết về token mạo danh.