背景

第三届致理杯使用了一种全新的题目类型:PvP 题。PvP 题可以理解为选手之间互相下棋,并按照胜负情况确定自己的 rating ,再根据 rating 确定最终的得分。遗憾的是,由于选手们的时间有限,只有很少的选手选择深入研究这道题。

想要实现 PvP 的功能,需要三个进程之间互相通信: manager 进程, player0 进程, player1 进程。其中,player0 进程和 player1 进程分别为选手的进程,而 manager 进程负责判断选手操作的合法性,以及最终的输赢。这三个进程之间需要进行高频的通信,以保证评测的效率。与此同时,三个程序可能使用不同的编程语言,所以需要使用一个通用的通信接口实现。

Linux

在 Linux 系统下,一切都显得平静而美好。由于 Unix “一切皆文件” 的设计哲学,在 Linux 上实现进程间通信只需要简单地使用 mkfifo 建立一些管道,再让程序像普通文件一样打开它们,就可以实现进程间的通信。例如:

FILE *fifo_in, *fifo_out;
inline int read()
{
    int res = fscanf(fifo_in, "%d", &a);
    return a;
}
inline void output(int k)
{   
    fprintf(fifo_out, "%d\n",k);
    fflush(fifo_out);
}
int main(int argc, char **argv)
{
    fifo_in=fopen(argv[1],"r");
    fifo_out=fopen(argv[2],"w");
    // ...
    fclose(fifo_in);
    fclose(fifo_out);
    return 0;
}
#!/bin/bash

g++ -o manager manager.cpp
g++ -o player0 player0.cpp
g++ -o player1 player1.cpp

mkfifo /tmp/player0_out /tmp/player0_in /tmp/player1_out /tmp/player1_in
./player0 /tmp/player0_in /tmp/player0_out &
./player1 /tmp/player1_in /tmp/player1_out &
./manager /tmp/player0_out /tmp/player0_in /tmp/player1_out /tmp/player1_in
rm /tmp/player0_out /tmp/player0_in /tmp/player1_out /tmp/player1_in

Windows

由于我们的一些选手并没有使用 Linux 系统的经验,我们最终决定使用 Windows 作为选手计算机上的系统。而由于另一些原因,我们不希望给选手提供任何的第三方库,所以我们自己的程序也必须在不使用任何第三方库的前提下完成,这意味着我们不能使用第三方已封装好的接口,只能手动调用底层的 Windows API。

在 Windows 上,事情开始变得复杂而混乱。为了创造并使用一个管道(在 Windows 上,这被叫做一个 NamedPipe,命名管道),我们不得不去调用复杂的 Windows API。糟糕的是,由于在 Windows 下,一个命名管道与一个文件有很大的不同,我们不得不修改 managerstub (用于帮助选手的代码和 manager 通信) 的代码,来实现这样的功能。

C++

由于大部分 Windows API 由 C 实现,C++ 对 Windows API 的支持相对较好,只需要 #include <windows.h>,就可以找到所需要的 Windows API(例如创建一个 NamedPipe,连接到一个 NamedPipe)。下面是完整的代码实现:

NamedPipe.h

#ifndef NAMEDPIPE_H
#define NAMEDPIPE_H

#include <windows.h>
#include <iostream>
#include <string>

class NamedPipeServer {
private:
    HANDLE hPipe;
    std::string pipeName;

public:
    NamedPipeServer(const std::string& name);
    ~NamedPipeServer();

    bool waitForClient();
    int readNumber();
    void sendNumber(int number);
    void closeConnection();
};

class NamedPipeClient {
private:
    HANDLE hPipe;
    std::string pipeName;

public:
    NamedPipeClient(const std::string& name);
    ~NamedPipeClient();

    bool connectToServer();
    void sendNumber(int number);
    int readNumber();
    void closeConnection();
};

#endif // NAMEDPIPE_H

NamedPipe.cpp

#include "NamedPipe.h"

