c#网络编程之TCP/IP(二)Thread方式服务器端编写

版权声明:本文为博主原创文章,未经博主允许不得转载。 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类
在new一个User时初始化了br,bw,client,如果是以任务形式则Task.Run收消息任务,线程形式则开启一个线程执行从客户端接收消息的方法
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监听器
我们new一个CC类的时候会初始化一些网络操作。
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。
原理上面已经说明。这样服务器端的代码已经写完。完整的客户端服务器代码我会在下一篇博客中给出。
先给个服务器端截图吧:
服务器端截图

猜你喜欢

转载自blog.csdn.net/specialshoot/article/details/51382576