闲来无事,研究了下socket~ 文章最后会给出实例链接,如果不想看介绍的可以直接下下来运行。 本人小白一枚,如有错误请看者不奢赐教。
客户端为unity,服务端为vs,都是用c#语言编写。
先说下我对socket的理解,建立连接、通信、释放连接。此为socket通信的三次握手。
对于socket通信,c#底层已经为我们封装好了,我们可以选择使用UDP还是TCP,这里我使用的是TCP连接。
先介绍下socket通信的基础 : **建立连接,发送数据,接收数据,释放连接**。
建立连接 :
// 该类用于阻塞当前线程,并等待信号,接收到继续进行的信号后,释放等待线程
private static ManualResetEvent connectDone = new ManualResetEvent (false);
// 设置ip地址与端口号
IPEndPoint remoteEP = new IPEndPoint (IPAddress.Parse (serverIP), port);
// 创建socket
tcpSockt = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 异步创建socket连接
tcpSockt.BeginConnect (remoteEP, new AsyncCallback (connectCallback), tcpSockt);
if (connectDone.WaitOne (timeOutSec, false)) { // 阻塞当前线程, socket连接成功或超时,流程继续
if (zSocketManager.IsConnected) {
receive (tcpSockt);
return;
} else {
throw new ArgumentException ("连接失败,请检查网络设置");
}
} else {
tcpSockt.Close ();
throw new ArgumentException ("连接失败,请检查网络设置");
}
} catch (Exception e) {
Debug.Log (e.ToString ());
}
发送数据
发送数据时,我们需要与服务定义好一些包头,包括服务器版本号、协议数据长度、协议号等。将这些包头数据与协议数据拼接成真正的数据后通过Socket.Send方法发送给服务器。
public void Send (string data, NetCommand _command)
{
byte[] bytesData = Encoding.UTF8.GetBytes (data); // 字符串转成字节
bytesData = packMsg (bytesData, _command); // 数据添加包头
if (tcpSockt == null || !tcpSockt.Connected)
throw new ArgumentException ("参数socket为null,或者未连接到远程计算机");
else if (bytesData.Length == 0) {
throw new ArgumentException ("参数data为null ,或者长度为 0");
} else {
send (bytesData);
}
}
// 拼接包头和协议数据
byte[] packMsg (byte[] _data, NetCommand _command)
{
byte[] head = new byte[17];
head [0] = 20;
head [1] = 15;
head [2] = 8;
head [3] = 18;
head [4] = 0; // protoVersion
Array.Copy (System.BitConverter.GetBytes (0), 0, head, 5, 4); // serverVersion
byte[] _dataLength = new byte[4]; // 把数据长度添加到 包头中
_dataLength = System.BitConverter.GetBytes (_data.Length);
Array.Copy (_dataLength, 0, head, 9, 4);
byte[] _commandByte = new byte[4];
_commandByte = System.BitConverter.GetBytes ((int)_command);
Array.Copy (_commandByte, 0, head, 13, 4);
byte[] _result = new byte[head.Length + _data.Length];
Array.Copy (head, 0, _result, 0, head.Length);
Array.Copy (_data, 0, _result, head.Length, _data.Length);
return _result;
}
void send (byte[] _data)
{
int leftLength = _data.Length;
int hasSend = 0;
int flag = 0;
while (true) { // 通过循环将数据全部发送出去
if (tcpSockt.Poll (timeOutSec, SelectMode.SelectWrite)) {
var _sendLen = tcpSockt.Send (_data, hasSend, leftLength, SocketFlags.None);
leftLength -= _sendLen;
hasSend += _sendLen;
if (leftLength == 0) { // 协议数据已全部发送完
flag = 0;
break;
} else {
if (_sendLen > 0)
continue;
else { // 发送数据时出错
flag = -2;
}
}
} else { // 发送超时
flag = -1;
break;
}
}
if (flag != 0)
Debug.LogError ("send flag :[" + flag + "]");
}
接收数据
接收数据我用的是异步接收。接收数据时会出现粘包和半包问题(我对于粘包的理解是要接收的数据大于缓存数据大小,需要多次接收。而半包就是要接收的数据小于缓存数据大小),主要的解决思路就是设置一个整形变量a,当接收到数据时,判断a是否为0,如果为0,则该数据为新的协议数据),对其进行拆包,获取包头和协议数据。如果a大于0,则为粘包,需要接收剩余数据。
// 通过该方法让socket监听接收数据
void receive (Socket _client)
{
try {
StateObject state = new StateObject ();
state.CreateBuffer ();
state.workSocket = _client;
// socket开始异步接收数据,当有接收到数据时,会调用receiveCallback方法
_client.BeginReceive (state.Buffer, 0, StateObject.BufferSize, SocketFlags.None, new AsyncCallback (receiveCallback), state);
} catch (Exception e) {
Debug.LogError (e.ToString ());
}
}
void receiveCallback (IAsyncResult _ar)
{
try {
StateObject state = (StateObject)_ar.AsyncState;
Socket client = state.workSocket;
if (!client.Connected) {
Debug.LogError ("服务器断开连接");
DisConnect ();
return;
}
// 开始接收数据, 本次最多接收 BufferSize 个字节
int bytesRead = client.EndReceive (_ar);
Debug.Log ("已接收 字节数 " + bytesRead);
if (bytesRead > 0) {
if (leftLength > 0) { // 继续接收剩余数据
leftLength -= bytesRead;
bytebuffer.Add (state.Buffer);
if (leftLength <= 0) { // 数据接收完毕
receiveData.bytebuffer = bytebuffer.GetBytes();
receiveData.data = getString(receiveData.bytebuffer);
if (receiveDataCallback != null)
receiveDataCallback (receiveData);
}
} else {
receiveData = null;
receiveData = new ReceiveData ();
unpackMsg (state.Buffer,bytesRead, out receiveData.command, out leftLength);
if (leftLength <= 0) { // 数据接收完毕
receiveData.bytebuffer = bytebuffer.GetBytes();
receiveData.data = getString(receiveData.bytebuffer);
if (receiveDataCallback != null)
receiveDataCallback (receiveData);
}
}
}
receive (client);
} catch (Exception e) {
Debug.LogError (e.ToString ());
}
}
// 拆包
void unpackMsg (byte[] _data, int _hasReceived, out NetCommand _command, out int _leftSize)
{
byte[] head = new byte[17]; // 包头大小为17
for (int i = 0; i < head.Length; i++) {
head [i] = _data [i];
}
byte[] _dataLengthByte = new byte[4];
for (int i = 0; i < _dataLengthByte.Length; i++) {
_dataLengthByte [i] = head [i + 9];
}
// 获取协议数据大小
_leftSize = System.BitConverter.ToInt32 (_dataLengthByte, 0);
byte[] _commandByte = new byte[4];
for (int i = 0; i < _commandByte.Length; i++) {
_commandByte [i] = head [i + 13];
}
// 获取协议号
_command = (NetCommand)System.BitConverter.ToInt32 (_commandByte, 0);
if (_leftSize + 17 > _data.Length)
_leftSize -= _data.Length - 17; // 还有数据没发完
else
_leftSize = 0; // 数据发送完毕
bytebuffer.Clear ();
bytebuffer.Add (_data, 17, _hasReceived);
}
释放链接
释放链接就比较简单了,一句代码的事情,调用 socket.Close()方法就能断开连接。但是,这里有一个问题,比如客户端要断开连接,做完了断开连接的处理后,向服务端发送了断开连接协议,然后调用socket.Close方法断开了连接,但是服务端并不会接收到断开连接的协议,因为socket已经断开了,也没办法做断开连接的操作,且服务端照样会发送数据给该客户端,这就出现了问题。解决的方法是,当客户端要断开连接时,先发送断开连接协议,服务端收到协议后,处理断开连接的方法,之后回给客户端断开连接的协议,客户端处理断开连接的方法,之后发送向服务端发送确认断开协议,服务端收到后,会给客户端确认断开协议,并关闭socket,客户端收到回过来的确认断开协议后,调用socket.Close方法断开链接。
至此,socket通信的理论知识就讲解到这里。如有错误请看者不奢赐教。
工程地址 : 链接:http://pan.baidu.com/s/1miNyrCs 密码:jxm7