您的位置:首页 > 理论基础 > 计算机网络

C# TCP多线程服务器示例

2017-03-07 11:35 204 查看

前言

之前一直很少接触多线程这块。这次项目中刚好用到了网络编程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>
/// 转换成十六进制数
<
bdf0
/span>/// </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 

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: