C# TCP多线程服务器示例

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: 前言 之前一直很少接触多线程这块。这次项目中刚好用到了网络编程TCP这块,做一个服务端,需要使用到多线程,所以记录下过程。希望可以帮到自己的同时能给别人带来一点点收获~ 关于TCP的介绍就不多讲,神马经典的三次握手、四次握手,可以参考下面几篇博客学习了解: TCP三次握手扫盲 效果预览 客户端是一个门禁设备,主要是向服务端发送实时数据(200ms)。

前言

之前一直很少接触多线程这块。这次项目中刚好用到了网络编程TCP这块,做一个服务端,需要使用到多线程,所以记录下过程。希望可以帮到自己的同时能给别人带来一点点收获~

关于TCP的介绍就不多讲,神马经典的三次握手、四次握手,可以参考下面几篇博客学习了解:

TCP三次握手扫盲

效果预览

客户端是一个门禁设备,主要是向服务端发送实时数据(200ms)。服务端解析出进出人数并打印显示。

 

实现步骤

因为主要是在服务器上监听各设备的连接请求以及回应并打印出入人数,所以界面我设计成这样:

可以在窗体事件中绑定本地IP,代码如下:

       //获取本地的IP地址
            string AddressIP = string.Empty;
            foreach (IPAddress _IPAddress in Dns.GetHostEntry(Dns.GetHostName()).AddressList)
            {
                if (_IPAddress.AddressFamily.ToString() == "InterNetwork")
                {
                    AddressIP = _IPAddress.ToString();
                }
            }
            //给IP控件赋值
            txtIp.Text = AddressIP;

首先我们需要定义几个全局变量

Thread threadWatch = null; // 负责监听客户端连接请求的 线程;
Socket socketWatch = null;
Dictionary<string, Socket> dict = new Dictionary<string, Socket>();//存放套接字
Dictionary<string, Thread> dictThread = new Dictionary<string, Thread>();//存放线程

然后可以开始我们的点击事件启动服务啦

首先我们创建负责监听的套接字,用到了 System.Net.Socket 下的寻址方案AddressFamily ,然后后面跟套接字类型,最后是支持的协议。

 在Bind绑定后,我们创建了负责监听的线程。代码如下:

       // 创建负责监听的套接字,注意其中的参数;
            socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            // 获得文本框中的IP对象;
            IPAddress address = IPAddress.Parse(txtIp.Text.Trim());
            // 创建包含ip和端口号的网络节点对象;
            IPEndPoint endPoint = new IPEndPoint(address, int.Parse(txtPort.Text.Trim()));
            try
            {
                // 将负责监听的套接字绑定到唯一的ip和端口上;
                socketWatch.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
                socketWatch.Bind(endPoint);
            }
            catch (SocketException se)
            {
                MessageBox.Show("异常:" + se.Message);
                return;
            }
            // 设置监听队列的长度;
            socketWatch.Listen(10000);
            // 创建负责监听的线程;
            threadWatch = new Thread(WatchConnecting);
            threadWatch.IsBackground = true;
            threadWatch.Start();
            ShowMsg("服务器启动监听成功!");

其中 WatchConnecting方法是负责监听新客户端请求的

相信图片中注释已经很详细了,主要是监听到有客户端的连接请求后,开辟一个新线程用来接收客户端发来的数据,有一点比较重要就是在Start方法中传递了当前socket对象

     /// <summary>
        /// 监听客户端请求的方法;
        /// </summary>
        void WatchConnecting()
        {
            ShowMsg("新客户端连接成功!");
            while (true)  // 持续不断的监听客户端的连接请求;
            {
                // 开始监听客户端连接请求,Accept方法会阻断当前的线程;
                Socket sokConnection = socketWatch.Accept(); // 一旦监听到一个客户端的请求,就返回一个与该客户端通信的 套接字;
                var ssss = sokConnection.RemoteEndPoint.ToString().Split(':');
                //查找ListBox集合中是否包含此IP开头的项,找到为0,找不到为-1
                if (lbOnline.FindString(ssss[0]) >= 0)
                {
                    lbOnline.Items.Remove(sokConnection.RemoteEndPoint.ToString());
                }
                else
                {
                    lbOnline.Items.Add(sokConnection.RemoteEndPoint.ToString());
                }
                // 将与客户端连接的 套接字 对象添加到集合中;
                dict.Add(sokConnection.RemoteEndPoint.ToString(), sokConnection);
                Thread thr = new Thread(RecMsg);
                thr.IsBackground = true;
                thr.Start(sokConnection);
                dictThread.Add(sokConnection.RemoteEndPoint.ToString(), thr);  //  将新建的线程 添加 到线程的集合中去。
            }
        }

其中接收数据 RecMsg方法如下:

