C# 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();
// 创建负责监听的线程;
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[]) >= )
{
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[];
// 将接受到的数据存入到输入 arrMsgRec中;
int length = -;
try
{
length = sokClient.Receive(arrMsgRec); // 接收数据,并返回数据的长度;
if (length > )
{
//主业务 }
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, ))
{
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[] << ) | data[];
return ch2In;
}
4 在接收到客户端数据的时候,有些地方要注意转换成十六进制再看结果是否正确
public int Get_ch3In(byte[] data)
{
int ch3In = ;
for (int i = ; i < ; 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 > )
{
AscNum = AscNum % ;
}
TStr = AscNum.ToString("X");
if (TStr.Length == )
{
TStr = "" + 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, , structPtr, size);
object obj = Marshal.PtrToStructure(structPtr, type);
//释放内存空间
Marshal.FreeHGlobal(structPtr);
return obj;
}
调用方法如下,注意,此处的package的结构应该和协议中客户端发送的数据结构一致才能转换
如协议中是这样的定义的话:
那在代码中就可以这样定义一个package结构体
/// <summary>
/// 数据包结构体
/// </summary>
[StructLayoutAttribute(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = )]
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 = )]
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, );
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 = , Name = "张三", Phone = "", sex = , 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[];//手动准备空间
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 = ; 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
C# TCP多线程服务器示例的更多相关文章
- c#tcp多线程服务器实例代码
using System;using System.Collections.Generic;using System.ComponentModel;using System.Data;using Sy ...
- UDP和多线程服务器
UDP: UDP是数据报文传输协议,这个传输协议比较野蛮,发送端不需要理会接收端是否存在,直接就发送数据,不会像TCP协议一样建立连接.如果接收端不存在的话,发送的数据就会丢失,UDP协议不会去理会数 ...
- UNIX网络编程 第5章 TCP客户/服务器程序示例
UNIX网络编程 第5章 TCP客户/服务器程序示例
- TCP粘包/拆包 ByteBuf和channel 如果没有Netty? 传统的多线程服务器,这个也是Apache处理请求的模式
通俗地讲,Netty 能做什么? - 知乎 https://www.zhihu.com/question/24322387 谢邀.netty是一套在java NIO的基础上封装的便于用户开发网络应用程 ...
- 基于事件的 NIO 多线程服务器--转载
JDK1.4 的 NIO 有效解决了原有流式 IO 存在的线程开销的问题,在 NIO 中使用多线程,主要目的已不是为了应对每个客户端请求而分配独立的服务线程,而是通过多线程充分使用用多个 CPU 的处 ...
- TCP多线程聊天室
TCP协议,一个服务器(ServerSocket)只服务于一个客户端(Socket),那么可以通过ServerSocket+Thread的方式,实现一个服务器服务于多个客户端. 多线程服务器实现原理— ...
- 华为AR配置内部服务器示例(只有1个公网IP)
AR配置公网和私网用户都可以通过公网地址访问内部服务器示例(只有1个公网IP) 适用于:V200R003C01及以后的系统软件版本. 组网需求: 由于只有1个公网IP(100.100.1.2),想实现 ...
- Java如何创建多线程服务器?
在Java编程中,如何创建多线程服务器? 以下示例演示如何使用ServerSocket类的MultiThreadServer(socketname)方法和Socket类的ssock.accept()方 ...
- 用Java实现多线程服务器程序
一.Java中的服务器程序与多线程 在Java之前,没有一种主流编程语言能够提供对高级网络编程的固有支持.在其他语言环境中,实现网络程序往往需要深入依赖于操作平台的网络API的技术中去,而Java提供 ...
随机推荐
- Angular - - $sce 和 $sceDelegate
$sce $sce 服务是AngularJs提供的一种严格上下文转义服务. 严格的上下文转义服务 严格的上下文转义(SCE)是一种需要在一定的语境中导致AngularJS绑定值被标记为安全使用语境的模 ...
- java me 旋转的X案例
package com.xushouwei.cn; import javax.microedition.lcdui.Command;import javax.microedition.lcdui.Co ...
- 在Unity3D中实现安卓平台的本地通知推送
[前言] 对于手游来说,什么时候需要推送呢?玩过一些带体力限制的游戏就会发现,我的体力在恢复满后,手机会收到一个通知告诉我体力已完全恢复了.这类通知通常是由本地的客户端发起的,没有经过服务端. 在安卓 ...
- Delphi中点击DBGrid某一行获得其详细数据方法
http://www.cnblogs.com/leewiki/archive/2011/12/16/2290172.html Delphi中点击DBGrid某一行获得其详细数据方法 前提是用ADOTa ...
- EhLib DBGridEh组件在Delphi中应用全攻略总结(转)
EhLib DBGridEh组件在Delphi中应用全攻略总结(转) http://blog.sina.com.cn/s/blog_94b1b40001013xn0.html 优化SQL查询:如何写出 ...
- 决策树ID3算法
决策树 (Decision Tree)是在已知各种情况发生概率的基础上,通过构成 决策树 来求取净现值的期望值大于等于零的概率,评价项目风险,判断其可行性的决策分析方法,是直观运用概率分析的一种图解法 ...
- 使用ActionBar实现Tab导航
ActionBar还有常用的功能,实现Tab导航.ActionBar在顶端生成多个Tab标签,当用户单击点击某个Tab标签时,系统根据用户点击事件导航指定Tab页面. 为了使用ActionBar实现T ...
- Cocos2d-x 多分辨率支持
最近遇到多分辨率支持问题,所以查了一些资料.将一些收获共享一下,以便自己和其他需要的朋友日后参考. 如果我要建立一个cocos2d-x项目,我的目标是支持iphone3G( 480, 320 ),ip ...
- 深入React事件系统(React点击空白部分隐藏弹出层;React阻止事件冒泡失效)
只关注括号内问题的同学,可直接跳转到蓝字部分.(标题起的有点大,其实只讨论一个问题) 两个在React组件上绑定的事件,产生冲突后,使用e.stopPropagation(),阻止冒泡,即可防止事件冲 ...
- 在C++中反射调用.NET(三)
在.NET与C++之间传输集合数据 上一篇<在C++中反射调用.NET(二)>中,我们尝试了反射调用一个返回DTO对象的.NET方法,今天来看看如何在.NET与C++之间传输集合数据. 使 ...