如何开发TCP调试助手,记录详尽的调试开发笔记?

摘要:0 动图: 1 先利用VS自带的socket类来写好TCP_CORE: 类目录如下: 点击查看TCP_CORE class的完整代码 using System; using System.Collections.Generic; using
0 动图: 1 先利用VS自带的socket类来写好TCP_CORE: 类目录如下: 点击查看TCP_CORE class的完整代码 using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows.Forms; namespace WindowsFormsApp1 { //【01】声明委托 public delegate void SetListBoxDelegate(string str); class TCP_Core { //【02】创建委托对象 public SetListBoxDelegate SetLibxBoxDelegate; private Socket _socket;//定义私有字段存放soket句柄 private string IP; private int port; private int receiveCount = 0; private int sendCount = 0; private Boolean isConnected = false; public Boolean IsConnected { get { return isConnected; } set { isConnected = value; } } public int SendCount { get { return sendCount; } } public int RecieveCount { get { return receiveCount; } } public void ResetCount( ) { sendCount = 0; receiveCount = 0; } public Socket Socket {//提供给外部访问的属性 get { return _socket; } set { _socket = value; } } private void GetIP_PortByParameter( string par ) {//从参数获取到IP和port string st = par.Trim( ); string[] sArray = st.Split(':');// 一定是单引 IP = sArray[0]; port =Convert.ToInt32(sArray[1]); } public int TCP_Open( string par ) { try { Socket client_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); GetIP_PortByParameter(par); IPAddress ipAdress = IPAddress.Parse(IP); //网络端点:为待请求连接的IP地址和端口号 IPEndPoint ipEndpoint = new IPEndPoint(ipAdress, port); //connect()向服务端发出连接请求。客户端不需要bind()绑定ip和端口号, //因为系统会自动生成一个随机的地址(具体应该为本机IP+随机端口号) client_socket.Connect(ipEndpoint); _socket = client_socket; isConnected = true; return 0; } catch (Exception) { _socket = null; return -1; } } public int TCP_Send( Socket sk,string sd ) { try { if (isConnected) { sk.Send(Encoding.UTF8.GetBytes(sd)); sendCount += sd.Length; return 0; } return -1; } catch (Exception) { return -1; } } public enum EndChar { None=0, OD=1, OA=2, ODOA=3, } public int TCP_Read( Socket sk, string match, EndChar endChar, int timeout_ms,out string str) { str = "NullYK"; if (isConnected) { Stopwatch stopwatch = new Stopwatch( ); string recvStr = ""; byte[] recvBytes = new byte[1024]; int bytes; stopwatch.Start( ); sk.ReceiveTimeout = timeout_ms; while (true) { try { bytes = sk.Receive(recvBytes, recvBytes.Length, SocketFlags.None);//从客户端接受信息 recvStr += Encoding.ASCII.GetString(recvBytes, 0, bytes); receiveCount += bytes; if (recvStr.Length != 0 ) { SetListBox(recvStr); break; } else if (stopwatch.ElapsedMilliseconds > timeout_ms);//超时退出while;) { break; } } catch (Exception) { break; } } if (match != null) { if (recvStr.Contains(match)) { str = recvStr; return 0; } } else//没有match标志就判断结束符; { switch (endChar) { case EndChar.None: break; case EndChar.OD: if (recvStr.Contains("\r")) { str = recvStr; return 0; } break; case EndChar.OA: if (recvStr.Contains("\n")) { str = recvStr; return 0; } break; case EndChar.ODOA: if (recvStr.Contains("\r\n")) { str = recvStr; return 0; } break; default: break; } } str = recvStr; } return 0; } private void SetListBox(string dataREC ) { if (dataREC.Length > 0) { String str = $"{CommonTool.GetShortTimeMillisecond( )}←⯁{dataREC}"; SetLibxBoxDelegate?.Invoke(str);//【执行委托】 } } public int TCP_Close( Socket sk ) { if (isConnected) { sk.Close( ); sk.Dispose( ); } return 0; } } } 说明下:这个委托的目的,用来在此类中能操作触发对界面控件的刷新。 2 Form1的编程: 点击查看Form1代码 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.Drawing; using System.Linq; using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace WindowsFormsApp1 { public partial class Form1 : Form { Form2 form2=null; readonly TCP_Core TCP_Core1 = new TCP_Core( ); readonly CommonTool commonTool = new CommonTool(); private string dataREC = ""; const int Intervaltime = 2000;//读取中断时间 public Boolean AppIsRun = false; /// <summary> /// 具体刷新Listbox的函数 /// </summary> /// <param name="str"></param> /// 定义一个委托(delegate),委托(delegate)可以将参数与方法传递给控件所在的线程,并由控件所在的线程执行,通过Invoke来调用,这样可以完美的解决此类问题。 public void RefreshLisBox(String str ) {//【03】委托函数(匿名委托),否则独立线程中试图刷新界面,会报错 Action action = ( ) => { listBox1.Items.Add(str); ScrollListBox(listBox1); toolStripStatusLabelSendcount.Text = TCP_Core1.SendCount.ToString( ); toolStripStatusLabelReceive.Text = TCP_Core1.RecieveCount.ToString( ); //更新数据到Form2的dataGridView控件上 if (form2!=null) { form2.newString = str; form2.SetNewString( ); } }; Invoke(action); } public Form1( ) { InitializeComponent( ); //【04】委托绑定 TCP_Core1.SetLibxBoxDelegate+= RefreshLisBox; } private void button1_Click( object sender, EventArgs e ) { if (TCP_Core1.TCP_Open(textBox1.Text) == 0) { listBox1.Items.Add($"Connect [{textBox1.Text}] Successfully."); btn_send.Enabled = true; btn_close.Enabled = true; this.toolStripStatusLabel0.Text = "Ready"; //Task task = new Task(TaskReadLoop); //task.Start( ); Thread thread1 = new Thread(TaskReadLoop);//独立线程运行TCP接受函数,此函数内部有 执行委托TCP_Core1.SetLibxBoxDelegate 动作 thread1.Start( ); timer1.Start( ); } else { listBox1.Items.Add("Connect Failed."); this.toolStripStatusLabel0.Text = "Error"; timer1.Stop( ); } } private void btnSend_Click( object sender, EventArgs e ) { string data = $"{CommonTool.GetShortTimeMillisecond( )}→⟐{textBox2.Text}"; listBox1.Items.Add(data); TCP_Core1.TCP_Send(TCP_Core1.Socket,textBox2.Text ); ScrollListBox( listBox1); if (form2!=null) { form2.newString = data; form2.SetNewString( ); } } private void button3_Click( object sender, EventArgs e ) { timer1.Stop( ); btn_open.Enabled = true; btn_send.Enabled = false; btn_close.Enabled = false; TCP_Core1.IsConnected = false; TCP_Core1.TCP_Close( TCP_Core1.Socket); } private void Form1_Load( object sender, EventArgs e ) { textBox1.Text = "127.0.0.1:7200"; textBox2.Text = "yk test TCP CORE"; tableLayoutPanel1.Dock = DockStyle.Fill; timer1.Interval =100; this.Text = "TCP Debug Tool"; label1.Text = "Log:"; label2.Text = "IP:port"; label3.Text = "Send:"; listBox1.Items.Clear( ); listBox1.GetType( ).GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).SetValue(listBox1, true, null); btn_open.Enabled = true; btn_send.Enabled = false; btn_close.Enabled = false; this.toolStripStatusLabel0.Text = "Please Open a connection"; AppIsRun = true; } private void ScrollListBox(ListBox listBox ) {//在添加新记录前,先计算滚动条是否在底部,从而决定添加后是否自动滚动。 // 既可以在需要时实现自动滚动,又不会在频繁添加记录时干扰用户对滚动条的控制。 int ctl_rows =Convert.ToInt32( listBox1.Height / this.listBox1.ItemHeight); if (listBox1.Items.Count > (ctl_rows - 1)){ listBox1.TopIndex = listBox1.Items.Count - ctl_rows+3; } else { listBox1.TopIndex = 0; } } public void TaskReadLoop( ) { while (AppIsRun) { TCP_Core1.TCP_Read(TCP_Core1.Socket, null, TCP_Core.EndChar.ODOA, Intervaltime, out dataREC); } } private void timer1_Tick( object sender, EventArgs e ) { //在timer里面来接受数据会造成界面卡顿。 } private void btn_clear_Click( object sender, EventArgs e ) { listBox1.Items.Clear( ); if (form2!=null) { form2.CleardataGridView( ); } TCP_Core1.ResetCount( ); } private void checkBox1_CheckedChanged( object sender, EventArgs e ) { commonTool.Enable= checkBox1.Checked; } private void button1_Click_1( object sender, EventArgs e ) { } private void groupBox1_Enter( object sender, EventArgs e ) { } private void openTestWindowToolStripMenuItem_Click( object sender, EventArgs e ) { if (form2==null) { form2 = new Form2( ); form2.NewDataIn += form2.OnNewString;//[A4]挂接委托 form2.Show( ); } else { form2.Visible = true; } } private void tb_time_TextChanged( object sender, EventArgs e ) { } private void Form1_FormClosing( object sender, FormClosingEventArgs e ) { if (MessageBox.Show("Are you sure to exit?","Information",MessageBoxButtons.YesNo,MessageBoxIcon.Question)==DialogResult.Yes) { TCP_Core1.IsConnected = false; TCP_Core1.TCP_Close(TCP_Core1.Socket); AppIsRun = false; e.Cancel = false; } else { e.Cancel = true; } } } } 这里有几点重要的要说明: (1)我们定义了一个一直循环接受的方法: public void TaskReadLoop( ) { while (AppIsRun) { TCP_Core1.TCP_Read(TCP_Core1.Socket, null, TCP_Core.EndChar.ODOA, Intervaltime, out dataREC); } } AppIsRun是存储软件知否在运行的字段; (2)如果我们把上述TaskReadLoop( )直接在界面线程中运行的话,那么UI就会卡死; 所以我们得单独开辟个线程来运行: private void button1_Click( object sender, EventArgs e ) { if (TCP_Core1.TCP_Open(textBox1.Text) == 0) { listBox1.Items.Add($"Connect [{textBox1.Text}] Successfully."); btn_send.Enabled = true; btn_close.Enabled = true; this.toolStripStatusLabel0.Text = "Ready"; //Task task = new Task(TaskReadLoop); //task.Start( ); Thread thread1 = new Thread(TaskReadLoop);//独立线程运行TCP接受函数,此函数内部有 执行委托TCP_Core1.SetLibxBoxDelegate 动作 thread1.Start( ); (3)委托 TCP_Core1.SetLibxBoxDelegate+= RefreshLisBox的挂接的方法,RefreshLisBox这里要使用匿名委托,否则会报错:不可以跨线程访问Listbox控件; public void RefreshLisBox(String str ) {//【03】委托函数(匿名委托),否则独立线程中试图刷新界面,会报错 Action action = ( ) => { listBox1.Items.Add(str); ScrollListBox(listBox1); toolStripStatusLabelSendcount.Text = TCP_Core1.SendCount.ToString( ); toolStripStatusLabelReceive.Text = TCP_Core1.RecieveCount.ToString( ); //更新数据到Form2的dataGridView控件上 if (form2!=null) { form2.newString = str; form2.SetNewString( ); } }; Invoke(action); } 3 Form2的编程 由于Form1上使用的Listbox控件来显示收发数据记录,会发生闪烁,尤其是在将窗口最大化时候,更明显。尝试了很多方法,比如 开双缓存等,几乎没效果。 而Form2上我用dataGridView来显示收发数据记录,我们会发现,同样的情况下,dataGridView控件不闪烁。关于Form2有几个知识点: (1)如何将收发的数据也能同事传到Form2上的dataGridView控件? Form2的文件列表: 为了当有新的数据传过来时,有变量存储,我们首先定义了一个 字段: public string newString= ""; 然后定义了一个自定义事件—— 代码: namespace WindowsFormsApp1 { public delegate void deleUserEvent( );//【A1】声明委托类型 public partial class Form2 : Form { public event deleUserEvent NewDataIn = null;//[A2]声明Form2类的一个事件 public string newString= ""; 当有新的数据时触发事件,我们的事件处理器如下: public void OnNewString( ) {//[A3]事件处理器 dataGridView1.Rows.Add(newString); this.dataGridView1.FirstDisplayedScrollingRowIndex = dataGridView1.Rows[dataGridView1.Rows.Count - 1].Index; } 事件的挂架:在打开Form2窗口时,我们变用Form2自己的事件处理器来挂接自己的事件 private void openTestWindowToolStripMenuItem_Click( object sender, EventArgs e ) { if (form2==null) { form2 = new Form2( ); form2.NewDataIn += form2.OnNewString;//[A4]挂接委托 form2.Show( ); } else { form2.Visible = true; } } 接下来在Form1中修改如下RefreshListBox代码: public void RefreshLisBox(String str ) {//【03】委托函数(匿名委托),否则独立线程中试图刷新界面,会报错 Action action = ( ) => { listBox1.Items.Add(str); ScrollListBox(listBox1); toolStripStatusLabelSendcount.Text = TCP_Core1.SendCount.ToString( ); toolStripStatusLabelReceive.Text = TCP_Core1.RecieveCount.ToString( ); //更新数据到Form2的dataGridView控件上 if (form2!=null) { form2.newString = str; form2.SetNewString( ); } }; Invoke(action); } 其中form2.SetNewString( )的方法是为了执行委托: public void SetNewString() { NewDataIn?.Invoke( );//【A5】执行方法 } 点击查看Form2的完整代码 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace WindowsFormsApp1 { public delegate void deleUserEvent( );//【A1】声明委托类型 public partial class Form2 : Form { public event deleUserEvent NewDataIn = null;//[A2]声明Form2类的一个事件 public string newString= ""; public void SetNewString() { NewDataIn?.Invoke( );//【A5】执行方法 } public void CleardataGridView( ) { dataGridView1.Rows.Clear( ); } public void OnNewString( ) {//[A3]事件处理器 dataGridView1.Rows.Add(newString); this.dataGridView1.FirstDisplayedScrollingRowIndex = dataGridView1.Rows[dataGridView1.Rows.Count - 1].Index; } public Form2( ) { InitializeComponent( ); /// 新添加这一句调用就行了,如果有ListViews也是这样添加, /// 但要注意方法里改为有关ListViews的声明即可 dataGridView1.DoubleBufferedDataGirdView(true); this.Text = "Showed in \"dataGridView\""; } private void Form2_Load( object sender, EventArgs e ) { dataGridView1.Columns.Add( "data","Data"); dataGridView1.Columns.Add( "Note","note"); dataGridView1.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells; dataGridView1.Rows.Add( "test data show with no shake!!!!"); dataGridView1.Dock = DockStyle.Fill; } private void Form2_FormClosing( object sender, FormClosingEventArgs e ) { e.Cancel = true; this.Visible = false; } } public static class DoubleBufferDataGridView { /// <summary> /// 双缓冲,解决闪烁问题 /// </summary> public static void DoubleBufferedDataGirdView( this DataGridView dgv, bool flag ) { Type dgvType = dgv.GetType( ); PropertyInfo pi = dgvType.GetProperty("DoubleBuffered", BindingFlags.Instance | BindingFlags.NonPublic); pi.SetValue(dgv, flag, null); } } } 4 还有一个CommonTool类 这个类用来获取各种格式的时间信息,几个方法都返回不同格式的时间字符串,有一个enable属性控制是否启用; 点击查看class CommonTool代码 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace WindowsFormsApp1 { /*This class contain some unival tool like time stamp,string convertion etc.*/ class CommonTool { static bool enable=false; static string formattedDateTime = string.Empty; public bool Enable { get { return enable; } set { enable = value; } } public static string GetLongTime( ) { if (enable) { DateTime dateTime = DateTime.Now; formattedDateTime = dateTime.ToString("[yyyy-MM-dd HH:mm:ss]"); // } else { formattedDateTime = string.Empty; } return formattedDateTime; } public static string GetShortTime( ) { if (enable) { DateTime dateTime = DateTime.Now; formattedDateTime = dateTime.ToString("[HH:mm:ss]"); // } else { formattedDateTime = string.Empty; } return formattedDateTime; } public static string GetShortTimeMillisecond( ) { if (enable) { DateTime dateTime = DateTime.Now; formattedDateTime = dateTime.ToString("[HH:mm:ss.fff]"); // } else { formattedDateTime = string.Empty; } return formattedDateTime; } } }