如何在 code-server 上使用 CPH-NG 与 Competitive Companion
前言
本文使用环境:本机 Windows 11 专业版,code-server 服务器 Ubuntu 20.04.6 LTS。
作为 OIER,我经常会切换电脑,用移动盘存储携带不方便,正好手里有一台 Ubuntu 的服务器,我就用上了 code-server,web 版 VScode。如何安装 code-server
最开始我用的也是 CPH,但是发现 CPH 还要手动复制样例,大数据复制很卡,确实体验不好,在网上一翻,找到了 CPH-NG,有更强大的功能,可以通过 Competitive Companion 导入数据。Chrome 安装 FireFox 安装 (官方没有给出 Edge 的安装链接)
本地操作
只不过 Competitive Companion 插件似乎只能导入到本地 VScode,去看了一下实现原理,实际上是通过端口发送题目信息的 json 到本地来实现快速导入,于是我们想到了端口转发。这里我们以 21257 作为示例。
这就需要本地有一个转发程序了。
我们首先需要在浏览器中设置 Competitive Companion 的转发端口,点击扩展右侧的三个点,点击选项,Custom ports 那里输入你自定的端口,只不过一定要先看一看这个端口是否有被占用。
在本机打开 PowerShell,输入:
netstat -ano | findstr 21257
如果显示 >> 那就再按一次回车。
看显示内容,如果显示类似:
TCP 0.0.0.0:21257 0.0.0.0:0 LISTENING [PID(一串数字)]
那就说明当前的端口有程序正在占用,如果确定不需要该程序,可以运行下面的代码关闭进程:
taskkill /PID [PID(刚刚那串数字)] /F
如果不确定该程序是否需要,建议切换端口。
确定好端口之后,我们要在本地创建一个端口转发的脚本,这里以 c++ 为例,编译器 g++。
如果本机和 code-server 服务器在同一个内网中,你需要获取到 code-server 的内网 ip,可以在服务器终端输入:
ip addr
会看到类似:
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP>
inet 192.168.1.105/24 brd 192.168.1.255 scope global eth0
的内容,其中 192.168.1.105 服务器的内网 IP。
在你的转发脚本中定义:
#define TARGET_IP "server-ip"
#define TARGET_PORT POST // 你需要的端口
并定义转发函数:
void forward_json(const std::string& json) {
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(TARGET_PORT);
addr.sin_addr.s_addr = inet_addr(TARGET_IP);
// 如果这里报错,可以将这一行替换为下面的代码
// inet_pton(AF_INET, TARGET_IP, &addr.sin_addr);
if (connect(s, (sockaddr*)&addr, sizeof(addr)) != 0) {
std::cerr << "Forward failed\n";
closesocket(s);
return;
}
std::string req =
"POST / HTTP/1.1\r\n"
"Host: " TARGET_IP "\r\n"
"Content-Type: application/json\r\n"
"Content-Length: " + std::to_string(json.size()) + "\r\n\r\n" +
json;
send(s, req.c_str(), req.size(), 0);
closesocket(s);
std::cout << "Forwarded to " TARGET_IP << ":" << TARGET_PORT << "\n";
}
如果本机和 code-server 服务器不在同一个内网中,你需要服务器的外网 IP(既然能用上 code-server,相信还是有外网 ip 的吧,或者 cloudflared 隧道转发也可以),我这里用域名代替了。
要在脚本中定义:
#define TARGET_HOST "your server ip or domain"
#define TARGET_PORT "POST" // 注意这里的两项都需要打引号
并添加函数:
void forward_json(const std::string& json) {
addrinfo hints{}, *res = nullptr;
hints.ai_family = AF_INET; // IPv4
hints.ai_socktype = SOCK_STREAM; // TCP
int ret = getaddrinfo(
TARGET_HOST,
TARGET_PORT,
&hints,
&res
);
if (ret != 0 || !res) {
std::cerr << "DNS resolve failed: " << gai_strerrorA(ret) << "\n";
return;
}
SOCKET s = socket(
res->ai_family,
res->ai_socktype,
res->ai_protocol
);
if (connect(s, res->ai_addr, (int)res->ai_addrlen) != 0) {
std::cerr << "Connect failed\n";
freeaddrinfo(res);
closesocket(s);
return;
}
std::string req =
"POST / HTTP/1.1\r\n"
"Host: " TARGET_HOST "\r\n"
"Content-Type: application/json\r\n"
"Content-Length: " + std::to_string(json.size()) + "\r\n\r\n" +
json;
send(s, req.c_str(), (int)req.size(), 0);
freeaddrinfo(res);
closesocket(s);
std::cout << "Forwarded to " TARGET_HOST ":" << TARGET_PORT << "\n";
}
接下来,无论使用了内网 ip 还是公网 ip(域名),都是一样的了,引入头文件和 lib 库。
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iostream>
#include <string>
#pragma comment(lib, "ws2_32.lib")
主函数:
int main() {
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
SOCKET server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = INADDR_ANY;
bind(server, (sockaddr*)&addr, sizeof(addr));
listen(server, 5);
std::cout << "Listening on " << TARGET_POST << "...\n";
while (true) {
SOCKET client = accept(server, nullptr, nullptr);
std::cout << "Client connected\n";
std::string data;
char buf[4096];
int len;
while ((len = recv(client, buf, sizeof(buf), 0)) > 0) {
data.append(buf, len);
}
// 提取 HTTP Body(JSON)
size_t pos = data.find("\r\n\r\n");
if (pos != std::string::npos) {
std::string json = data.substr(pos + 4);
std::cout << "===== JSON =====\n";
std::cout << json << "\n";
std::cout << "===============\n";
forward_json(json);
}
// 必须回应 200,否则插件会重试
const char* ok =
"HTTP/1.1 200 OK\r\n"
"Content-Length: 0\r\n\r\n";
send(client, ok, strlen(ok), 0);
closesocket(client);
}
WSACleanup();
return 0;
}
注意:需要添加 -lws2_32 编译参数。
如果编译运行后显示 Listening on [POST]...,那么说明前面的步骤成功了。
本地测试
打开一道题目,在扩展列表中点击 Competitive Companion,等待页面蓝色加载条完成,查看本地运行脚本的终端,如果出现类似:
===== JSON =====
{"name":"A - XXX","group":"Virtual Judge - XXX性","url":"https://vjudge.net/contest/XXX#problem/A","interactive":false,"memoryLimit":1024,"timeLimit":1000,"tests":[{"input":"5\n5 1 2 3 4\n0\n6\n2 1 3\n5 4 6 2\n0\n0\n","output":"1\n2\n"}],"testType":"single","input":{"type":"stdin"},"output":{"type":"stdout"},"languages":{"java":{"mainClass":"Main","taskClass":"XXX"}},"batch":{"id":"03112856-30ee-4e26-XXX-1721f38320fb","size":1}}
===============
格式的内容,说明我们的脚本可以成功接收消息了。
如果运行终端继续显示
Forwarded to [HOST]:[POST]
就说明成功转发了。
此时本地操作已经完成,测试成功。
服务器操作
服务器操作就没那么复杂了,只不过还是需要先判断是否端口被占用。(这里服务器使用了 Ubuntu,如果是 Windows,参见上面本地操作中端口占用情况的部分)
输入:
lsof -i:[POST]
如果有一行类似:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node [PID] USER 34u IPv6 XXXXX 0t0 TCP *:[POST] (LISTEN)
不用杀,一般是 CPH-NG 插件的监听,如果有其他,建议关闭(kill -9 [PID])或者换端口。
接下来在 code-server 中找到 Cph-ng › Companion: Listen Port 这项参数,将端口填成你转换到服务器的那个端口,最好重启一下插件(说一个方便的方法,把上面 lsof -i 命令拿到的 CPH-NG 的进程 PID 杀了,code-server 会自动重启插件),保证本地的转发一直在线,便可以使用啦。
后记
实际上 Competitive Companion 直接自动创建的文件不一定在我们想要的位置,文件名也不会因为 Contest 而改变,最好的办法是自定义(既然我们都拿到 json 了),只不过篇幅问题,而且改内容不在本文主要内容范围内,敬请期待下一篇博客。
使用过程中如遇问题,欢迎邮件联系 [email protected]。 不一定每封都能回复,但会尽量看。