在 Windows 下编写多人游戏
b9113fced86a32cad0d8 · · 科技·工程
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
作为函数的参数。
- 注意:在实际编写中,建议使用
mutex
保护资源,避免数据竞争,这里为了简洁,不再编写。 - 更要注意:在这之后,必须通过
t
的detach
方法对线程进行分离,否则你就等着 RE 吧。5 完成服务器
(为了方便管理,建议增加一个
vector
,用来记录所有的客户端的套接字描述符即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()
的返回值是受到的字节大小。要是其小于等于 socks.erase(find(socks.begin(),socks.end(),sock))
删除连接记录,并利用closesocket(sock)
从服务端断开连接;否则就要遍历连接列表 socks
并向其它客户端转发消息。
如你所见,我们可以通过 recv()
接收数据,send()
发送数据。
- 注意:因为
recv()
也有阻滞效果,所以游戏逻辑需要一个新的线程,并通过消息队列传输数据。6 完成客户端
客户端那可太简单了,还记得我们上文提到的客户端初始化代码吗?\ 把它写完之后只要一行
connect(sock, (sockaddr*)&server_addr, sizeof(server_addr);
就可以向指定地址和端口发送连接请求。
如你所知,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++ 网络通信的一些基本操作,具体的内容还需大家发挥想象力自由编写。
希望本文对你有帮助。