NamedPipeServer::NamedPipeServer(const std::string& name) {
    pipeName = "\\\\.\\pipe\\" + name;

    hPipe = CreateNamedPipeA(
        pipeName.c_str(),
        PIPE_ACCESS_DUPLEX,
        PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
        1,
        1024 * 16,
        1024 * 16,
        NMPWAIT_USE_DEFAULT_WAIT,
        NULL
    );

    if (hPipe == INVALID_HANDLE_VALUE) {
        std::cerr << "Failed to create named pipe. Error: " << GetLastError() << std::endl;
    } else {
        std::cout << "Server started on pipe: " << pipeName << std::endl;
    }
}

NamedPipeServer::~NamedPipeServer() {
    CloseHandle(hPipe);
}

bool NamedPipeServer::waitForClient() {
    return ConnectNamedPipe(hPipe, NULL) != FALSE;
}

int NamedPipeServer::readNumber() {
    int number;
    DWORD bytesRead;
    if (ReadFile(hPipe, &number, sizeof(int), &bytesRead, NULL) && bytesRead == sizeof(int)) {
        return number;
    }
    return -114515;
}

void NamedPipeServer::sendNumber(int number) {
    DWORD bytesWritten;
    WriteFile(hPipe, &number, sizeof(int), &bytesWritten, NULL);
}

void NamedPipeServer::closeConnection() {
    DisconnectNamedPipe(hPipe);
}

NamedPipeClient::NamedPipeClient(const std::string& name) {
    pipeName = "\\\\.\\pipe\\" + name;
    hPipe = INVALID_HANDLE_VALUE;
}

NamedPipeClient::~NamedPipeClient() {
    closeConnection();
}

bool NamedPipeClient::connectToServer() {
    hPipe = CreateFileA(
        pipeName.c_str(),
        GENERIC_READ | GENERIC_WRITE,
        0,
        NULL,
        OPEN_EXISTING,
        0,
        NULL
    );

    if (hPipe == INVALID_HANDLE_VALUE) {
        std::cerr << "Failed to connect to pipe: " << GetLastError() << std::endl;
        return false;
    }
    std::cout << "Connected to server on pipe: " << pipeName << std::endl;
    return true;
}

void NamedPipeClient::sendNumber(int number) {
    DWORD bytesWritten;
    WriteFile(hPipe, &number, sizeof(int), &bytesWritten, NULL);
}

int NamedPipeClient::readNumber() {
    int number;
    DWORD bytesRead;
    if (ReadFile(hPipe, &number, sizeof(int), &bytesRead, NULL) && bytesRead == sizeof(int)) {
        return number;
    }
    return -1;
}

void NamedPipeClient::closeConnection() {
    if (hPipe != INVALID_HANDLE_VALUE) {
        CloseHandle(hPipe);
        hPipe = INVALID_HANDLE_VALUE;
    }
}

server.cpp

#include "NamedPipe.h"
#include <string>

int main() {
    std::string pipeName;
    std::cout << "Enter pipe name for server: ";
    std::cin >> pipeName;

    NamedPipeServer server(pipeName);

    while (true) {
        if (server.waitForClient()) {
            std::cout << "Client connected on pipe: " << pipeName << std::endl;

            while (true) {
                int number = server.readNumber();
                if (number == -1) {
                    std::cout << "Client disconnected." << std::endl;
                    break;
                }
                std::cout << "Received number: " << number << std::endl;

                int response = number * 2;
                server.sendNumber(response);
                std::cout << "Sent response: " << response << std::endl;
            }

            server.closeConnection();
        }
    }

    return 0;
}

client.cpp

#include "NamedPipe.h"
#include <string>

int main() {
    std::string pipeName;
    std::cout << "Enter pipe name for client: ";
    std::cin >> pipeName;

    NamedPipeClient client(pipeName);

    if (!client.connectToServer()) {
        return 1;
    }

    while (true) {
        int number;
        std::cout << "Enter an integer to send (-1 to exit): ";
        std::cin >> number;

        client.sendNumber(number);

        if (number == -1) {
            break;
        }

        int response = client.readNumber();
        if (response != -1) {
            std::cout << "Server response: " << response << std::endl;
        } else {
            std::cout << "Error receiving response from server." << std::endl;
            break;
        }
    }

    client.closeConnection();
    return 0;
}

