(16)C#传智:线程,Socket网络编程,模式窗体与非模式窗体(第16天)

一、复习


    进程与线程的关系

    Process.Start()
    p.StartInfo = new ProcessStartInfo(@"E:\1.txt");
    Control.CheckForIllegalCrossThreadCalls = false;
    Thread t = new Thread(Test);
    t.IsBackground = true;//设置为后台线程
    t.Start();
    t.Abort();


    
    练习:导入wav歌曲,双击播放,上/下一曲播放。
    界面:

 


    
    代码:

    private List<string> lstSongs = new List<string>();
    private SoundPlayer sp = new SoundPlayer();

    private void Form1_Load(object sender, EventArgs e)
    {
        btnNext.Enabled = false;
        btnPre.Enabled = false;
    }

    private void btnOpen_Click(object sender, EventArgs e)
    {
        OpenFileDialog ofd = new OpenFileDialog();
        ofd.InitialDirectory = @"E:\";
        ofd.Filter = "音乐文件|*.wav";
        ofd.Multiselect = true;
        if (ofd.ShowDialog() == DialogResult.OK)
        {
            string[] s = ofd.FileNames;
            for (int i = 0; i < s.Length; i++)
            {
                listBox1.Items.Add(s[i]);
                lstSongs.Add(s[i]);
            }
            btnNext.Enabled = true;//有音乐激活
            btnPre.Enabled = true;
        }
    }

    private void listBox1_DoubleClick(object sender, EventArgs e)
    {
        if (listBox1.SelectedIndex == -1) return;//无音乐或未选择
        sp.SoundLocation = lstSongs[listBox1.SelectedIndex];
        sp.Play();
    }

    private void btnNext_Click(object sender, EventArgs e)
    {
        if (listBox1.SelectedIndex == -1) listBox1.SelectedIndex = 0;
        int n = listBox1.SelectedIndex;
        if (++n == listBox1.Items.Count) n = 0;
        listBox1.SelectedIndex = n;
        sp.SoundLocation = lstSongs[n];
        sp.Play();
    }

    private void btnPre_Click(object sender, EventArgs e)
    {
        if (listBox1.SelectedIndex == -1) listBox1.SelectedIndex = 0;
        int n = listBox1.SelectedIndex;
        if (--n == -1) n = listBox1.Items.Count - 1;
        listBox1.SelectedIndex = n;
        sp.SoundLocation = lstSongs[n];
        sp.Play();
    }


    
    
二、继续Thread


    线程调用带参数的方法
    
    如果线程执行的方法需要参数,那么要求这个参数必须是object类型.
    
    参数的传入,在th.Start(params)中进入带参。到了真正的执行方法体内
    可以由object转换成真正需要的类型。

    
    注意下面代码,参数类型是object,进入方法体后才转换成需要类型
    (忽略交叉线程检查)

    private void button1_Click(object sender, EventArgs e)
    {
        Thread t = new Thread(Test);
        t.IsBackground = true;
        t.Start(1234);
    }

    private void Test(object obj)
    {
        int n = (int)obj;
        for (int i = 0; i < n; i++)
        {
            textBox1.Text = i.ToString();
        }
    }


    
    线程可以访问全局变量。
    
    练习:摇奖。通过设置全局变量,来控制线程中的运行情况。
    
    界面:

 


    
    代码:

    private bool b = false;

    private void Form1_Load(object sender, EventArgs e)
    {
        Control.CheckForIllegalCrossThreadCalls = false;
    }

    private void button1_Click(object sender, EventArgs e)
    {
        if (b == false)
        {
            b = true;
            button1.Text = "停止";
            Thread th = new Thread(ShowNum);
            th.IsBackground = true;
            th.Start();
        }
        else
        {
            b = false;//与th无关,只与全局赋值相关,线程可访问全局
            button1.Text = "开始";
        }
    }

    private void ShowNum()
    {
        Random r = new Random();
        while (b) //由全局变量来控制变化
        {
            label1.Text = r.Next(0, 10).ToString();
            label2.Text = r.Next(0, 10).ToString();
            label3.Text = r.Next(0, 10).ToString();
        }
    }


    

    
