前言:本文将会以制作一个简易双端网络框架的目标,带领读者熟悉游戏开发中Socket网络编程的概念和流程,知道是怎样从零去构建一个双端的网络。
建议带着疑问去学习:
-
什么是Socket?
-
为什么需要用到Socket去实现网络编程?
-
Socket的实现原理是什么?
-
Socket通信的流程是什么?
-
TCP和UDP协议有什么区别?能否实现可靠的UDP协议?
并且需要了解以下知识:
-
Socket
-
异步Socket
-
状态检测Poll
-
多路复用Select
服务端:
服务端的通信流程:
-
创建socket
-
绑定IP和端口
-
监听
-
接收
-
分发消息
-
发送
服务端的搭建流程:
NetServer: 负责创建socket和监听接收
NetSession:负责维护客户端的连接
服务端的代码实现:
服务端采用的是Select多路复用阻塞连接和接收
cs
using System.Net;
using System.Net.Sockets;
namespace Server
{
public class NetSession
{
public Socket socket;//socket对象
public byte[] readBuffer;//用于接收的字节数组
}
public class NetServer
{
static Socket socket; //用于监听的全局socket
//用字典来维护每一个Socket连接
static Dictionary<Socket, NetSession> clients = new Dictionary<Socket, NetSession>();
public void Init()
{
//创建socket对象(参数分别是地址族:Ipv4,socket类型:字节流socket,通信协议:udp
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Udp);
//绑定服务器本地IP和端口
IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
socket.Bind(ipEp);
//监听,参数是最多可容纳等待接受的连接数,0表示不限制
socket.Listen(0);
Console.WriteLine("服务器启动成功!");
//可读的Socket列表,用于多路复用状态检测
List<Socket> readSockets = new List<Socket>();
while (true)
{
#region 多路复用select
//待监听的socket列表
readSockets.Clear();
readSockets.Add(socket);
foreach (var kv in clients)
{
readSockets.Add(kv.Value.socket);
}
/*
参数
checkRead 检测是否有可读的Socket列表
checkWrite 检测是否有可写的Socket列表
checkError 检测是否有出错的Socket列表
m1croScconds 等待回应的时间,以微秒为单位,如果该参数为-1表示一直等待,如果为0表示非阻塞
Select可以确定一个或多个Socket对象的状态,一个或多个套接字放入ILsit中。
通过调用Select(将IList作为checkRead参数),可检查Socket是否具有可读性。
若要检查套接字是否具有可写性,可使用checkWrite参数。若要检测错误条件,可使用checkError。
在调用Select之后,Select将修改ILsit列表 , 仅保留那些满足条件的套接字。
把包含6个Socket的列表传给Select, Select方法将会阻塞 ,
等到超时或某个(或多个)Socket可读时返同 ,并且修改checkRead列表 ,
仅保存可诙的socket A和socket C。 当没有任何可读Socket时,程序将会阻塞 ,不占用CPU资源
*/
Socket.Select(readSockets, null, null, 1000);
foreach (var kv in readSockets)
{
if (kv == socket)
{
this.ReadListenfd(kv);
}
else
{
this.ReadClientfd(kv);
}
}
#endregion
}
}
//当监听到客户端连接时的处理
void ReadListenfd(Socket listen)
{
/如果监听到客户端的连接,则维护该连接
Socket temp = listen.Accept();
NetSession state = new NetSession
{
socket = temp
};
clients.Add(temp, state);
}
//当接收到客户端消息时的处理
bool ReadClientfd(Socket client)
{
//判断字典中是否还包含该连接
if (!clients.ContainsKey(client))
{
return false;
}
NetSession state = clients[client];
//Reveive
int count = 0;
try
{
//接收
count = client.Receive(state.readBuffer);
}
catch (SocketException ex)
{
//如果发生异常则关闭并移除该连接
client.Close();
clients.Remove(client);
Console.WriteLine("接收失败:" + ex.Message);
return false;
}
//如果count为0,则说明客户端关闭了连接
if (count == 0)
{
client.Close();
clients.Remove(client);
Console.WriteLine("接收失败,count为0");
return false;
}
//将字节数组转换为字符串
string readStr = System.Text.Encoding.Default.GetString(state.readBuffer, 0, count);
Console.WriteLine("服务区接收:" + readStr);
this.sendMessage("服务器接收成功,返回给客户端:" + readStr);
return true;
}
//发送消息
void sendMessage(string message)
{
byte[] sendMsg = System.Text.Encoding.Default.GetBytes(message);
foreach (var kv in clients.Values)
{
kv.socket.Send(sendMsg);
}
}
}
}
客户端
客户端的通信流程:
-
创建Socket对象
-
连接服务器
-
发送消息
-
接收消息
6.关闭连接
客户端的代码实现:
客户端采用的是异步连接和接收
cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using UnityEngine;
using UnityEngine.UI;
public class Echo : MonoBehaviour
{
public Button connectButton;//连接按钮
public Button sendButton;//发送按钮
public InputField input;//输入的消息
private byte[] readBuffer = new byte[1024];//用于接收的字节数组
private Socket socket;//客户端Socket
public void Connect(){
//创建socket
socket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Udp);
//开始连接
socket.BeginConnect("127.0.0.1",8000,ConnectCallBack,socket);
}
public void ConnectCallBack(IAsyncResult result){
try{
Socket con = result.AsyncState as Socket;
con.EndConnect(result);//链接成功的回调方法
con.BeginReceive(readBuffer,0,1024,0,ReceiveCallBack,con);//开始接收
Debug.Log("Connect Success!");
}
catch(SocketException e){
Debug.Log("Connect Error:"+e.Message);
}
}
private void ReceiveCallBack(IAsyncResult ar)
{
try{
Socket rec = ar.AsyncState as Socket;
int count = rec.EndReceive(ar);
string mes = System.Text.Encoding.Default.GetString(readBuffer,0,count);
Debug.Log("接收服务器数据成功:"+mes);
//接收完这窜数据后准备接收下一串
rec.BeginReceive(readBuffer,0,1024,0,ReceiveCallBack,rec);
}
catch(SocketException ex){
Debug.Log("Connect Error:"+ex.Message);
}
}
void Send(string mes){
string sendStr = input.text;
byte[] bytes = System.Text.Encoding.Default.GetBytes(sendStr);
socket.BeginSend(bytes,0,bytes.Length,0,SendCallBack,socket);
}
void SendCallBack(IAsyncResult ar){
try{
Socket sen = ar.AsyncState as Socket;
int count = sen.EndSend(ar);
Debug.Log("发送成功!count = "+count);
}
catch(SocketException ex){
Debug.Log("发送失败! "+ex.Message);
}
}
}
需要说明的是,上述代码仅仅是为了熟悉基本的客户端和服务端网络通信,是一个非常简单的网络框架,建议动手亲自实战,后续将会使用protobuf协议来定义消息格式,以及添加重连、心跳检测、消息分发、消息队列等内容,搭建一个成熟的网络框架。