版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/specialshoot/article/details/51382576
用户类:User.cs
先上代码:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ChatServer
{
public class User
{
public BinaryReader br { get; private set; }
public BinaryWriter bw { get; private set; }
private string userName;
private TcpClient client;
public User(TcpClient client, bool isTask)
{
this.client = client;
NetworkStream networkStream = client.GetStream();
br = new BinaryReader(networkStream);
bw = new BinaryWriter(networkStream);
if (isTask)
{
Task.Run(() => ReceiveFromClient());
}
else
{
Thread t = new Thread(ReceiveFromClient);
t.Start();
}
}
public void Close()
{
br.Close();
bw.Close();
client.Close();
}
/// <summary>处理接收的客户端数据</summary>
public void ReceiveFromClient()
{
while (true)
{
string receiveString = null;
try
{
receiveString = br.ReadString();
}
catch
{
CC.RemoveUser(this); return;
}
string[] split = receiveString.Split(':');
switch (split[0])
{
case "Login": //格式:Login,用户名
userName = split[1];
CC.SendToAllClient(split[1] + "登录成功,当前用户数:" + CC.userList.Count);
break;
case "Logout": //格式:Logout,用户名
CC.RemoveUser(this);
return;
case "Talk": //格式:Talk,对话信息
CC.SendToAllClient(userName + "说:" + receiveString.Remove(0, 5));
break;
case "CMD":
CC.haveHeart();
break;
default:
CC.SendToAllClient(userName + " heart : " + receiveString);
break;
}
}
}
}
}
User类我们定义了四个属性:
- br:用于读从客户端发来的消息
- bw:用于发送消息给客户端
- username:用户名
- TcpClient:C#提供的tcp类
Close是关闭br,bw以及client。
ReceiveFromClient()用于处理接收到的消息,br.ReadString()可以从读取网络流接收数据。
处理客户类:CC.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace ChatServer
{
public interface ICallBackListener {
void onCallBack(string message);
}
public class CC
{
public static List<User> userList { get; set; } //保存连接的所有用户
public static IPAddress localIP { get; set; } //使用的本机IPv4地址
public static int port { get; set; } //监听端口
private static ICallBackListener mListener;
static CC()
{
userList = new List<User>();
port = 51888;
IPAddress[] ips = Dns.GetHostAddresses(Dns.GetHostName());
Console.WriteLine("ips数量->" + ips.Length);
foreach (var v in ips)
{
if (v.AddressFamily == AddressFamily.InterNetwork)
{
localIP = v;
Console.WriteLine("ips->"+localIP);
break;
}
}
}
public static void RemoveUser(User user)
{
userList.Remove(user);
Console.WriteLine("有用户退出");
if (mListener != null) {
mListener.onCallBack("有用户退出");
}
SendToAllClient("有用户退出或失去连接,当前用户数:" + userList.Count);
}
public static void SendToAllClient(string message) {
if (mListener != null) {
mListener.onCallBack("发送消息给所有用户:"+message);
}
for (int i = 0; i < userList.Count; i++)
{
try
{
userList[i].bw.Write(message);
userList[i].bw.Flush();
}
catch
{
RemoveUser(userList[i]);
}
}
}
public static void haveHeart() {
if (mListener != null) {
mListener.onCallBack("heart");
}
}
public static void ChangeState(Button btn1, bool b1, Button btn2, bool b2)
{
btn1.IsEnabled = b1;
btn2.IsEnabled = b2;
}
public static void setOnListener(ICallBackListener listener) {
mListener = listener;
}
}
}
为了方便后面的ThreadServer类可以调用接口,我们加入了一个回调接口ICallBackListener,回调方法为onCallBack(),学过java的同学应该比较熟悉,这里面使用的回调写法和java基本一样。
我们定义了四个量:
- userList:用于保存连接的所有用户
- localIP:使用本机IPv4地址
- port:监听端口
- mListener:用于回调的ICallBackListener监听器
RemoveUser移除用户,SendToAllClient发送消息给所有用户,haveHeart调用回调接口传送给ThreadServer类进行处理操作。SetOnListener设置监听器。ChangeState改变界面上按钮的状态。
可以看到User类中有CC的处理方式,当收到字符串开头为CMD时触发haveHeart()心跳发送机制
线程服务端类:ThreadServer.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Timers;
namespace ChatServer
{
/// <summary>
/// ThreadServer.xaml 的交互逻辑
/// </summary>
public partial class ThreadServer : Window,ICallBackListener
{
private TcpListener myListener;
System.Timers.Timer timerHeartBeat = null;//用于检测心跳包个数
int heartCount = 0;//心跳计数
public ThreadServer()
{
InitializeComponent();
this.Closing += ThreadServer_Closing;
btnStop.IsEnabled = false;
}
async void ThreadServer_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
if (myListener != null) {
await Task.Run(() => removeAllClient());
myListener.Stop();
}
}
private void btnStart_Click(object sender, RoutedEventArgs e)
{
CC.setOnListener(this);
CC.ChangeState(btnStart, false, btnStop, true);
//myListener = new TcpListener(CC.localIP, CC.port);
myListener = new TcpListener(IPAddress.Any, CC.port);
myListener.Start();
AddInfo("开始在{0}:{1}监听客户连接", CC.localIP, CC.port);
Thread myThread = new Thread(ListenClientConnect);
myThread.IsBackground = true;
myThread.Start();
}
public void onCallBack(string message) {
if (message.Equals("heart")) {
heartCount++;
Console.WriteLine("heartCount++ -> "+heartCount);
return;
}
AddInfo(message);
}
private async void btnStop_Click(object sender, RoutedEventArgs e)
{
if (myListener != null) {
await Task.Run(() => removeAllClient());
myListener.Stop();
stopHeartTimer();
AddInfo("监听已停止!");
CC.ChangeState(btnStart, true, btnStop, false);
}
}
private void ListenClientConnect()
{
TcpClient newClient = null;
while (true)
{
try
{
newClient = myListener.AcceptTcpClient();
User user = new User(newClient, false);
CC.userList.Add(user);
startHeartTimer();
AddInfo("新用户加入");
}
catch {
Console.WriteLine("enter catch");
stopHeartTimer();
break;
}
}
}
private void startHeartTimer() {
Console.WriteLine("enter startHeartTimer");
heartCount = 0;
timerHeartBeat = new System.Timers.Timer(1000);
timerHeartBeat.Elapsed += new System.Timers.ElapsedEventHandler(checkConnect);
timerHeartBeat.AutoReset = true;
timerHeartBeat.Enabled = true;
Console.WriteLine("gg");
}
/*
* 检查心跳
*/
private void checkConnect(object sender, EventArgs e) {
if (heartCount < -10) {
Console.WriteLine("掉线");
stopHeartTimer();
return;
}
heartCount--;
AddInfo("heartCount -> " + heartCount);
}
private void stopHeartTimer() {
if (timerHeartBeat != null) {
Console.WriteLine("stop timer");
heartCount = 0;
timerHeartBeat.Stop();
}
}
private void removeAllClient() {
foreach (User usr in CC.userList) {
usr.Close();
}
}
private void AddInfo(string format, params object[] args) {
textBlock1.Dispatcher.InvokeAsync(() => {
textBlock1.Text += string.Format(format, args) + "\n";
});
}
}
}
这里说一下我心跳机制的设置,客户端每1秒给服务器发送一个心跳包格式是CMD开头,服务器端有个计数器heartCount,每次得到心跳则heartCount清0,服务器端有个定时器,每一秒将heartCount-1,如果客户端长时没有响应,heartCount则会一直减,到一定阈值则说明客户端掉线。这里计数很简单,只是考虑一个客户端(心跳只考虑连接一个的情况,服务器是支持多个客户端连入哦,多个心跳我以后有时间会完善,现在做的项目只允许连接一个不允许连接多个),如果多个心跳可以创建一个Map类去进行操作。
大家注意,定时器要使用System.Timers.Timer类而不使用DispatcherTimer,开始我使用DispatcherTimer,客户端没问题,服务器端就有问题了,原因是
DispatcherTimer是单线程定时器!System.Timers.Timer是多线程定时器!!!我这边是多线程,所以要用System.Timers.Timer。后面客户端也改为多线程了,所以也用System.Timers.Timer来做定时任务。
定时任务的用法如下:
System.Timers.Timer timerHeartBeat;
timerHeartBeat = new System.Timers.Timer(1000);//设置定时任务间隔时长1000ms
timerHeartBeat.Elapsed += new System.Timers.ElapsedEventHandler([定时任务执行的方法]);
timerHeartBeat.AutoReset = true;//是否一直循环运行下去,true为是
timerHeartBeat.Enabled = true;//启动定时任务
timerHeartBeat.Stop();//停止定时任务
多线程要更改UI主线程,需要使用InvokeAsync方法,比如textblock控件要使用如下方法:
textBlock1.Dispatcher.InvokeAsync(() => {
//Do What You Want To Do
});
在点击开始按钮后,进行一些初始化操作,并且开了一个线程去监听客户端
Thread myThread = new Thread(ListenClientConnect);
myThread.IsBackground = true;
myThread.Start();
ThreadServer.cs要继承ICallBackListener接口(这个接口是上面CC类提供的),这样当客户端发送消息或心跳的时候就可以在ThreadServer中触发并处理了:
public void onCallBack(string message) {
if (message.Equals("heart")) {
heartCount=0;
Console.WriteLine("heartCount++ -> "+heartCount);
return;
}
AddInfo(message);
}
我们在这里判断如果是心跳就让heartCount清0。
原理上面已经说明。这样服务器端的代码已经写完。完整的客户端服务器代码我会在下一篇博客中给出。
先给个服务器端截图吧: