1.SCOKET原理
Socket又叫套接字,它是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口。
应用层通过传输层进行数据通信时,TCP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个 TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了套接字(Socket)接口。应用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。
2.怎样建立SOCKET
建立Socket连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket ,另一个运行于服务器端,称为ServerSocket 。
套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。
服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。
客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
3.具体实现代码
1.服务器
服务器实例类
class GameServer
{
private static GameServer _instance;
public static GameServer Instance
{
get
{
if (_instance == null)
{
_instance = new GameServer();
}
return _instance;
}
}
//string ip = "127.0.0.1";
string ip = "192.168.11.20";
int port = 8500;
TcpListener listener;
Dictionary<string, SocketServer> clients = new Dictionary<string, SocketServer>();
public GameServer()
{
IPAddress address = IPAddress.Parse(ip);
listener = new TcpListener(address, port);
listener.Start();//开始侦听
Console.WriteLine("服务器开始侦听");
listener.BeginAcceptTcpClient(AcceptTcp, null);
}
void AcceptTcp(IAsyncResult ar)
{
try
{
TcpClient client = listener.EndAcceptTcpClient(ar);//异步接收远程连接
SocketServer socket = new SocketServer(client);
string key = client.Client.RemoteEndPoint.ToString();
clients.Add(key, socket);
//再次异步接收客户端连接
listener.BeginAcceptTcpClient(AcceptTcp, null);
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
public void Broadcast(string msg)
{
foreach (var clt in clients)
{
clt.Value.Send(msg);
}
}
public void Broadcast(byte[] buffer)
{
foreach (var item in clients)
{
item.Value.Send(buffer);
}
}
//广播消息:string
public void Broadcast(Protocol type, string msg)
{
foreach (var clt in clients)
{
clt.Value.Send(type, msg);
}
}
//广播消息:byte[]
public void Broadcast(Protocol type, byte[] buffer)
{
foreach (var clt in clients)
{
clt.Value.Send(type, buffer);
}
}
public void DisConnect(string clientKey)
{
clients.Remove(clientKey);
Console.WriteLine(clientKey + "断开连接!");
}
/// <summary>
/// 接收客户端消息事件分发
/// </summary>
/// <param name="buffer">消息[type + content]</param>
public static void OnEvent(ByteBuffer buffer)
{
Protocol type = (Protocol)buffer.ReadByte();
Console.WriteLine("OnEvent"+ type);
switch (type)
{
case Protocol.login_Cmd:
break;
case Protocol.register_Cmd:
break;
case Protocol.chat_Cmd:
//string message = buffer.ReadString();
//Console.WriteLine("Client聊天消息:" + message);
//Instance.Broadcast(Protocol.chat_Cmd, message);
byte[] chatBuffer = buffer.ReadBytes();
Instance.Broadcast(Protocol.chat_Cmd, chatBuffer);
// 解消息,打印
Chat chat = ProtoHelper.Deserialize<Chat>(chatBuffer);
Console.WriteLine("聊天信息:" + chat.word);
break;
default:
break;
}
}
}
包装从客户端获取的 TCPClient
class SocketServer
{
const int BufferSize = 8192;//定义buffer缓冲区长度
byte[] buffer = new byte[BufferSize];
TcpClient client;
NetworkStream stream;
MemoryStream mem;//缓存流
BinaryReader reader;//缓存流的读取器
public SocketServer(TcpClient _client)
{
client = _client;
Console.WriteLine("客户端连接成功:" + client.Client.RemoteEndPoint);
stream = client.GetStream();//获取网络流
mem = new MemoryStream();
reader = new BinaryReader(mem);
//while (true)
//{
// int readCount = stream.Read(buffer, 0, BufferSize);//阻塞读取网络流
// string msg = Encoding.UTF8.GetString(buffer, 0, readCount);
// Console.WriteLine("接收到" + client.Client.RemoteEndPoint + "的消息:" + msg);
//}
//开启异步读取数据
stream.BeginRead(buffer, 0, BufferSize, Read, null);
}
//子线程读取消息
private void Read(IAsyncResult ar)
{
try
{
int readCount = stream.EndRead(ar);
if (readCount<=0)
{
throw new Exception("读取异常!");
}
//接收一次,把消息抛给逻辑层
OnReceive(buffer, readCount);
//string msg = Encoding.UTF8.GetString(buffer, 0, readCount);
//Console.WriteLine("接收到客户端——" + client.Client.RemoteEndPoint + "的消息:" + msg);
//Console.WriteLine("-------------------------------------------");
广播给所有客户端
//GameServer.Instance.Broadcast(client.Client.RemoteEndPoint + ":" + msg);
lock (client)
{
Array.Clear(buffer, 0, BufferSize);
stream.BeginRead(buffer, 0, BufferSize, Read, null);
}
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
OnDisconnect();
}
}
//逻辑层,解包,处理[长度+消息]
void OnReceive(byte[] bytes, int count)
{
mem.Seek(0, SeekOrigin.End);//指针指到最后,
mem.Write(bytes, 0, count);//消息往后追加
mem.Seek(0, SeekOrigin.Begin);//指针指到开头,开始读消息
while (RemaindLength() > 4)//判断剩余长度是否够一个int32的长度
{
//获取消息长度,指针后移了4位
int length = reader.ReadInt32();
if (RemaindLength() >= length)//剩余字节数是否大于消息长度
{
byte[] content = reader.ReadBytes(length);
OnMessage(content);
}
else
{
mem.Seek(-4, SeekOrigin.Current);//把指针前移4位,保证消息的完整性是[长度+消息]
break;
}
}
byte[] remain = reader.ReadBytes(RemaindLength());//把剩余的半包读取出来
mem.SetLength(0);//把缓存流清空,
mem.Write(remain, 0, remain.Length);//写入剩余的半包
}
//接到一条消息
void OnMessage(byte[] content)
{
//var format = new BinaryFormatter();
//string msg = Encoding.UTF8.GetString(content, 0, content.Length);
//Console.WriteLine(msg);
#region 二进制以及ProtoBuf
//Person p = ReadBinaryFormat(content); //二进制流
//Person p = ProtobufFomat(content); // Protobuf
//Console.WriteLine(p.name + ":" + p.age);
#endregion
#region Json
//string json = Encoding.UTF8.GetString(content);
//Person p = JsonMapper.ToObject<Person>(json);
//Console.WriteLine(json);
#endregion
//GameServer.Instance.Broadcast(content);
//string msg = Encoding.UTF8.GetString(content, 0, content.Length);
//Console.WriteLine(msg);
// 使用工具类
GameServer.OnEvent(new ByteBuffer(content));
}
//获取剩余字节长度
int RemaindLength()
{
return (int)(mem.Length - mem.Position);
}
//客户端断开链接
void OnDisconnect()
{
GameServer.Instance.DisConnect(this.client.Client.RemoteEndPoint.ToString());
stream.Close();//流关闭
client.Close();//客户端关闭
}
//发送给客户端消息
void Write(string msg)
{
byte[] bytes = Encoding.UTF8.GetBytes(msg);
stream.Write(bytes, 0, bytes.Length);
Console.WriteLine("回复客户端 -> " + msg);
}
//往流里面写入==发消息
void Write(byte[] content)
{
获取消息和消息长度
//byte[] length = BitConverter.GetBytes(content.Length);
使用内存流做缓存,拼接遵循 [长度+消息]协议 的消息
//MemoryStream mem = new MemoryStream();
//mem.Write(length, 0, 4);
//mem.Write(content, 0, content.Length);
把最终消息发送出去
//byte[] bytes = mem.ToArray();
//stream.Write(bytes, 0, bytes.Length);
#region 使用工具类
ByteBuffer buf = new ByteBuffer();
buf.WriteInt(content.Length);
buf.WriteBytes(content);
byte[] bytes = buf.ToBytes();
stream.Write(bytes, 0, bytes.Length);
#endregion
}
public void Send(string msg)
{
byte[] content = Encoding.UTF8.GetBytes(msg);
Write(content);
}
public void Send(byte[] content)
{
Write(content);
}
//向服务器发送消息
public void Send(Protocol type, string msg)
{
ByteBuffer buf = new ByteBuffer();
buf.WriteByte((byte)type);
buf.WriteString(msg);
Write(buf.ToBytes());
}
public void Send(Protocol type, byte[] content)
{
ByteBuffer buf = new ByteBuffer();
buf.WriteByte((byte)type);
buf.WriteBytes(content);
Write(buf.ToBytes());
}
//protobuf的序列化
Person ProtobufFomat(byte[] bytes)
{
using (MemoryStream ms = new MemoryStream(bytes))
{
Person p = Serializer.Deserialize<Person>(ms);
return p;
}
}
//解析二进制格式序列化
object ReadBinaryFormat(byte[] bytes)
{
using (MemoryStream ms = new MemoryStream())
{
ms.Write(bytes, 0, bytes.Length);
ms.Flush();
ms.Position = 0;
var formmat = new BinaryFormatter();
var obj = formmat.Deserialize(ms);
return obj;
}
}
}
2.客户端
class SocketClient
{
//string ip = "127.0.0.1";
string ip = "192.168.11.20";
int port = 8500;
TcpClient client;
NetworkStream stream;
MemoryStream mem;//缓存流
BinaryReader reader;//缓存流的读取器
const int BufferSize = 8192;
byte[] buffer = new byte[BufferSize];
public SocketClient()
{
client = new TcpClient();
//client.Connect(ip, port);//连接服务器
client.BeginConnect(ip, port, OnConnect, null);
}
void OnConnect(IAsyncResult ar)
{
Debug.Log("连接上服务器:" + client.Client.RemoteEndPoint);
stream = client.GetStream();//获取网络流
mem = new MemoryStream();
reader = new BinaryReader(mem);
//开启异步读取数据
stream.BeginRead(buffer, 0, BufferSize, Read, null);
}
//子线程读取消息
void Read(IAsyncResult ar)
{
try
{
int readCount = stream.EndRead(ar);
if (readCount == 0) throw new Exception("读取异常");
//接收一次,把消息抛给逻辑层
OnReceive(buffer, readCount);
lock (client)
{
Array.Clear(buffer, 0, BufferSize);
stream.BeginRead(buffer, 0, BufferSize, Read, null);
}
}
catch (Exception e)
{
Debug.Log(e.ToString());
OnDisconnect();
}
}
//逻辑层,解包,处理[长度+消息]
void OnReceive(byte[] bytes, int count)
{
mem.Seek(0, SeekOrigin.End);//指针指到最后,
mem.Write(bytes, 0, count);//消息往后追加
mem.Seek(0, SeekOrigin.Begin);//指针指到开头,开始读消息
while (RemaindLength() > 4)//判断剩余长度是否够一个int32的长度
{
//获取消息长度,指针后移了4位
int length = reader.ReadInt32();
if (RemaindLength() >= length)//剩余字节数是否大于消息长度
{
byte[] content = reader.ReadBytes(length);
OnMessage(content);
}
else
{
mem.Seek(-4, SeekOrigin.Current);//把指针前移4位,保证消息的完整性是[长度+消息]
break;
}
}
byte[] remain = reader.ReadBytes(RemaindLength());//把剩余的半包读取出来
mem.SetLength(0);//把缓存流清空,
mem.Write(remain, 0, remain.Length);//写入剩余的半包
}
//获取剩余字节长度
int RemaindLength()
{
return (int)(mem.Length - mem.Position);
}
//接到一条消息
void OnMessage(byte[] content)
{
//NetworkManager.Instance.AddEvent(content);
NetworkManager.Instance.AddEvent(new ByteBuffer(content));
}
//客户端断开链接
void OnDisconnect()
{
stream.Close();//流关闭
client.Close();//客户端关闭
}
//往流里面写入==给服务器发消息
void Write(byte[] content)
{
使用bytebuffer工具类
//ByteBuffer btbf = new ByteBuffer();
//btbf.WriteInt(content.Length);//获取消息和消息长度
//btbf.WriteBytes(content);
//byte[] bytes = btbf.ToBytes();
//stream.Write(bytes,0,bytes.Length);
//获取消息和消息长度
byte[] length = BitConverter.GetBytes(content.Length);
//使用内存流做缓存,拼接遵循 [长度+消息]协议 的消息
MemoryStream mem = new MemoryStream();
mem.Write(length, 0, 4);
mem.Write(content, 0, content.Length);
//把最终消息发送出去
byte[] bytes = mem.ToArray();
stream.Write(bytes, 0, bytes.Length);
}
//向服务器发送消息
public void Send(string msg)
{
byte[] content = Encoding.UTF8.GetBytes(msg);
Write(content);
}
public void Send(byte[] content)
{
Write(content);
}
//向服务器发送消息
//往流里面写入 [长度+消息]
void OnWrite(byte[] content)
{
try
{
ByteBuffer bytebuff = new ByteBuffer();
bytebuff.WriteInt(content.Length); //获取消息和消息长度
bytebuff.WriteBytes(content);
//把最终消息发送出去
byte[] bytes = bytebuff.ToBytes();
stream.Write(bytes, 0, bytes.Length);
}
catch (Exception e)
{
Debug.Log(e.ToString());
throw;
}
}
public void Send(byte type, string msg)
{
ByteBuffer buf = new ByteBuffer();
buf.WriteByte(type);
buf.WriteString(msg);
OnWrite(buf.ToBytes());
}
public void Send(byte type, byte[] content)
{
ByteBuffer buf = new ByteBuffer();
buf.WriteByte(type);
buf.WriteBytes(content);
OnWrite(buf.ToBytes());
}
}
4.连接技术之C/S结构示例(网络游戏的大厅结构)
网络游戏的最典型的结构是C/S结构,如下图所示,特别适合于多人在线游戏,如RPG,成千上万人进行同一场游戏。服务器上有完整的游戏世界模型,玩家在客户端去观察这个世界,并与之互动。
C/S结 构的优点是能够充分发挥客户端计算机的处理能力,很多工作可以在客户端处理后再提交给服务器,使得客户端响应速度快。对于网络游戏来讲,这是十分关键的。 其缺点就是网络游戏的升级比较麻烦,每次升级需要客户端下载体积庞大的更新软件,然后进行安装,对于一些带宽并不是十分充足的用户而言,升级就成了游戏的 梦魇。不过,良好的客户端程序框架设计完全可以避免升级时下载庞大的更新软件。比如,客户端软件可以把场景定义文件独立出来,客户端程序在运行时动态加载 场景定义文件。这样,当网络游戏的设计者发现需要增加游戏场景时,他只需要更新一个场景定义文件就可以了,当然还需要一些必需的场景资源文件,如新增加的 图像、背景音效等。
结构依然划分为服务端和客户端,
1.服务器的运行流程如下:
(1)创建一个套接字,绑定IP和端口。
(2)服务器监听端口,等待连接。创建一个游戏大厅。
(3)服务器处于等待状态,因此需要两个进程/线程在服务器端运行:一个界面线程,处理游戏菜单操作;一个网络线程,等待连接。
(4)在每个客户端创建一个Socket,连接游戏服务器。
(5)服务器更新连接信息,并在游戏大厅上显示。同时,将连接用户信息发送给每个连接用户。
(6)当所有的用户都连接到服务器上开始游戏,关闭服务器侦听的套接字,中断所有等待的连接,新的用户将不可能连接上服务器。
采用上面描述的方法可以容易地设计出类似星际争霸的网络游戏架构,游戏服务器只在开始的时候处理连接请求,后继的所有工作是数据的传送。然而这种方法也存在着一些隐患,如某个客户端在游戏中断掉,它将无法连接上服务器。因此,服务器必须通过一些测试方法(如长时间没有收到某个客户端的数据,或者是套接字关闭的通知)来探知是否有客户端掉线。此时,服务器必须打开一个临时的接收套接字,等待掉线客户端的重新连接。
客户端设计和实现主要包括以下内容:
¤ 游戏的基本功能——处理声音,动画等。
¤ 游戏框架——玩家信息,游戏界面,广告信息等。
¤ 游戏通信——客户与服务器之间的网络传输细节。
¤ 游戏应用程序——程序线程管理、各种显示对话框、图形的处理与显示。
¤ 游戏处理——处理游戏逻辑、解析和处理游戏数据、游戏运行维护等。
而游戏的服务器端设计则主要包括:
¤ 游戏通信——负责游戏中客户服务器之间的网络传输细节。
¤ 游戏协议——对传递的数据进行打包和解包,并根据所包含的指令进行相应的操作。
¤ 游戏逻辑——负责处理游戏逻辑。
¤ 线程管理——线程的生成、结束和分配任务等。
服务器数据库的内容则包括:
¤ 角色表——角色ID、账号ID、角色的名称、属性和帮派等。
¤ 道具表——道具ID、所属角色ID、道具各类属性等。
¤ 帮派表——帮派ID、名称、帮派头目、介绍、总人数等。
¤ 邮件表——邮件ID、发件人角色ID、收件人角色ID、邮件内容等。