什么是心跳机制
心跳机制出现在tcp长连接中,客户端和服务器之见定时发送一种特殊的数据包通知对方还在线,以确保tcp链接地可靠性,有可能tcp链接由于某些原因(列入网线被拔了,突然断电)导致客户端断了,但是服务器不知道客户端断了,服务器还保持与客户端连接的状态,所以为了不浪费资源,需要知道客户端非正常中断,服务器把断开客户端断开链接,需要加入心跳包机制
tcp 需不需要心跳?
需要心跳机制tcp本身内置了keeplive心跳机制,但是这种内置的心跳机制不足以满足所有的情况,所以有必要自己写心跳机制
有必要自己写心跳机制
3 那些网络情况下不满足keepalive心跳机制
1 tcp 属于 keeepalive心跳机制 有些设备不会处理keepalive心跳包
2 keeepalive心跳机制只能说明连接是活的,应用实现心跳机制,可以保持连接是活的应用正常工作
心跳检测步骤
1.客户端每隔一个时间间隔发生一个探测包给服务器
2.客户端发包时启动一个超时定时器
3.服务器端接收到检测包,应该回应一个包
4. 如果客户机收到服务器的应答包,则说明服务器正常,删除超时定时器
5. 如果客户端的超时定时器超时,依然没有收到应答包,则说明服务器挂了
实例
拥有单发群发指定部分功能
客户端:
搭建客户端连接的界面
代码如下:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
// 点击链接按钮
// 1 创建客户端对象
// 2 连接服务器
// 3 创建网络基础流发消息,write发消息
// 4 创建网络基础流接消息,read接消息
// 5 断开链接close()
TcpClient client;
private void button1_Click(object sender, EventArgs e)
{
if(button1.Text == "连接")
{
try
{
// 开始连接
client = new TcpClient();
client.Connect(comboBox1.Text, int.Parse(comboBox2.Text));
button1.Text = "断开";
// 读取数据
StartRead();
// 启动心跳机制
HeartBeat();
}
catch(Exception ex)
{
MessageBox.Show("连接失败");
}
}
else
{
// 断开连接
client.Close();
timer1.Stop(); // 关闭心跳包
// 把心跳定时器关闭
button1.Text = "连接";
}
}
void StartRead()
{
byte[] bs = new byte[1024];
Task.Run(() =>
{
try
{
while (true)
{
int count = client.GetStream().Read(bs,0,bs.Length);
string msg = Encoding.UTF8.GetString(bs,0,count);
richTextBox1.Invoke((Action)(() =>
{
richTextBox1.AppendText(msg+"\t\n");
}));
}
}catch(Exception ex)
{
button1.Text = "连接";
}
});
}
// 开启心跳方法
Timer timer1;
void HeartBeat()
{
timer1 = new Timer();
timer1.Interval = 3000;
timer1.Tick += Timer_Tick;
timer1.Start();
}
// 定时器方法 定时发送心跳包
private void Timer_Tick(object sender, EventArgs e)
{
// 心跳包发送数据的内容需要跟后台事先约定好,什么数据是心跳包
//
client.GetStream().Write(new byte[] { 1 }, 0, 1);
}
// 点击发送按钮设置普通的消息包
private void button2_Click(object sender, EventArgs e)
{
timer1.Stop();
timer1.Interval = 3000;
timer1.Tick += Timer_Tick;
timer1.Start();
byte[] bs = Encoding.UTF8.GetBytes(textBox1.Text);
// 普通消息在发送时候,需要字节数组的第一位置为0
byte[] bs1 = new byte[bs.Length+1];
bs1[0] = 0;// 字节数组的第一位设置为0
bs.CopyTo(bs1, 1); // 把消息复制到新数组的第一位开始
client.GetStream().Write(bs1,0,bs1.Length);
}
}
服务端:
Server类:
internal class Server
{
TcpListener listen;
//1通过构造函数创建服务器对象
public Server(IPAddress ip,int port)
{
listen = new TcpListener(ip, port);
}
//2 封装开启监听的方法
public void Start()
{
listen.Start(100);//开启监听
//接受客户端的连接
StartConnect();
//调用扫描心跳方法
SaoMiao();
}
//3接受客户端的连接 封装一监听客户端连接的方法
//保存所有的客户端字典, 键是ip 值是客户端,
Dictionary<string,TcpClient> clientDic = new Dictionary<string,TcpClient>();
//字典保存客户端和当前连接服务器时间点
Dictionary<string,DateTime> heartDic = new Dictionary<string,DateTime>();
public event Action<TcpClient> 有客户端连入的事件; //当客户端连入触发。绑定事件,
void StartConnect()
{
Task.Run(() =>
{
while (true) //接入多个客户端
{
TcpClient client = listen.AcceptTcpClient();
string ip = client.Client.RemoteEndPoint.ToString(); //获取远程ip
//保存当前客户端
clientDic.Add(ip, client);
//记录当前客户端心跳 连接成功时候记录当前客户端时间点
heartDic.Add(ip, DateTime.Now);
//调用事件函数 触发事件,
有客户端连入的事件?.Invoke(client);
//4 接收客户端发来的消息
ReceiveMsg(client);
}
});
}
//4 接收客户端发来的消息
//封装接受的消息
public event Action<string> 客户端断开事件; //当客户端断开时候调用事件
public event Action<TcpClient, byte[]> 接受到消息的事件;//接收到消息调用
void ReceiveMsg(TcpClient t1)
{
NetworkStream stream = t1.GetStream();
string ip = t1.Client.RemoteEndPoint.ToString() ;
byte[] bs = new byte[1024];
Task.Run(() =>
{
try
{
while (true)
{
int count = stream.Read(bs, 0, bs.Length);
if(count == 0)
{
//客服端断开
throw new Exception("客户端断开连接");
}
//如果接收到数据长度不为0,
//必须判断是否是心跳包 事先约定好:如果数据第一位是0的时候,当成普通数据包
//如果数据第一位是1说明是心跳包
switch (bs[0]) //判断第一位数据是不是0
{
case 0: //普通数据 取出来的时候不需要显示第一位标识符
//skip 从第一位开始截取
// take 到指定位置的元素为止
//第一位0、1代表是否是心跳包标识符,
//最后一位占位符
byte[] body= bs.Skip(1).Take(count - 1).ToArray();
//要么群发 要么单发
接受到消息的事件?.Invoke(t1, body);
break;
case 1: //发的是心跳包
//修改是心跳包发的时间点
heartDic[ip] = DateTime.Now;
break;
}
}
}
catch(Exception e)
{
//从字典把客户端清除掉
clientDic.Remove(ip);
//如果客户端断开了,打印客户断开
客户端断开事件?.Invoke(ip);
//删除心跳记录
heartDic.Remove(ip);
}
});
}
//遍历所有客户端 扫描是否在未超时的时间内
void SaoMiao()
{
Task.Run(() =>
{
while (true)
{
Thread.Sleep(4000);//线程休眠4s
DateTime now1 = DateTime.Now;
foreach (var item in heartDic)//遍历所有心跳记录
{
//now1 当前时间点
// item.Value 服务器接收客户端发来的心跳包时间
//new TimeSpan(0, 0, 4) 时分秒
if (now1 - item.Value > new TimeSpan(0, 0, 4))
{
Console.WriteLine(item.Key + "掉线了");
if (clientDic.Keys.Contains(item.Key))
{
Send("你已经掉线了骚年:" + item.Key, item.Key);
clientDic.Remove(item.Key);
}
else
{
Console.WriteLine(item.Key + "在线");
}
}
}
}
});
}
// 无参数的构造函数
public Server()
{
}
//群发方法 向所有的客户端发消息
public void Send(string content)
{
byte[] bs = Encoding.UTF8.GetBytes(content);
foreach (var item in clientDic) //遍历所有的客户端
{
item.Value.GetStream().Write(bs, 0, bs.Length);
}
}
//指定给谁发
public void Send(string content, string ip)
{
byte[] bs = Encoding.UTF8.GetBytes(content);
//根据ip取出客户端,从字典取
clientDic[ip].GetStream().Write(bs, 0, bs.Length);
}
//指定给哪些客户端发
//send("你好", ["192.","127"])
public void Send(string content, string[] ips)
{
byte[] bs = Encoding.UTF8.GetBytes(content);
foreach (var item in clientDic) //所有客户端
{
//item.key 键 ip字符串
//item.value 值 客户端对象
if (ips.Contains(item.Key))
{
//如果ips数组包含目标客户端
item.Value.GetStream().Write(bs, 0, bs.Length);
}
}
}
}
Program:
internal class Program
{
static Server server;
static void Main(string[] args)
{
server = new Server(IPAddress.Any,3333);
server.Start(); // 除了服务器监听方法, 监听客户连接的方法 扫描客户端是否在线的方法
//如果监听到有客户端连接的时候,打印哪个终端连入到服务器了 使用事件封装
server.有客户端连入的事件 += 有客户端连入服务器方法; //绑定事件
server.客户端断开事件 += f2;
server.接受到消息的事件 += f3;
Console.ReadKey();
}
//相当于点击之后的回调方法,再客户端连接成功之后调用这个方法
public static void 有客户端连入服务器方法(object obj)
{
TcpClient t1 = obj as TcpClient;
Console.WriteLine(t1.Client.RemoteEndPoint+"连接到服务器");
}
public static void f2(object obj)
{
Console.WriteLine(obj.ToString()+"断开连接");
}
public static void f3(TcpClient t1, byte[] b1)
{
// t1.GetStream().Write(b1, 0, b1.Length);
string content = Encoding.UTF8.GetString(b1);
// 如果群发
// server.Send(content);
// 如果单发
// server.Send(content, "这里是端口号");
// 如果指定部分
string[] ips = new string[] { "这里是端口号", "这里是端口号" };
server.Send(content, ips);
}
}