(注意:devc++可用,请各位大神赏脸进来看一下吧)
1.前言
本来是想写恶魔轮盘赌的,但是出现了一堆神秘bug,只能鸽一下,先来讲这个。
这个东西后面的部分游戏中会用到,虽然非常繁琐且困难,但确实好用(说起来地牢的那一篇也能写个游戏出来?先挖个坑awa)
(不得不说zzb挖的坑确实挺多的。恶魔轮盘赌,动物园怪谈,地牢探索/元气骑士?)
(三个大坑啊qwq,尤其是中间那玩意,我™想剧情快想疯了qwq)
(地牢探索/元气骑士补充一下,是 zzb 最近想到的,用地牢的那份代码写的一个游戏,就是类似元气的地牢探索类,但没写完qwq)
(再吐槽一句:为什么是全网最详细?因为网上的太杂了,zzb 看了两天才勉强看懂,两天啊qwq)
温馨提示:本篇干货较多,难度略高(比多线程难),zzb 将尽量讲懂,不懂的评论区见
同样的,这篇文章在将来对你的帮助也会很大,毕竟用了这个,就再也不会两个人在一个键盘上面玩了awa。
必备前置知识:小技巧14(掌握多线程,不要求线程池,这个我将来会再水一篇来详细讲解)
zzb不会告诉你本来恶魔轮盘赌都快写好了结果一个神秘的bug出现卡了zzb贼久导致zzb心态失衡直接重构代码了
同样的,zzb绝对不会告诉你下一篇的恶魔轮盘赌在写这一篇时已经写完一部分了
2.正文
(其实最开始,zzb 一直以为dev就是个fw,该有的都没有)
(事实证明,除了没有图形库以外,dev 还是有很多神奇的语法等待他人去探索的)
(不过设计dev的人怎么想的?这么复杂的东西连个完完整整的说明书都没有,全™靠探索是把awa)
首先,我们要了解一下联机的大概原理(具体zzb也不是很懂,大概就行了)
1.原理
举个例子:你手中有一个二座插头,面前有许多插座,一个二孔插头和一堆三孔插头
你会把它插到哪里?
很明显,其实都行是那个二孔插座
转换一下:
你手中那的是一个叫端口的东西,一段插在服务器(不确定叫啥鬼名字,就干脆总端叫服务器,链接的叫客户端)上。
而你接下来就是要把端口插到合适的位置,使电流能够从那个位置流向服务器再流回来
就是把信息带给服务器,以服务器为中控,分发到每一个小的客户端上。
再举个例子:
就是有一堆点,都连向同一个虚点。
而其中的某个点想发出信息时,只能把信息传输到虚点再传给其他所有点。
(这下估计懂了吧awa)
2.Winsock
在头文件中某个不为人知的角落中,藏有一个神秘的头文件(万能头好像包含了这个的?)
#include<Winsock2.h>
(好熟悉的感觉,好像上一次的某篇文章也是这么写的?)
但是,注意一点:这个头文件最好写在 windows.h 的前面。
因为 WinSock2.h 重定义了旧版本的 Winsock,而 Winsock 被包含在了 Windows.h 中。
所以懂?
它就是 dev 封装的,连接系统和用户使用的软件之间用于交流的一个接口
当然,它之中包含了链接库函数,需要
#pragma comment (lib,"ws2_32")
来进行各种函数的调用。(不过我把它注释掉了也能用)
还要补上在编译命令(工具[T]->编译选项[C])中加上
-lws2_32
在这个头文件中,常见(其实都不常见)的其实不多?:
3.WSADATA
struct WSAData//函数原型
{
WORD wVersion;
WORD wHighVersion;
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYSSTATUS_LEN+1];
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char *lpVendorInfo;
};
大概里面成员的作用也比较简单
成员 | 作用 |
---|---|
wVersion | 存储Winsockets规范的版本号 |
wHighVersion | 与上面没啥区别 |
szDescription | Windows Sockets实现的描述拷贝到这个字符串中 |
szSystemStatus | ↑ 把有关的状态等信息拷贝到该字符串中 |
iMaxSochets | 单个进程能够打开的socket的最大数目 |
iMaxUdpDg | 表示数据报的最大长度,现在被废弃了 |
lpVendowInfo | 为Winsock实现而保留的制造商信息,鸟用没有,现在被废弃了 |
它通常拿来存储被 WSAStartup 函数调用后返回的 Windows Sockets 数据。它包含 Winsock.dll 执行的数据。
你也看到了,上面说的是 WSAStartup 调用后返回的数据
所以 WSAStartup 很明显是必须要用的
使用Socket(一会讲解)的程序在使用Socket之前必须调用WSAStartup函数。
这样应用程序才可以调用所请求的Socket库中的其它Socket函数了。
至于使用吗,就是个板子
WSADATA wsaData;
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0) exit(0);//这个表示调用失败
//其中MAKEWORD表示socket的版本,这里就是2.2版本
//就用2.2就行了,不要乱改
当然,它既然是一把打开Socket库的钥匙,那我们用完了就不能留下来。
WSACleanup();
WSACleanup 可以释放它所占用的系统资源。
这样子,我们才算打开了Socket库。
不过,要注意一点(网上搬的)
在 Windows 下,Socket 是以 DLL 的形式实现的。在 DLL 的内部维持着一个计数器,只有第一次调用 WSAStartup 时才算真正加载 DLL,以后的调用就是增加计数器。而 WSACleanup 的功能则是使计数器减 1,减完后 DLL 就会从内存中被卸载
所以,使用了多少次 startup 就要用多少次 cleanup
那么接下来,高潮就来了。
4.Socket
Socket,zzb 给的翻译即是 端口。(其实真经的翻译是插座或者插口)
而官方定义差不多(其实差得多):(by 百度百科)
SOCKET中首先我们要理解如下几个定义概念:
一是IP地址:IP Address我想很容易理解,就是依照TCP/IP协议分配给本地主机的网络地址,就向两个进程要通讯,任一进程要知道通讯对方的位置,位置如何来确定,就用对方的IP。
二是端口号:用来标识本地通讯进程,方便OS提交数据.就是说进程指定了对方进程的网络IP,但这个IP只是用来标识进程所在的主机,如何来找到运行在这个主机的这个进程呢,就用端口号。
三是连接:指两个进程间的通讯链路。
四是半相关:网络中用一个三元组可以在全局唯一标志一个进程:(协议,本地地址,本地端口号)这样一个三元组,叫做一个半相关,它指定连接的每半部分。
五是全相关:一个完整的网间进程通信需要由两个进程组成,并且只能使用同一种高层协议。也就是说,不可能通信的一端用TCP协议,而另一端用UDP协议。因此一个完整的网间通信需要一个五元组来标识:(协议,本地地址,本地端口号,远地地址,远地端口号)
这样一个五元组,叫做一个相关(association),即两个协议相同的半相关才能组合成一个合适的相关,或完全指定组成一连接。
这就是 客户/服务器模式,即使 Socket
(怎么样,高级不高级?看懂了多少??)
所以,在 zzb 片面 的看来,Socket 就是端口,链接服务器与客户端中的线路。
所以,我们就能简单的描述一下服务器与客户端之间的关系了
5.客户服务器模式
在TCP/IP网络应用中,通信的两个进程间相互作用的主要模式是客户/服务器模式,即客户向服务器发出服务请求,服务器接收到请求后,提供相应的服务。
客户/服务器模式的建立基于以下两点:首先,建立网络的起因是网络中软硬件资源、运算能力和信息不均等,需要共享,从而造就拥有众多资源的主机提供服务,资源较少的客户请求服务这一非对等作用。
其次,网间进程通信完全是异步的,相互通信的进程间既不存在父子关系,又不共享内存缓冲区,因此需要一种机制为希望通信的进程间建立联系,为二者的数据交换提供同步,这就是客户/服务器模式的TCP/IP。
客户/服务器模式通信过程中采取的是主动请求方式:
服务器:
-
打开一通信通道并告知本地主机,它愿意在某一公认地址上接收客户请求
-
等待客户请求到达该端口
-
接收到重复服务请求,处理该请求并发送应答信号。接收到并发服务请求,要激活一新进程来处理这个客户请求。新进程处理此客户请求,并不需要对其它请求作出应答。服务完成后,关闭此新进程与客户的通信链路,并终止
-
返回第二步,等待另一客户请求。
-
关闭服务器
客户方:
-
打开一通信通道,并连接到服务器所在主机的特定端口
-
向服务器发请求,等待并接收应答;然后继续提出请求
-
请求结束后关闭通信通道并终止
看起来很高级,不过用通俗一点的方式就是:
A 把信息传输到服务器中
而 B,C,D,E 则收到从服务器传来的信息
同样,B、C、D、E 也可以发出消息让 A 来收
服务器它就是一个中转站而已。
6.函数
接下来 zzb 将会介绍亿些 API 函数来完成服务器与客户端的搭建
1.socket
SOCKET PASCAL FAR socket(int af,int type,int protocol);
这个是 socket 的初始化,af是协议族,就是从传输协议( AF_UNIX、AF_INET、AF_NS等 )中选择一个。常见的是 AF_INET,用这个就行了。
而 type 指明了 socket 发送与分组的方式。建议使用 SOCK_STREAM (TCP流式),这个相对来说面向连接、可靠,数据无差错、无重复,且按发送顺序接收。(一句话,就这个最好用)
最后的 protocol 表示发送数据时应使用的协议,值为0时表示用type默认的形式。建议还是用 TCP。
直接给一个板子,用的是 ipv4+TCP
server=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
//其中server是SOCKET类型。
注意,同那啥 WSAStartup 一样,用了也要释放资源。
closesocket(SOCKET sock);
2.bind与listen
(bind 我最开始以为是监视的意思,后来才知道,原来是绑定,,,绝对不是我翻译成了盲人)
int PASCAL FAR bind(SOCKET s,const struct sockaddr FAR*name,int namelen);
当然,绑定中要用到 sockaddr,我们接下来先介绍 sockaddr,再回来介绍 bind
而listen,很明显就是监听的意思了
int listen(
SOCKET sock,
int backlog//队列中允许传入的最大连接数,达到最大值后后续连接将会被丢弃
);
它成功时返回0,错误时返回-1
3.sockaddr与sockaddr_in
每一个网络层数据包都需要一个源地址和一个目的地址,如果数据包封装传输层数据,还需要一个源端口和一个目的端口。
为了将地址信息传入和传出socket库,API提供了sockaddr数据类型
struct sockaddr
{
uint16_t sa_family;//常数,指定地址类型,应与创建socket时使用的参数af一致
char sa_data[14];//存储地址
};
为了更加方便,它还封装了一个更厉害的
sockaddr_in
struct sockaddr_in
{
short sin_family;//和sockaddr中的sa_family具有相同含义
uint16_t sin_port;//存储地址中的16位端口部分
struct in_addr sin_addr;//存储4字节的IPv4地址
char sin_zero[8];//不使用,仅为了使sockaddr_in与sockaddr的大小一致,应全为0
};
所以,用这个来暂时转换变量后,bind 也可以用了
sockaddr_in bindser;//绑定本地地址
bindser.sin_family=AF_INET;
bindser.sin_port=htons(port);
bindser.sin_addr.s_addr=htonl(INADDR_ANY);
if(bind(server,(const struct sockaddr*)&bindser,sizeof(bindser))==(SOCKET)(~0)){WSACleanup();closesocket(server);return 0;}//如果有服务器存在了
然后接下来,该处理接入的问题了
4.accept与connect
accept 翻译是接收,很明显,就是服务器接收客户端
int connect(
SOCKET sock,//待连接的socket
const sockaddr*addr,//指向目的远程主机的地址指针
int addrlen//addr参数所指向地址的长度
);
connect 翻译是连接,也很明显,是客户端连接服务器
SOCKET accept(
SOCKET sock,//接收传入连接的监听socket
sockaddr*addr,//将会被写入请求连接的远程主机地址,不需要初始化
int* addrlen//指向addr缓冲区大小的指针,以字节为单位,真正写入地址之后将更新这个参数
);
用法就是填空awa
然后最后,就是最重要的两个:
5.send与recv
很明显(又来了),send是发消息,recv是接收消息。
int send(
SOCKET sock,//端口
const char*buf,//要发送的字符串
int len,//字符串长度
int flags//传输方式
);
如果send成功,返回发送数据的大小。如果socket的输出缓冲区有一些空余的空间,但不足以容纳整个buf时,这个值可能会比参数len小。如果没有空间,默认情况下,调用线程将被阻塞,直到调用超时或者发送了足够的数据后产生空间。如果发生错误,send函数返回-1。请注意,非零的返回值并不代表数据已经成功发送出去了,只能说明数据被存入队列中等待发送。
而recv也差不多
int recv(
SOCKET sock,//端口
char* buf,//要接收的字符串
int len,//要接收的长度
int flags//传输方式
);
常用的就这些,所以我们就讲这么多。
那么接下来,工具到手了,还要看你会不会用。
7.客户服务器模式实现
在这里,给一个我认为写的不错的实现代码
这个在我看来还是不错的
不过要注意:我们是要写出尽量完美的代码
所以,我们来看看它的不足在哪里。
- 客户端与服务器分开放置。(也许有的人会认为不怎么样,但是你想想,你每次要运行两份代码,这肯定是懒人无法接收的)
- 服务器产生的运行框
有点碍眼(对不起,这只是 zzb 的强迫症) - 没什么了,拿来凑个字数
所以,改正不足的方式是什么?
A了这道题吃掉它awa
- 这个还是比较好想的。利用 bind ,我们能判断是否已经有服务器存在。如果没有,就把当前程序改造成服务器,反之,就改造成客户端。
- 如果它被改造成服务器,隐藏到后台,然后重新再创造一个运行框出来
不过,这样也有一个问题,就是:你怎么把后台的服务器调出来关掉?
(一个 fk 问题,不过只是为了提供一种思路)
首先,很明显,当我们打开服务器时,里面至少要有一个客户端存在才行对吧,毕竟你当前的电脑运行着一个服务器和一个客户端,别人再加进来客户端肯定会增加。
所以,如果客户端的数量为 0 了,就是没有人在使用了,服务器自然没用了,直接 exit(0);
再注意一点:发送消息和接收消息最好用多线程,不然,,,,,,
所以没了,最后,就是属于模板的时代!
8.代码
1.模板 - Server
模板不全,请注意
Server 为了方便大家,我封装了一下。
给大家说明一下:
zzb 的 Server 会发送 3 中消息
- Somebody加入了服务器
- Somebody离开了
- Somebody:A B C D,,,E
其中,Somebody 是用户名,而 A B C D 建议你传数字,用 int_to_string 转换,atoi 接收就行了
Server 的板子就不用大家改了,但需要注意:
Server只是中间商还不要差价的那种
所以为了代码的美观,不要在中间商那里放置一些奇奇怪怪的东西好不好
(zzb 的意思是:你游戏中的过程那些不要写进 Server 里面,不然真的很乱很乱)
namespace Server
{
const int N=1e5+10;
queue<char*> sent;//发送信息缓冲
char a[N][105];//存储昵称
SOCKET server,lt[N];//接受 SOCKET 用 最多可承载次数
int t_lt[N],t,online;//存储承载下标 ( 用来删除下线状态 ) 承载过的次数 在线人数
bool p1,p2=1;//延时判断
DWORD WINAPI recvs(LPVOID param)//接收信息
{
SOCKET sclient=lt[t];
p1=0;
int reg;
char rc[10000];
memset(rc,0,sizeof(rc));
while(1)
{
memset(rc,0,sizeof(rc));
reg=recv(sclient,rc,size,0);//尝试接受信息
if(reg==-1) break;//对方离开了
else
{
char Sent[1000];
memset(Sent,0,sizeof(Sent));
memcpy(Sent,a[sclient],strlen(a[sclient]));//拼接用户名
Sent[strlen(Sent)]=':';//拼接:
strcat(Sent,rc);//复制收到的信息
strcat(Sent,"\n");//加上换行
printf("%s",Sent);//输出收到的信息
sent.push(Sent);//分发给每一个人
}
}
strcat(a[sclient],"离开!\n");//离开了
printf("%s\n",a[sclient]);//输出信息
lt[t_lt[sclient]]=-1;//下标改为-1
sent.push(a[sclient]);//分发给每一个人
online--;//在线人数-1
closesocket(sclient);//释放
if(online==0) exit(0);/*没有在线的人了,这个服务器就没必要存在了,直接关掉*/
return 0;
}
DWORD WINAPI sends(LPVOID param)
{
while(1)
if(!sent.empty()&&p2)
{
int ans=0;
for(int i=1;i<=t;i++)
if(lt[i]!=-1) send(lt[i],sent.front(),strlen(sent.front()),0),ans++;//同一条消息分发给每一个人
else if(ans==online) break;//分发完了
sent.pop();//切下一条消息
}
}
void find_socket()
{
sockaddr_in client;
int client_len=sizeof(client);
CreateThread(NULL,0,sends,NULL,0,NULL);//开线程
puts("端口已打开!\n");
while(1)
{
if(t>N-10){t=0;memset(lt,0,sizeof lt);}//信息达到上线,初始化掉它
if(online>=people)//在线人数超标
{
puts("错误 - 在线人数超标\n请等待\n\n");
while(online>=people);
}
while(p1);
lt[++t]=accept(server,(sockaddr FAR*)&client,&client_len);//来新人了
p2=0;
if(lt[t]==(SOCKET)(~0)){puts("错误 - 连接失败!\n");t--;continue;}//尝试连接
t_lt[lt[t]]=t;//记录下标
char phj[1000];
memset(phj,0,sizeof(phj));
int ret=recv(lt[t],phj,size,0);//接受用户名
int pi=0;
while(phj[pi]!=',')a[lt[t]][pi]=phj[pi],pi++;//记录用户名
online++;//在线人数增加
printf("来自 [%s] , 姓名 :[%s] 成功加入!\n",inet_ntoa(client.sin_addr),a[lt[t]]);//广播消息
char Sent[1000];
memset(Sent,0,sizeof(Sent));
memcpy(Sent,a[lt[t]],strlen(a[lt[t]]));//拼接名字
strcat(Sent,"加入了服务器\n");//广播消息
sent.push(Sent);
p1=p2=1;//延迟取消
CreateThread(NULL,0,recvs,NULL,0,NULL);//为它建立接受消息的线程
}
}
bool open_socket()//尝试打开服务器
{
WSADATA wsaData;//WSA
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0) puts("错误1 - Winsock打开失败!"),exit(0);//打开WSA
server=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//初始化协议
if(server==(SOCKET)(~0))//协议初始化失败
{
puts("错误2 - socket初始化失败");
WSACleanup();//释放WSA
exit(0);
}
sockaddr_in bindser;//绑定
bindser.sin_family=AF_INET;//一个一个设置过去
bindser.sin_port=htons(port);//指向端口号
bindser.sin_addr.s_addr=htonl(INADDR_ANY);//指向全部人可连接
if(bind(server,(const struct sockaddr*)&bindser,sizeof(bindser))==(SOCKET)(~0))//尝试绑定
{
puts("错误3 - 绑定失败,已有服务器生成");
WSACleanup();//释放WSA
closesocket(server);//释放socket
return 1;
}
if(listen(server,people)==(SOCKET)(~0))//尝试监听
{
puts("错误4 - 监听失败");
WSACleanup();//释放
closesocket(server);//释放
exit(0);
}
return 0;
}
void main(bool&f)
{
f=open_socket();
if(!f)find_socket();
}
}
(等一下,好像还有几个需要提醒的东西:)
- 注意,zzb 用的是 namespace 封装,所以一定注意,不要 using 了它
- 用法:
namespace Server
{
//略
}
//不要using namespace Server;
int main()
{
bool f=0;
Server::main(f);//这样使用
//f=1时表示已经存在服务器了,f=0时表示还没有服务器,会直接进入Server中的造服务器阶段,不会再返回到main这里
if(f) system("cls"),try_join();//已经存在服务器了,那就以客户端的方式进去。
}
2.模板 - 客户端
模板不全,请注意
这个也是,zzb 已经帮你划分好了哪些地方是要填空的,直接填空就行了
(不要想的太复杂,其实就是把你游戏中P1 while(1) 的操作搬进来,然后其他玩家的操作就是recv之后拆解信息,直接更改就行了)
const int N=20,P=26;
SOCKET server;
char a[1000],ipv4[1000];
bool cut_rc(char*rc,vector<int>&v,int&tp,string&s)
{
s="";
char r[1000][1000];
int i=0,t=0;
if(rc[0]==':') return 1;
for(i=0;i<strlen(rc);i++)
{
if(rc[i]==':') break;
if(rc[i]<0) return 1;//拆分名字
s+=rc[i];
}
if(s==a) return 1;//自己发的信息
while(i<strlen(rc))
{
i++,tp++,t=0;
memset(r[tp],0,sizeof r[tp]);//拆分一个个数字
for(;i<strlen(rc);i++)
{
if(rc[i]==' ')break;
if(rc[i]<0) return 1;
r[tp][t++]+=rc[i];
}
t--;
v.push_back(atoi(r[tp]));
}
return 0;
}
void Client()
{
char rc[100000];
memset(rc,0,sizeof(rc));
while(1)
{
sl(10);
memset(rc,0,sizeof(rc));
int rag=recv(server,rc,sz,0);
if(rag==-1){system("cls"),puts("错误 - 服务器终端关闭\n");exit(0);}
int tp=0;
string s;
vector<int> v;
bool f=cut_rc(rc,v,tp,s);
if(f) continue;//不满足使用条件,比如收到的消息是谁进入了服务器或是自己发的消息被自己收到
//拆解的信息留在v里,s是用户名,最好hash一下用下标存放相应信息
//一定注意:v中的信息下标从0开始!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
}
}
DWORD WINAPI c_sends(LPVOID param)
{
string sendn;
sendn="";
bool ms=0;
while(1)
{
sl(50);
if(ms==1) ms=0;
//操作,操作后ms=1
//发送所有信息,信息就是你要发送的数字,中间用空格隔开,转换为char*类
int rag=0;
if(ms) rag=send(server,sendn.c_str(),sendn.size(),0);
if(rag==-1) break;
}
return 0;
}
void try_join()
{
WSADATA wsaData;
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0) exit(0);//WSA,启动!
server=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//初始化
if(server==(SOCKET)(~0)) WSACleanup(),exit(0);//初始化失败
set_name:
puts("请设置用户名(长度小于20):\n");
gets(a);
puts("");
if(strlen(a)>=20)
{
printf("输入用户名过长\n\n");
goto set_name;//goto暴力让你用户名满足要求
exit(0);
}
set_ipv4:
puts("请输入ip\n");
gets(ipv4);
puts("");
sockaddr_in conser;
conser.sin_family=AF_INET;
conser.sin_port=htons(port);
conser.sin_addr.S_un.S_addr=inet_addr(ipv4);
for(int i=1;i<=10;i++)
{
if(connect(server,(LPSOCKADDR)&conser,sizeof(conser))==(SOCKET)(~0))//connect暴力判断是否连上
{
if(i==10)
{
puts("错误 - IP地址错误!\n");
goto set_ipv4;
exit(0);
}
}
else break;
}
system("cls");
send(server,a,strlen(a),0);//发送你进入了服务器
HANDLE hThread1=CreateThread(NULL,0,c_sends,NULL,0,NULL);//创立发东西的线程
Client();
::CloseHandle(hThread1);//关闭线程
closesocket(server);//关闭socket
WSACleanup();//关闭WSA
}
3.总模板
zzb 多么的良心啊,前面的两个小模版代码不完整,但注释完整,而总模板注释不完整
这样,你们就会看完3份模板,从而有更深的印象绝对不是 zzb 把博客写到这里时发现总模板还有可以优化的地方,但 zzb 用的是实例代码改的,所以只能再改成总模板,然后又懒得加注释
所以,三份模板最好都看看,前两个看注释,最后一个看代码
注释一定要看!!!一些很重要的都在注释里面
//编译命令(工具[T]->编译选项[C])加上-std=c++17 -lws2_32
#include<bits/stdc++.h>//win+r 输入cmd后 输入ipconfig,ipv4后面的就是你的ip
#include<Winsock2.h>
#include<windows.h>
#include<queue>
#pragma comment (lib,"ws2_32")
#define port 9999//端口号
#define sz 6400//缓冲区长度
#define people 10//限制人数
using namespace std;
namespace init
{
#define kd(VK_NONAME) ((GetAsyncKeyState(VK_NONAME) & 0x8000)?1:0)
#define sl(n) Sleep(n)
void gotoxy(int x,int y){COORD pos={(short)x,(short)y};HANDLE hOut=GetStdHandle(STD_OUTPUT_HANDLE);SetConsoleCursorPosition(hOut,pos);return;}
void noedit(){HANDLE hStdin=GetStdHandle(STD_INPUT_HANDLE);DWORD mode;GetConsoleMode(hStdin,&mode);mode&=~ENABLE_QUICK_EDIT_MODE;mode&=~ENABLE_INSERT_MODE;mode&=~ENABLE_MOUSE_INPUT;SetConsoleMode(hStdin,mode);}
void hide(){CONSOLE_CURSOR_INFO cur={1,0};SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cur);}
void show(){CONSOLE_CURSOR_INFO cur={1,1};SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cur);}
}
using namespace init;
namespace Server
{
bool Hi=0;
const int N=1e5+10;
queue<char*> sent;//发送信息缓冲
char a[N][105];//存储昵称
SOCKET server,lt[N];//接受 SOCKET用 最多可承载次数
int t_lt[N],t,online;//存储承载下标 ( 用来删除下线状态 ) 承载过的次数 在线人数
bool p1,p2=1;//延时判断
DWORD WINAPI recvs(LPVOID param)//接收信息
{
SOCKET sclient=lt[t];
p1=0;
int reg;
char rc[10000];
memset(rc,0,sizeof(rc));
while(1)
{
memset(rc,0,sizeof(rc));
reg=recv(sclient,rc,sz,0);//尝试接受信息
if(reg==-1) break;//对方离开了
else
{
char Sent[1000];
memset(Sent,0,sizeof(Sent));
memcpy(Sent,a[sclient],strlen(a[sclient]));//拼接用户名
Sent[strlen(Sent)]=':';//拼接:
strcat(Sent,rc);//复制收到的信息
strcat(Sent,"\n");//加上换行
printf("%s",Sent);//输出收到的信息
sent.push(Sent);//分发给每一个人
}
}
strcat(a[sclient],"离开!\n");//离开了
printf("%s\n",a[sclient]);//输出信息
lt[t_lt[sclient]]=-1;//下标改为-1
sent.push(a[sclient]);//分发给每一个人
online--;//在线人数-1
closesocket(sclient);//释放
if(online==0) exit(0);/*没有在线的人了,这个服务器就没必要存在了,直接关掉*/
return 0;
}
DWORD WINAPI whide(LPVOID param)//隐藏窗口,这个很爽真的
{
HWND hWnd=GetConsoleWindow();
while(1)
{
if(kd(VK_ESCAPE))
{
if(Hi) Hi=0;
else Hi=1;
}
if(Hi) ShowWindow(hWnd,SW_HIDE);
else ShowWindow(hWnd,SW_SHOW);
Sleep(100);
}
}
DWORD WINAPI sends(LPVOID param)
{
while(1)
if(!sent.empty()&&p2)
{
int ans=0;
for(int i=1;i<=t;i++)
if(lt[i]!=-1) send(lt[i],sent.front(),strlen(sent.front()),0),ans++;//同一条消息分发给每一个人
else if(ans==online) break;//分发完了
sent.pop();//切下一条消息
}
}
void find_socket()
{
sockaddr_in client;
int client_len=sizeof(client);
CreateThread(NULL,0,sends,NULL,0,NULL);//开线程
CreateThread(NULL,0,whide,NULL,0,NULL);//开线程
puts("端口已打开!\n按一次ESC隐藏,按第二次唤起");
while(1)
{
if(t>N-10){t=0;memset(lt,0,sizeof lt);}//信息达到上线,初始化掉它
if(online>=people)//在线人数超标
{
puts("错误 - 在线人数超标\n请等待\n\n");
while(online>=people);
}
while(p1);
lt[++t]=accept(server,(sockaddr FAR*)&client,&client_len);//来新人了
p2=0;
if(lt[t]==(SOCKET)(~0)){puts("错误 - 连接失败!\n");t--;continue;}//尝试连接
t_lt[lt[t]]=t;//记录下标
char phj[1000];
memset(phj,0,sizeof(phj));
int ret=recv(lt[t],phj,sz,0);//接受用户名
int pi=0;
while(phj[pi]!=',')a[lt[t]][pi]=phj[pi],pi++;//记录用户名
online++;//在线人数增加
printf("来自 [%s] , 姓名 :[%s] 成功加入!\n",inet_ntoa(client.sin_addr),a[lt[t]]);//广播消息
char Sent[1000];
memset(Sent,0,sizeof(Sent));
memcpy(Sent,a[lt[t]],strlen(a[lt[t]]));//拼接名字
strcat(Sent,"加入了服务器\n");//广播消息
sent.push(Sent);
p1=p2=1;//延迟取消
CreateThread(NULL,0,recvs,NULL,0,NULL);//为它建立接受消息的线程
}
}
bool open_socket()//尝试打开服务器
{
WSADATA wsaData;//WSA
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0) puts("错误1 - Winsock打开失败!"),exit(0);//打开WSA
server=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//初始化协议
if(server==(SOCKET)(~0))//协议初始化失败
{
puts("错误2 - socket初始化失败");
WSACleanup();//释放WSA
exit(0);
}
sockaddr_in bindser;//绑定
bindser.sin_family=AF_INET;//一个一个设置过去
bindser.sin_port=htons(port);//指向端口号
bindser.sin_addr.s_addr=htonl(INADDR_ANY);//指向全部人可连接
if(bind(server,(const struct sockaddr*)&bindser,sizeof(bindser))==(SOCKET)(~0))//尝试绑定
{
puts("错误3 - 绑定失败,已有服务器生成");
WSACleanup();//释放WSA
closesocket(server);//释放socket
return 1;
}
if(listen(server,people)==(SOCKET)(~0))//尝试监听
{
puts("错误4 - 监听失败");
WSACleanup();//释放
closesocket(server);//释放
exit(0);
}
return 0;
}
void main(bool&f)
{
f=open_socket();
if(!f)find_socket();
}
}
const int P=26;
SOCKET server;
char a[1000],ipv4[1000];
bool cut_rc(char*rc,vector<int>&v,int&tp,string&s)
{
s="";
char r[1000][1000];
int i=0,t=0;
if(rc[0]==':') return 1;
for(i=0;i<strlen(rc);i++)
{
if(rc[i]==':') break;
if(rc[i]<0) return 1;//拆分名字
s+=rc[i];
}
if(s==a) return 1;//自己发的信息
while(i<strlen(rc))
{
i++,tp++,t=0;
memset(r[tp],0,sizeof r[tp]);
for(;i<strlen(rc);i++)
{
if(rc[i]==' ')break;
if(rc[i]<0) return 1;
r[tp][t++]+=rc[i];
}
t--;
v.push_back(atoi(r[tp]));
}
return 0;
}
void Client()
{
char rc[100000];
memset(rc,0,sizeof(rc));
while(1)
{
sl(10);
memset(rc,0,sizeof(rc));
int rag=recv(server,rc,sz,0);
if(rag==-1){system("cls"),puts("错误 - 服务器终端关闭\n");exit(0);}
int tp=0;
string s;
vector<int> v;
bool f=cut_rc(rc,v,tp,s);
if(f) continue;//不满足使用条件,比如收到的消息是谁进入了服务器或是自己发的消息被自己收到
}
}
DWORD WINAPI c_sends(LPVOID param)
{
string sendn;
sendn="";
bool ms=0;
while(1)
{
sl(50);
if(ms==1) ms=0;
//操作
//发送所有信息
int rag=0;
if(ms) rag=send(server,sendn.c_str(),sendn.size(),0);
if(rag==-1) break;
}
return 0;
}
void try_join()
{
WSADATA wsaData;
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0) exit(0);//WSA,启动!
server=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//初始化
if(server==(SOCKET)(~0)) WSACleanup(),exit(0);//初始化失败
gets(a);
set_name:
puts("请设置用户名(长度小于20):\n");
gets(a);
puts("");
auto check=[](string s)
{
for(int i=0;i<s.size();i++)
if(s[i]=='\0'||s[i]=='\n'||s[i]<0) return 0;
return s.size()>0?1:0;
};
if(strlen(a)>=20||check(a)==0)
{
printf("输入用户名违规\n\n");
Sleep(1000);
system("cls");
goto set_name;//goto暴力让你用户名满足要求
exit(0);
}
set_ipv4:
puts("请输入ip(输入return返回上一步)\n");
gets(ipv4);
puts("");
if(!strncmp("return",ipv4,6))
{
system("cls");
goto set_name;
exit(0);
}
sockaddr_in conser;
conser.sin_family=AF_INET;
conser.sin_port=htons(port);
conser.sin_addr.S_un.S_addr=inet_addr(ipv4);
for(int i=1;i<=10;i++)
{
if(connect(server,(LPSOCKADDR)&conser,sizeof(conser))==(SOCKET)(~0))//connect暴力判断是否连上
{
if(i==10)
{
puts("错误 - IP地址错误!\n");
goto set_ipv4;
exit(0);
}
}
else break;
}
system("cls");
send(server,a,strlen(a),0);//发送你进入了服务器
HANDLE hThread1=CreateThread(NULL,0,c_sends,NULL,0,NULL);//创立发东西的线程
Client();
::CloseHandle(hThread1);//关闭线程
closesocket(server);//关闭socket
WSACleanup();//关闭WSA
}
int main()
{
system("mode con cols=120 lines=30");
noedit();
hide();
bool f=0,F=0;
puts("请选择:是否已经启动了服务器\n0:否\n1.是");
cin>>f;
F=f;
system("cls");
if(!f) Server::main(f);//调用Server里面的main,其中有个保险,但是只能查询本地是否有服务器
if(F!=f)
{
printf("本地已有服务器存在,将更换为客户端\n(按 空格 继续)");
while(1)if(kd(VK_SPACE))break;
}
if(f) system("cls"),try_join();//已经存在服务器了,那就以客户端的方式进去。
return 0;
}
这份代码已经很全了,但是还有一个 zzb 没解决的问题:
有时候传用户名的时候会失败,不知道为什么
所以,在组队的时候注意一下:
一定要打开服务器看一下用户名传起没,不然你收不到对方的消息qwq
还有一点
客户端应该输入选择的服务器的IP,客户端应该输入选择的服务器的IP,客户端应该输入选择的服务器的IP
重要的事情说三遍
你选择的是哪个服务器的IP,你就进的那个服务器,懂?
最后,给一个实例
4.实例
本来想写个联机走迷宫的,但为了赶进度就抛弃它了
这个联机再挖个坑(反正够多了,也不差这一个),zzb 会尽力把它用到 地牢探索/元气骑士? 里面(愿脏脏包之神保佑我)
所以只写了一个只能移动的代码
实例说明:
选择本机房任意一台电脑,运行服务器。(建议不要隐藏,如果有人用户名传输失败还能看见)
WSAD移动
//编译命令(工具[T]->编译选项[C])加上-std=c++17 -lws2_32
#include<bits/stdc++.h>//win+r 输入cmd后 输入ipconfig,ipv4后面的就是你的ip
#include<Winsock2.h>
#include<windows.h>
#include<queue>
#pragma comment (lib,"ws2_32")
#define port 9999//端口号
#define sz 6400//缓冲区长度
#define people 10//限制人数
using namespace std;
namespace init
{
#define kd(VK_NONAME) ((GetAsyncKeyState(VK_NONAME) & 0x8000)?1:0)
#define sl(n) Sleep(n)
void gotoxy(int x,int y){COORD pos={(short)x,(short)y};HANDLE hOut=GetStdHandle(STD_OUTPUT_HANDLE);SetConsoleCursorPosition(hOut,pos);return;}
void noedit(){HANDLE hStdin=GetStdHandle(STD_INPUT_HANDLE);DWORD mode;GetConsoleMode(hStdin,&mode);mode&=~ENABLE_QUICK_EDIT_MODE;mode&=~ENABLE_INSERT_MODE;mode&=~ENABLE_MOUSE_INPUT;SetConsoleMode(hStdin,mode);}
void hide(){CONSOLE_CURSOR_INFO cur={1,0};SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cur);}
void show(){CONSOLE_CURSOR_INFO cur={1,1};SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cur);}
}
using namespace init;
namespace Server
{
bool Hi=0;
const int N=1e5+10;
queue<char*> sent;//发送信息缓冲
char a[N][105];//存储昵称
SOCKET server,lt[N];//接受 SOCKET用 最多可承载次数
int t_lt[N],t,online;//存储承载下标 ( 用来删除下线状态 ) 承载过的次数 在线人数
bool p1,p2=1;//延时判断
DWORD WINAPI recvs(LPVOID param)//接收信息
{
SOCKET sclient=lt[t];
p1=0;
int reg;
char rc[10000];
memset(rc,0,sizeof(rc));
while(1)
{
memset(rc,0,sizeof(rc));
reg=recv(sclient,rc,sz,0);//尝试接受信息
if(reg==-1) break;//对方离开了
else
{
char Sent[1000];
memset(Sent,0,sizeof(Sent));
memcpy(Sent,a[sclient],strlen(a[sclient]));//拼接用户名
Sent[strlen(Sent)]=':';//拼接:
strcat(Sent,rc);//复制收到的信息
strcat(Sent,"\n");//加上换行
printf("%s",Sent);//输出收到的信息
sent.push(Sent);//分发给每一个人
}
}
strcat(a[sclient],"离开!\n");//离开了
printf("%s\n",a[sclient]);//输出信息
lt[t_lt[sclient]]=-1;//下标改为-1
sent.push(a[sclient]);//分发给每一个人
online--;//在线人数-1
closesocket(sclient);//释放
if(online==0) exit(0);/*没有在线的人了,这个服务器就没必要存在了,直接关掉*/
return 0;
}
DWORD WINAPI whide(LPVOID param)//隐藏窗口,这个很爽真的
{
HWND hWnd=GetConsoleWindow();
while(1)
{
if(kd(VK_ESCAPE))
{
if(Hi) Hi=0;
else Hi=1;
}
if(Hi) ShowWindow(hWnd,SW_HIDE);
else ShowWindow(hWnd,SW_SHOW);
Sleep(100);
}
}
DWORD WINAPI sends(LPVOID param)
{
while(1)
if(!sent.empty()&&p2)
{
int ans=0;
for(int i=1;i<=t;i++)
if(lt[i]!=-1) send(lt[i],sent.front(),strlen(sent.front()),0),ans++;//同一条消息分发给每一个人
else if(ans==online) break;//分发完了
sent.pop();//切下一条消息
}
}
void find_socket()
{
sockaddr_in client;
int client_len=sizeof(client);
CreateThread(NULL,0,sends,NULL,0,NULL);//开线程
CreateThread(NULL,0,whide,NULL,0,NULL);//开线程
puts("端口已打开!\n按一次ESC隐藏,按第二次唤起");
while(1)
{
if(t>N-10){t=0;memset(lt,0,sizeof lt);}//信息达到上线,初始化掉它
if(online>=people)//在线人数超标
{
puts("错误 - 在线人数超标\n请等待\n\n");
while(online>=people);
}
while(p1);
lt[++t]=accept(server,(sockaddr FAR*)&client,&client_len);//来新人了
p2=0;
if(lt[t]==(SOCKET)(~0)){puts("错误 - 连接失败!\n");t--;continue;}//尝试连接
t_lt[lt[t]]=t;//记录下标
char phj[1000];
memset(phj,0,sizeof(phj));
int ret=recv(lt[t],phj,sz,0);//接受用户名
int pi=0;
while(phj[pi]!=',')a[lt[t]][pi]=phj[pi],pi++;//记录用户名
online++;//在线人数增加
printf("来自 [%s] , 姓名 :[%s] 成功加入!\n",inet_ntoa(client.sin_addr),a[lt[t]]);//广播消息
char Sent[1000];
memset(Sent,0,sizeof(Sent));
memcpy(Sent,a[lt[t]],strlen(a[lt[t]]));//拼接名字
strcat(Sent,"加入了服务器\n");//广播消息
sent.push(Sent);
p1=p2=1;//延迟取消
CreateThread(NULL,0,recvs,NULL,0,NULL);//为它建立接受消息的线程
}
}
bool open_socket()//尝试打开服务器
{
WSADATA wsaData;//WSA
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0) puts("错误1 - Winsock打开失败!"),exit(0);//打开WSA
server=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//初始化协议
if(server==(SOCKET)(~0))//协议初始化失败
{
puts("错误2 - socket初始化失败");
WSACleanup();//释放WSA
exit(0);
}
sockaddr_in bindser;//绑定
bindser.sin_family=AF_INET;//一个一个设置过去
bindser.sin_port=htons(port);//指向端口号
bindser.sin_addr.s_addr=htonl(INADDR_ANY);//指向全部人可连接
if(bind(server,(const struct sockaddr*)&bindser,sizeof(bindser))==(SOCKET)(~0))//尝试绑定
{
puts("错误3 - 绑定失败,已有服务器生成");
WSACleanup();//释放WSA
closesocket(server);//释放socket
return 1;
}
if(listen(server,people)==(SOCKET)(~0))//尝试监听
{
puts("错误4 - 监听失败");
WSACleanup();//释放
closesocket(server);//释放
exit(0);
}
return 0;
}
void main(bool&f)
{
f=open_socket();
if(!f)find_socket();
}
}
const int N=30,M=119,P=26;
SOCKET server;
char a[1000],ipv4[1000];
int top=1;
map<string,int> mp;
struct player{int x,y;void check(){x=max(1,x),x=min(M-2,x),y=max(1,y),y=min(N-2,y);}}p[P];
void sent()
{
for(int i=1;i<=M;i++) printf("#");
puts("");
for(int i=2;i<N;i++)
{
printf("#");
for(int j=2;j<M;j++) printf(" ");
printf("#");
puts("");
}
for(int i=1;i<=M;i++) printf("#");
}
bool cut_rc(char*rc,vector<int>&v,int&tp,string&s)
{
s="";
char r[1000][1000];
int i=0,t=0;
if(rc[0]==':') return 1;
for(i=0;i<strlen(rc);i++)
{
if(rc[i]==':') break;
if(rc[i]<0) return 1;//拆分名字
s+=rc[i];
}
if(s==a) return 1;//自己发的信息
while(i<strlen(rc))
{
i++,tp++,t=0;
memset(r[tp],0,sizeof r[tp]);
for(;i<strlen(rc);i++)
{
if(rc[i]==' ')break;
if(rc[i]<0) return 1;
r[tp][t++]+=rc[i];
}
t--;
v.push_back(atoi(r[tp]));
}
return 0;
}
void Client()
{
char rc[100000];
memset(rc,0,sizeof(rc));
while(1)
{
sl(10);
memset(rc,0,sizeof(rc));
int rag=recv(server,rc,sz,0);
if(rag==-1){system("cls"),puts("错误 - 服务器终端关闭\n");exit(0);}
int tp=0;
string s;
vector<int> v;
bool f=cut_rc(rc,v,tp,s);
if(f) continue;//不满足使用条件,比如收到的消息是谁进入了服务器或是自己发的消息被自己收到
if(!mp[s]) mp[s]=++top;
if(p[mp[s]].x!=0) gotoxy(p[mp[s]].x,p[mp[s]].y);
printf(" ");
p[mp[s]].x=v[0];
p[mp[s]].y=v[1];
p[mp[s]].check();
gotoxy(p[mp[s]].x,p[mp[s]].y);
printf("%c",(mp[s]-1+'A'));
}
}
DWORD WINAPI c_sends(LPVOID param)
{
string sendn;
sendn="";
bool ms=0;
while(1)
{
sl(50);
if(ms==1) ms=0;
//操作
if(kd('W'))
{
p[1].check();
if(p[1].x!=0) gotoxy(p[1].x,p[1].y),printf(" ");
p[1].y--,ms=1;
}
if(kd('S'))
{
p[1].check();
if(p[1].x!=0) gotoxy(p[1].x,p[1].y),printf(" ");
p[1].y++,ms=1;
}
if(kd('A'))
{
p[1].check();
if(p[1].x!=0) gotoxy(p[1].x,p[1].y),printf(" ");
p[1].x--,ms=1;
}
if(kd('D'))
{
p[1].check();
if(p[1].x!=0) gotoxy(p[1].x,p[1].y),printf(" ");
p[1].x++,ms=1;
}
p[1].check();
//发送所有信息
if(ms) gotoxy(p[1].x,p[1].y),printf("A");
sendn=to_string(p[1].x)+" "+to_string(p[1].y)+" ";
int rag=0;
if(ms) rag=send(server,sendn.c_str(),sendn.size(),0);
if(rag==-1) break;
}
return 0;
}
void try_join()
{
WSADATA wsaData;
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0) exit(0);//WSA,启动!
server=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//初始化
if(server==(SOCKET)(~0)) WSACleanup(),exit(0);//初始化失败
gets(a);
set_name:
puts("请设置用户名(长度小于20):\n");
gets(a);
puts("");
auto check=[](string s)
{
for(int i=0;i<s.size();i++)
if(s[i]=='\0'||s[i]=='\n'||s[i]<0) return 0;
return s.size()>0?1:0;
};
if(strlen(a)>=20||check(a)==0)
{
printf("输入用户名违规\n\n");
Sleep(1000);
system("cls");
goto set_name;//goto暴力让你用户名满足要求
exit(0);
}
set_ipv4:
puts("请输入ip(输入return返回上一步)\n");
gets(ipv4);
puts("");
if(!strncmp("return",ipv4,6))
{
system("cls");
goto set_name;
exit(0);
}
sockaddr_in conser;
conser.sin_family=AF_INET;
conser.sin_port=htons(port);
conser.sin_addr.S_un.S_addr=inet_addr(ipv4);
for(int i=1;i<=10;i++)
{
if(connect(server,(LPSOCKADDR)&conser,sizeof(conser))==(SOCKET)(~0))//connect暴力判断是否连上
{
if(i==10)
{
puts("错误 - IP地址错误!\n");
goto set_ipv4;
exit(0);
}
}
else break;
}
system("cls");
sent();
send(server,a,strlen(a),0);//发送你进入了服务器
HANDLE hThread1=CreateThread(NULL,0,c_sends,NULL,0,NULL);//创立发东西的线程
Client();
::CloseHandle(hThread1);//关闭线程
closesocket(server);//关闭socket
WSACleanup();//关闭WSA
}
int main()
{
system("mode con cols=120 lines=30");
noedit();
hide();
bool f=0,F=0;
puts("请选择:是否已经启动了服务器\n0:否\n1.是");
cin>>f;
F=f;
system("cls");
if(!f) Server::main(f);//调用Server里面的main,其中有个保险,但是只能查询本地是否有服务器
if(F!=f)
{
printf("本地已有服务器存在,将更换为客户端\n(按 空格 继续)");
while(1)if(kd(VK_SPACE))break;
}
if(f) system("cls"),try_join();//已经存在服务器了,那就以客户端的方式进去。
return 0;
}
3.后文
这一篇如此纠结的文章就写完了
相信你没有看懂多少(没关系,多看几遍,不懂的评论区 @zzb ,zzb最近长时间在线)
这一篇与线程一样,用的好以后你的游戏质量会暴增
没什么好说的了(骗个三连),看在zzb这么辛苦的份上,给个三连行不行qwq
最后,祝大家 中高考顺利,极道万岁!!!
closesocket(zzb);//qwq
参考文章:
- 科技系列 - 2 < 更新第二期 >:DEV - C++ 不同主机下的同局域网联机 更新 第 2 期
- WSADATA
- c++ socket
- C++socket(udp、tcp)常用基础函数笔记
- WSAStartup( )详解
下一篇:未完待续