跳转至

01-联机游戏开发基础与HTTP服务器搭建

1 网络拓扑结构

我们先了解在联机游戏中最常见的网络拓扑结构,星型拓扑。
在这种设计下,服务器程序作为世界的权威,实际运行和掌握着游戏世界的状态数据。众多客户端程序都与这个中心服务器进行连接,从中获取最新的世界状态数据。然后在本地根据客户端预先设计好的逻辑,将这些内容渲染到窗口上。
也就是说在这种模式下,客户端相当于是一个接入到网络的显示器和手柄。进处理画面显示和玩家输入的操作。而整个世界的运转逻辑被拆分到了服务端程序上。
在一般情况下,服务端只需要关注数据逻辑,而不需要关注使用何种素材进行画面渲染的客户端功能。
在一些支持客户端创建服务器的游戏中,也是相似的道理。对于这些游戏,我们可以简单理解为客户端和服务端绑定在一起,游戏房间运行时使用房主的服务端程序作为权威服务器。

2 域名 IP 端口

我们知道,要想在网络世界中发送数据包,那就必然需要知晓其目的地。而IP就是用来描述网络地址的符号。而端口可以看作是在一台设备上的多个网络数据入口。不同的应用程序从不同的端口上收发数据。

域名,也就是常说的“网址”。这些网址其实可以看作是IP的“别名”。网络通信时,会借助DNS服务器从域名得到IP。DNS服务器相当于维护着一张巨大的map映射表。记录着域名到IP的映射关系,这样我们的数据包就可以从本地出发,发送到对应的IP地址。然后从指定的端口进入设备。交付给监听此处端口的程序,完成网络通信。

HTTP,日常生活中所见的网页网址基本都以HTTP开头,它的全称是超文本传输协议,HyperText Transfer Protocol。
这里的协议,通俗的讲就是“规范”或者“约定”。网络数据包的传输是二进制结构。我们要怎么让服务器和客户端明白这个二进制数据包中,每一部分都是什么含义呢?
协议描述的就是这部分内容。HTTP的理论知识细节有很多,我们目前只需要关注两个概念:请求方法和状态码。

首先是请求方法,HTTP协议在通信时,笼统而言可以由两部分组成:请求和响应。请求便是客户端向服务端使用不同的请求方法发送数据的过程。而响应则是服务端在接受并处理后向客户端返回数据的过程。

请求方法有很多,分别有着不同的含义和应用场景。在本项目中我们只需要关注POST请求即可。


Pasted image 20260314194851.png

而状态码,则是用来描述服务端对客户端的响应结果。在本项目中,我们只需要检测状态码是否为200即可,200状态码意味着HTTP请求成功。


Pasted image 20260314194927.png

下面我们便可以开始http服务器的搭建了
在本项目中,我们使用了cpp-httplib这个三方库。来帮助我们处理HTTP相关的内容。这时一个单头文件的库,我们只需要引入一个.h头文件即可。
github仓库的连接 https://github.com/yhirose/cpp-httplib

3 简单程序

我们在项目目录下新建三个文件夹分别是client,server和thirdparty。
在thirdparty中放入之前下载的httplib.h
在client和server中分别新建client.cpp和server.cpp

在客户端代码client.cpp中,我们连接到本地端口25565,发送hello路由的消息请求。并打印输出服务器响应的内容

#include "../thirdparty/httplib.h"

int main(int argc, char** argv)
{
    httplib::Client client("localhost:25565");

    httplib::Result result = client.Post("/hello");
    if (!result || result->status != 200)
    {
        std::cout << "Hello Failed!" << std::endl;

        return -1;
    }

    std::cout << result->body << std::endl;

    system("pause");

    return 0;
}

在服务端代码server.cpp中,我们创建了服务器对象,设置请求的路由为hello时执行的回调函数。在lambda回调函数中,我们设置了响应的内容为纯文本形式的“Hello From Server!”。最后让服务端监听本地的25565端口。

#include "../thirdparty/httplib.h"

int main(int argc, char** argv)
{
    httplib::Server server;

    server.Post("/hello", [&](const httplib::Request& req, httplib::Response& res)
        {
            std::cout << "Hello from Client!" << std::endl;

            res.set_content("Hello From Server!", "text/plain");
        });

    server.listen("localhost", 25565);

    return 0;
}

4 配置CMake


Pasted image 20260314225237.png

首先在workspaceFolder新建一个全局的CMakeLists.txt, 其中配置头文件目录,并且设置server和client两个子目录。这里设置的环境变量都会被子目录的CMakeLists.txt继承

cmake_minimum_required(VERSION 3.10)
project(hakimi_adventure)

set(CMAKE_CXX_STANDARD 17)

add_compile_options(-finput-charset=UTF-8 -fexec-charset=GBK)

# 设置EasyX路径
set(EasyX_INCLUDE_DIR "C:/msys64/mingw64/include")
set(EasyX_LIB_DIR "C:/msys64/mingw64/lib")

set(EasyX_LIB_PATH "${EasyX_LIB_DIR}/libeasyx.a")

# 包含头文件目录
include_directories(${EasyX_INCLUDE_DIR})

include_directories(${PROJECT_SOURCE_DIR}/include)

add_subdirectory(server)
add_subdirectory(client)

在client/CMakeLists.txt中,编写下述代码

# 编译客户端可执行文件
add_executable(client client.cpp)

# 如果客户端也用到网络/图形库,链接相同的库;如果不用 EasyX,可删除对应项
target_link_libraries(client PRIVATE
    ${EasyX_LIB_PATH}          # 若客户端无图形界面,删除此行
    ws2_32                     # 必须保留(httplib 网络依赖)
    winmm                      # 若客户端无图形界面,删除此行
    msimg32                    # 若客户端无图形界面,删除此行
    pthread                    # 必须保留(httplib 线程依赖)
)

在server/CMakeLists.txt中,编写下述代码

# 编译服务端可执行文件
add_executable(server server.cpp)

# 链接必要的库(核心修正:添加 ws2_32 解决网络函数缺失)
target_link_libraries(server PRIVATE
    ${EasyX_LIB_PATH}          # EasyX 图形库
    ws2_32                     # Windows 套接字库(解决 setsockopt 错误)
    winmm                      # 多媒体库(EasyX 依赖)
    msimg32                    # 图形相关库(EasyX 依赖)
    pthread                    # 线程库(httplib 依赖)
)

为了让运行时,能够同时运行server和client两个程序,我们在.vscode中新建一个launch.json,
{
    // 使用 IntelliSense 了解相关属性。 
    // 悬停以查看现有属性的描述。
    // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Run Server",
            "type": "cppvsdbg",
            "request": "launch",
            "program": "${workspaceFolder}/build/server/server.exe",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "console": "externalTerminal",
        },

        {
            "name": "Run Client",
            "type": "cppvsdbg",
            "request": "launch",
            "program": "${workspaceFolder}/build/client/client.exe",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "console": "externalTerminal"
        }
    ],
    "compounds": [
        {
            "name": "Run Server & Client",
            "configurations": ["Run Server", "Run Client"],
            "stopAll": true
        }
    ]
}


Pasted image 20260314225903.png

之后在vscode的“运行和调试(ctrl shift D)”中可以看到我们定义的启动配置,选择Run Server & Client,点击绿色的三角,就可以开始运行了。

5 测试


Pasted image 20260314230134.png

在vscode的左下角可以看到我们的启动配置,以及Cmake tools插件的“生成”按钮。点击“生成”可以对整个项目进行编译,点击“Run Server & Client”可以同时运行server和client两个程序
编译运行,可以看到最终结果符合我们的预期


Pasted image 20260314230437.png

评论

如果你已登录 GitHub,就可以直接在这里评论。