前言
在网络通讯中,默认情况下如果TCP客户端或服务端异常断开了(由于网络原因),另外一端不收发数据的情况下,是无法知道对方已经断开了的,如果后续要继续发送出数据,就会产生异常。Server/Client真的会不知道已经建立连接的对方已经不在了吗❓ 会他当然会知道但是预设为两小时后😶。通过查阅资料和测试,发现在C#中原来可以设定Keep-Alive来保持Socket长连接,并且侦测网络异常抛出Exception,当Server/Client端发生不正常断线时,Server端将会立刻知道~~~
C#中KeepAlive的实现
KeepAlive配置数据
通过以下函数来实现KeepAlive的配置数据的生成:
/// <summary>
/// 获取KeepAlive的控制参数
/// </summary>
/// <param name="onOff">是否开启KeepAlive</param>
/// <param name="keepAliveTime">当开启KeepAlive后,经过多长时间开始侦测,单位ms</param>
/// <param name="keepAliveInterval">侦测间隔时间,单位ms</param>
/// <returns></returns>
private static byte[] GetKeepAliveConfig(int onOff, int keepAliveTime, int keepAliveInterval)
{
byte[] buffer = new byte[12];
BitConverter.GetBytes(onOff).CopyTo(buffer, 0);
BitConverter.GetBytes(keepAliveTime).CopyTo(buffer, 4);
BitConverter.GetBytes(keepAliveInterval).CopyTo(buffer, 8);
return buffer;
}
服务端的代码
服务端的主要流程就是创建Socket并绑定到要监听的端口,然后通过IOControl配置KeepAlive,并启动异步接收。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace TCP_Server
{
class Program
{
/// <summary>
/// 获取KeepAlive的控制参数
/// </summary>
/// <param name="onOff">是否开启KeepAlive</param>
/// <param name="keepAliveTime">当开启KeepAlive后,经过多长时间开始侦测,单位ms</param>
/// <param name="keepAliveInterval">侦测间隔时间,单位ms</param>
/// <returns></returns>
private static byte[] GetKeepAliveConfig(int onOff, int keepAliveTime, int keepAliveInterval)
{
byte[] buffer = new byte[12];
BitConverter.GetBytes(onOff).CopyTo(buffer, 0);
BitConverter.GetBytes(keepAliveTime).CopyTo(buffer, 4);
BitConverter.GetBytes(keepAliveInterval).CopyTo(buffer, 8);
return buffer;
}
static byte[] tcpRecvBuffer = new byte[65536];
static void Main(string[] args)
{
//ip地址
IPAddress ip = IPAddress.Any;
//端口号
IPEndPoint point = new IPEndPoint(ip, 61003);
//创建监听用的Socket
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//绑定监听的端口
socket.Bind(point);
socket.Listen(10);
socket.IOControl(IOControlCode.KeepAliveValues, GetKeepAliveConfig(1, 1000, 1000), null);
while (true)
{
try
{
var newClient = socket.Accept();
Console.WriteLine("Connect from {0} at {1}", newClient.RemoteEndPoint, DateTime.Now.TimeOfDay);
newClient.BeginReceive(tcpRecvBuffer, 0, tcpRecvBuffer.Length, 0, new AsyncCallback(ReceivedCallBack), newClient);
}
catch
{
}
}
}
private static void ReceivedCallBack(IAsyncResult ar)
{
Socket state = (Socket)ar.AsyncState;
if (state.Connected == true)
{
try
{
int bytesRead = state.Available;
if (bytesRead > 0) //当 bytesRead大于0时,表示Client传送数据过来,为0时表示Client"正常"断线
{
Console.WriteLine("bytesRead={0}", bytesRead);
state.EndReceive(ar);
state.BeginReceive(tcpRecvBuffer, 0, tcpRecvBuffer.Length, 0, new AsyncCallback(ReceivedCallBack), state);
}
else
{
//处理Client正常断开
Console.WriteLine("{0}正常断线", state.RemoteEndPoint);
}
}
catch (Exception ee)
{
//客户端异常断开会执行到这里
Console.WriteLine("{0}异常断线", state.RemoteEndPoint);
}
}
else
{
state.Close();
Console.WriteLine("{0}已断开", state.RemoteEndPoint);
}
}
}
}
客户端的代码
客户端先创建Socket并与服务器建立连接,然后通过IOControl配置KeepAlive,并启动异步接收。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace TCP_Client
{
class Program
{
private static byte[] GetKeepAliveConfig(int onOff, int keepAliveTime, int keepAliveInterval)
{
byte[] buffer = new byte[12];
BitConverter.GetBytes(onOff).CopyTo(buffer, 0);
BitConverter.GetBytes(keepAliveTime).CopyTo(buffer, 4);
BitConverter.GetBytes(keepAliveInterval).CopyTo(buffer, 8);
return buffer;
}
private static byte[] recvBuffer = new byte[65536];
static void Main(string[] args)
{
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(new IPEndPoint(IPAddress.Any, 0));
socket.Connect("172.16.1.11", 61003);
socket.IOControl(IOControlCode.KeepAliveValues, GetKeepAliveConfig(1, 1000, 1000), null);
socket.BeginReceive(recvBuffer, 0, recvBuffer.Length, 0, new AsyncCallback(ReceivedCallBack), socket);
Console.ReadKey();
}
private static void ReceivedCallBack(IAsyncResult ar)
{
Socket state = (Socket) ar.AsyncState;
if (state.Connected == true)
{
try
{
int bytesRead = state.EndReceive(ar);
if (bytesRead == 0)
{
Console.WriteLine("正常断开");
}
else
{
state.BeginReceive(recvBuffer, 0, recvBuffer.Length, 0, new AsyncCallback(ReceivedCallBack), state);
}
}
catch
{
Console.WriteLine("异常断开");
}
}
else
{
Console.WriteLine("已断开");
}
}
}
}
测试结果
先运行服务端,再运行客户端(分别配置KeepAlive和不配置KeepAlive),客户端与服务端建立连接后,将客户端的网线拔出,造成客户端异常断线的场景。我们程序中设置的KeepAlive的间隔是1秒,KeepAlive机制会重试10次,如果都无法保活成功,这认为连接已经断掉了,运行结果下图所示:
- 服务端:
Connect from 172.16.1.8:50527 at 16:32:05.2478575
172.16.1.8:50527异常断线
- 客户端1(配置了KeepAlive)等待10秒左右
异常断线
- 客户端2(未配置了KeepAlive)等待10秒左右
并无打印输出,说明未配置KeepAlive短时间之内并不能检测到与服务端已经断线了。
评论区