游戏服务器编程-iocp及封包处理

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

        本人不是专业的服务器程序员,所写的文章难免错漏,仅供参考,欢迎指正。
        游戏中用到的网络编程多数是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		
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值