Python3

为了吸引更多的选手参赛,我们还为比赛增加了 Python3 支持。遗憾的是,只有 2 名来自线上的选手使用 Python3 提交了代码,而且他们的代码看起来更像是由 AI 生成,无法正常运行。(由于Python3 解释型语言的特性,无法正常运行的代码也为我们带来了一些麻烦)

Python3 自带的 multiprocessing 库可以实现 Python3 代码之间的通信,但由于 manager 由 C++ 编写,我们无法使用这种方案。同时,Python3 的一个第三方库 pywin32 简化了调用 Windows API 的过程,但由于前文提到过的原因,我们无法使用这个第三方库。我们的解决方案是,使用 Python3 自带的 ctypes 库调用 Widows 下的 kernel32.dll 中的函数,以此来调用 Windows API 。完整的代码实现如下:

named_pipe.py

import ctypes
import ctypes.wintypes
import struct
import time

kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)

PIPE_ACCESS_DUPLEX = 0x00000003
PIPE_TYPE_BYTE = 0x00000000
PIPE_READMODE_BYTE = 0x00000000
PIPE_WAIT = 0x00000000
GENERIC_READ = 0x80000000
GENERIC_WRITE = 0x40000000
OPEN_EXISTING = 3

class NamedPipeServer:
    def __init__(self, name):
        self.pipe_name = rf"\\.\pipe\{name}"
        self.pipe = None

    def start(self):
        self.pipe = kernel32.CreateNamedPipeW(
            self.pipe_name,
            PIPE_ACCESS_DUPLEX,
            PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
            1, 4, 4, 0, None
        )

        if self.pipe == -1:
            raise RuntimeError("Failed to create named pipe")

        print(f"Waiting for client on {self.pipe_name}...")
        kernel32.ConnectNamedPipe(self.pipe, None)
        print("Client connected!")

    def read_number(self):
        buffer = ctypes.create_string_buffer(4)
        bytes_read = ctypes.wintypes.DWORD()

        success = kernel32.ReadFile(self.pipe, buffer, 4, ctypes.byref(bytes_read), None)
        if not success:
            raise RuntimeError("Failed to read from pipe")

        return struct.unpack("i", buffer.raw)[0]

    def send_number(self, number):
        data = struct.pack("i", number)
        bytes_written = ctypes.wintypes.DWORD()

        success = kernel32.WriteFile(self.pipe, data, 4, ctypes.byref(bytes_written), None)
        if not success:
            raise RuntimeError("Failed to write to pipe")

    def close(self):
        kernel32.CloseHandle(self.pipe)

class NamedPipeClient:
    def __init__(self, name):
        self.pipe_name = rf"\\.\pipe\{name}"

    def connect(self):
        while True:
            self.pipe = kernel32.CreateFileW(
                self.pipe_name,
                GENERIC_READ | GENERIC_WRITE,
                0, None,
                OPEN_EXISTING,
                0, None
            )
            if self.pipe != -1:
                print(f"Connected to {self.pipe_name}")
                return
            time.sleep(1)

    def send_number(self, number):
        data = struct.pack("i", number)
        bytes_written = ctypes.wintypes.DWORD()

        success = kernel32.WriteFile(self.pipe, data, 4, ctypes.byref(bytes_written), None)
        if not success:
            raise RuntimeError("Failed to write to pipe")

    def read_number(self):
        buffer = ctypes.create_string_buffer(4)
        bytes_read = ctypes.wintypes.DWORD()

        success = kernel32.ReadFile(self.pipe, buffer, 4, ctypes.byref(bytes_read), None)
        if not success:
            raise RuntimeError("Failed to read from pipe")

        return struct.unpack("i", buffer.raw)[0]

    def close(self):
        kernel32.CloseHandle(self.pipe)

server.py

from named_pipe import NamedPipeServer

pipe_name = input("Enter pipe name for server: ")
server = NamedPipeServer(pipe_name)

server.start()

while True:
    number = server.read_number()
    print(f"Received number: {number}")

    response = number * 2
    server.send_number(response)
    print(f"Sent response: {response}")

client.py

