本人不是专业的服务器程序员,所写的文章难免错漏,仅供参考,欢迎指正。
游戏中用到的网络编程多数是TCP连接,比如RPG类游戏都需要建立一个可靠的长连接来不停收发消息。有的游戏比如ACT类需要低延迟的游戏可能用的是P2P连接。还有一部分局域网对战类游戏会用到UDP广播来发送和同步局域网内的部分游戏主机信息。
1.封包处理
不管那种类型的网络连接首先要处理的都是消息的封包。
TCP消息的传输是可靠并且有序的,但是传输的过程中可能出现合并包和断包,因此消息的封包格式中必须包含一个字段用来保存包的长度。
一般windows上服务器可能是基于iocp,消息包设定了最大长度,大包是需要手动拆分的,比如发送商城物品列表时一次只发送一部分,并添加一个发送状态字段。
参见附录源码
PacketBase 是消息包的基类:
PacketBase::WriteHeader(); 发送时自动写入头部,消息包的前三个int字段分别是checksum&ID、size、debugID。
PacketBase::WriteValue(); 一系列函数用于向待发送缓冲写入数据
PacketBase::Read......(); 一系列函数用于读取包中的数据
PacketQueue 是带锁的消息包队列
发送消息的包分配在栈上,不会产生内存碎片无需内存池,接收消息的包需要使用内存池,这一点还未确定也没实现,留个坑不填了。
TcpStream 用于保存断包数据,等待接收到后续数据再拼接成完整包。关于断包处理类TcpStream一直有个疑惑,是一个接收线程对应一个TcpStream 还是一个socket对应一个TcpStream, 咨询了很多服务器开发人员,网上到处查资料也没有一个肯定的答案。纳闷有些小游戏服务器直接没有处理断包的代码也能玩。 个人倾向于一个socket对应一个TcpStream,但是并不用为每个连接的socket分配一个TcpStream,这样太浪费空间(因为断包可能出现在任意位置所以TcpStream要设置为最大封包长度)。 我们可以建立一个TcpStream的池子,某个socket需要的时候到池子中申请。暂时测试下来池子中TcpStream个数不会很大。
2.完成端口IOCP
做了一个带界面的服务器,很多服务器的程序都是不带界面的,甚至连黑框控制台都没有,可能是出于防止误操作考虑吧,效率上应该没啥影响。大约有几个要注意的地方
windows api向界面控制台打印输出时要注意必须在主线程打印,否则是无效的,所以添加了子线程打印函数LogSub。
玩家在登录完成之前发送的某些游戏内消息是直接丢弃的。
如果客户端卡死到达一段时间,则服务器不再给其发消息,否则会存在很多write overlapped,会占用很多发送缓冲,必要时要主动关闭该客户端,此时占用的write overlapped会被释放。
客户端关闭或断开连接时,服务器并不是总能收到通知,大约是四次挥手过程没有完成。有时客户端断开连接服务器会反复收到长度为0的消息,这时也需要主动关闭该客户端连接。可能心跳包检测是必须的。
测试服务器:凑合着先用,就一个进程,没做分布式 ,界面是win32api做的很简易。




封包代码:
//========================================================
// @Date: 2016.05
// @File: Include/Net/PacketList.h
// @Brief: PacketList
// @Author: LouLei
// @Email: twopointfive@163.com
// @Copyright (Crapell) - All Rights Reserved
//========================================================
#pragma once
#ifndef __PacketList__H__
#define __PacketList__H__
typedef void* PSocket;
#define Ptr2Socket(socket) (*((SOCKET*)socket))
#define Socket2Ptr(socket) (&socket)
void* CreateSocket();
void FreeSocket(void*& sokcet);
#include "General/Singleton.h"
#include "General/Thread.h"
//#include "General/ObjectPool.h"
//!减小缓冲使占用内存减少 1000player*2packet/per player = 2M级别
#define MaxPacketSize 1024*10
#define MaxValueLength 512
typedef short int Short;
enum PacketGrabRes
{
PR_ErrorStream, //
PR_ErrorChecksum, //
PR_Complete, //!包接收完成
PR_Incomplete, //!只收到一半的包流
PR_EmptyStream, //!空流
};
class OverlappedCustumRecv;
//!tcp是顺序接收数据,但可能出现合并包、分包的情况, 发送端发送A,B,C,D四个包,协议栈可能会发送A,BC,D,也就是把BC合成一个包发出去。
//!TCP协议栈在收到一个包的时候会同时计算下一个包的sequenceID,nextSequenceID = sequenceID + sizeof(currentPackage); 可以检测到包丢失
class PacketBase
{
friend class PacketQueue;
friend class NetClient;
friend class NetRobotsClient;
friend class NetRobot;
friend class NetServerIocp;
friend class NetPointUdp;
friend class PacketTemplateLib;
friend class InnerClient;
friend class InnerServer;
public:
PacketBase(int packetID);
virtual ~PacketBase();
virtual PacketBase* Alloc();
virtual void operator=(PacketBase* other){}
int GetPacketID();
void* GetParm();
int GetBufferSize();
char* GetBuffer();
virtual bool PreProcess();
virtual bool Process();
virtual void ToBuffer();
virtual void FromBuffer(void* syncGameInfo=NULL);
void SeekPos(int pos);
//!字段操作
void ReadHeader();
void ReadArray(void*value,int size);
template<class T>
void ReadValue(T& value);
void ReadString(char* value,int size);
template <int size>
void ReadString(char(&value)[size]);
void WriteHeader();
void WriteArray(const void* value,int size);
template<class T>
void WriteValue(const T& value);
void WriteString(const char* value);
//!准备发送:加校验 加密等
void PrepareSend();
//!直接复制转发
void CopyPacket(PacketBase* packet);
//!添加错误码m_res+复制转发
void CopyPacketRes(PacketBase* packet);
public:
//!buf前两个int分别是checksum&ID 和size,接收到包中会解除校验和
static PacketGrabRes GenPacketFromBuf(PacketBase*& packet, const char* buf, int &readSsize, int streamSize, PSocket fromSocket);
static void PushInTemplate(PacketBase* packet);
double GetAccumTime();
protected:
//!大包需要手动拆分
char m_buffer[MaxPacketSize];
int m_curPos;
//为了跨平台,不包含平台相关文件。
//如果使用外部socket指针,需要外部保证socket有效性。
//断开连接时,socket被先一步delete不好保证有效性(packet的处理要放在后面的主线程)。所以此处指向自己new出来的socket,自己负责delete。
PSocket m_fromSocket;
void* m_parm; //比如owner robot指针
//重叠字中可以保存 player指针,省去每个消息的map查找,提高效率
OverlappedCustumRecv* m_overlapedRecv;
public:
int m_packetID;
int m_streamSize;
//
int m_res; //错误码
static int HeadSize; //消息的前三个int分别是checksum&ID、size、debugID
#define DebugPacketID
//#ifdef DebugPacketID
unsigned int m_debugID;
static unsigned int DebugSendID;
static unsigned int DebugRecvID;
//#endif
static char PacketLogFlag[1024*10];
//static PacketBase* PacketTemplate[1024*10];
};
template<class T>
void PacketBase::WriteValue(const T& value)
{
int lenval=sizeof(T);
if(lenval>MaxValueLength)lenval=MaxValueLength;
memcpy(m_buffer+m_curPos,&value,lenval);
m_curPos+=lenval;
m_streamSize=m_curPos;
}
template<class T>
void PacketBase::ReadValue(T& value)
{
int lenval=sizeof(T);
if(lenval>MaxValueLength)lenval=MaxValueLength;
memcpy(&value,m_buffer+m_curPos,lenval);
m_curPos+=lenval;
}
template <int size>
void PacketBase::ReadString(char(&value)[size])
{
int lenstr=0;
ReadValue(lenstr);
if(lenstr>MaxValueLength)lenstr=MaxValueLength;
if(lenstr>=size)
{
memcpy(value, m_buffer+m_curPos,size-1);
value[size-1]='\0';
}
else
{
memcpy(value, m_buffer+m_curPos,lenstr);
value[lenstr]='\0';
}
m_curPos+=lenstr;
}
template<class T>
class PacketTemplateRegister
{
public:
PacketTemplateRegister()
{
//!确保全局变量构造顺序
PacketBase::PushInTemplate(new T);
}
void Space(){}
};
//
template<class T>
class PacketVisual:public PacketBase
{
public:
PacketVisual(int packetID):PacketBase(packetID){}
virtual PacketBase* Alloc(){return new T;}
virtual void operator=(PacketBase* other)
{
T* this_ = dynamic_cast<T*>(this);
T* other_ = dynamic_cast<T*>(other);
if (this_ && other_)
{
PSocket oldSocket = m_fromSocket;
//char* oldBuffer = m_buffer;
(*this_) = (*other_);
//深拷贝 恢复指针指向
Ptr2Socket(oldSocket) = Ptr2Socket(m_fromSocket);
m_fromSocket = oldSocket;
//memcpy(oldBuffer,m_buffer,m_bufferMax);
//m_buffer = oldBuffer;
}
}
//有的消息可能根据code写入不同的数据,或者list类消息数据不在结构体内,或者带有变长string、指针等,需要重写下面两个函数。
virtual void ToBuffer()
{
WriteHeader();
//copy packet T中的数据段,sizeof(T)-4-baseMember
WriteArray(this+sizeof(PacketBase),sizeof(T) - sizeof(PacketBase));
}
virtual void FromBuffer(void* syncGameInfo=NULL)
{
ReadHeader();
ReadArray(this+sizeof(PacketBase),sizeof(T) - sizeof(PacketBase));
}
protected:
//!利用虚函数必须被实现,使得静态成员被链接进来,
//!否则静态成员constructor不被链接,PacketTemplateRegister构造函数的断点都无法加
virtual void Space(){constructor.Space();}
static PacketTemplateRegister<T> constructor;
//todo
//send packet是栈数据 不会产生内存碎片 无需池子
//recv packet修改为 PacketT p; p.Parse(packetBase& b); 这样p为临时栈对象 b可以统一放在一个池子里 只需一把锁 (?p未知类型 无法定义临时变量?)
//!对象池, 一千消息就需要一千个池子,一千把锁??
//ObjectPool<T>
//PacketBase
//客户端发包限制
#ifdef CLIENT_APP
public:
static bool IsReady();
static bool m_waitingPacket;//某些消息在没收到反馈前不能多发
protected:
static double m_sendPeriod; //时间即时消息,一帧只能一次移动,捡取,技能等? 其它消息0.1秒间隔。? 列表1秒
static double m_nextSendTime;
#endif
};
template<class T>
PacketTemplateRegister<T> PacketVisual<T>::constructor;
#ifdef CLIENT_APP
template<class T> double PacketVisual<T>::m_waitingPacket = false;
template<class T> double PacketVisual<T>::m_sendPeriod = 0.033;
template<class T> double PacketVisual<T>::m_nextSendTime = 0;
template<class T> bool PacketVisual<T>::IsReady()
{
if (IsZero(m_sendPeriod)) return true;
if (m_sendPeriod < 0) return false;
double curTime = GetAccumTime();
if (curTime >= m_nextSendTime)
{
m_nextSendTime = curTime + m_sendPeriod;
return true;
}
}
#endif
class PacketQueue
{
friend class NetServerIocp;
public:
PacketQueue();
~PacketQueue();
int GetSize();
void

本文介绍了TCP/IP网络编程的基础,包括封包处理、完成端口IOCP的概念,并展示了如何使用C++实现一个简单的TCP服务器,涉及到封包、接收、发送流程,以及异步IOCP的使用。文章还提到了一些服务器编程注意事项,如错误处理、内存管理和并发控制。
最低0.47元/天 解锁文章
736

被折叠的 条评论
为什么被折叠?



