【发布时间】:2019-12-31 02:18:57
【问题描述】:
我正在尝试用 C++ 实现 Windows 命名管道客户端,它将 RPC 请求发送到用 Go 编写的命名管道服务器。这一切都适用于较短的服务器响应长度。但是,如果服务器响应的长度超过 4096 字节,客户端将不会读取超过 4096 字节并且响应被缩短。我在下面包含了一个客户端和服务器代码的最小可重现示例,为简洁起见,删除了大部分错误处理。要重现该错误,请将服务器代码中的“一些大数据字符串”更改为约 5000 个字符的字符串。
我尝试了以下方法,但没有任何运气:
- 将所有缓冲区的长度设置为比 4096 大得多的值。
- 尝试在客户端和服务器中同时使用 MESSAGE 和 BYTE 模式。
- 检查了 http 响应标头:响应未分块。
任何建议将不胜感激。
C++ 客户端代码:
//Minimal implementation of C++ named pipe client. Most error handling removed for brevity.
//Adapted from https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipe-client
#include <windows.h>
#include <stdio.h>
#include <conio.h>
#include <tchar.h>
#define BUFSIZE 1048576
int _tmain(int argc, TCHAR *argv[])
{
HANDLE hPipe;
const char *lpvMessage="POST / HTTP/1.0\r\nHost: localhost\r\nContent-Length: 33\r\n\r\n{\"method\":\"test\",\"params\":[\"\"]}\r\n\n";
char chBuf[BUFSIZE];
BOOL fSuccess = FALSE;
DWORD cbRead, cbToWrite, cbWritten, dwMode;
LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\mypipe.ipc");
// Try to open a named pipe then close it - attempt 1.
while (1)
{
hPipe = CreateFile(
lpszPipename, // pipe name
GENERIC_READ | // read and write access
GENERIC_WRITE,
0, // no sharing
NULL, // default security attributes
OPEN_EXISTING, // opens existing pipe
0, // default attributes
NULL); // no template file
// Break if the pipe handle is valid.
if (hPipe != INVALID_HANDLE_VALUE)
break;
// Exit if an error occurs.
_tprintf( TEXT("Could not open pipe. GLE=%d\n"), GetLastError() );
return -1;
}
CloseHandle(hPipe);
// If successful, open pipe again for use. For some reason, pipe must be opened and closed once (attempt 1) before actually using.
hPipe = CreateFile(
lpszPipename, // pipe name
GENERIC_READ | // read and write access
GENERIC_WRITE,
0, // no sharing
NULL, // default security attributes
OPEN_EXISTING, // opens existing pipe
0, // default attributes
NULL); // no template file
// The pipe connected; change to message-read mode.
dwMode = PIPE_READMODE_MESSAGE; //PIPE_READMODE_BYTE doesn't solve the problem either;
fSuccess = SetNamedPipeHandleState(
hPipe, // pipe handle
&dwMode, // new pipe mode
NULL, // don't set maximum bytes
NULL); // don't set maximum time
if ( ! fSuccess)
{
_tprintf( TEXT("SetNamedPipeHandleState failed. GLE=%d\n"), GetLastError() );
return -1;
}
// Send a message to the pipe server.
cbToWrite = (lstrlen(lpvMessage)+1)*sizeof(char);
fSuccess = WriteFile(
hPipe, // pipe handle
lpvMessage, // message
cbToWrite, // message length
&cbWritten, // bytes written
NULL); // not overlapped
do
{
// Read from the pipe.
fSuccess = ReadFile(
hPipe, // pipe handle
chBuf, // buffer to receive reply
BUFSIZE*sizeof(char), // size of buffer
&cbRead, // number of bytes read
NULL); // not overlapped
if ( ! fSuccess && GetLastError() != ERROR_MORE_DATA )
break;
printf(chBuf);
} while ( ! fSuccess); // repeat loop if ERROR_MORE_DATA
printf("\n<End of message, press ENTER to terminate connection and exit>");
_getch();
CloseHandle(hPipe);
return 0;
}
转到服务器代码:
//Minimal implementation of Golang named pipe server. Most error handling removed for brevity.
// +build windows
package main
import (
"github.com/Microsoft/go-winio"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"os"
)
func main() {
log.Print("Starting IPC server...")
StartIPCServer()
}
func HandleDefault(w http.ResponseWriter, req *http.Request) {
body, _ := ioutil.ReadAll(io.LimitReader(req.Body, 1048576))
defer req.Body.Close()
log.Printf("Received: '%q'", string(body))
response:= "some large data string" //If length of response plus http headers >4096 bytes, client will not read past 4096.
io.WriteString(w, response)
}
func serve(l net.Listener) error {
http.HandleFunc("/", HandleDefault)
return http.Serve(l, nil)
}
func StartIPCServer() {
var c winio.PipeConfig
c.SecurityDescriptor = ""
c.MessageMode = true //changing to false (byte mode) does not solve the problem.
c.InputBufferSize = 1048576
c.OutputBufferSize = 1048576
path:= `\\.\pipe\mypipe.ipc`
listener, err := winio.ListenPipe(path, &c)
log.Print("IPC server running!")
defer listener.Close()
err = serve(listener)
if err != nil {
log.Fatalf("Serve: %v", err)
os.Exit(1)
}
}
【问题讨论】:
-
在C阅读器中,
ReadFile执行了多少次,cbRead和GetLastError()每次的值是多少?您看到打印的接收数据是什么?请注意,printf(chBuf);将截断 chBuf 的第一个零字节(如果有的话)。此外,任何“%”字符都可能被误解为未提供的参数格式。 -
ReadFile 循环只执行一次。 cbRead 为 4096 字节,GetLastError() 为 0。我看到正常打印的字符(我的测试字符串是纯字母数字 ASCII),但在 4096 字节后缺少字符串的最后一部分。
-
在 Go pipe-proxy 程序中,
io.WriteString(w, response)的返回值是什么?那里可能有错误提示。 -
好的,我找到了一个似乎可行的解决方案。在调用 ReadFile 之前,调用 PeekNamedPipe 以获取管道中的字节数。然后在调用 ReadFile 时将此值传递给缓冲区大小。当 PeekNamedPipe 获得 4096 字节时重复循环。在将此作为答案发布之前,我将进行更多测试以确保它对更广泛的条件有效。我也很想知道为什么 ReadFile 只读取数据的前 4096 个字节。希望比我更了解 C++ 中的命名管道的人会看到这一点并给我们一个答案。
-
如果在原始代码中,您可以在不使用 PeekNamedPipe 的情况下实现相同的操作,当
fSuccess为真时,您只需继续循环。您必须知道响应正文中有多少字节(通过解析 HTTP 响应标头)才能知道何时停止读取。
标签: c++ go named-pipes