背景
第三届致理杯使用了一种全新的题目类型: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 下,一个命名管道与一个文件有很大的不同,我们不得不修改 manager
与 stub
(用于帮助选手的代码和 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