from named_pipe import NamedPipeClient

pipe_name = input("Enter pipe name for client: ")
client = NamedPipeClient(pipe_name)

client.connect()

while True:
    number = int(input("Enter an integer (-1 to exit): "))
    client.send_number(number)

    if number == -1:
        break

    response = client.read_number()
    print(f"Server response: {response}")

client.close()

Java

由于一位同学向我们提出了支持 Java 的请求,我们同样加入了对 Java 代码的支持。非常遗憾的是,整场比赛中共有 0 人使用 Java 提交了代码。

不幸的是, Java 对 Windows API 的支持同样糟糕。不过,Java 可以原生调用 C 代码,这使得我们找到了一种替代的解决方案:用 Java 调用 C 代码,再用 C 代码调用 Windows API。以下是完整的代码实现:

Kernel32Native.c

#include <windows.h>
#include <jni.h>
#include <stdio.h>

JNIEXPORT jlong JNICALL Java_NamedPipeServer_createNamedPipe(JNIEnv *env, jobject obj, jstring name) {
    const char *pipeName = (*env)->GetStringUTFChars(env, name, 0);
    HANDLE pipe = CreateNamedPipeA(pipeName, PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
                                   1, 4, 4, 0, NULL);
    (*env)->ReleaseStringUTFChars(env, name, pipeName);
    return (jlong) pipe;
}

JNIEXPORT jboolean JNICALL Java_NamedPipeServer_connectPipe(JNIEnv *env, jobject obj, jlong handle) {
    return ConnectNamedPipe((HANDLE) handle, NULL);
}

JNIEXPORT jboolean JNICALL Java_NamedPipeServer_readFile(JNIEnv *env, jobject obj, jlong handle, jbyteArray buffer) {
    jbyte *buf = (*env)->GetByteArrayElements(env, buffer, NULL);
    DWORD bytesRead;
    BOOL success = ReadFile((HANDLE) handle, buf, 4, &bytesRead, NULL);

    (*env)->ReleaseByteArrayElements(env, buffer, buf, 0);
    return success;
}

JNIEXPORT jboolean JNICALL Java_NamedPipeServer_writeFile(JNIEnv *env, jobject obj, jlong handle, jbyteArray data) {
    jbyte *buf = (*env)->GetByteArrayElements(env, data, NULL);
    DWORD bytesWritten;
    BOOL success = WriteFile((HANDLE) handle, buf, 4, &bytesWritten, NULL);

    (*env)->ReleaseByteArrayElements(env, data, buf, 0);
    return success;
}

JNIEXPORT jboolean JNICALL Java_NamedPipeServer_closeHandle(JNIEnv *env, jobject obj, jlong handle) {
    return CloseHandle((HANDLE) handle);
}