解释如图,一目了然,代码如下

 void RecMsg(object sokConnectionparn)
        {
            Socket sokClient = sokConnectionparn as Socket;
            while (true)
            {
                // 定义一个缓存区;
                byte[] arrMsgRec = new byte[1024];
                // 将接受到的数据存入到输入  arrMsgRec中;
                int length = -1;
                try
                {
                    length = sokClient.Receive(arrMsgRec); // 接收数据,并返回数据的长度;
                    if (length > 0)
                    {
                        //主业务

                     }
                    else
                    { 
                        // 从 通信套接字 集合中删除被中断连接的通信套接字;
                        dict.Remove(sokClient.RemoteEndPoint.ToString());
                        // 从通信线程集合中删除被中断连接的通信线程对象;
                        dictThread.Remove(sokClient.RemoteEndPoint.ToString());
                        // 从列表中移除被中断的连接IP
                        lbOnline.Items.Remove(sokClient.RemoteEndPoint.ToString());
                        ShowMsg("" + sokClient.RemoteEndPoint.ToString() + "断开连接\r\n");
                        //log.log("遇见异常"+se.Message);
                        break;
                    }
                }
                catch (SocketException se)
                {
                    // 从 通信套接字 集合中删除被中断连接的通信套接字;
                    dict.Remove(sokClient.RemoteEndPoint.ToString());
                    // 从通信线程集合中删除被中断连接的通信线程对象;
                    dictThread.Remove(sokClient.RemoteEndPoint.ToString());
                    // 从列表中移除被中断的连接IP
                    lbOnline.Items.Remove(sokClient.RemoteEndPoint.ToString());
                    ShowMsg("" + sokClient.RemoteEndPoint.ToString() + "断开,异常消息:" + se.Message + "\r\n");
                    //log.log("遇见异常"+se.Message);
                    break;
                }
                catch (Exception e)
                {
                    // 从 通信套接字 集合中删除被中断连接的通信套接字;
                    dict.Remove(sokClient.RemoteEndPoint.ToString());
                    // 从通信线程集合中删除被中断连接的通信线程对象;
                    dictThread.Remove(sokClient.RemoteEndPoint.ToString());
                    // 从列表中移除被中断的连接IP
                    lbOnline.Items.Remove(sokClient.RemoteEndPoint.ToString());
                    ShowMsg("异常消息:" + e.Message + "\r\n");
                    // log.log("遇见异常" + e.Message);
                    break;
                }
            }
        }                    

其中那个ShowMsg方法主要是在窗体中打印当前接收情况和一些异常情况,方法如下:

     void ShowMsg(string str)
        {
            if (!BPS_Help.ChangeByte(txtMsg.Text, 2000))
            {
                txtMsg.Text = "";
                txtMsg.AppendText(str + "\r\n");
            }
            else
            {
                txtMsg.AppendText(str + "\r\n");
            }

        }

