一直以来,flash就是我非常喜爱的平台,
因为他简单,完整,但是功能强大,
很适合游戏软件的开发,
只不过处理复杂的算法和海量数据的时候,
速度慢了一些,
但是这并不意味着flash不能做,
我们需要变通的方法去让flash做不善长的事情,
这个贴子用来专门讨论用flash作为客户端来开发网络游戏,
持续时间也不会很长,在把服务器端的源代码公开完以后,
就告一段落,
注意,仅仅用flash作为客户端,
服务器端,我们使用vc6,
我将陆续的公开服务器端的源代码和大家共享,
并且将讲解一些网络游戏开发的原理,
希望对此感兴趣的朋友能够使用今后的资源或者理论开发出完整的网络游戏。
我们从简单到复杂,
从棋牌类游戏到动作类的游戏,
从2个人的游戏到10个人的游戏,
因为工作忙的关系,我所做的一切仅仅起到抛砖引玉的作用,
希望大家能够热情的讨论,为中国的flash事业垫上一块砖,添上一片瓦。
现在的大型网络游戏(mmo game)都是基于server/client体系结构的,
server端用c(windows下我们使用vc.net+winsock)来编写,
客户端就无所谓,
在这里,我们讨论用flash来作为客户端的实现,
实践证明,flash的xml socket完全可以胜任网络传输部分,
在别的贴子中,我看见有的朋友谈论msn中的flash game
他使用msn内部的网络接口进行传输,
这种做法也是可以的,
我找很久以前对于2d图形编程的说法,"给我一个打点函数,我就能创造整个游戏世界",
而在网络游戏开发过程中,"给我一个发送函数和一个接收函数,我就能创造网络游戏世界."
我们抽象一个接口,就是网络传输的接口,
对于使用flash作为客户端,要进行网络连接,
一个网络游戏的客户端,
可以简单的抽象为下面的流程
1.与远程服务器建立一条长连接
2.用账号密码登陆
3.循环
接收消息
发送消息
4.关闭
我们可以直接使用flash 的xml socket,也可以使用类似msn的那种方式,
这些我们先不管,我们先定义接口,
Connect( "127.0.0.1", 20000 ); 连接远程服务器,建立一条长连接
Send( data, len ); 向服务器发送一条消息
Recv( data, len ); 接收服务器传来的消息
项目开发的基本硬件配置
一台普通的pc就可以了,
安装好windows 2000和vc6就可以了,
然后连上网,局域网和internet都可以,
接下去的东西我都简化,不去用晦涩的术语,
既然是网络,我们就需要网络编程接口,
服务器端我们用的是winsock 1.1,使用tcp连接方式,
[tcp和udp]
tcp可以理解为一条连接两个端子的隧道,提供可靠的数据传输服务,
只要发送信息的一方成功的调用了tcp的发送函数发送一段数据,
我们可以认为接收方在若干时间以后一定会接收到完整正确的数据,
不需要去关心网络传输上的细节,
而udp不保证这一点,
对于网络游戏来说,tcp是普遍的选择。
[阻塞和非阻塞]
在通过socket发送数据时,如果直到数据发送完毕才返回的方式,也就是说如果我们使用send( buffer, 100.....)这样的函数发送100个字节给别人,我们要等待,直到100个自己发送完毕,程序才往下走,这样就是阻塞的,
而非阻塞的方式,当你调用send(buffer,100....)以后,立即返回,此时send函数告诉你发送成功,并不意味着数据已经向目的地发送完
毕,甚至有可能数据还没有开始发送,只被保留在系统的缓冲里面,等待被发送,但是你可以认为数据在若干时间后,一定会被目的地完整正确的收到,我们要充分
的相信tcp。
阻塞的方式会引起系统的停顿,一般网络游戏里面使用的都是非阻塞的方式,
[有状态服务器和无状态服务器]
在c/s体系中,如果server不保存客户端的状态,称之为无状态,反之为有状态,
在这里要强调一点,
我们所说的服务器不是一台具体的机器,
而是指服务器应用程序,
一台具体的机器或者机器群组可以运行一个或者多个服务器应用程序,
我们的网络游戏使用的是有状态服务器,
保存所有玩家的数据和状态,
一些有必要了解的理论和开发工具
[开发语言]
vc6
我们首先要熟练的掌握一门开发语言,
学习c++是非常有必要的,
而vc是windows下面的软件开发工具,
为什么选择vc,可能与我本身使用vc有关,
而且网上可以找到许多相关的资源和源代码,
[操作系统]
我们使用windows2000作为服务器的运行环境,
所以我们有必要去了解windows是如何工作的,
同时对它的编程原理应该熟练的掌握
[数据结构和算法]
要写出好的程序要先具有设计出好的数据结构和算法的能力,
好的算法未必是繁琐的公式和复杂的代码,
我们要找到又好写有满足需求的算法,
有时候,最笨的方法同时也是很好的方法,
很多程序员沉迷于追求精妙的算法而忽略了宏观上的工程,
花费了大量的精力未必能够取得好的效果,
举个例子,
我当年进入游戏界工作,学习老师的代码,
发现有个函数,要对画面中的npc位置进行排序,
确定哪个先画,那个后画,
他的方法太“笨”,
任何人都会想到的冒泡,
一个一个去比较,没有任何的优化,
我当时想到的算法就有很多,
而且有一大堆优化策略,
可是,当我花了很长时间去实现我的算法时,
发现提升的那么一点效率对游戏整个运行效率而言几乎是没起到什么作用,
或者说虽然算法本身快了几倍,
可是那是多余的,老师的算法虽然“笨”,
可是他只花了几十行代码就搞定了,
他的时间花在别的更需要的地方,
这就是他可以独自完成一个游戏,
而我可以把一个函数优化100倍也只能打杂的原因
[tcp/ip的理论]
推荐数据用tcp/ip进行网际互连,tcp/ip详解,
这是两套书,共有6卷,
都是国外的大师写的,
可以说是必读的,
网络传输中的“消息”
[消息]
消息是个很常见的术语,
在windows中,消息机制是个十分重要的概念,
我们在网络游戏中,也使用了消息这样的机制,
一般我们这么做,
一个数据块,头4个字节是消息名,后面接2个字节的数据长度,
再后面就是实际的数据
为什么使用消息??
我们来看看例子,
在游戏世界,
一个玩家想要和别的玩家聊天,
那么,他输入好聊天信息,
客户端生成一条聊天消息,
并把聊天的内容打包到消息中,
然后把聊天消息发送给服务器,
请求服务器把聊天信息发送给另一个玩家,
服务器接收到一条消息,
此刻,服务器并不知道当前的数据是什么东西,
对于服务器来讲,这段数据仅仅来自于网络通讯的底层,
不加以分析的话,没有任何的信息,
因为我们的通讯是基于消息机制的,
我们认为服务器接收到的任何数据都是基于消息的数据方式组织的,
4个字节消息名,2字节长度,这个是不会变的,
通过消息名,服务器发现当前数据是一条聊天数据,
通过长度把需要的数据还原,校验,
然后把这条消息发送给另一个玩家,
大家注意,消息是变长的,
关于消息的解释完全在于服务器和客户端的应用程序,
可以认为与网络传输低层无关,
比如一条私聊消息可能是这样的,
MsgID:4 byte
Length:2 byte
TargetPlayerID:2 byte
String:anybyte < 256
一条移动消息可能是这样的,
MsgID:4 byte
Length:2 byte
TargetPlayerID:2 byte
TargetPosition:4 byte (x,y)
编程者可以自定义消息的内容以满足不同的需求
队列
[队列]
队列是一个很重要的数据结构,
比如说消息队列,
服务器或者客户端,
发送的消息不一定是立即发送的,
而是等待一个适当时间,
或者系统规定的时间间隔以后才发送,
这样就需要创建一个消息队列,以保存发送的消息,
消息队列的大小可以按照实际的需求创建,
队列又可能会满,
当队列满了,可以直接丢弃消息,
如果你觉得这样不妥,
也可以预先划分一个足够大的队列,
可以使用一个系统全局的大的消息队列,
也可以为每个对象创建一个消息队列,
这个我们的一个数据队列的实现,
开发工具vc.net,使用了C++的模板,
关于队列的算法和基础知识,我就不多说了,
DataBuffer.h
#ifndef __DATABUFFER_H__
#define __DATABUFFER_H__
#include <windows.h>
#include <assert.h>
#include "g_assert.h"
#include <stdio.h>
#ifndef HAVE_BYTE
typedef unsigned char byte;
#endif // HAVE_BYTE
//数据队列管理类
template <const int _max_line, const int _max_size>
class DataBufferTPL
{
public:
bool Add( byte *data ) // 加入队列数据
{
G_ASSERT_RET( data, false );
m_ControlStatus = false;
if( IsFull() )
{
//assert( false );
return false;
}
memcpy( m_s_ptr, data, _max_size );
NextSptr();
m_NumData++;
m_ControlStatus = true;
return true;
}
bool Get( byte *data ) // 从队列中取出数据
{
G_ASSERT_RET( data, false );
m_ControlStatus = false;
if( IsNull() )
return false;
memcpy( data, m_e_ptr, _max_size );
NextEptr();
m_NumData--;
m_ControlStatus = true;
return true;
}
bool CtrlStatus() // 获取操作成功结果
{
return m_ControlStatus;
}
int GetNumber() // 获得现在的数据大小
{
return m_NumData;
}
public:
DataBufferTPL()
{
m_NumData = 0;
m_start_ptr = m_DataTeam[0];
m_end_ptr = m_DataTeam[_max_line-1];
m_s_ptr = m_start_ptr;
m_e_ptr = m_start_ptr;
}
~DataBufferTPL()
{
m_NumData = 0;
m_s_ptr = m_start_ptr;
m_e_ptr = m_start_ptr;
}
private:
bool IsFull() // 是否队列满
{
G_ASSERT_RET( m_NumData >=0 && m_NumData <= _max_line, false );
if( m_NumData == _max_line )
return true;
else
return false;
}
bool IsNull() // 是否队列空
{
G_ASSERT_RET( m_NumData >=0 && m_NumData <= _max_line, false );
if( m_NumData == 0 )
return true;
else
return false;
}
void NextSptr() // 头位置增加
{
assert(m_start_ptr);
assert(m_end_ptr);
assert(m_s_ptr);
assert(m_e_ptr);
m_s_ptr += _max_size;
if( m_s_ptr > m_end_ptr )
m_s_ptr = m_start_ptr;
}
void NextEptr() // 尾位置增加
{
assert(m_start_ptr);
assert(m_end_ptr);
assert(m_s_ptr);
assert(m_e_ptr);
m_e_ptr += _max_size;
if( m_e_ptr > m_end_ptr )
m_e_ptr = m_start_ptr;
}
private:
byte m_DataTeam[_max_line][_max_size]; //数据缓冲
int m_NumData; //数据个数
bool m_ControlStatus; //操作结果
byte *m_start_ptr; //起始位置
byte *m_end_ptr; //结束位置
byte *m_s_ptr; //排队起始位置
byte *m_e_ptr; //排队结束位置
};
//////////////////////////////////////////////////////////////////////////
// 放到这里了!
//ID自动补位列表模板,用于自动列表,无间空顺序列表。
template <const int _max_count>
class IDListTPL
{
public:
// 清除重置
void Reset()
{
for(int i=0;i<_max_count;i++)
m_dwList[i] = G_ERROR;
m_counter = 0;
}
int MaxSize() const { return _max_count; }
int Count() const { return m_counter; }
const DWORD operator[]( int iIndex ) {
G_ASSERTN( iIndex >= 0 && iIndex < m_counter );
return m_dwList[ iIndex ];
}
bool New( DWORD dwID )
{
G_ASSERT_RET( m_counter >= 0 && m_counter < _max_count, false );
//ID 唯一性,不能存在相同ID
if ( Find( dwID ) != -1 )
return false;
m_dwList[m_counter] = dwID;
m_counter++;
return true;
}
// 没有Assert的加入ID功能
bool Add( DWORD dwID )
{
if( m_counter <0 || m_counter >= _max_count )
return false;
//ID 唯一性,不能存在相同ID
if ( Find( dwID ) != -1 )
return false;
m_dwList[m_counter] = dwID;
m_counter++;
return true;
}
bool Del( int iIndex )
{
G_ASSERT_RET( iIndex >=0 && iIndex < m_counter, false );
for(int k=iIndex;k<m_counter-1;k++)
{
m_dwList[k] = m_dwList[k+1];
}
m_dwList[k] = G_ERROR;
m_counter--;
return true;
}
int Find( DWORD dwID )
{
for(int i=0;i<m_counter;i++)
{
if( m_dwList[i] == dwID )
return i;
}
return -1;
}
IDListTPL():m_counter(0)
{
for(int i=0;i<_max_count;i++)
m_dwList[i] = G_ERROR;
}
virtual ~IDListTPL()
{}
private:
DWORD m_dwList[_max_count];
int m_counter;
};
//////////////////////////////////////////////////////////////////////////
#endif //__DATABUFFER_H__
socket
我们采用winsock作为网络部分的编程接口,
接下去编程者有必要学习一下socket的基本知识,
不过不懂也没有关系,我提供的代码已经把那些麻烦的细节或者正确的系统设置给弄好了,
编程者只需要按照规则编写游戏系统的处理代码就可以了,
这些代码在vc6下编译通过,
是通用的网络传输底层,
这里是socket部分的代码,
我们需要安装vc6才能够编译以下的代码,
因为接下去我们要接触越来越多的c++,
所以,大家还是去看看c++的书吧,
// socket.h
#ifndef _socket_h
#define _socket_h
#pragma once
//定义最大连接用户数目 ( 最大支持 512 个客户连接 )
#define MAX_CLIENTS 512
//#define FD_SETSIZE MAX_CLIENTS
#pragma comment( lib, "wsock32.lib" )
#include <winsock.h>
class CSocketCtrl
{
void SetDefaultOpt();
public:
CSocketCtrl(): m_sockfd(INVALID_SOCKET){}
BOOL StartUp();
BOOL ShutDown();
BOOL IsIPsChange();
BOOL CanWrite();
BOOL HasData();
int Recv( char* pBuffer, int nSize, int nFlag );
int Send( char* pBuffer, int nSize, int nFlag );
BOOL Create( UINT uPort );
BOOL Create(void);
BOOL Connect( LPCTSTR lpszHostAddress, UINT nHostPort );
void Close();
BOOL Listen( int nBackLog );
BOOL Accept( CSocketCtrl& sockCtrl );
BOOL RecvMsg( char *sBuf );
int SendMsg( char *sBuf,unsigned short stSize );
SOCKET GetSockfd(){ return m_sockfd; }
BOOL GetHostName( char szHostName[], int nNameLength );
protected:
SOCKET m_sockfd;
static DWORD m_dwConnectOut;
static DWORD m_dwReadOut;
static DWORD m_dwWriteOut;
static DWORD m_dwAcceptOut;
static DWORD m_dwReadByte;
static DWORD m_dwWriteByte;
};
#endif
// socket.cpp
#include <stdio.h>
#include "msgdef.h"
#include "socket.h"
// 吊线时间
#define ALL_TIMEOUT 120000
DWORD CSocketCtrl::m_dwConnectOut = 60000;
DWORD CSocketCtrl::m_dwReadOut = ALL_TIMEOUT;
DWORD CSocketCtrl::m_dwWriteOut = ALL_TIMEOUT;
DWORD CSocketCtrl::m_dwAcceptOut = ALL_TIMEOUT;
DWORD CSocketCtrl::m_dwReadByte = 0;
DWORD CSocketCtrl::m_dwWriteByte = 0;
// 接收数据
BOOL CSocketCtrl::RecvMsg( char *sBuf )
{
if( !HasData() )
return FALSE;
MsgHeader header;
int nbRead = this->Recv( (char*)&header, sizeof( header ), MSG_PEEK );
if( nbRead == SOCKET_ERROR )
return FALSE;
if( nbRead < sizeof( header ) )
{
this->Recv( (char*)&header, nbRead, 0 );
printf( "\ninvalid msg, skip %ld bytes.", nbRead );
return FALSE;
}
if( this->Recv( (char*)sBuf, header.stLength, 0 ) != header.stLength )
return FALSE;
return TRUE;
}
// 发送数据
int CSocketCtrl::SendMsg( char *sBuf,unsigned short stSize )
{
static char sSendBuf[ 4000 ];
memcpy( sSendBuf,&stSize,sizeof(short) );
memcpy( sSendBuf + sizeof(short),sBuf,stSize );
if( (sizeof(short) + stSize) != this->Send( sSendBuf,stSize+sizeof(short),0 ) )
return -1;
return stSize;
}
// 启动winsock
BOOL CSocketCtrl::StartUp()
{
WSADATA wsaData;
WORD wVersionRequested = MAKEWORD( 1, 1 );
int err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 )
{
return FALSE;
}
return TRUE;
}
// 关闭winsock
BOOL CSocketCtrl::ShutDown()
{
WSACleanup();
return TRUE;
}
// 得到主机名
BOOL CSocketCtrl::GetHostName( char szHostName[], int nNameLength )
{
if( gethostname( szHostName, nNameLength ) != SOCKET_ERROR )
return TRUE;
return FALSE;
}
BOOL CSocketCtrl::IsIPsChange()
{
return FALSE;
static int iIPNum = 0;
char sHost[300];
hostent *pHost;
if( gethostname(sHost,299) != 0 )
return FALSE;
pHost = gethostbyname(sHost);
int i;
char *psHost;
i = 0;
do
{
psHost = pHost->h_addr_list[i++];
if( psHost == 0 )
break;
}while(1);
if( iIPNum != i )
{
iIPNum = i;
return TRUE;
}
return FALSE;
}
// socket是否可以写
BOOL CSocketCtrl::CanWrite()
{
int e;
fd_set set;
timeval tout;
tout.tv_sec = 0;
tout.tv_usec = 0;
FD_ZERO(&set);
FD_SET(m_sockfd,&set);
e=::select(0,NULL,&set,NULL,&tout);
if(e==SOCKET_ERROR) return FALSE;
if(e>0) return TRUE;
return FALSE;
}
// socket是否有数据
BOOL CSocketCtrl::HasData()
{
int e;
fd_set set;
timeval tout;
tout.tv_sec = 0;
tout.tv_usec = 0;
FD_ZERO(&set);
FD_SET(m_sockfd,&set);
e=::select(0,&set,NULL,NULL,&tout);
if(e==SOCKET_ERROR) return FALSE;
if(e>0) return TRUE;
return FALSE;
}
int CSocketCtrl::Recv( char* pBuffer, int nSize, int nFlag )
{
return recv( m_sockfd, pBuffer, nSize, nFlag );
}
int CSocketCtrl::Send( char* pBuffer, int nSize, int nFlag )
{
return send( m_sockfd, pBuffer, nSize, nFlag );
}
BOOL CSocketCtrl::Create( UINT uPort )
{
m_sockfd=::socket(PF_INET,SOCK_STREAM,0);
if(m_sockfd==INVALID_SOCKET) return FALSE;
SOCKADDR_IN SockAddr;
memset(&SockAddr,0,sizeof(SockAddr));
SockAddr.sin_family = AF_INET;
SockAddr.sin_addr.s_addr = INADDR_ANY;
SockAddr.sin_port = ::htons( uPort );
if(!::bind(m_sockfd,(SOCKADDR*)&SockAddr, sizeof(SockAddr)))
{
SetDefaultOpt();
return TRUE;
}
Close();
return FALSE;
}
void CSocketCtrl::Close()
{
::closesocket( m_sockfd );
m_sockfd = INVALID_SOCKET;
}
BOOL CSocketCtrl::Connect( LPCTSTR lpszHostAddress, UINT nHostPort )
{
if(m_sockfd==INVALID_SOCKET) return FALSE;
SOCKADDR_IN sockAddr;
memset(&sockAddr,0,sizeof(sockAddr));
LPSTR lpszAscii=(LPSTR)lpszHostAddress;
sockAddr.sin_family=AF_INET;
sockAddr.sin_addr.s_addr=inet_addr(lpszAscii);
if(sockAddr.sin_addr.s_addr==INADDR_NONE)
{
HOSTENT * lphost;
lphost = ::gethostbyname(lpszAscii);
if(lphost!=NULL)
sockAddr.sin_addr.s_addr = ((IN_ADDR *)lphost->h_addr)->s_addr;
else return FALSE;
}
sockAddr.sin_port = htons((u_short)nHostPort);
int r=::connect(m_sockfd,(SOCKADDR*)&sockAddr,sizeof(sockAddr));
if(r!=SOCKET_ERROR) return TRUE;
int e;
e=::WSAGetLastError();
if(e!=WSAEWOULDBLOCK) return FALSE;
fd_set set;
timeval tout;
tout.tv_sec = 0;
tout.tv_usec = 100000;
UINT n=0;
while( n< CSocketCtrl::m_dwConnectOut)
{
FD_ZERO(&set);
FD_SET(m_sockfd,&set);
e=::select(0,NULL,&set,NULL, &tout);
if(e==SOCKET_ERROR) return FALSE;
if(e>0) return TRUE;
if( IsIPsChange() )
return FALSE;
n += 100;
}
return FALSE;
}
// 设置监听socket
BOOL CSocketCtrl::Listen( int nBackLog )
{
if( m_sockfd == INVALID_SOCKET ) return FALSE;
if( !listen( m_sockfd, nBackLog) ) return TRUE;
return FALSE;
}
// 接收一个新的客户连接
BOOL CSocketCtrl::Accept( CSocketCtrl& ms )
{
if( m_sockfd == INVALID_SOCKET ) return FALSE;
if( ms.m_sockfd != INVALID_SOCKET ) return FALSE;
int e;
fd_set set;
timeval tout;
tout.tv_sec = 0;
tout.tv_usec = 100000;
UINT n=0;
while(n< CSocketCtrl::m_dwAcceptOut)
{
//if(stop) return FALSE;
FD_ZERO(&set);
FD_SET(m_sockfd,&set);
e=::select(0,&set,NULL,NULL, &tout);
if(e==SOCKET_ERROR) return FALSE;
if(e==1) break;
n += 100;
}
if( n>= CSocketCtrl::m_dwAcceptOut ) return FALSE;
ms.m_sockfd=accept(m_sockfd,NULL,NULL);
if(ms.m_sockfd==INVALID_SOCKET) return FALSE;
ms.SetDefaultOpt();
return TRUE;
}
BOOL CSocketCtrl::Create(void)
{
m_sockfd=::socket(PF_INET,SOCK_STREAM,0);
if(m_sockfd==INVALID_SOCKET) return FALSE;
SOCKADDR_IN SockAddr;
memset(&SockAddr,0,sizeof(SockAddr));
SockAddr.sin_family = AF_INET;
SockAddr.sin_addr.s_addr = INADDR_ANY;
SockAddr.sin_port = ::htons(0);
//if(!::bind(m_sock,(SOCKADDR*)&SockAddr, sizeof(SockAddr)))
{
SetDefaultOpt();
return TRUE;
}
Close();
return FALSE;
}
// 设置正确的socket状态,
// 主要是主要是设置非阻塞异步传输模式
void CSocketCtrl::SetDefaultOpt()
{
struct linger ling;
ling.l_onoff=1;
ling.l_linger=0;
setsockopt( m_sockfd, SOL_SOCKET, SO_LINGER, (char *)&ling, sizeof(ling));
setsockopt( m_sockfd, SOL_SOCKET, SO_REUSEADDR, 0, 0);
int bKeepAlive = 1;
setsockopt( m_sockfd, SOL_SOCKET, SO_KEEPALIVE, (char*)&bKeepAlive, sizeof(int));
BOOL bNoDelay = TRUE;
setsockopt( m_sockfd, IPPROTO_TCP, TCP_NODELAY, (char*)&bNoDelay, sizeof(BOOL));
unsigned long nonblock=1;
::ioctlsocket(m_sockfd,FIONBIO,&nonblock);
}
今天晚上写了一些测试代码,
想看看flash究竟能够承受多大的网络数据传输,
我在flash登陆到服务器以后,
每隔3毫秒就发送100次100个字符的串
"0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
给flash,
然后在flash里面接收数据的函数里面统计数据,
var g_nTotalRecvByte = 0;
var g_time = new Date();
var g_nStartTime = g_time.getTime();
var g_nCounter = 0;
mySocket.onData=function(xmlDoc)
{
g_nTotalRecvByte += xmlDoc.length;
// 每接收超过1k字节的数据,输出一次信息,
if( g_nTotalRecvByte-g_nCounter > 1024 )
{
g_time = new Date();
var nPassedTime = g_time.getTime()-g_nStartTime;
trace( "花费时间:"+nPassedTime+"毫秒" );
g_nCounter = g_nTotalRecvByte;
trace( "接收总数:"+g_nTotalRecvByte+"字节" );
trace( "接收速率:"+g_nTotalRecvByte*1000/nPassedTime+"字节/秒" );
}
结果十分令我意外,
这是截取的一段调试信息,
//
花费时间:6953毫秒
接收总数:343212字节
接收速率:49361.7143678988字节/秒
花费时间:7109毫秒
接收总数:344323字节
接收速率:48434.800956534字节/秒
花费时间:7109毫秒
接收总数:345434字节
接收速率:48591.0817273878字节/秒
。。。
。。。
。。。
。。。
花费时间:8125毫秒
接收总数:400984字节
接收速率:49351.8769230769字节/秒
花费时间:8125毫秒
接收总数:402095字节
接收速率:49488.6153846154字节/秒
花费时间:8125毫秒
接收总数:403206字节
接收速率:49625.3538461538字节/秒
我检查了几遍源程序,没有发现逻辑错误,
如果程序没有问题的话,
那么我们得出的结论是,flash的xml socket每秒可以接收至少40K的数据,
这还没有计算xmlSocket.onData事件的触发,调试代码、信息输出占用的时间。
比我想象中快了一个数量级,
够用了,
flash网络游戏我们可以继续往下走了,
有朋友问到lag的问题,
问得很好,不过也不要过于担心,
lag的产生有的是因为网络延迟,
有的是因为服务器负载过大,
对于游戏的设计者和开发者来说,
首先要从设计的角度来避免或者减少lag产生的机会,
如果lag产生了,
也不要紧,找到巧妙的办法骗过玩家的眼睛,
这也有很多成熟的方法了,
比如航行预测法,路径插值等等,
都可以产生很好的效果,
还有最后的绝招,就是提高服务器的配置和网络带宽,
从我开发网络游戏这段时间的经验来看,
我们的服务器是vc开发的,
普通pc跑几百个玩家,几百个怪物是没有问题的,
又作了一个flash发送的测试,
网络游戏的特点是,
出去的信息比较少,
进来的信息比较多,
这个很容易理解,
人操作游戏的速度是很有限的,
控制指令的产生也是随机的,
离散的,
但是多人游戏的话,
因为人多,信息的流量也就区域均匀分布了,
在昨天接收数据的基础上,
我略加修改,
这次,
我在_root.enterFrame写了如下代码,
_root.onEnterFrame = function()
{
var i;
for( i = 0; i < 10; i++ )
mySocket.send( ConvertToMsg( "01234567890123456789012345678901234567890123456789" ) );
return;
}
服务器端要做的是,
把所有从flash客户端收到的信息原封不动的返回来,
这样,我又可以通过昨天onData里面的统计算法来从侧面估算出flash发送数据的能力,
这里是输出的数据
//
花费时间:30531毫秒
接收总数:200236字节
接收速率:6558.44878975468字节/秒
花费时间:30937毫秒
接收总数:201290字节
接收速率:6506.44858906811字节/秒
花费时间:31140毫秒
接收总数:202344字节
接收速率:6497.88053949904字节/秒
花费时间:31547毫秒
接收总数:203398字节
接收速率:6447.45934637208字节/秒
可以看出来,发送+接收同时做,
发送速率至少可以达到5k byte/s
有一点要注意,要非常注意,
不能让flash的网络传输满载,
所谓满载就是flash在阻塞运算的时候,
不断的有数据从网络进来,
而flash又无法在预计的时间内处理我这些信息,
或者flash发送数据过于频繁,
导致服务器端缓冲溢出导致错误,
对于5k的传输速率,
已经足够了,
因为我也想不出来有什么产生这么大的数据量,
而且如果产生了这么大的数据量,
也就意味着服务器每时每刻都要处理所有的玩家发出的海量数据,
还要把这些海量数据转发给其他的玩家,
已经引起数据爆炸了,
所以,5k的上传从设计阶段就要避免的,
我想用flash做的网络游戏,
除了动作类游戏可能需要恒定1k以内的上传速率,
其他的200个字节/秒以内就可以了,
使用于Flash的消息结构定义
我们以前讨论过,
通过消息来传递信息,
消息的结构是
struct msg
{
short nLength; // 2 byte
DWORD dwId; // 4 byte
....
data
}
但是在为flash开发的消息中,
不能采用这种结构,
首先Flash xmlSocket只传输字符串,
从xmlSocket的send,onData函数可以看出来,
发出去的,收进来的都应该是字符串,
而在服务器端是使用vc,java等高级语言编写的,
消息中使用的是二进制数据块,
显然,简单的使用字符串会带来问题,
所以,我们需要制定一套协议,
就是无论在客户端还是服务器端,
都用统一的字符串消息,
通过解析字符串的方式来传递信息,
我想这就是flash采用xml document来传输结构化信息的理由之一,
xml document描述了一个完整的数据结构,
而且全部使用的是字符串,
原来是这样,怪不得叫做xml socket,
本来socket和xml完全是不同的概念,
flash偏偏出了个xml socket,
一开始令我费解,
现在,渐渐理解其中奥妙。
Flash Msg结构定义源代码和相关函数
在服务器端,我们为flash定义了一种msg结构,
使用语言,vc6
#define MSGMAXSIZE 512
// 消息头
struct MsgHeader
{
short stLength;
MsgHeader():stLength( 0 ){}
};
// 消息
struct Msg
{
MsgHeader header;
short GetLength(){ return header.stLength; }
};
// flash 消息
struct MsgToFlashublic Msg
{
// 一个足够大的缓冲,但是不会被整个发送,
char szString[MSGMAXSIZE];
// 计算设置好内容后,内部会计算将要发送部分的长度,
// 要发送的长度=消息头大小+字符串长度+1
void SetString( const char* pszChatString )
{
if( strlen( pszChatString ) < MSGMAXSIZE-1 )
{
strcpy( szString, pszChatString );
header.stLength = sizeof( header )+
(short)strlen( pszChatString )+1;
}
}
};
在发往flash的消息中,整个处理过后MsgToFlash结构将被发送,
实践证明,在flash 客户端的xmlSocket onData事件中,
接收到了正确的消息,消息的内容是MasToFlash的szString字段,
是一个字符串,
比如在服务器端,
MsgToFlash msg;
msg.SetString( "move player0 to 100 100" );
SendMsg( msg,............. );
那么,在我们的flash客户端的onData( xmlDoc )中,
我们trace( xmlDoc )
结果是
move player0 to 100 100
然后是flash发送消息到服务器,
我们强调flash只发送字符串,
这个字符串无论是否内部拥有有效数据,
服务器都应该首先把消息收下来,
那就要保证发送给服务器的消息遵循统一的结构,
在flash客户端中,
我们定义一个函数,
这个函数把一个字符串转化为服务器可以识别的消息,
补充:现在我们约定字符串长度都不大于97个字节长度,
var num_table = new array( "0","1","2","3","4","5","6","7","8","9" );
function ConvertToMsg( str )
{
var l = str.length+3;
var t = "";
if( l > 10 )
t = num_table[Math.floor(l/10)]+num_table[Number(l%10)]+str;
else
t = num_table[0]+num_table[l]+str;
return t;
}
比如
var msg = ConvertToMsg( "client login" );
我们trace( msg );
看到的是
15client login
为什么是这个结果呢?
15是消息的长度,
头两个字节是整个消息的长度的asc码,意思是整个消息有15个字节长,
然后是信息client login,
最后是一个0(c语言中的字符串结束符)
当服务器收到15client login,
他首先把15给分析出来,
把"15"字符串转化为15的数字,
然后,根据15这个长度把后面的client login读出来,
这样,网络传输的底层就完成了,
client login的处理就交给逻辑层,
谢谢大家的支持,
很感谢斑竹把这个贴子置顶,
我写这文章的过程也是我自己摸索的过程,
文章可以记录我一段开发的历史,
一个思考分析的历程,
有时候甚至作为日志来写,
由于我本身有杂务在身,
所以贴子的更新有点慢,
请大家见谅,
我喜爱flash,
虽然我在帝国中,但我并不能称之为闪客,
因为我制作flash的水平实在很低,
但是我想设计开发出让其他人能更好的使用flash的工具,
前阵子我开发了Match3D,
一个可以把三维动画输出成为swf的工具,
而且实现了swf渲染的实时三维角色动画,
这可以说是我真正推出的第一个flash第三方软件,
其实这以前,
我曾经开发过几个其他的flash第三方软件,
都中途停止了,
因为不实用或者市场上有更好的同类软件,
随着互联网的发展,
flash的不断升级,
我的flash第三方软件目光渐渐的从美术开发工具转移到网络互连,
web应用上面来,
如今已经到了2004版本,
flash的种种新特性让我眼前发光,
我最近在帝国的各个板块看了很多贴子,
分析里面潜在的用户需求,
总结了以下的几个我认为比较有意义的选题,
可能很片面,
flash源代码保护,主要是为了抵御asv之类的软件进行反编译和萃取
flash与远端数据库的配合,应该出现一个能够方便快捷的对远程数据库进行操作的方法或者控件,
flash网际互连,我认为flash网络游戏是一块金子,
这里我想谈谈flash网络游戏,
我要谈的不仅仅是技术,而是一个概念,
用flash网络游戏,
我本身并不想把flash游戏做成rpg或者其他剧烈交互性的游戏,
而是想让flash实现那些节奏缓慢,玩法简单的游戏,
把网络的概念带进来,
你想玩游戏的时候,登上flash网络游戏的网站,
选择你想玩的网络游戏,
因为现在几乎所有上网的电脑都可以播放swf,
所以,我们几乎不用下载任何插件,
输入你的账号和密码,
就可以开始玩了,
我觉得battle.net那种方式很适合flash,
开房间或者进入别人开的房间,
然后2个人或者4个人就可以交战了,
这种游戏可以是棋类,这是最基本的,
用户很广泛,
我脑海中的那种是类似与宠物饲养的,
就像当年的电子宠物,
每个玩家都可以到服务器认养宠物,
然后在线养成宠物,
还可以邀请别的玩家进行宠物比武,
看谁的宠物厉害,
就这样简简单单的模式,
配合清新可爱的画面,
趣味的玩法,
加入网络的要素,
也许可以取得以想不到的效果,
今天就说到这里吧,
想法那么多,要实现的话还有很多路要走,
希望大家多多支持,积极参与,
让我们的想法不仅仅停留于纸上。
大家好,
非常抱歉,
都很长时间没有回贴了,
因为手头项目的原因,
几乎没有时间做flash multiplayer的研究,
很感谢大家的支持,
现在把整个flash networking的源代码共享出来,
大家可以任意的使用,
其实里面也没有多少东西,
相信感兴趣的朋友还是可以从中找到一些有用的东西,
这一次的源代码做的事情很简单,
服务器运行,
客户端登陆到服务器,
然后客户端不断的发送字符串给服务器,
服务器收到后,在发还给客户端,
客户端统计一些数据,
posted on 2009-09-23 23:43
暗夜教父 阅读(594)
评论(0) 编辑 收藏 引用 所属分类:
Game Development