以前都是在客户端这边研究一些逻辑和画面效果,乘着大四最后一个寒假学习一些服务端和客户端交互的内容,正好买了这本书,边看边做边写笔记,希望能用到毕设里面
另外多提一句。微软居然把暴雪收购了!!我超,这下压力来到了索尼这边
目录
Socket相关
socket概念
网络上的两个程序通过一个双侠奴工的通信连接实现数据交换,这个连接的一端称为一个Socket。一个Socket包含了进行网络通信必需的五种信息:
连接使用的协议、本地主机的IP地址、本地的协议端口、远程主机的IP地址和远程协议端口
Socket通信的流程
- 开启一个连接之前,需要创建一个Socket对象(使用API Socket),然后绑定本地使用的端口(使用API Bind)。对服务端而言,绑定的步骤相当于给手机插上SIM卡,确定了“手机号”。对客户端而言,连接时(使用API Connect)会由系统分配端口,可以省去绑定的步骤
- 服务端开启监听(使用API Listen),等待客户端接入。相当于电话开机,等待别人呼叫
- 客户端连接服务器(使用API Connect),相当于手机拨号
- 服务器接收连接(使用API Accept),相当于接听电话并说出“喂”
- 客户端和服务端通过Send和Receive等API收发数据,操作系统会自动完成数据的确认、重传等步骤,确保传输的数据准确无误
- 某一方关闭连接(使用API Close),操作系统会执行“四次挥手”的步骤,关闭双方连接,相当于挂断电话
上图画出了整个通信流程,而后续的代码基本就是根据这个流程图来写的
TCP和UDP协议
TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,与TCP相对应的UDP协议是无连接的、不可靠的、但传输效率较高的协议
Unity异步Echo程序
客户端部分
首先给使用UGUI搭建一个简单的界面
客户端代码如下
using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;
using System.Net.Sockets;
using UnityEngine.UI;
public class Echo : MonoBehaviour
{
Socket socket;//定义套接字
//UGUI 这里声明界面元素
public InputField inputField;
public Text text;
//接收缓冲区
byte[] readBuff = new byte[1024];
string recvStr = "";
//点击链接按钮
public void Connection()
{
//Socket
socket = new Socket(AddressFamily.InterNetwork,
SocketType.Stream,ProtocolType.Tcp);
socket.BeginConnect("127.0.0.1",8888,ConnectCallback,socket);//改为异步连接
// socket.Connect("127.0.0.1",8888);//连接函数
}
//Connect回调函数
public void ConnectCallback(IAsyncResult ar)
{
try{
Socket socket = (Socket) ar.AsyncState;
socket.EndConnect(ar);
Debug.Log("Socket Connect Succ");
socket.BeginReceive(readBuff,0,1024,0,ReceiveCallback,socket);//异步接收
}
catch(SocketException ex){
Debug.Log("Socket Connect fail" + ex.ToString());
}
}
public void ReceiveCallback(IAsyncResult ar)
{
try{
Socket socket = (Socket) ar.AsyncState;
int count = socket.EndReceive(ar);
recvStr = System.Text.Encoding.Default.GetString(readBuff,0,count);
socket.BeginReceive(readBuff,0,1024,0,ReceiveCallback,socket);//这里是一个递归的调用
}
catch(SocketException ex){
Debug.Log("Socket Receive fail" + ex.ToString());
}
}
//点击发送按钮
public void Send()
{
//Send
string sendStr = inputField.text;//得到发送的消息
byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
socket.BeginSend(sendBytes,0,sendBytes.Length,0,SendCallback,socket);
// socket.Send(sendBytes);
}
//Send回调
public void SendCallback(IAsyncResult ar)
{
try{
Socket socket = (Socket) ar.AsyncState;
int count = socket.EndSend(ar);
Debug.Log("Socket Send Succ" + count);
}
catch(SocketException ex){
Debug.Log("Socket Send fail" + ex.ToString());
}
}
private void Update()
{
text.text = recvStr;
}
}
注释部分
-
Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
用于创建一个Socket对象,它的三个参数分别代表地址族,套接字类型和协议。地址族知名使用的是IPV4还是IPV6,
InterNetwork代表IPV4,InterNetworkV6代表IPV6。
SocketType是套接字类型,游戏中最常用的是字节流套接字,即Stream。ProtocolType指明协议 -
客户端通过socket.Connect(远程IP地址,远程端口)连接服务端。
Connect是一个阻塞方法,程序会卡住直到服务端回应(接受、拒绝或者超时) -
客户端通过socket.send发送数据。GetBytes(字符串)把字符串转换成byte这也是一个阻塞方法。该方法接受一个byte[]类型的参数指明要发送的内容。Send的返回值指明发送数据的长度程序用System.Text.Encoding.Default.[]数组,然后发送给服务端
-
客户端使用socket.Receive接受服务端数据。
Receive也是阻塞方法,没有收到服务端的数据时,程序将卡在Receive不会往下执行
Receive带有一个byte[]类型的参数,它存储接收到的数据
Receive的返回值指明接收到的数据的长度。之后使用System.Text.Encoding.Default.GetString(readBuff,0,count)将byte[] -
通过socket.close()关闭连接
-
通过BeginConnect和EndConnect来让客户端代码变成异步进行,防止程序卡死
IAsyncResult是.NET提供的一种异步操作,通过名为BeginXXX和EndXXX的两个方法来为实现原本同步方法的异步调用
BeginXXX方法中包含同步方法中所需的参数,此外还包含两个参数:一个AsyncCallback委托和一个用户定义的状态对象委托用来调用回调方法,状态对象用来向回调方法传递状态信息,且BeginXXX方法返回一个实现IAsyncResult接口的对象,EndXXX方法用于结束异步操作并且返回结果
EndXXX方法含有一个IAsyncResult参数,用于获取异步操作是否完成的信息,它的返回值与同步方法相同 -
BeginReceive的参数为(readBuff,0,1024,ReceiveCallback,socket)
第一个参数readBuff表示接收缓冲区,第二个参数0表示从readBuff第0位开始接收数据,这个参数和TCP粘包问题有关
第三个参数1024代表每次最多接收1024个字节,假如服务端回应一串长长的数据,那一次也只会收到1024个字节 -
BeginReceive的调用位置
程序在两个地方调用了BeginReceive,一个是ConnectCallback,在连接成功后,就开始接收数据,接收到数据之后,回调函数ReceiveCallback被调用
另一个是BeginReceive内部,接受完一串数据之后,等待下一串数据的到来 -
Update和recvStr
在Unity中,只有主线程可以操作UI组件。由于异步回调是在其他线程执行的,如果在BeginReceive给text.text赋值,Unity会弹出异常信息,所以只能在主线程中 -
异步BeginSend参数说明
buffer Byte类型的数组,包含需要发送的数据
offset 从Buffer中的offset位置开始发送
size 将要发送的字节数
socketFlags SocetFlags值的按位组合,这里设置为0
callback 回调函数,一个AsyncCallbakc委托
state 一个用户定义对象,其中包含发送操作的相关信息。当操作完成时,此对象会传递给EndSend委托
服务器部分
服务器这边使用Visual Studio建立一个C#控制台程序,代码如下
using System;
using System.Net;
using System.Net.Sockets;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EchoServer
{
//该类用于保存客户端的信息
class ClientState
{
public Socket socket;
public byte[] readBuff = new byte[1024];
}
class Program
{
//监听Socket
static Socket listenfd;
//定义一个字典集合保存所有客户端的信息,这样的结构可以通过clientState = clients[Socket]进行快速访问
static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>();
static void Main(string[] args)
{
Console.WriteLine("Hello world");
//Socket
listenfd = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
//Bind
IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
IPEndPoint ipEp = new IPEndPoint(ipAdr, 8888);
listenfd.Bind(ipEp);
//Listen
listenfd.Listen(0);
Console.WriteLine("[服务器]启动成功");
//Accept
listenfd.BeginAccept(AccpetCallback,listenfd);
//waiting
Console.ReadLine();
}
//Accept回调
//处理三件事情:
//1.给新的连接分配ClientState,并且把它添加到clients列表之中
//2.异步接收客户端数据
//3.再次调用BeginAccept实现循环
public static void AccpetCallback(IAsyncResult ar)
{
try
{
Console.WriteLine("[服务器]Accept");
Socket listenfd = (Socket)ar.AsyncState;
Socket clientfd = listenfd.EndAccept(ar);
//clients列表,将连接上的客户端Socket加入列表
ClientState state = new ClientState();
state.socket = clientfd;
clients.Add(clientfd,state);
//接收数据BeginReceive
clientfd.BeginReceive(state.readBuff,0,1024,0,ReceiveCallback,state);
//继续接收Accept
listenfd.BeginAccept(AccpetCallback,listenfd);
}
catch(SocketException ex)
{
Console.WriteLine("Socket Accept fail" + ex.ToString());
}
}
//ReceiveCallBack是BeginReceive的回调函数,它也处理了三件事情
//1.服务端收到消息之后,回应客户端
//2.如果收到客户端关闭连接的信号“if(count==0)”,断开连接
//3.继续调用BeginReceive接收下一个数据
public static void ReceiveCallback(IAsyncResult ar)
{
try
{
ClientState state = (ClientState)ar.AsyncState;
Socket clientfd = state.socket;
int count = clientfd.EndReceive(ar);
//客户端关闭
if(count == 0)
{
clientfd.Close();
clients.Remove(clientfd);
Console.WriteLine("Socket Close");
return;
}
string recvStr = System.Text.Encoding.Default.GetString(state.readBuff,0,count);
byte[] sendBytes = System.Text.Encoding.Default.GetBytes("echo"+ recvStr);
clientfd.Send(sendBytes);//减少代码量,不适用异步
clientfd.BeginReceive(state.readBuff,0,1024,0,ReceiveCallback,state);
}
catch(SocketException ex)
{
Console.WriteLine("Socket Receive fail" + ex.ToString());
}
}
}
}
//注释部分
//1.绑定Bind
//listenfd.Bind(ipEp)将给listenfd套接字绑定Ip和端口,这里也可以更改为真实的IP地址,一样有效
//2.监听Listen
//服务端通过listenfd.Listen(backlog)开启监听,等待客户端连接。参数backlog指定队列中最多可以容纳等待接受的连接数,0代表不限制
//3.应答Accept
//开启监听后,服务器调用listenfd.Accept()接受客户端连接。Accept()返回了一个新客户端的Socket对象,对于服务器来说,它有一个监听Socket,用来监听和应答客户端的连接
//对每一个客户端还有一个专门的Socket用来处理该客户端的数据
//4.IPAddress和IPEndPoint
//使用IPAddress指定IP地址,使用IPEndPoint指定IP和端口
//5.Receive方法将接收到的字节流保存到readBuff上