在 Windows 下编写多人游戏

· · 科技·工程

0 前言

作为一个不止步于单人游戏的 C++ 游戏制作者,学习网络通信是必然的虽然我也没有写过多少游戏。为了帮助大家更好的发挥想象力,不被游玩人数限制,特地编写了该文章。\ (本文是一个以聊天软件为例子、以游戏软件为目的的文章,所以一会聊天一会游戏的,勿喷)

1 准备工作

其实者没有什么好准备的,一般情况下,你只要用的是 Windows 系统和 DEV-C++,就不会有什么需要另外下载的头文件或库文件。

为了编写相关代码,你需要导入一些专用于网络通信的头文件,就像这样:

#include<winsock2.h>
#include<ws2tcpip.h>

除此之外,你要需要连接库文件,方法就像这样:

#pragma comment(lib, "Ws2_32.lib")

2 初始化

现在,你要开始编写你的代码了,它分为客户端和服务端,下面的代码是两者共用的代码:

WSADATA wsa_data;
WSAStartup(MAKEWORD(2,2),&wsa_data);
SOCKET sock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
sockaddr_in server_addr;
server_addr.sin_family = AF_INET;

你需要把它放在主函数的最开头。\ 接着,如果是服务端,你就需要设置监听端口,方法就像下面这样:

int port;
cin>>port;
server_addr.sin_port = htons(port);
server_addr.sin_addr.s_addr = INADDR_ANY;

对于客户端,在设置连接端口的同时,要需要设置连接地址,方法见下:

cin>>addr;//string
cin>>port;//int
server_addr.sin_port = htons(port);
server_addr.sin_addr.s_addr = inet_addr(addr.c_str());

然后呢,对于服务器你还需要绑定套接字才能监听。

bind(sock,(sockaddr*)&server_addr,sizeof(server_addr));

3 编写服务器

你只要使用一行代码,你的服务器就能开始运行,监听客户端发来的连接请求。

listen(sock,100);

其中 100 是最大连接数。

然而,你只是监听了客户端的请求,为了实际创建连接,你不仅仅要监听,还要同意。\ 我们不妨写一个 while(true),不断地使用 accept() 函数同意所有连接请求。 就像这样:

while(true){
    sockaddr_in client_addr;
    int client_addr_len = sizeof(client_addr);
    SOCKET client_sock = accept(sock,(sockaddr*)&client_addr,&client_addr_len);
}

不幸的是,appect() 函数有阻滞效果,只有等到下一个请求才会同意连接并运行接下来的代码,所以这么操作会导致服务器卡死,几乎无法运行任何游戏逻辑。这时我们需要用到多线程。

4 多线程

为了使用多线程,需要导入一个头文件:

#include<thread>

我们可以定义一个用户函数 void user(SOCKET sock),用户发来的发来的数据包在这里被处理。

回到主函数,我们需要为所有接受的连接都建立一个新的线程。\ 为了完成此功能,我们要用到:

thread t(user,client_sock); 

这表示建立一个新的叫做 t 的线程,线程执行 user 函数,并以 client_sock 作为函数的参数。

我们不妨假设我们正在编写一个聊天服务器,需要将所有受到的消息转发给其它所有客户端。

我们的 user 函数就可以这么写:

void user(SOCKET sock){
    char recv_data[1024];
    while(recv(sock,recv_data,1024,0) > 0){
        for(int i = 0;i < socks.size();i++){
            if(socks[i] == sock) continue;
            send(socks[i],recv_data,1024,0);
        }
    }
    socks.erase(find(socks.begin(),socks.end(),sock));
    closesocket(sock);
}

解释一下上述代码:我们通过定义一个字符数组 char recv_data[1024] 来接收和发送数据;随后,我们编写了一个循环,每当受到一个消息或断开连接时运行接下来的内容。 recv() 的返回值是受到的字节大小。要是其小于等于 0 ,就说明连接断开,需要通过 socks.erase(find(socks.begin(),socks.end(),sock)) 删除连接记录,并利用closesocket(sock)从服务端断开连接;否则就要遍历连接列表 socks 并向其它客户端转发消息。