三、Socket编程


    成都的张三,要与北京的李四进行联系,通常用电话进行信息交流。
    同样,两台电脑的程序进行信息交换,就是通过Socket来通信,相当于"电话".
    
    张三与李四的交流,规定好语言,比如都用中文,普通话。
    同样,电脑与电脑进行通信,也需要规定好语言,就是“协议”:UDP与TCP.
    哪怕是土匪交流也必须用双方规定识别的“黑话”。
    
    socket:孔或插座。作为进程通信机制,取后面"插座"之意,通常也称作"套接字",
        用于描述IP地址和端口,是一个通信链的句柄.(其实就是两个程序通信用的)
    
    socket非常类似于电话的插座。以一个电话网为例:电话的通话双方相当于相互通
        信的2个程序,电话号码就是IP地址。
        任何用户在通话之前,首先要占有一部电话机,相当于申请一个socket,同时
        要知道对方的号码,相当于对方有一个固定的socket.
        然后向对方拨号呼叫,相当于发出连接请求。对方假如在场并空闲,拿起电话
        话筒,双方就可以正式通话,相当于连接成功。双方通话的过程,是一方向电话
        机发出信号和对方从电话机接收信号的过程,相当于向socket发送数据和从socket
        接收数据。
        通话结束后,一方挂起电话机相当于关闭socket,撤消连接。
    
    端口:
    socket描述了IP与端口。IP指明了网络中的具体的电脑,端口号代表了电脑中具体的
        某一个程序。

    Internet上有很多主机,主机内运行多个服务软件,同时提供多种服务。每种服务都
    打开了一个socket,并绑定到一个端口上,不同的端口对应于不同的服务(应用程序)
    例如:http使用80端口,ftp使用21端口,smtp使用25端口
        
    协议:有两种类型TCP与UDP:50000
    1)流式socket(stream):是一种面向连接的socket,针对于面向连接的Tcp服务应用,
        安全,但是效率低。(连接前有三次握手)
    2)数据报式socket(DATAGRAM):是一种无连接的socket,对应于无连接的UDP服务
        应用,不安全(丢失,顺序混乱,在接收端要分析重排及要求重发)但是它的
        效率高。
    
    
    socket一般应用模式(c/s)
        1)服务端welcoming socket开始监听端口(负责监听客户端连接信息);
        2)客户端client socket连接服务端指定端口(负责接收和发送服务端消息);
        3)服务端welcoming socket监听到客户端连接,创建connection socket.
            (负责和客户端通信)
        
    操作流程:

 


    大体过程:
    服务端创建套接字对象,绑定网络终结点,然后循环监听。如果有消息来了,创建一个连接的
    Socket对象用于接收或发送(字节数组 )。
    客服端创建套接字对象,发起连接,连接成功后的对象用于发送或接收服务端的数据(字节数组)
    
    注意的是两个死循环控制:
        1)服务器监听一直循环,无消息来时阻塞中,若来消息进入接收消息。
        2)客户端连接成功后一直接收消息状态,直到有消息来时解除阻塞状态。 
        
    功能:
    服务端监听客服端文字消息,向客户端发送文字、文件、震动。
    客服端发起连接,只发出文字消息。接收服务端的文字、文件、震动。
    
    
    实现传送文件:
        由于传送中都是字节数组buffer,无法区别传送过来的是文字消息还是一个文件。为了这
    个目的。我们自己规定一个约定0表示消息,1表示文件。
        但协议已定,我们就得设计“协议”,伪造成正规协议。设计协议:
        把要传递的字节数组前面都增加一个字节作为标识。以此判断。0即文字,1即文件。
        数组就变成了: 文字:0+文字(字节数组表示)
                       文件:1+文件的二进制信息。
        这个协议对于程序双方都遵守,就能很好地识别。如果放的是2表示是震动...等等.
    因此,问题就变成了在原buffer字节数组前面增加一个元素。使长度buffer.length+1.
    但因数组长度不可变,就得另声明一数组使其长度增加1,后面的复制原buffer即可。这是可
    行的,但变得麻烦。
        有一种简单的就是可变长度的数组-->集合:List<byte>就可以解决上面的问题。另外就是
    在发送时前面增加了一字节,那么在接收时,应该不要前面那一字节。
        另外一个就是大文件时,一次发送不完,就需要用断点续传,到了接收端还需要将各小
    文件进行组装,应该里面涉及多个线程调配,接收端根据标志位进行组装,包括还差的部分
    再向另一端请求,好像很复杂。        
        
    
    服务端界面:

 


    
    代码:
    创建全局变量,用于监听,发送,接收。
    创建字典,每一个连接对应一个socket对象,并对应显示到cboUser控件上

    private Socket socketSend;//监听到时创建的新连接,回复消息要用。
    private Dictionary<string, Socket> dicSocket = new Dictionary<string, Socket>();


    
    创建对象,开始监听,新开后台线程,循环监听

    private void btnStart_Click(object sender, EventArgs e)
    {
        //监听时,服务器端创建一个负责监听IP与端口的Socket
        //使用指定的地址族、套接字类型和协议初始化 Socket 类的新实例。
        Socket socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        IPAddress ip = IPAddress.Any;//提供一个IP地址,指示服务器必须侦听所有网络接口上的客户端活动。 此字段为只读。
        //IPAddress ip = IPAddress.Parse(txtServer.Text);
        //网络终结点:用指定的地址和端口号初始化 IPEndPoint 类的新实例。
        IPEndPoint point = new IPEndPoint(ip, Convert.ToInt32(txtPorts.Text));
        socketWatch.Bind(point);//使 Socket 与一个本地终结点相关联
        ShowLog("监听成功");
        socketWatch.Listen(10);//将 Socket 置于侦听状态。允许侦听上限10个
        Thread th = new Thread(Listen);
        th.IsBackground = true;
        th.Start(socketWatch);
    }


    
    Accept将阻塞,直到有消息来了,返回创建Socket对象.然后继续监听。
    为了接收,再开一个线程专用于接收数据,直到无消息,该线程结束。

    private void Listen(object o)//线程中监听并创建socket
    {
        Socket socketWatch = o as Socket;//类型转换,失败为null
        while (true)
        {
            socketSend = socketWatch.Accept();//为新建连接创建新的 Socket。
            ShowLog(socketSend.RemoteEndPoint.ToString() + ":连接成功");
            string str = socketSend.RemoteEndPoint.ToString();
            dicSocket[str] = socketSend;
            cboUser.Items.Add(str);
            cboUser.Text = str;
            //--------下面只会接收一个字符就会中止
            //byte[] buffer = new byte[1024 * 1024 * 2];
            //int count = sockSend.Receive(buffer);
            //string str = Encoding.UTF8.GetString(buffer, 0, count);
            //txtLog.AppendText(sockSend.RemoteEndPoint + ":" + str);

            //---------为了这个新连接正常信息,应连续不断处理,再新开线程处理
            Thread th = new Thread(Receive);
            th.IsBackground = true;
            th.Start(socketSend);
        }
    }


    
    监听成功后,接收消息 

    private void Receive(object o)//线程中接收消息,直到对方关闭
    {
        Socket socketSend = o as Socket;
        while (true)
        {
            byte[] buffer = new byte[1024 * 1024 * 2];
            int count = socketSend.Receive(buffer);
            if (count == 0) break;//无,表示远程客户端已经关闭,应中止
            string str = Encoding.UTF8.GetString(buffer, 0, count);
            ShowLog(socketSend.RemoteEndPoint + ":" + str);
        }
    }


    
    显示日志

    private void ShowLog(string str)
    {
        txtLog.AppendText(str + "\r\n");
    }


    
    忽略交叉线程检查

    private void Form1_Load(object sender, EventArgs e)
    {
        Control.CheckForIllegalCrossThreadCalls = false;
    }


    
    利用Socket发送消息(首字节为0)

    private void btnSend_Click(object sender, EventArgs e)
    {
        string str = txtMsg.Text.Trim();
        byte[] buffer = Encoding.UTF8.GetBytes(str);
        //增加标志位
        List<byte> lst = new List<byte>();
        lst.Add(0);//标志位0,文字
        lst.AddRange(buffer);
        byte[] newBuffer = lst.ToArray();

        dicSocket[cboUser.SelectedItem.ToString()].Send(newBuffer);
        txtMsg.Clear();
    }


    
    发送震动(首字节为2)

    private void btnZD_Click(object sender, EventArgs e)
    {
        byte[] buffer = new byte[1];
        buffer[0] = 2;//震动标志
        dicSocket[cboUser.SelectedItem.ToString()].Send(buffer);
    }


    
    选择发送的文件

    private void btrSelect_Click(object sender, EventArgs e)
    {
        OpenFileDialog ofd = new OpenFileDialog();
        ofd.InitialDirectory = @"E:\";
        ofd.Multiselect = false;
        ofd.Filter = "所有文件|*.*";
        if (ofd.ShowDialog() == DialogResult.OK)
        {
            txtFile.Text = ofd.FileName;
        }
    }


    
    接收文字消息(首字节为0)

    private void btnSendFile_Click(object sender, EventArgs e)
    {
        string p = txtFile.Text.Trim();
        using (FileStream fs = new FileStream(p, FileMode.Open, FileAccess.Read))
        {
            byte[] buffer = new byte[1024 * 1024 * 5];
            int count = fs.Read(buffer, 0, buffer.Length);
            List<byte> lst = new List<byte>();
            lst.Add(1);
            lst.AddRange(buffer);
            byte[] newBuffer = lst.ToArray();
            dicSocket[cboUser.SelectedItem.ToString()].Send(newBuffer, 0, count + 1, SocketFlags.None);
        }
    }


    
    
    
    客户端界面:

 


    
    代码
    连接对象创建,接收,发送都要用,设置为全局变量:

    private Socket socketSend;


    
    开始监听,创建socket对象,向服务器发起连接,新开后台线程以等待服务器的消息

    private void btnStart_Click(object sender, EventArgs e)
    {
        socketSend = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        IPAddress ip = IPAddress.Parse(txtServer.Text);
        IPEndPoint point = new IPEndPoint(ip, Convert.ToInt32(txtPort.Text));
        socketSend.Connect(point);
        ShowMsg("连接成功");

        Thread th = new Thread(Receive);//新建线程一直等待消息的来到
        th.IsBackground = true;
        th.Start();
    }


    
    
    接收消息Receive方法后阻塞一直等待,首字节0:文字,1:文件,2:震动
    注意:sfd.ShowDialog(this) 必须加上this,否则窗体不会弹出.
        ShowDialog (System.Windows.Forms.IWin32Window owner);
        owner:任何实现IWin32Window(表示将拥有模式对话框的顶级窗口)的对象。
        此版本的 ShowDialog 方法允许您指定将拥有所显示对话框的特定窗体或控件。 如果
        使用没有参数的此方法版本,则应用程序的当前活动窗口将自动拥有显示的对话框。

            
    private void Receive()
    {
        while (true)
        {
            byte[] buffer = new byte[1024 * 1024 * 2];
            int count = socketSend.Receive(buffer);
            if (count == 0) break;
            if (buffer[0] == 0)//消息
            {
                string str = Encoding.UTF8.GetString(buffer, 1, count - 1);
                ShowMsg(socketSend.RemoteEndPoint + ":" + str);
            }
            else if (buffer[0] == 1)//文件
            {
                SaveFileDialog sfd = new SaveFileDialog();
                sfd.InitialDirectory = @"E:\";
                if (sfd.ShowDialog(this) == DialogResult.OK)
                {
                    string p = sfd.FileName;
                    using (FileStream fs = new FileStream(p, FileMode.OpenOrCreate, FileAccess.Write))
                    {
                        fs.Write(buffer, 1, count - 1);
                    }
                    MessageBox.Show("保存成功");
                }
            }
            else if (buffer[0] == 2)//震动
            {
                Point p = this.Location;
                for (int i = 0; i < 300; i++)
                {
                    this.Location = new Point(p.X + 10, p.Y + 10);
                    this.Location = p;
                }
            }
        }
    }


    
    日志框中显示追加信息

    private void ShowMsg(string str)
    {
        txtLog.AppendText(str + "\r\n");
    }


    
    使用前面的Socket对象发送对象。
    注意:无论是发送还是接收,都是字节数组形式。

    private void button2_Click(object sender, EventArgs e)
    {
        string str = txtMsg.Text.Trim();
        byte[] buffer = Encoding.UTF8.GetBytes(str);

        socketSend.Send(buffer);
        txtMsg.Clear();
    }


    
    忽略交叉线程检查,以免线程中调用主线程控件报错。

    private void Form1_Load(object sender, EventArgs e)
    {
        Control.CheckForIllegalCrossThreadCalls = false;
    }


    
    
    
