上篇.net平台下C#socket通信(上)介绍了socket通信的基本原理及最基本的通信方式。本文在此基础上就socket通信时经常遇到的问题做一个简单总结,都是项目中的一些小问题,拿来此处便于下次使用,同时对在使用socket时出现些许问题的同仁们多一个粗浅建议。不足之处请提出,谢谢。
本文主要讲述:
1、正常通信中握手建立
2、一对多的通信
3、发送接收数据格式转换
4、资源释放
5、开启并保持服务监听
1、握手建立正常的通信通道
项目需要通信的双方(假设是一个上位机、一个下位机)之间需要建立一个稳定的通道,以便进行通信。本项目中具体操作是:上位机作为服务器,下位机作为客户端,同时制定通信协议。上位机首先打开监听等待建立通道,下位机主动连接上位机后发送连接成功的信息到上位机,上位机根据通信协议发送数据到下位机,此时通道已经建立。但为了保险起见(同时遵循三次握手),客户端再次发送数据到上位机告知通道建立完毕。
2、一对多通信
项目需求是一个上位机多个下位机,这就确定了上位机做为服务器端,下位机作为客户端主动连接服务器。一对一通信时只有一个socket通道,因此无论是上位机还是下位机在发送和接收数据的时候都不会存在数据乱发乱收的情况。一对多意味着上位机和下位机会建立起多个通道,因此在发送数据时需要记录哪一个下位机处于哪个socket通道中,以便进行逻辑处理。本文处理一对多通信的过程是:
1)首先建立一个对话类Session:
public class Session { public Socket ClientSocket { get; set; }//客户端的socket public string IP;//客户端的ip public Session(Socket clientSocket) { this.ClientSocket = clientSocket; this.IP = GetIPString(); } public string GetIPString() { string result = ((IPEndPoint)ClientSocket.RemoteEndPoint).Address.ToString(); return result; } }
2)在服务端socket监听时:
IPEndPoint loaclEndPoint = new IPEndPoint(IPAddress.Any, Port); SocketLister = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); SocketLister.Bind(loaclEndPoint); try { SocketLister.Listen(MaxConnection); while (IsRunning) { ClientSocket = SocketLister.Accept(); //保存socket Session newSession = new Session(ClientSocket); lock (sessionLock) { sessionTable.Add(newSession.IP, newSession); } SocketConnection socketConnection = new SocketConnection(ClientSocket); socketConnection.ReceiveDatagram();//接收数据 } } catch (SocketException ex) { }
//声明
public Hashtable sessionTable = new Hashtable ();//包括客户端会话
private object sessionLock = new object();
为了便于理解,把整个服务端socket的建立都写在上面。
3)发送数据到不同的客户端
Hashtable ht = serverSocket.sessionTable; foreach (Session session in ht.Values) { if (session.IP == "127.0.0.1")//example { SocketConnection socketConnection = new SocketConnection(session.ClientSocket); string str = "C300010002D2"; byte[] sendBytes = StrToHexByte(str); socketConnection.Send(sendBytes); } }
SocketConnection类已经被使用多次,写在下面:
3、处理需要发送和接收到的数据
项目需要是上位机获取数据进行逻辑处理,然后通过tcp/ip协议发送给下位机,下位机在接收到数据的同时发送确认信息到上位机。项目过程中,上位机在开发过程中需要调试其发送数据、接收数据是否成功,此处借助于USR- TCP232小工具。但是涉及到一个问题,下位机发送和接收都是byte字节数组,那么开发的上位机应该如何发送和接收数据?在.net平台下C#socket通信(上),有服务器端的发送和接收函数,发送数据时将要发送的字符串转换为byte数组,接收时再将字节数组转换为16进制字符串。如下:
//字节数组转换为16进制字符串 public string ByteToHexStr(byte[] bytes) { string str = ""; if (bytes != null) { for (int i = 0; i < bytes.Length; i++) { str += bytes[i].ToString("X2"); } } return str; } //字符串转换为16进制byte数组 public byte[] StrToHexByte(string data) { data = data.Replace(" ", ""); if ((data.Length % 2) != 0) { data += " "; } byte[] bytes = new byte[data.Length / 2]; for (int i = 0; i < bytes.Length; i++) { bytes[i] = Convert .ToByte (data.Substring (i * 2,2),16); } return bytes; }
4、资源释放
开发项目使用平台是.net,工具vs2010,语言是C#,因为.net有垃圾回收机制,因此在实际开发中产生的托管资源都是系统自动释放完成。在做本项目时采用winform进行开发的,在此过程中发现一个问题:在关闭Form窗体是运行的系统并没有完全关闭。查找原因,应该是有资源没有被释放。而socket套接字产生的资源恰好是非托管资源,此现象表明系统中有socket资源没有被完全释放掉。因此写了一个资源释放函数:
public void Dispose() { try { this.ClientSocket.Shutdown(SocketShutdown.Both); this.ClientSocket.Dispose(); this.ClientSocket.Close(); this.ClientSocket = null; } catch { } }
上述函数的功能就是释放socket所产生的资源,调用后发现还是存在此问题,几经调试发现虽然把产生socket通道的监听客户端资源释放完毕,服务器端的serversocket并没有被释放,于是有了下一个函数:
public void CloseSocket() { if (serverSocket != null) { serverSocket.SocketLister.Dispose(); serverSocket.SocketLister = null; serverSocket.Dispose();//调用的上一个函数 serverSocket = null; } }
在上述函数完成后,套接字socket所产生的资源确实被释放完毕,系统在form关闭后能真正关闭。到此资源好像已经被释放掉,但紧接着新的问题产生了:
在什么时候什么地方调用释放资源的函数?
个人简单看法:
1)系统中止时调用
2)socket通道中断时调用
补充:
5、开启并保持服务监听
在socket通信中,服务端的socket监听其实是需要一直打开并且保持的,只有这样才能随时监听连接的客户端。项目中示例:
private void button1_Click(object sender, RoutedEventArgs e) { Thread thread = new Thread(new ThreadStart(StartServer)); thread.Start(); } public void StartServer() { int port = Convert.ToInt32(GetText(this.tbPort)); string ipStr = GetText (this.tbServerIPStr); if (serverSocket == null) { serverSocket = new ServerSocket(port); serverSocket.Start(ipStr);// } else { MessageBox.Show("监听已开启"); } } public void Start(string ipStr) { IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, Port); //IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Parse(ipStr), Port); SocketLister = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); SocketLister.Bind(localEndPoint); try { SocketLister.Listen(MaxConnection); while (IsRunning) { ClientSocket = SocketLister.Accept(); //保存socket Session newSession = new Session(ClientSocket); lock (sessionLock) { sessionTable.Add(newSession.IP, newSession); } SocketConnection socketConnection = new SocketConnection(ClientSocket); socketConnection.ReceiveDatagram(); } } catch (SocketException ex) { } }
解释:点击按钮开启新的线程thread,执行方法StartServer,StartServer调用方法Start,Start方法中使用死循环开启并保持监听。