因為他簡單,完整,但是功能強(qiáng)大,
很適合游戲軟件的開發(fā),
只不過處理復(fù)雜的算法和海量數(shù)據(jù)的時候,
速度慢了一些,
但是這并不意味著flash不能做,
我們需要變通的方法去讓flash做不善長的事情,
這個貼子用來專門討論用flash作為客戶端來開發(fā)網(wǎng)絡(luò)游戲,
持續(xù)時間也不會很長,在把服務(wù)器端的源代碼公開完以后,
就告一段落,
注意,僅僅用flash作為客戶端,
服務(wù)器端,我們使用vc6,
我將陸續(xù)的公開服務(wù)器端的源代碼和大家共享,
并且將講解一些網(wǎng)絡(luò)游戲開發(fā)的原理,
希望對此感興趣的朋友能夠使用今后的資源或者理論開發(fā)出完整的網(wǎng)絡(luò)游戲。
我們從簡單到復(fù)雜,
從棋牌類游戲到動作類的游戲,
從2個人的游戲到10個人的游戲,
因為工作忙的關(guān)系,我所做的一切僅僅起到拋磚引玉的作用,
希望大家能夠熱情的討論,為中國的flash事業(yè)墊上一塊磚,添上一片瓦。
現(xiàn)在的大型網(wǎng)絡(luò)游戲(mmo game)都是基于server/client體系結(jié)構(gòu)的,
server端用c(windows下我們使用vc.net+winsock)來編寫,
客戶端就無所謂,
在這里,我們討論用flash來作為客戶端的實現(xiàn),
實踐證明,flash的xml socket完全可以勝任網(wǎng)絡(luò)傳輸部分,
在別的貼子中,我看見有的朋友談?wù)搈sn中的flash game
他使用msn內(nèi)部的網(wǎng)絡(luò)接口進(jìn)行傳輸,
這種做法也是可以的,
我找很久以前對于2d圖形編程的說法,"給我一個打點(diǎn)函數(shù),我就能創(chuàng)造整個游戲世界",
而在網(wǎng)絡(luò)游戲開發(fā)過程中,"給我一個發(fā)送函數(shù)和一個接收函數(shù),我就能創(chuàng)造網(wǎng)絡(luò)游戲世界."
我們抽象一個接口,就是網(wǎng)絡(luò)傳輸?shù)慕涌冢?br> 對于使用flash作為客戶端,要進(jìn)行網(wǎng)絡(luò)連接,
一個網(wǎng)絡(luò)游戲的客戶端,
可以簡單的抽象為下面的流程
1.與遠(yuǎn)程服務(wù)器建立一條長連接
2.用賬號密碼登陸
3.循環(huán)
接收消息
發(fā)送消息
4.關(guān)閉
我們可以直接使用flash 的xml socket,也可以使用類似msn的那種方式,
這些我們先不管,我們先定義接口,
Connect( "127.0.0.1", 20000 ); 連接遠(yuǎn)程服務(wù)器,建立一條長連接
Send( data, len ); 向服務(wù)器發(fā)送一條消息
Recv( data, len ); 接收服務(wù)器傳來的消息
項目開發(fā)的基本硬件配置
一臺普通的pc就可以了,
安裝好windows 2000和vc6就可以了,
然后連上網(wǎng),局域網(wǎng)和internet都可以,
接下去的東西我都簡化,不去用晦澀的術(shù)語,
既然是網(wǎng)絡(luò),我們就需要網(wǎng)絡(luò)編程接口,
服務(wù)器端我們用的是winsock 1.1,使用tcp連接方式,
[tcp和udp]
tcp可以理解為一條連接兩個端子的隧道,提供可靠的數(shù)據(jù)傳輸服務(wù),
只要發(fā)送信息的一方成功的調(diào)用了tcp的發(fā)送函數(shù)發(fā)送一段數(shù)據(jù),
我們可以認(rèn)為接收方在若干時間以后一定會接收到完整正確的數(shù)據(jù),
不需要去關(guān)心網(wǎng)絡(luò)傳輸上的細(xì)節(jié),
而udp不保證這一點(diǎn),
對于網(wǎng)絡(luò)游戲來說,tcp是普遍的選擇。
[阻塞和非阻塞]
在通過socket發(fā)送數(shù)據(jù)時,如果直到數(shù)據(jù)發(fā)送完畢才返回的方式,也就是說如果我們使用send( buffer, 100.....)這樣的函數(shù)發(fā)送100個字節(jié)給別人,我們要等待,直到100個自己發(fā)送完畢,程序才往下走,這樣就是阻塞的,
而非阻塞的方式,當(dāng)你調(diào)用send(buffer,100....)以后,立即返回,此時send函數(shù)告訴你發(fā)送成功,并不意味著數(shù)據(jù)已經(jīng)向目的地發(fā)送完
畢,甚至有可能數(shù)據(jù)還沒有開始發(fā)送,只被保留在系統(tǒng)的緩沖里面,等待被發(fā)送,但是你可以認(rèn)為數(shù)據(jù)在若干時間后,一定會被目的地完整正確的收到,我們要充分
的相信tcp。
阻塞的方式會引起系統(tǒng)的停頓,一般網(wǎng)絡(luò)游戲里面使用的都是非阻塞的方式,
[有狀態(tài)服務(wù)器和無狀態(tài)服務(wù)器]
在c/s體系中,如果server不保存客戶端的狀態(tài),稱之為無狀態(tài),反之為有狀態(tài),
在這里要強(qiáng)調(diào)一點(diǎn),
我們所說的服務(wù)器不是一臺具體的機(jī)器,
而是指服務(wù)器應(yīng)用程序,
一臺具體的機(jī)器或者機(jī)器群組可以運(yùn)行一個或者多個服務(wù)器應(yīng)用程序,
我們的網(wǎng)絡(luò)游戲使用的是有狀態(tài)服務(wù)器,
保存所有玩家的數(shù)據(jù)和狀態(tài),
一些有必要了解的理論和開發(fā)工具
[開發(fā)語言]
vc6
我們首先要熟練的掌握一門開發(fā)語言,
學(xué)習(xí)c++是非常有必要的,
而vc是windows下面的軟件開發(fā)工具,
為什么選擇vc,可能與我本身使用vc有關(guān),
而且網(wǎng)上可以找到許多相關(guān)的資源和源代碼,
[操作系統(tǒng)]
我們使用windows2000作為服務(wù)器的運(yùn)行環(huán)境,
所以我們有必要去了解windows是如何工作的,
同時對它的編程原理應(yīng)該熟練的掌握
[數(shù)據(jù)結(jié)構(gòu)和算法]
要寫出好的程序要先具有設(shè)計出好的數(shù)據(jù)結(jié)構(gòu)和算法的能力,
好的算法未必是繁瑣的公式和復(fù)雜的代碼,
我們要找到又好寫有滿足需求的算法,
有時候,最笨的方法同時也是很好的方法,
很多程序員沉迷于追求精妙的算法而忽略了宏觀上的工程,
花費(fèi)了大量的精力未必能夠取得好的效果,
舉個例子,
我當(dāng)年進(jìn)入游戲界工作,學(xué)習(xí)老師的代碼,
發(fā)現(xiàn)有個函數(shù),要對畫面中的npc位置進(jìn)行排序,
確定哪個先畫,那個后畫,
他的方法太“笨”,
任何人都會想到的冒泡,
一個一個去比較,沒有任何的優(yōu)化,
我當(dāng)時想到的算法就有很多,
而且有一大堆優(yōu)化策略,
可是,當(dāng)我花了很長時間去實現(xiàn)我的算法時,
發(fā)現(xiàn)提升的那么一點(diǎn)效率對游戲整個運(yùn)行效率而言幾乎是沒起到什么作用,
或者說雖然算法本身快了幾倍,
可是那是多余的,老師的算法雖然“笨”,
可是他只花了幾十行代碼就搞定了,
他的時間花在別的更需要的地方,
這就是他可以獨(dú)自完成一個游戲,
而我可以把一個函數(shù)優(yōu)化100倍也只能打雜的原因
[tcp/ip的理論]
推薦數(shù)據(jù)用tcp/ip進(jìn)行網(wǎng)際互連,tcp/ip詳解,
這是兩套書,共有6卷,
都是國外的大師寫的,
可以說是必讀的,
網(wǎng)絡(luò)傳輸中的“消息”
[消息]
消息是個很常見的術(shù)語,
在windows中,消息機(jī)制是個十分重要的概念,
我們在網(wǎng)絡(luò)游戲中,也使用了消息這樣的機(jī)制,
一般我們這么做,
一個數(shù)據(jù)塊,頭4個字節(jié)是消息名,后面接2個字節(jié)的數(shù)據(jù)長度,
再后面就是實際的數(shù)據(jù)
為什么使用消息??
我們來看看例子,
在游戲世界,
一個玩家想要和別的玩家聊天,
那么,他輸入好聊天信息,
客戶端生成一條聊天消息,
并把聊天的內(nèi)容打包到消息中,
然后把聊天消息發(fā)送給服務(wù)器,
請求服務(wù)器把聊天信息發(fā)送給另一個玩家,
服務(wù)器接收到一條消息,
此刻,服務(wù)器并不知道當(dāng)前的數(shù)據(jù)是什么東西,
對于服務(wù)器來講,這段數(shù)據(jù)僅僅來自于網(wǎng)絡(luò)通訊的底層,
不加以分析的話,沒有任何的信息,
因為我們的通訊是基于消息機(jī)制的,
我們認(rèn)為服務(wù)器接收到的任何數(shù)據(jù)都是基于消息的數(shù)據(jù)方式組織的,
4個字節(jié)消息名,2字節(jié)長度,這個是不會變的,
通過消息名,服務(wù)器發(fā)現(xiàn)當(dāng)前數(shù)據(jù)是一條聊天數(shù)據(jù),
通過長度把需要的數(shù)據(jù)還原,校驗,
然后把這條消息發(fā)送給另一個玩家,
大家注意,消息是變長的,
關(guān)于消息的解釋完全在于服務(wù)器和客戶端的應(yīng)用程序,
可以認(rèn)為與網(wǎng)絡(luò)傳輸?shù)蛯訜o關(guān),
比如一條私聊消息可能是這樣的,
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)
編程者可以自定義消息的內(nèi)容以滿足不同的需求
隊列
[隊列]
隊列是一個很重要的數(shù)據(jù)結(jié)構(gòu),
比如說消息隊列,
服務(wù)器或者客戶端,
發(fā)送的消息不一定是立即發(fā)送的,
而是等待一個適當(dāng)時間,
或者系統(tǒng)規(guī)定的時間間隔以后才發(fā)送,
這樣就需要創(chuàng)建一個消息隊列,以保存發(fā)送的消息,
消息隊列的大小可以按照實際的需求創(chuàng)建,
隊列又可能會滿,
當(dāng)隊列滿了,可以直接丟棄消息,
如果你覺得這樣不妥,
也可以預(yù)先劃分一個足夠大的隊列,
可以使用一個系統(tǒng)全局的大的消息隊列,
也可以為每個對象創(chuàng)建一個消息隊列,
這個我們的一個數(shù)據(jù)隊列的實現(xiàn),
開發(fā)工具vc.net,使用了C++的模板,
關(guān)于隊列的算法和基礎(chǔ)知識,我就不多說了,
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
//數(shù)據(jù)隊列管理類
template <const int _max_line, const int _max_size>
class DataBufferTPL
{
public:
bool Add( byte *data ) // 加入隊列數(shù)據(jù)
{
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 ) // 從隊列中取出數(shù)據(jù)
{
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() // 獲取操作成功結(jié)果
{
return m_ControlStatus;
}
int GetNumber() // 獲得現(xiàn)在的數(shù)據(jù)大小
{
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]; //數(shù)據(jù)緩沖
int m_NumData; //數(shù)據(jù)個數(shù)
bool m_ControlStatus; //操作結(jié)果
byte *m_start_ptr; //起始位置
byte *m_end_ptr; //結(jié)束位置
byte *m_s_ptr; //排隊起始位置
byte *m_e_ptr; //排隊結(jié)束位置
};
//////////////////////////////////////////////////////////////////////////
// 放到這里了!
//ID自動補(bǔ)位列表模板,用于自動列表,無間空順序列表。
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作為網(wǎng)絡(luò)部分的編程接口,
接下去編程者有必要學(xué)習(xí)一下socket的基本知識,
不過不懂也沒有關(guān)系,我提供的代碼已經(jīng)把那些麻煩的細(xì)節(jié)或者正確的系統(tǒng)設(shè)置給弄好了,
編程者只需要按照規(guī)則編寫游戲系統(tǒng)的處理代碼就可以了,
這些代碼在vc6下編譯通過,
是通用的網(wǎng)絡(luò)傳輸?shù)讓樱?br>
這里是socket部分的代碼,
我們需要安裝vc6才能夠編譯以下的代碼,
因為接下去我們要接觸越來越多的c++,
所以,大家還是去看看c++的書吧,
// socket.h
#ifndef _socket_h
#define _socket_h
#pragma once
//定義最大連接用戶數(shù)目 ( 最大支持 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;
// 接收數(shù)據(jù)
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;
}
// 發(fā)送數(shù)據(jù)
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;
}
// 關(guān)閉winsock
BOOL CSocketCtrl::ShutDown()
{
WSACleanup();
return TRUE;
}
// 得到主機(jī)名
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是否有數(shù)據(jù)
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;
}
// 設(shè)置監(jiān)聽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;
}
// 設(shè)置正確的socket狀態(tài),
// 主要是主要是設(shè)置非阻塞異步傳輸模式
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究竟能夠承受多大的網(wǎng)絡(luò)數(shù)據(jù)傳輸,
我在flash登陸到服務(wù)器以后,
每隔3毫秒就發(fā)送100次100個字符的串
"0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
給flash,
然后在flash里面接收數(shù)據(jù)的函數(shù)里面統(tǒng)計數(shù)據(jù),
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字節(jié)的數(shù)據(jù),輸出一次信息,
if( g_nTotalRecvByte-g_nCounter > 1024 )
{
g_time = new Date();
var nPassedTime = g_time.getTime()-g_nStartTime;
trace( "花費(fèi)時間:"+nPassedTime+"毫秒" );
g_nCounter = g_nTotalRecvByte;
trace( "接收總數(shù):"+g_nTotalRecvByte+"字節(jié)" );
trace( "接收速率:"+g_nTotalRecvByte*1000/nPassedTime+"字節(jié)/秒" );
}
結(jié)果十分令我意外,
這是截取的一段調(diào)試信息,
//
花費(fèi)時間:6953毫秒
接收總數(shù):343212字節(jié)
接收速率:49361.7143678988字節(jié)/秒
花費(fèi)時間:7109毫秒
接收總數(shù):344323字節(jié)
接收速率:48434.800956534字節(jié)/秒
花費(fèi)時間:7109毫秒
接收總數(shù):345434字節(jié)
接收速率:48591.0817273878字節(jié)/秒
。。。
。。。
。。。
。。。
花費(fèi)時間:8125毫秒
接收總數(shù):400984字節(jié)
接收速率:49351.8769230769字節(jié)/秒
花費(fèi)時間:8125毫秒
接收總數(shù):402095字節(jié)
接收速率:49488.6153846154字節(jié)/秒
花費(fèi)時間:8125毫秒
接收總數(shù):403206字節(jié)
接收速率:49625.3538461538字節(jié)/秒
我檢查了幾遍源程序,沒有發(fā)現(xiàn)邏輯錯誤,
如果程序沒有問題的話,
那么我們得出的結(jié)論是,flash的xml socket每秒可以接收至少40K的數(shù)據(jù),
這還沒有計算xmlSocket.onData事件的觸發(fā),調(diào)試代碼、信息輸出占用的時間。
比我想象中快了一個數(shù)量級,
夠用了,
flash網(wǎng)絡(luò)游戲我們可以繼續(xù)往下走了,
有朋友問到lag的問題,
問得很好,不過也不要過于擔(dān)心,
lag的產(chǎn)生有的是因為網(wǎng)絡(luò)延遲,
有的是因為服務(wù)器負(fù)載過大,
對于游戲的設(shè)計者和開發(fā)者來說,
首先要從設(shè)計的角度來避免或者減少lag產(chǎn)生的機(jī)會,
如果lag產(chǎn)生了,
也不要緊,找到巧妙的辦法騙過玩家的眼睛,
這也有很多成熟的方法了,
比如航行預(yù)測法,路徑插值等等,
都可以產(chǎn)生很好的效果,
還有最后的絕招,就是提高服務(wù)器的配置和網(wǎng)絡(luò)帶寬,
從我開發(fā)網(wǎng)絡(luò)游戲這段時間的經(jīng)驗來看,
我們的服務(wù)器是vc開發(fā)的,
普通pc跑幾百個玩家,幾百個怪物是沒有問題的,
又作了一個flash發(fā)送的測試,
網(wǎng)絡(luò)游戲的特點(diǎn)是,
出去的信息比較少,
進(jìn)來的信息比較多,
這個很容易理解,
人操作游戲的速度是很有限的,
控制指令的產(chǎn)生也是隨機(jī)的,
離散的,
但是多人游戲的話,
因為人多,信息的流量也就區(qū)域均勻分布了,
在昨天接收數(shù)據(jù)的基礎(chǔ)上,
我略加修改,
這次,
我在_root.enterFrame寫了如下代碼,
_root.onEnterFrame = function()
{
var i;
for( i = 0; i < 10; i++ )
mySocket.send( ConvertToMsg( "01234567890123456789012345678901234567890123456789" ) );
return;
}
服務(wù)器端要做的是,
把所有從flash客戶端收到的信息原封不動的返回來,
這樣,我又可以通過昨天onData里面的統(tǒng)計算法來從側(cè)面估算出flash發(fā)送數(shù)據(jù)的能力,
這里是輸出的數(shù)據(jù)
//
花費(fèi)時間:30531毫秒
接收總數(shù):200236字節(jié)
接收速率:6558.44878975468字節(jié)/秒
花費(fèi)時間:30937毫秒
接收總數(shù):201290字節(jié)
接收速率:6506.44858906811字節(jié)/秒
花費(fèi)時間:31140毫秒
接收總數(shù):202344字節(jié)
接收速率:6497.88053949904字節(jié)/秒
花費(fèi)時間:31547毫秒
接收總數(shù):203398字節(jié)
接收速率:6447.45934637208字節(jié)/秒
可以看出來,發(fā)送+接收同時做,
發(fā)送速率至少可以達(dá)到5k byte/s
有一點(diǎn)要注意,要非常注意,
不能讓flash的網(wǎng)絡(luò)傳輸滿載,
所謂滿載就是flash在阻塞運(yùn)算的時候,
不斷的有數(shù)據(jù)從網(wǎng)絡(luò)進(jìn)來,
而flash又無法在預(yù)計的時間內(nèi)處理我這些信息,
或者flash發(fā)送數(shù)據(jù)過于頻繁,
導(dǎo)致服務(wù)器端緩沖溢出導(dǎo)致錯誤,
對于5k的傳輸速率,
已經(jīng)足夠了,
因為我也想不出來有什么產(chǎn)生這么大的數(shù)據(jù)量,
而且如果產(chǎn)生了這么大的數(shù)據(jù)量,
也就意味著服務(wù)器每時每刻都要處理所有的玩家發(fā)出的海量數(shù)據(jù),
還要把這些海量數(shù)據(jù)轉(zhuǎn)發(fā)給其他的玩家,
已經(jīng)引起數(shù)據(jù)爆炸了,
所以,5k的上傳從設(shè)計階段就要避免的,
我想用flash做的網(wǎng)絡(luò)游戲,
除了動作類游戲可能需要恒定1k以內(nèi)的上傳速率,
其他的200個字節(jié)/秒以內(nèi)就可以了,
使用于Flash的消息結(jié)構(gòu)定義
我們以前討論過,
通過消息來傳遞信息,
消息的結(jié)構(gòu)是
struct msg
{
short nLength; // 2 byte
DWORD dwId; // 4 byte
....
data
}
但是在為flash開發(fā)的消息中,
不能采用這種結(jié)構(gòu),
首先Flash xmlSocket只傳輸字符串,
從xmlSocket的send,onData函數(shù)可以看出來,
發(fā)出去的,收進(jìn)來的都應(yīng)該是字符串,
而在服務(wù)器端是使用vc,java等高級語言編寫的,
消息中使用的是二進(jìn)制數(shù)據(jù)塊,
顯然,簡單的使用字符串會帶來問題,
所以,我們需要制定一套協(xié)議,
就是無論在客戶端還是服務(wù)器端,
都用統(tǒng)一的字符串消息,
通過解析字符串的方式來傳遞信息,
我想這就是flash采用xml document來傳輸結(jié)構(gòu)化信息的理由之一,
xml document描述了一個完整的數(shù)據(jù)結(jié)構(gòu),
而且全部使用的是字符串,
原來是這樣,怪不得叫做xml socket,
本來socket和xml完全是不同的概念,
flash偏偏出了個xml socket,
一開始令我費(fèi)解,
現(xiàn)在,漸漸理解其中奧妙。
Flash Msg結(jié)構(gòu)定義源代碼和相關(guān)函數(shù)
在服務(wù)器端,我們?yōu)閒lash定義了一種msg結(jié)構(gòu),
使用語言,vc6
#define MSGMAXSIZE 512
// 消息頭
struct MsgHeader
{
short stLength;
MsgHeader():stLength( 0 ){}
};
// 消息
struct Msg
{
MsgHeader header;
short GetLength(){ return header.stLength; }
};
// flash 消息
struct MsgToFlash
ublic Msg
{
// 一個足夠大的緩沖,但是不會被整個發(fā)送,
char szString[MSGMAXSIZE];
// 計算設(shè)置好內(nèi)容后,內(nèi)部會計算將要發(fā)送部分的長度,
// 要發(fā)送的長度=消息頭大小+字符串長度+1
void SetString( const char* pszChatString )
{
if( strlen( pszChatString ) < MSGMAXSIZE-1 )
{
strcpy( szString, pszChatString );
header.stLength = sizeof( header )+
(short)strlen( pszChatString )+1;
}
}
};
在發(fā)往flash的消息中,整個處理過后MsgToFlash結(jié)構(gòu)將被發(fā)送,
實踐證明,在flash 客戶端的xmlSocket onData事件中,
接收到了正確的消息,消息的內(nèi)容是MasToFlash的szString字段,
是一個字符串,
比如在服務(wù)器端,
MsgToFlash msg;
msg.SetString( "move player0 to 100 100" );
SendMsg( msg,............. );
那么,在我們的flash客戶端的onData( xmlDoc )中,
我們trace( xmlDoc )
結(jié)果是
move player0 to 100 100
然后是flash發(fā)送消息到服務(wù)器,
我們強(qiáng)調(diào)flash只發(fā)送字符串,
這個字符串無論是否內(nèi)部擁有有效數(shù)據(jù),
服務(wù)器都應(yīng)該首先把消息收下來,
那就要保證發(fā)送給服務(wù)器的消息遵循統(tǒng)一的結(jié)構(gòu),
在flash客戶端中,
我們定義一個函數(shù),
這個函數(shù)把一個字符串轉(zhuǎn)化為服務(wù)器可以識別的消息,
補(bǔ)充:現(xiàn)在我們約定字符串長度都不大于97個字節(jié)長度,
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
為什么是這個結(jié)果呢?
15是消息的長度,
頭兩個字節(jié)是整個消息的長度的asc碼,意思是整個消息有15個字節(jié)長,
然后是信息client login,
最后是一個0(c語言中的字符串結(jié)束符)
當(dāng)服務(wù)器收到15client login,
他首先把15給分析出來,
把"15"字符串轉(zhuǎn)化為15的數(shù)字,
然后,根據(jù)15這個長度把后面的client login讀出來,
這樣,網(wǎng)絡(luò)傳輸?shù)牡讓泳屯瓿闪耍?br>
client login的處理就交給邏輯層,
謝謝大家的支持,
很感謝斑竹把這個貼子置頂,
我寫這文章的過程也是我自己摸索的過程,
文章可以記錄我一段開發(fā)的歷史,
一個思考分析的歷程,
有時候甚至作為日志來寫,
由于我本身有雜務(wù)在身,
所以貼子的更新有點(diǎn)慢,
請大家見諒,
我喜愛flash,
雖然我在帝國中,但我并不能稱之為閃客,
因為我制作flash的水平實在很低,
但是我想設(shè)計開發(fā)出讓其他人能更好的使用flash的工具,
前陣子我開發(fā)了Match3D,
一個可以把三維動畫輸出成為swf的工具,
而且實現(xiàn)了swf渲染的實時三維角色動畫,
這可以說是我真正推出的第一個flash第三方軟件,
其實這以前,
我曾經(jīng)開發(fā)過幾個其他的flash第三方軟件,
都中途停止了,
因為不實用或者市場上有更好的同類軟件,
隨著互聯(lián)網(wǎng)的發(fā)展,
flash的不斷升級,
我的flash第三方軟件目光漸漸的從美術(shù)開發(fā)工具轉(zhuǎn)移到網(wǎng)絡(luò)互連,
web應(yīng)用上面來,
如今已經(jīng)到了2004版本,
flash的種種新特性讓我眼前發(fā)光,
我最近在帝國的各個板塊看了很多貼子,
分析里面潛在的用戶需求,
總結(jié)了以下的幾個我認(rèn)為比較有意義的選題,
可能很片面,
flash源代碼保護(hù),主要是為了抵御asv之類的軟件進(jìn)行反編譯和萃取
flash與遠(yuǎn)端數(shù)據(jù)庫的配合,應(yīng)該出現(xiàn)一個能夠方便快捷的對遠(yuǎn)程數(shù)據(jù)庫進(jìn)行操作的方法或者控件,
flash網(wǎng)際互連,我認(rèn)為flash網(wǎng)絡(luò)游戲是一塊金子,
這里我想談?wù)刦lash網(wǎng)絡(luò)游戲,
我要談的不僅僅是技術(shù),而是一個概念,
用flash網(wǎng)絡(luò)游戲,
我本身并不想把flash游戲做成rpg或者其他劇烈交互性的游戲,
而是想讓flash實現(xiàn)那些節(jié)奏緩慢,玩法簡單的游戲,
把網(wǎng)絡(luò)的概念帶進(jìn)來,
你想玩游戲的時候,登上flash網(wǎng)絡(luò)游戲的網(wǎng)站,
選擇你想玩的網(wǎng)絡(luò)游戲,
因為現(xiàn)在幾乎所有上網(wǎng)的電腦都可以播放swf,
所以,我們幾乎不用下載任何插件,
輸入你的賬號和密碼,
就可以開始玩了,
我覺得battle.net那種方式很適合flash,
開房間或者進(jìn)入別人開的房間,
然后2個人或者4個人就可以交戰(zhàn)了,
這種游戲可以是棋類,這是最基本的,
用戶很廣泛,
我腦海中的那種是類似與寵物飼養(yǎng)的,
就像當(dāng)年的電子寵物,
每個玩家都可以到服務(wù)器認(rèn)養(yǎng)寵物,
然后在線養(yǎng)成寵物,
還可以邀請別的玩家進(jìn)行寵物比武,
看誰的寵物厲害,
就這樣簡簡單單的模式,
配合清新可愛的畫面,
趣味的玩法,
加入網(wǎng)絡(luò)的要素,
也許可以取得以想不到的效果,
今天就說到這里吧,
想法那么多,要實現(xiàn)的話還有很多路要走,
希望大家多多支持,積極參與,
讓我們的想法不僅僅停留于紙上。
大家好,
非常抱歉,
都很長時間沒有回貼了,
因為手頭項目的原因,
幾乎沒有時間做flash multiplayer的研究,
很感謝大家的支持,
現(xiàn)在把整個flash networking的源代碼共享出來,
大家可以任意的使用,
其實里面也沒有多少東西,
相信感興趣的朋友還是可以從中找到一些有用的東西,
這一次的源代碼做的事情很簡單,
服務(wù)器運(yùn)行,
客戶端登陸到服務(wù)器,
然后客戶端不斷的發(fā)送字符串給服務(wù)器,
服務(wù)器收到后,在發(fā)還給客戶端,
客戶端統(tǒng)計一些數(shù)據(jù),