四、模式窗体与非模式窗体


    1、区别:
        模式窗体以独占方式运行。即,一个进程里某模式窗体没有运行完毕(关闭),那么就不
    能使用其它窗体,直到这个模式窗体关闭为止。所以模式窗体是比较霸道的。
        非模式窗体相反,它的运行与关闭,不影响其它窗体的使用。
    
    2、语法
    1)模式窗体一般为ShowDialog(),如:
        Form.ShowDialog();
        Form.ShowDialog(IWin32Window)
    2)非模式窗体一般为Show(),如:
        Form.Show();
        Form.Show(IWin32Window);
    
        上面参数IWin32Window在模式/非模式中都可重载,通过它指定弹出窗体的父窗体。
    即可以设置其父窗体为同一进程中的其它窗体(非当前活动窗体),还可以为其它进程中的指定
    窗体,以满足程序设计不同的需求。
    
    例子:注意对比下面各窗体情况:
    
    界面:form1再添加两个form2与form3。form1中成对添加按键11个,用于对比观察。
    
    
    因为要用到进程调用窗体:

    private void Form1_Load(object sender, EventArgs e)
    {
        Control.CheckForIllegalCrossThreadCalls = false;
    }


    
    
    模式与非模式的区别,可以看到,模式下是独占,无法操作form1

    private void button1_Click(object sender, EventArgs e)
    {
        Form2 form2 = new Form2();
        form2.Show();//非模式
    }

    private void button2_Click(object sender, EventArgs e)
    {
        Form2 form2 = new Form2();
        form2.ShowDialog();//模式
    }

    
    模式与非模式指定父窗体区别:
        非模式可以任意切换,但其指定的父窗体关闭,则子窗体也关闭。
        模式无法切换,独占,除父窗体外,Form1也无法操作。

    private void button3_Click(object sender, EventArgs e)
    {
        Form2 form2 = new Form2();
        form2.Show();
        Form3 form3 = new Form3();
        form3.Show(form2);//非模式设置父窗体
    }

    private void button4_Click(object sender, EventArgs e)
    {
        Form2 form2 = new Form2();
        form2.Show();
        Form3 form3 = new Form3();
        form3.ShowDialog(form2);//模式设置父窗体
    }


    
    
    开线程,分别调用模式与非模式的弹出。
        非模式,一闪而过,看不到弹出的窗体。
        模式,直接弹出窗体,虽然独占,但可以切换到Form1窗体中。与上面button4的点击
    时不能切换到form1中,两者有区别。说明在线程中的form1不是form2的父窗体(上级窗体).
 

    private void button5_Click(object sender, EventArgs e)
    {
        Thread th = new Thread(Test);
        th.IsBackground = true;
        th.Start();
    }

    private void Test()
    {
        Form2 form2 = new Form2();
        form2.Show();//一闪而过,非模式
    }

    private void button6_Click(object sender, EventArgs e)
    {
        Thread th = new Thread(Test66);
        th.IsBackground = true;
        th.Start();
    }

    private void Test66()
    {
        Form2 form2 = new Form2();
        form2.ShowDialog();//强显,虽未指定父窗体,但form2的父窗体是form.同级中模式
    }


    
    开线程,指定父窗体。非模式弹出窗体还是一闪而过。
        模式窗体显示,独占,此时点击form1无法切换,因为this就是form1被指定为父窗体。
        模式下,父窗体只能等模式窗体关闭才能激活。

    private void button7_Click(object sender, EventArgs e)
    {
        Thread th = new Thread(Test77);
        th.IsBackground = true;
        th.Start();
    }

    private void Test77()
    {
        Form2 form2 = new Form2();
        form2.Show(this);//一闪而过,非模式
    }

    private void button8_Click(object sender, EventArgs e)
    {
        Thread th = new Thread(Test88);
        th.IsBackground = true;
        th.Start();
    }

    private void Test88()
    {
        Form2 form2 = new Form2();
        form2.ShowDialog(this);//强行显示,父窗体form1
    }


    
    
    保存对话框情况,指定父窗体与不指定父窗体.
        不指定时,不知道它是线程中是谁的,所以也就不能强行显示在form1前面.
        指定时,明确form1是父窗体,强行将显示在父窗体前面,也就显示出来了。

    private void button9_Click(object sender, EventArgs e)
    {
        Thread th = new Thread(Test9);
        th.IsBackground = true;
        th.Start();
    }

    private void Test9()
    {
        SaveFileDialog sfd = new SaveFileDialog();
        sfd.ShowDialog();//强显,默认父窗体不知在哪儿。
    }

    private void button10_Click(object sender, EventArgs e)
    {
        Thread th = new Thread(Test10);
        th.IsBackground = true;
        th.Start();
    }

    private void Test10()
    {
        SaveFileDialog sfd = new SaveFileDialog();
        sfd.ShowDialog(this);//强显,指定父窗体form1
    }


    
    
    设置为其它进程为父窗体。
        进程取记事本中的一个(提前至少打开一个记事本),由其句柄构造一个继承Iwin32Window
    的类。它被指定为父窗体。因此,程序运行后,由于form3与form1都来自Form,在模式窗体弹出后
    不能切换到form1中;同时在记事本中的某一个,也不能切换,因为它被指定为父窗体,其它的
    已打开的记事本不受影响可以切换。

    private void button11_Click(object sender, EventArgs e)
    {
        Process[] procs = Process.GetProcessesByName("notepad");
        if (procs.Length != 0)
        {
            IntPtr hwnd = procs[0].MainWindowHandle;
            Form3 form3 = new Form3();
            form3.ShowDialog(new WindowWrapper(hwnd));
        }
    }    
    //需要的类
    public class WindowWrapper : IWin32Window
    {
        private IntPtr _hwnd;

        public WindowWrapper(IntPtr handle)
        {
            _hwnd = handle;
        }

        public IntPtr Handle
        {
            get { return _hwnd; }
        }
    }


    

   

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值