Windows上如何在进程间传递socket句柄

问题的背景是这样的:我需要写一个程序A,来launch另一个程序B。B是一个server程序,它需要监听一个TCP端口。这个端口是A来分配的,A需要确保这个端口没有被占用。

我最初的办法是这样:写一个函数,遍历现在的tcp table,来判断哪些端口已经在使用中。

std::vector<int> getFreePorts(int begin, int end)
{
    char errbuf[256];
    if (end <= begin) {
        THROW_EXCEPTION("illegal argument");
    }
    std::unique_ptr<MIB_TCPTABLE2, decltype(std::free)*> pTcpTable(nullptr, std::free);
    ULONG ulSize = sizeof(MIB_TCPTABLE);
    for (int iter = 0; iter != 2; ++iter) {
        pTcpTable.reset((MIB_TCPTABLE2*)malloc(ulSize));
        ULONG dwRetVal = GetTcpTable2(pTcpTable.get(), &ulSize, FALSE);
        if (dwRetVal == NO_ERROR) break;
        if (dwRetVal != ERROR_INSUFFICIENT_BUFFER) {
            THROW_EXCEPTION("GetTcpTable2 failed,error = %ul", dwRetVal);
        }
    }
    std::vector<uint8_t> avail(end - begin, 1);
    for (int i = 0; i != pTcpTable->dwNumEntries; ++i) {
        auto& table = pTcpTable->table[i];
        int off = table.dwLocalPort - begin;
        if (off >= 0 && off < avail.size()) {
            avail[off] = 0;
        }
    }
    std::vector<int> ret;
    for (int i = 0; i != (int)avail.size(); ++i) {
        if (avail[i]) ret.push_back(i + begin);
    }
    return ret;
}
但是很奇怪,子进程bind有时候还是会fail。于是我就开始我的第二个版本:父进程创建socket并bind,子进程去listen并且accept。但是这时候就冒出来一个问题:windows的handle是指针,而不是int。指针怎么跨进程传递……出乎意料的是,它直接强转成size_t来传递。

于是父进程这样创建socket:
 SOCKET ListenSocket = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
 if (ListenSocket == INVALID_SOCKET) {
  THROW_EXCEPTION("socket function failed with error: %u", WSAGetLastError());   
 }
 // The socket address to be passed to bind
 sockaddr_in service;
 service.sin_family = AF_INET;
 service.sin_addr.s_addr = inet_addr("0.0.0.0");
 service.sin_port = htons(port);
 int iResult = bind(ListenSocket, (SOCKADDR *)&service, sizeof(service));
 if (iResult == SOCKET_ERROR) {
  int err = WSAGetLastError();
  closesocket(ListenSocket);
  THROW_EXCEPTION("bind failed with error %u", err);   
 }
 return ListenSocket;
把上面的代码放入一个for循环中,然后把得到的socket通过命令行参数传递给子进程。在调用CreateProcess函数时,bInheritHandles参数应该是TRUE。如果是用java的process类创建子进程,那么不用做什么特别设置,bInheritHandles一直是TRUE。在子进程启动之后,父进程应当关闭这个socket。

子进程这样使用:
#include <WinSock2.h>
#include <Windows.h>
#include <stdio.h>
int main(int argc,char* argv[]) {
 WSADATA wsaData;
 WSAStartup(MAKEWORD(2, 2), &wsaData);
 FILE* fd=fopen("server.log","w");
 SOCKET ListenSocket = _strtoi64(argv[1],nullptr,10);
 //----------------------
 // Listen for incoming connection requests 
 // on the created socket
 if (listen(ListenSocket, SOMAXCONN) == SOCKET_ERROR) {
  int err = WSAGetLastError();
  closesocket(ListenSocket);
  fprintf(fd,"listen function failed with error: %d", err);
  return -1;
 }
 while (true) {
  SOCKET AcceptSocket = accept(ListenSocket, NULL, NULL);
  if (AcceptSocket == INVALID_SOCKET) {
   fprintf(fd, "accept failed with error: %ld", WSAGetLastError());
   fflush(fd);
   break;
  }
  else {
   fprintf(fd, "Client connected.");
   fflush(fd);
  }
 }
 fprintf(fd, "shutdown");
 fflush(fd);
 closesocket(ListenSocket); 
 fclose(fd);
 WSACleanup();
 return 0;
}

比较有趣的是,如果通过netstat这样的命令查看,会显示是父进程在监听端口并接受连接。这是一个绕过防火墙的很好的办法。Windows用户喜欢把一些程序列为可信任的,这些程序(比如IE、QQ)不受防火墙规则的限制。那么你只需要往这样的程序注入一小段代码,就可以让任意的程序悄悄绕过防火墙。这比直接修改防火墙规则要隐蔽多了。

上述方法适用于父进程和子进程存在父子关系的情况。如果不是父子关系,那么就得使用WSADuplicateSocket函数。source进程要先获知target进程的pid,然后通过WSADuplicateSocket函数得到一个WSAPROTOCOL_INFO结构体,然后自己想办法把WSAPROTOCOL_INFO这个结构体传递给target进程。

此博客中的热门博文

少写代码,多读别人写的代码

在windows下使用llvm+clang

tensorflow distributed runtime初窥