对于网络游戏来说,它和单机版游戏最大的区别就在于网络通信部分。可以说,网络通信是构成网游的最基本元素。在这里我不想详细的论述如何使用
DELPHI
在
WINDOWS
下进行网络编程,因为这是一个非常复杂的话题。这里我只是想说明在我设计的棋牌类游戏中如何实现游戏网络部分的。
对于
WINDOWS
来说,它的通信模型大概分为
5
种。分别是:
1
:
SELECT
模型。
2
:
WSAAsynSelect
模型。
3
:
WSAEventSelect
模型。
4
:
IO
重叠模型
5
:完成端口模型。
对于性能来说,完成端口可以管理上万连接(理论连接数量可以达到
65535
个
),所以我选择它来作为我游戏服务器的通信模型。
由于游戏客户端和游戏服务器的连接只需要一条就可以,所以我选择了
Select
模型作为客户端通信模型。
关于如何编写完成端口以及在完成端口中如何加入心跳,如何避免网络通信中的粘包现象在我以前的
BLOG
中已经有了详细的讲解,由于篇幅太多,我这里就不在将其贴出。详细的可以参看我的
BLOG
《
DELPHI
中完成端口
(IOCP)
的简单分析(
1
)》
-
《
DELPHI
中完成端口
(IOCP)
的简单分析(
4
)》和《网络通信中的心跳机制的实现!》
但是在我最近重新检查代码的时候发现我以前关于
IOCP
的一些处理方式不合适或者有些地方是错误的,我已经在以前的
BLOG
帖子中做了相关的修改。
在这里我们讨论一下如何实现客户端通信模型
(Select
模型
)
。对于熟悉网络编程的程序员来说,编写一个
SELECT
通信模型是一件很简单的事,因为它是
5
中通信模型中最简单的一种。
编写
select
模型的代码如下:
Var
Fsocket
:
Tsokcet;
procedure Star;
var
CliAddrIn:TSockAddrIn;
hThread:THandle;
ThreadID,outByte:DWORD;
keep_alive,out_keep_alive:TTimeVal;
iAddrSize,opt,I:Integer;
begin
//
加载
SOCKET,
我使用的是
2.2
版为了后面方便加入心跳
if WSAStartUp($202, wsData) <> 0 then
begin
WSACleanup();
end;
//
创建一个套接字
Fsocket
:=socket(AF_INET,SOCK_STREAM,0);
if
Fsocket
=SOCKET_ERROR then
begin
closesocket(
Fsocket
);
end
else
begin
//
这里填入实际的服务器
IP
地址和服务器监听端口
CliAddrIn.sin_addr.s_addr:=inet_addr(Pchar(‘127.0.0.1’));
CliAddrIn.sin_family:=AF_INET;
CliAddrIn.sin_port:=htons(5500);
//
连接服务器
if (connect(
Fsocket
,CliAddrIn,sizeof(CliAddrIn))<>SOCKET_ERROR) then
begin
//
设置套接字的心跳属性
opt:=1;
if setsockopt(
Fsocket
,SOL_SOCKET,SO_KEEPALIVE,@opt,sizeof(opt))=SOCKET_ERROR then
begin
closesocket(
Fsocket
);
end;
FinKeepAlive.onoff:=1;
FinKeepAlive.keepalivetime:=FKeepTime;
FinKeepAlive.keepaliveinterval:=1;
Finsize:=sizeof(TTCP_KEEPALIVE);
Foutsize:=sizeof(TTCP_KEEPALIVE);
if WSAIoctl(
Fsocket
,SIO_KEEPALIVE_VALS,@FinKeepAlive,Finsize,@FoutKeepAlive,Foutsize,@outByte,nil,nil)=SOCKET_ERROR then
begin
closesocket(
Fsocket
);
end;
//
启动接收线程
hThread:=CreateThread(Nil,0,@RevData,Pointer(
Fsocket
),0,ThreadID);
CloseHandle(hThread);
end
else
begin
closesocket(
Fsocket
);
end;
end;
end;
//
接收线程
function RevData(sc:Pointer):Integer;stdcall;
var
szBuf:array[0..DATA_BUFSIZE-1] of char;
sRevSize:Integer;
DeBuf:array[0..DATA_BUFSIZE-1]of char;
Delen:Integer;
SpBuf:array[0..DATA_BUFSIZE-1]of char;
SpLen:Integer;
IsEnd:Boolean;
AnalyzeData:array [0..DATA_BUFSIZE-1]of char;
AnalyzeDataLen:Integer;
t_sc:TSocket;
InterlockedCrit:TRTLCriticalSection;
begin
//
初始化临界区
InitializeCriticalSection(InterlockedCrit);
FillChar(FBuffer,SizeOf(FBuffer),#0);
FBufferLen:=0;
t_sc:=TSocket(sc);
//
接收数据死循环
while TRUE do
begin
FillChar(szBuf,sizeof(szBuf),#0);
//
接收数据
sRevSize:=recv(t_sc,szBuf,sizeof(szBuf),0);
//
根据接收函数的返回值得到接收到的数据长度
if sRevSize = -1 then
begin
//
如果接收长度为
-1
便表示已经和服务器断开了连接,这样可以进入断开连接处理函数中
EnterCriticalSection(InterlockedCrit);
…….
closesocket(t_sc);
LeaveCriticalSection(InterlockedCrit);
Exit;
end
else if (Integer(t_sc)=-1) or (sRevSize=0) then
begin
//
如果套接字变为
-1
或者接收长度变为
0
也便是和服务器断开了连接,这样可以进入断开连接处理函数中
EnterCriticalSection(InterlockedCrit);
……..
closesocket(t_sc);
LeaveCriticalSection(InterlockedCrit);
Exit;
end
else if (sRevSize>0) and (sRevSize<=DATA_BUFSIZE) then
begin
IsEnd:=false;
while not IsEnd do
begin
Delen:=0;
SpLen:=0;
if sRevSize>0 then
begin
//
进入数据正确处理函数中
……
end
else
begin
IsEnd:=true;
end;
end;
end;
end;
DeleteCriticalSection(InterlockedCrit);
end;
//
数据发送函数的处理
function SendBuffer(Data: array of char; DataLen: Integer;sc:TSocket):Boolean;
var
SendData:array[0..DATA_BUFSIZE-1] of char;
SendDataLen:Integer;
RealLen:Integer;
LenStr:String;
begin
Result:=false;
//
在发送数据的前面加入
4
位的发送长度。
SetArrayLength(DataLen,LenStr);
//
初始化发送数组
Fillchar(SendData,sizeof(SendData),#0);
//
填充发送数组
strmove(SendData,Pointer(LenStr),4);
strmove(SendData+4,Data,DataLen);
SendDataLen:=DataLen+4;
//
发送数据
RealLen:=Send(FSocket,SendData,SendDataLen,0);
if RealLen = DataLen + 4 then
begin
Result:=true;
end;
以上的代码就是使用
SELECT
通信模型编写的一个客户端通信部分所需要的代码。如果要和服务器正确的通信,客户端自然少不了编写粘包处理的函数。其处理的过程类似于服务器中粘包的处理,我这里就不再重述。
通过对客户端和服务器端的编写,一个完整的网络通信部分已经建立。有了稳定的网络通信作为基础,我们就可以顺利的进行后续开发。下一篇我会和大家探讨如何实现棋牌类游戏界面效果。好的界面可以使游戏效果锦上添花,是不可忽视的。