其中用到了一个方法判断ChangeByte ,如果文本长度超过2000个字节,就清空再重新赋值。具体实现如下:

     /// <summary>
        /// 判断文本框混合输入长度
        /// </summary>
        /// <param name="str">要判断的字符串</param>
        /// <param name="i">长度</param>
        /// <returns></returns>
        public static bool ChangeByte(string str, int i)
        {
            byte[] b = Encoding.Default.GetBytes(str);
            int m = b.Length;
            if (m < i)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

 

 心得体会:其实整个流程并不复杂,但我遇到一个问题是,客户端每200毫秒发一次连接过来后,服务端会报一个远程主机已经强制关闭连接,开始我以为是我这边服务器线程间的问题或者是阻塞神马的,后来和客户端联调才发现问题,原来是服务器回应客户端心跳包的长度有问题,服务端定义的是1024字节,但是客户端只接受32字节的心跳包回应才会正确解析~所以,对接协议要沟通清楚,沟通清楚,沟通清楚,重要的事情说说三遍 

还有几个点值得注意

1,有时候会遇到窗体间的控件访问异常,需要这样处理

Control.CheckForIllegalCrossThreadCalls = false;

2 多线程调试比较麻烦,可以采用打印日志的方式,例如:

具体实现可以参考我的另一篇博客:点我跳转

3 ,接收解析客户端数据的时候,要注意大小端的问题,比如下面这个第9位和第8位如果解出来和实际不相符,可以把两边颠倒一下。

     public int Get_ch2In(byte[] data)
        {
            var ch2In = (data[9] << 8) | data[8];
            return ch2In;
        }

4 在接收到客户端数据的时候,有些地方要注意转换成十六进制再看结果是否正确

 public int Get_ch3In(byte[] data)
        {
            int ch3In = 0;
            for (int i = 12; i < 14; i++)
            {
                ch3In = int.Parse(ch3In + BPS_Help.HexOf(data[i]));
            }
            return ch3In;
        }

上面这个方法在对data[i]进行了十六进制的转换,转换方法如下:

     /// <summary>
        /// 转换成十六进制数
        /// </summary>
        /// <param name="AscNum"></param>
        /// <returns></returns>
        public static string HexOf(int AscNum)
        {
            string TStr;
            if (AscNum > 255)
            {
                AscNum = AscNum % 256;
            }
            TStr = AscNum.ToString("X");
            if (TStr.Length == 1)
            {
                TStr = "0" + TStr;
            }
            return TStr;
        }

5 还有个可以了解的是将数组转换成结构,参考代码如下:

 /// <summary>
        /// Byte数组转结构体
        /// </summary>
        /// <param name="bytes">byte数组</param>
        /// <param name="type">结构体类型</param>
        /// <returns>转换后的结构体</returns>
        public static object BytesToStuct(byte[] bytes, Type type)
        {
            //得到结构体的大小
            int size = Marshal.SizeOf(type);
            //byte数组长度小于结构体的大小
            if (size > bytes.Length)
            { return null; }
            IntPtr structPtr = Marshal.AllocHGlobal(size);
            Marshal.Copy(bytes, 0, structPtr, size);
            object obj = Marshal.PtrToStructure(structPtr, type);
            //释放内存空间
            Marshal.FreeHGlobal(structPtr);
            return obj;
        }

调用方法如下,注意,此处的package的结构应该和协议中客户端发送的数据结构一致才能转换

如协议中是这样的定义的话:

那在代码中就可以这样定义一个package结构体

  /// <summary>
    /// 数据包结构体
    /// </summary>
    [StructLayoutAttribute(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
    public struct Package
    {
        /// <summary>
        /// 确定为命令包的标识
        /// </summary>
        public int commandFlag;
        /// <summary>
        /// 命令
        /// </summary>
        public int command;
        /// <summary>
        ///数据长度(数据段不包括包头)
        /// </summary>
        public int dataLength;
        /// <summary>
        /// 通道编号
        /// </summary>
        public short channelNo;
        /// <summary>
        /// 块编号
        /// </summary>
        public short blockNo;
        /// <summary>
        /// 开始标记
        /// </summary>
        public int startFlag;
        /// <summary>
        /// 结束标记0x0D0A为结束符
        /// </summary>
        public int finishFlag;
        /// <summary>
        /// 校验码
        /// </summary>
        public int checksum;
        /// <summary>
        /// 保留 char数组,SizeConst表示数组个数,在转成
        /// byte数组前必须先初始化数组,再使用,初始化
        /// 的数组长度必须和SizeConst一致,例:test=new char[4];
        /// </summary>
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
        public char[] reserve;
    }

Demo下载

 TCP多线程服务器及客户端Demo

 点我跳去下载  密码:3hzs

 git一下:我要去Git 

 

收发实体对象

2017.3.11 补充

如果服务器和客户端公用一个实体类,那还好说,如果服务器和客户端分别使用结构相同但不是同一个项目下的实体类,该如何用正确的姿势收发呢?

首先简单看看效果如下:

 

 具体实现:

因为前面提到不在同一项目下,如果直接序列化和反序列化,就会反序列化失败,因为不能对不是同一命名空间下的类进行此类操作,那么解决办法可以新建一个类库Studnet,然后重新生成dll,在服务器和客户端分别引用此dll,就可以对此dll进行序列化和反序列化操作了。

项目结构如下图(这里是作为演示,将客户端和服务器放在同一解决方案下,实际上这种情况解决的就是客户端和服务器是两个单独的解决方案

客户端发送核心代码:

void showClient()
        {
            address = IPAddress.Parse("127.0.0.1");
            endpoint = new IPEndPoint(address, 5000);
            socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            try
            {
                socketClient.Connect(endpoint);
                Console.WriteLine("连接服务端成功\r\n准备发送实体Student");
                Student.Studnet_entity ms = new Student.Studnet_entity() { ID = 1, Name = "张三", Phone = "13237157517", sex = 1, Now_Time = DateTime.Now };
                using (MemoryStream memory = new MemoryStream())
                {
                    BinaryFormatter formatter = new BinaryFormatter();
                    formatter.Serialize(memory, ms);
                    Console.WriteLine("发送长度:" + memory.ToArray().Length);
                    socketClient.Send(memory.ToArray());
                    Console.WriteLine("我发送了 学生实体对象\r\n");
                }
            }
            catch (Exception)
            {

                throw;
            }
        }

服务端接收并解析实体对象核心代码

      /// <summary>  
        /// 服务端负责监听客户端发来的数据方法  
        /// </summary>  
        void RecMsg(object socketClientPara)
        {

            byte[] arrMsgRec = new byte[1024];//手动准备空间  
            Socket socketClient = socketClientPara as Socket;
            List<byte> listbyte = new List<byte>();
            while (true)
            {
                //将接受到的数据存入arrMsgRec数组,并返回真正接收到的数据长度  
                int length = socketClient.Receive(arrMsgRec);
                if (length > arrMsgRec.Length)
                {
                    listbyte.AddRange(arrMsgRec);
                }
                else
                {
                    for (int i = 0; i < length; i++)
                        listbyte.Add(arrMsgRec[i]);
                    break;
                }
            }
            //创建内存流
            using (MemoryStream m = new MemoryStream(listbyte.ToArray()))
            {
                //创建以二进制格式对对象进行序列化和反序列化
                BinaryFormatter bf = new BinaryFormatter();
                Console.WriteLine("m.length" + m.ToArray().Length);
                //反序列化
                object dataObj = bf.Deserialize(m);
                //得到解析后的实体对象
                Student.Studnet_entity dt = dataObj as Studnet_entity;
                Console.WriteLine("接收客户端长度:" + listbyte.Count + " 反序列化结果是:ID:" + dt.ID +
                    " 姓名:" + dt.Name + " 当前时间:" + dt.Now_Time);

            }
        }

收发实体对象Demo

点我前去下载Demo   密码:x2ke

 

  • 感谢你的阅读。如果你觉得这篇文章对你有帮助或者有启发,就请推荐一下吧~你的精神支持是博主强大的写作动力。欢迎转载!
  • 博主的文章没有高度、深度和广度,只是凑字数。由于博主的水平不高(其实是个菜B),不足和错误之处在所难免,希望大家能够批评指出。
  • 欢迎加入.NET 从入门到精通技术讨论群→523490820 期待你的加入
  • 不舍得打乱,就永远学不会复原。被人嘲笑的梦想,才更有实现的价值。
  • 我的博客:http://www.cnblogs.com/zhangxiaoyong/
目录
相关文章
|
2月前
|
安全 Java 调度
Java编程时多线程操作单核服务器可以不加锁吗?
Java编程时多线程操作单核服务器可以不加锁吗?
46 2
|
1月前
|
网络协议 网络性能优化 C#
C# 一分钟浅谈:UDP 与 TCP 协议区别
【10月更文挑战第8天】在网络编程中,传输层协议的选择对应用程序的性能和可靠性至关重要。本文介绍了 TCP 和 UDP 两种常用协议的基础概念、区别及应用场景,并通过 C# 代码示例详细说明了如何处理常见的问题和易错点。TCP 适用于需要可靠传输和顺序保证的场景,而 UDP 适用于对延迟敏感且可以容忍一定数据丢失的实时应用。
37 1
|
1月前
|
网络协议 Java API
【网络】TCP回显服务器和客户端的构造,以及相关bug解决方法
【网络】TCP回显服务器和客户端的构造,以及相关bug解决方法
62 2
|
1月前
|
存储 网络协议 Java
【网络】UDP和TCP之间的差别和回显服务器
【网络】UDP和TCP之间的差别和回显服务器
65 1
|
1月前
|
网络协议 Python
Python创建一个TCP服务器
Python创建一个TCP服务器
|
2月前
|
安全 数据库连接 API
C#一分钟浅谈:多线程编程入门
在现代软件开发中,多线程编程对于提升程序响应性和执行效率至关重要。本文从基础概念入手,详细探讨了C#中的多线程技术,包括线程创建、管理及常见问题的解决策略,如线程安全、死锁和资源泄露等,并通过具体示例帮助读者理解和应用这些技巧,适合初学者快速掌握C#多线程编程。
81 0
|
2月前
|
网络协议 数据处理 C语言
利用C语言基于poll实现TCP回声服务器的多路复用模型
此代码仅为示例,展示了如何基于 `poll`实现多路复用的TCP回声服务器的基本框架。在实际应用中,你可能需要对其进行扩展或修改,以满足具体的需求。
85 0
|
6月前
|
开发框架 前端开发 .NET
C#编程与Web开发
【4月更文挑战第21天】本文探讨了C#在Web开发中的应用,包括使用ASP.NET框架、MVC模式、Web API和Entity Framework。C#作为.NET框架的主要语言,结合这些工具,能创建动态、高效的Web应用。实际案例涉及企业级应用、电子商务和社交媒体平台。尽管面临竞争和挑战,但C#在Web开发领域的前景将持续拓展。
202 3
|
6月前
|
SQL 开发框架 安全
C#编程与多线程处理
【4月更文挑战第21天】探索C#多线程处理,提升程序性能与响应性。了解C#中的Thread、Task类及Async/Await关键字,掌握线程同步与安全,实践并发计算、网络服务及UI优化。跟随未来发展趋势,利用C#打造高效应用。
199 3
下一篇
无影云桌面