如你所见,我们可以通过 recv() 接收数据,send() 发送数据。

如你所知,recv() 有阻滞效果,所以客户端也需要多线程。 \ 你需要创建一个新的线程用于读取网络数据,在这里,我们直接原样输出,且在主线程读入数据并发送。

void net_read(SOCKET sock){
    char recv_data[1024];
    while(true){
        while(recv(sock,recv_data,1024,0) > 0){
            cout<<recv_data<<endl;
        }
    }
}

主线程太简单,懒得写了。

7 最终示例代码

我们添加了少量提示性文本,与上述代码可能略有不同。

服务端

#include<bits/stdc++.h>
#include<windows.h>
#include<thread>
#include<winsock2.h>
#include<ws2tcpip.h>
#pragma comment(lib,"Ws2_32.lib")
using namespace std;

vector<SOCKET> socks;
SOCKET clientSock;
int port;

void user(SOCKET sock){
    bool flag = false;
    char recv_data[1024];
    while(recv(sock,recv_data,1024,0) > 0){
        for(int i = 0;i < socks.size();i++){
            if(socks[i] == sock) continue;
            send(socks[i],recv_data,1024,0);
        }
    }
    socks.erase(find(socks.begin(),socks.end(),sock));
    closesocket(sock);
}

int main(){
    WSADATA wsa_data;
    WSAStartup(MAKEWORD(2,2),&wsa_data);
    SOCKET sock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
    sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    cout<<"在这之前,请给定一个端口号:"<<endl;
    cin>>port;
    server_addr.sin_port = htons(port);
    server_addr.sin_addr.s_addr = INADDR_ANY;
    if(bind(sock,(sockaddr*)&server_addr,sizeof(server_addr)) == SOCKET_ERROR){
        cerr<<"哥们你端口被占了,要不换一个?"<<endl;
        system("pause");
        return -1;
    }
    listen(sock,100);
    while(true){
        sockaddr_in client_addr;
        int client_addr_len = sizeof(client_addr);
        SOCKET client_sock = accept(sock,(sockaddr*)&client_addr,&client_addr_len);
        socks.push_back(client_sock);
        thread t(user,client_sock);
        t.detach();
    }
}

客户端

#include<bits/stdc++.h>
#include<windows.h>
#include<thread>
#include<winsock2.h>
#include<ws2tcpip.h>
#pragma comment(lib,"Ws2_32.lib")
using namespace std;

string addr;
int port;

void net_read(SOCKET sock){
    char recv_data[1024];
    while(true){
        while(recv(sock,recv_data,1024,0) > 0){
            cout<<recv_data<<endl;
        }
    }
}

int main(){
    cout<<"请指定你要连接的服务器的IP(仅IPv4):"<<endl;
    cin>>addr;
    cout<<"请指定你要连接的服务器的端口:"<<endl;
    cin>>port;
    WSADATA wsa_data;
    WSAStartup(MAKEWORD(2, 2), &wsa_data);
    SOCKET sock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);  
    sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port);
    server_addr.sin_addr.s_addr = inet_addr(addr.c_str());
    if(connect(sock, (sockaddr*)&server_addr, sizeof(server_addr)) == SOCKET_ERROR){
        cerr<<"无法连接至服务器";
        system("pause");
        return -1;
    }
    thread t(net_read,sock);
    char send_data[1024];
    while(true){
        cin>>send_data;
        send(sock,send_data,1024,0);
    }
    return 0;
}

8 结语

上述的代码只是一个简单的例子,BUG 很多,主要目的是让大家知道 C++ 网络通信的一些基本操作,具体的内容还需大家发挥想象力自由编写。

希望本文对你有帮助。