JNIEXPORT jlong JNICALL Java_NamedPipeClient_createFile(JNIEnv *env, jobject obj, jstring name) {
    const char *pipeName = (*env)->GetStringUTFChars(env, name, 0);
    HANDLE pipe = CreateFileA(pipeName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
    (*env)->ReleaseStringUTFChars(env, name, pipeName);

    if (pipe == INVALID_HANDLE_VALUE) {
        return -1; 
    }

    return (jlong) pipe;
}

JNIEXPORT jboolean JNICALL Java_NamedPipeClient_readFile(JNIEnv *env, jobject obj, jlong handle, jbyteArray buffer) {
    jbyte *buf = (*env)->GetByteArrayElements(env, buffer, NULL);
    DWORD bytesRead;
    BOOL success = ReadFile((HANDLE) handle, buf, 4, &bytesRead, NULL);

    (*env)->ReleaseByteArrayElements(env, buffer, buf, 0);
    return success;
}

JNIEXPORT jboolean JNICALL Java_NamedPipeClient_writeFile(JNIEnv *env, jobject obj, jlong handle, jbyteArray data) {
    jbyte *buf = (*env)->GetByteArrayElements(env, data, NULL);
    DWORD bytesWritten;
    BOOL success = WriteFile((HANDLE) handle, buf, 4, &bytesWritten, NULL);

    (*env)->ReleaseByteArrayElements(env, data, buf, 0);
    return success;
}

JNIEXPORT jboolean JNICALL Java_NamedPipeClient_closeHandle(JNIEnv *env, jobject obj, jlong handle) {
    return CloseHandle((HANDLE) handle);
}

NamedPipeServer.java

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class NamedPipeServer {
    static {
        System.loadLibrary("Kernel32Native");
    }

    private final String pipeName;
    private long pipeHandle;

    public NamedPipeServer(String name) {
        this.pipeName = "\\\\.\\pipe\\" + name;
    }

    public native long createNamedPipe(String name);
    public native boolean connectPipe(long handle);
    public native boolean readFile(long handle, byte[] buffer);
    public native boolean writeFile(long handle, byte[] data);
    public native boolean closeHandle(long handle);

    public void start() {
        pipeHandle = createNamedPipe(pipeName);
        if (pipeHandle == -1) {
            throw new RuntimeException("Failed to create named pipe");
        }
        System.out.println("Waiting for client on: " + pipeName);
        if (!connectPipe(pipeHandle)) {
            throw new RuntimeException("Failed to connect pipe");
        }
        System.out.println("Client connected!");
    }

    public int readNumber() {
        byte[] buffer = new byte[4];
        if (!readFile(pipeHandle, buffer)) {
            throw new RuntimeException("Failed to read from pipe");
        }
        return ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).getInt();
    }

    public void sendNumber(int number) {
        byte[] data = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(number).array();
        if (!writeFile(pipeHandle, data)) {
            throw new RuntimeException("Failed to write to pipe");
        }
    }

    public void close() {
        closeHandle(pipeHandle);
    }
}

NamedPipeClient.java

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class NamedPipeClient {
    static {
        System.loadLibrary("Kernel32Native");
    }

    private final String pipeName;
    private long pipeHandle;

    public NamedPipeClient(String name) {
        this.pipeName = "\\\\.\\pipe\\" + name;
    }

    public native long createFile(String name);
    public native boolean readFile(long handle, byte[] buffer);
    public native boolean writeFile(long handle, byte[] data);
    public native boolean closeHandle(long handle);

    public void connect() {
        while (true) {
            pipeHandle = createFile(pipeName);
            if (pipeHandle != -1) {
                System.out.println("Connected to " + pipeName);
                break;
            }
            try { Thread.sleep(500); } catch (InterruptedException ignored) {}
        }
    }

    public void sendNumber(int number) {
        byte[] data = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(number).array();
        if (!writeFile(pipeHandle, data)) {
            throw new RuntimeException("Failed to write to pipe");
        }
    }

    public int readNumber() {
        byte[] buffer = new byte[4];
        if (!readFile(pipeHandle, buffer)) {
            throw new RuntimeException("Failed to read from pipe");
        }
        return ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).getInt();
    }

    public void close() {
        closeHandle(pipeHandle);
    }
}

ServerMain.java

import java.util.Scanner;

public class ServerMain {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.print("Enter pipe name for server: ");
        String pipeName = scanner.nextLine();

        NamedPipeServer server = new NamedPipeServer(pipeName);
        server.start();

        while (true) {
            int number = server.readNumber();
            System.out.println("Received number: " + number);

            int response = number * 2;
            server.sendNumber(response);
            System.out.println("Sent response: " + response);
        }
    }
}

ClientMain.java

import java.util.Scanner;

public class ClientMain {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.print("Enter pipe name for client: ");
        String pipeName = scanner.nextLine();

        NamedPipeClient client = new NamedPipeClient(pipeName);
        client.connect();

        while (true) {
            System.out.print("Enter an integer (-1 to exit): ");
            int number = scanner.nextInt();
            client.sendNumber(number);

            if (number == -1) {
                break;
            }

            int response = client.readNumber();
            System.out.println("Server response: " + response);
        }

        client.close();
    }
}

使用时需要先将 Kernel32Native.c 编译为 Kernel32Native.dll,编译指令为:

gcc -shared -o Kernel32Native.dll -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" Kernel32Native.c -lKernel32


为天地立心,为生民立命,为往圣继绝学,为万世开太平。