基于UDP、TCP协议的C#网络编程

时间:2020-11-28 10:23:49

百度C#使用TCP,UDP协议:

http://wenku.baidu.com/view/45364834f111f18583d05a97.html

简介:

TCP(传输控制协议)是 TCP/IP 协议栈中的传输层协议,它通过序列确认以及包重发机制,提供可靠的数据流发送和到应用程序的虚拟连接服务。与IP协议相结合, TCP组成了因特网协议的核心。

 UDP(用户数据报协议)是ISO参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。 UDP协议基本上是 IP 协议与上层协议的接口。UDP协议适用端口分辨运行在同一台设备上的多个应用程序。

代码:

Form1做为服务器端,按下Send,将文本框的值发送出去,Form2做为客户端,接收信息并加入到ListBox控件中。

  Form1:

public partial class Form1 : Form
    {
        UdpClient udp; //声明UDPClient
        public Form1()
        {           
            udp = new UdpClient(); //初始化
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {           
            string temp = this.textBox1.Text; //保存TextBox文本

            //将该文本转化为字节数组
            byte[] b = System.Text.Encoding.UTF8.GetBytes(temp);

            //向本机的8888端口发送数据
            udp.Send(b, b.Length,Dns.GetHostName(),10000);
        }
    }

  Form2:

public partial class Form2 : Form
    {
        UdpClient udp = null; //声明UDPClient
        public Form1()
        {

            //屏蔽跨线程改控件属性那个异常
            CheckForIllegalCrossThreadCalls = false;
            InitializeComponent();

            //注意此处端口号要与发送方相同
            uc = new UdpClient(10000);

            //开一线程
            Thread th = new Thread(new ThreadStart(listen));

            //设置为后台
            th.IsBackground = true;
            th.Start();
        }
        private void listen()
        {

            //声明终结点
            IPEndPoint iep = new IPEndPoint(IPAddress.Parse("192.168.0.10"),8888);
            while (true)
            {

                //获得Form1发送过来的数据包
                string text = System.Text.Encoding.UTF8.GetString(uc.Receive(ref iep));

                //加入ListBox
                this.listBox1.Items.Add(text);
            }
        }
    }

 需要注意的地方非常之多,别看就这么几行,先看Form1中的UdpClient声明,这里使用了无参的构造函数uc = new UdpClient(); 我们写基于TCP的程序可以知道,TcpClient声明同时直接指出其端口是很方便的,也是必然的,不指定端口你上哪收数据去?因为UDP是一种无连接的传输层协议,想给谁发就给谁发,所以如果我们这么声明了UdpClient,但是接收方如果想收到数据包,就必须建立基于发送方发送数据端口的UdpClient(见Form2),这么说有点乱,接着往下看。当我们声明了uc = new UdpClient(); 那下面的写法就相对固定了,在Send数据的时候,需要指明其目标计算机,以及将要发送的端口,例如示例中的uc.Send(b, b.Length,Dns.GetHostName(),8888);Send有很多重载的方法,如果你想这么写uc.Send(b, b.Length);那就必须在Send之前在UdpClient与目标计算机之间做一下连接,否则无法发送,我们可以这么写:
uc = new UdpClient(); uc.Connect(IPAddress.Parse("192.168.0.10"), 8888);.....uc.Send(b, b.Length);
     这里注意,IP地址跟端口号可以随便写,只要对方监听着你的这个端口,说监听有点小错,UDP并不需要监听,姑且这么说,形象一点。      另外,很多人遇到这么个问题,无论在TCP还是UDP中,很多时候因为编码问题,接收到以字节数组发送的中文消息,还原后出现乱码,这个问题的解决办法是发送方与接收方都使用同一种Encoding,发送方用UTF-8.GetBytes,接收方也同样使用UTF-8.GetString这个方法便可传递中文,网上鸟多,墨迹半天也解决不了,汗个。      再来看Form2,与Form1相反,在Form2中实例化UdpClient时,需要指明其端口,因为我们要捕获发送过来的消息,注意这两句话:

IPEndPoint iep = new IPEndPoint(IPAddress.Parse("192.168.0.10"),8888);

.........
string text = System.Text.Encoding.UTF8.GetString(uc.Receive(ref iep));

     网上对这个貌似还是有点误解,很多人说,这里的IPEndPoint的端口号如果随便指定,也可以收到发送过来的消息,但是就是不知道为什么,我写的更简单:

    

IPEndPoint iep = null;

.........
string text = System.Text.Encoding.UTF8.GetString(uc.Receive(ref iep));

     看出问题来了吧,关键是uc.Receive方法里的ref参数,ref关键字使参数按引用传递。其效果是,当控制权传递回调用方法时,在方法中对参数所做的任何更改都将反映在该变量中。所以你只要扔给它一个值就得了,管他什么端口号,况且端口早在声明UdpClient的时候就指定好了。




讲讲基于TCP协议的网络编程,与UDP不同的是,基于TCP协议的编程的服务器端有一个监听对象:TcpListener,它负责监听来自客户端的消息并处理,并且必须在保持连接的情况下与客户端保持互动,下面举个例子,TCP不怎么复杂,只是综合要求较高,如果想编出个象样的东西,对多线程,事件委托等等都需要有较高的认识,当然,还要对协议本身有深刻的理解

  示例一:基于TCP协议的网络编程      窗体:           基于UDP、TCP协议的C#网络编程 基于UDP、TCP协议的C#网络编程       Form2做为本程序的服务器端,当按下Start后,启动服务,剩下的是一个Form1,我启动了两次,都连接到Form2,当在Form1的Send栏里写入小写字母并按下Send按钮后,将该字符串发送至Form2,同时Form2将该字符串转换为大写,返回给发送者,说明完毕,出个谜语,谁知道两个Form1里字母是啥意思?        Form2:(服务器端)

public partial class Form2 : Form
    {

        //声明监听对象
        private TcpListener tl;

        //声明网络流
        private NetworkStream ns;
        public Form1()
        {
            CheckForIllegalCrossThreadCalls = false;
            InitializeComponent();           
        }

        private void btnStart_Click(object sender, EventArgs e)
        {

            //开启8888端口的监听
            tl = new TcpListener(8888);
            tl.Start();

            //开启线程
            Thread th = new Thread(new ThreadStart(listen));
            th.IsBackground = true;
            th.Start();
        }
        private void listen()
        {           
            while (true)
            {   

                //获得响应的Socket
                Socket sock = tl.AcceptSocket();  

                //通过该Socket实例化网络流           
                ns = new NetworkStream(sock);

                //ClientTcp是添加的类,下面会做说明
                ClientTcp ct = new ClientTcp(ns);

                //ct_MyEvent方法注册ClientTcp类的MyEvent事件
                ct.MyEvent += new MyDelegate(ct_MyEvent);

                //开启线程
                Thread th = new Thread(new ThreadStart(ct.TcpThread));
                th.IsBackground = true;
                th.Start();
            }
        }

        void ct_MyEvent(string temp)
        {

            //设置服务器端TextBox的值
            this.textBox1.Text = temp;
        }
    }

 

       Form1:(客户端)

 

public partial class Form1 : Form
    {

        //声明Tcp客户端
        private TcpClient tc;

        //声明网络流
        private NetworkStream ns;
        public Form1()
        {
            CheckForIllegalCrossThreadCalls = false;
            InitializeComponent();
        }

        private void button2_Click(object sender, EventArgs e)
        {

            //注册本机8888端口
            tc = new TcpClient("localhost",8888);

            //实例化网络流对象
            ns = tc.GetStream();
            string temp = this.textBox1.Text;

            StreamWriter sw = new StreamWriter(ns);
            StreamReader sr = new StreamReader(ns);

            //将TextBox1的值传给服务器端
            sw.WriteLine(temp);
            sw.Flush();

            //接收服务器端回传的字符串
            string str = sr.ReadLine();
            this.textBox2.Text = str;
            sr.Close();
            sw.Close();
        }
    }

 

       ClientTcp类:

    //声明一个需要一个字符串参数的委托

    public delegate void MyDelegate(string temp);
    class ClientTcp
    {

        //设置网络流局部对象
        private NetworkStream ns;

        //声明类型为MyDelegate的事件MyEvent
        public event MyDelegate MyEvent;

        //构造函数中接收参数以初始化
        public ClientTcp(NetworkStream ns)
        {
            this.ns = ns;
        }

        //服务器端线程所调用的方法
        public void TcpThread()
        {

            //获得相关的封装流
            StreamReader sr = new StreamReader(ns);
            string temp = sr.ReadLine();

            //接收到客户端消息后触发事件将消息回传
            MyEvent(temp);
            StreamWriter sw = new StreamWriter(ns);

            //转换为大写后发送消息给客户端
            sw.WriteLine(temp.ToUpper());
            sw.Flush();
            sw.Close();
            sr.Close();
        }
    }

         这里说下为什么需要 ClientTcp 这么个类,说这个之前,先说一下为什么服务器端需要开启一个新的线程来监控端口,这个原因比较简单, Socket sock = tl.AcceptSocket();     这个方法会造成阻塞,也就是说如果没有得到客户端的响应, TcpListenr 将一直监听下去,这就会造成程序的假死,因此我们需要单独开一个线程来监听我们的8888端口,我们观察服务器端(Form2)可以看出, NetworkStream 是一个全局变量(实际上局部与全局都是一样),如果CPU忙的过来,直接把 ClientTcp 里的方法拿到Form2里写没问题,但是一旦客户端过多造成数据拥挤,那很可能当运算还未结束, NetworkStream 就已经换人了,因此当我们取得某客户端对应的 NetworkStream 后,应该考虑立刻将它封装到一个类中,再在该类中再对该 NetworkStream 做相应的操作, ClientTcp 这个类就是为这个设计的,而当封装了 NetworkStream 后,我们发现从客户端传过来的值是我们需要的,因此就用到了事件的回调,这个我前面有篇文章里讲过了,见 http://blog.sina.com.cn/u/4c459776010008ws,基于TCP协议的网络编程基础的东西就这些,写法很固定,但是需要很多的技巧,前几天试着写一个聊天室程序,差点没吐血,果然不是一般的麻烦。



基于TCP协议实现P2P(Peer To Peer)思想

 对等网络(P2P,Peer to Peer)是一种资源(计算、存储、通信与信息等)分布利用与共享的网络体系架构,与目前网络中占据主导地位的客户机服务器(Client/Server,C/S)体系架构相对应。P2P可以用来进行流媒体通信(如话音、视频或即时消息),也可以传送如控制信令、管理信息和其它数据文件,具体的应用如Napster MP3音乐文件搜索与共享、BitTorrent多点文件下载和Skype VoIP话音通信等。简单的理解,我前面写的TCP,UDP的程序,都是需要客户端、服务器,就是比较简单的C/S结构,而P2P则是集Server/Client于一身,本身即是服务器端,又是客户端,严格来说,P2P不算是一种技术,而是一种思想,此思想还被列入国家研究课题,现在我们简单的看看这个P2P基本都是些什么东西,把昨天做的项目扔出来。      示例:P2P聊天程序      窗体(界面Image没拷回来,做了个丑的,凑合看)   基于UDP、TCP协议的C#网络编程      上来先输入用户名及你要连接的IP地址,连接上以后,聊天界面如下图,因为程序应用了P2P,因此不要试图开两个窗口测试程序,因为开一个就已经把端口占用了。我们以连本机为例对该程序做一个测试:     基于UDP、TCP协议的C#网络编程      在该项目中,最好将所有的方法及对流的处理都新建一个类,在该类中做所有操作,当然,所有工作都在界面程序中做也可以。Form1就是个界面,你在它脑袋上面再using System.Net,using System.Net.Socket等等显得很不伦不类,为了规范起见,我新建一个类MethodsList操作所有方法。      MethodsList类:

//定义一个需要string类型参数的委托,用来将发送至本机的消息回调

public delegate void MyDelegate(string message);
    class MethodsList
    {

        //储存本机用户名
        private string m_Name; 

        //储存对方IP地址
        private string m_Ip;

        //本机的TcpListener
        private TcpListener m_MyListener;

        //指示变量
        private bool m_IsListen = true;

        //网络流
        private NetworkStream m_NetStream;

        //线程
        private Thread m_MyThread;

        //MyDelegate委托类型的事件
        public event MyDelegate MyEvent;

        //构造函数中接收用户名,对方IP地址

        public MethodsList(string name, string ip)
        {

            //储存
            m_Name = name;
            m_Ip = ip;

            //实例化TcpListener
            m_MyListener = new TcpListener(8888);
            m_MyListener.Start();

            //开启新线程监听8888端口
            m_MyThread = new Thread(new ThreadStart(Run));
            m_MyThread.IsBackground = true;
            m_MyThread.Start();           
        }

        //监听来自对方信息的方法
        private void Run()
        {
            while (m_IsListen)
            {
                Socket sock = m_MyListener.AcceptSocket();
                m_NetStream = new NetworkStream(sock);

                //读取对方传递过来的信息
                StreamReader sr = new StreamReader(m_NetStream);
                string tempChat = sr.ReadLine();

                //如果读取到,则触发事件将传来的信息回调
                MyEvent(tempChat);
                sr.Close();
            }
        }

        //本机发送信息方法
        public void Send(string message)
        {

            //实例化连接对方IP地址的TcpClient
            TcpClient tc = new TcpClient(m_Ip, 8888);

            StreamWriter sw =new StreamWriter(tc.GetStream());

            //注意这个发送消息的技巧
            string sendMessage = m_Name + "|" + message;
            sw.WriteLine(sendMessage);
            sw.Flush();
            sw.Close();
        }

        //断开按钮所调用的方法
        public void Close()
        {

            //线程终止
            m_MyThread.Abort();

            //指示变量设置为False
            m_IsListen = false;

            //停止监听
            m_MyListener.Stop();
        }
    }

     下面是主窗体类,对比来看:

     Form1:

public partial class Form1 : Form
    {

        //用户名
        private string m_Name;

        //对方IP
        private string m_Ip;

        //MethodList类对象
        private MethodsList m_Ml;

        //对方用户名
        private string m_HisName;

        public Form1()
        {

            //禁止跨线程修改窗体控件属性的异常
            CheckForIllegalCrossThreadCalls = false;
            InitializeComponent();
        }

        //登录界面按钮(我连~)事件

        private void btnConn_Click(object sender, EventArgs e)
        {

            //储存用户名,对方IP
            m_Name = this.txtName.Text;
            m_Ip = this.txtIp.Text;

            //实例化MethodsList对象,并将用户名,对方IP传递进去
            m_Ml = new MethodsList(m_Name, m_Ip);

            //为本类的m_Ml_MyEvent方法注册m_Ml类的MyEvent事件
            m_Ml.MyEvent += new MyDelegate(m_Ml_MyEvent);

            //相关的按钮恢复为可用状态

            this.btnConnect.Enabled = true;
            this.btnDisConnect.Enabled = true;
            this.rtbChat.Enabled = true;
            this.btnSend.Enabled = true;
            this.lbChat.Enabled = true;

            //隐藏登录界面

            this.panel3.Visible = false;
        }

        //订阅了MethodsList类MyEvent事件的方法

        void m_Ml_MyEvent(string message)
        {

            //将获得的消息按“|”分割,得到的第一部分是用户名,第二部分是消息文本
            string[] tempChat = message.Split(new char[] { '|'});

            //保存对方用户名
            m_HisName = tempChat[0];
            string chat = m_HisName + "对你说:" + tempChat[1];

            //加消息到ListBox
            this.lbChat.Items.Add(chat);
        }

        //发送按钮事件

        private void btnSend_Click(object sender, EventArgs e)
        {

            //获得将要发送的消息内容
            string message = this.rtbChat.Text;

            //调用MethodsList的Send方法,并将消息传递
            m_Ml.Send(message);

            //将显示在本机的ListBox上的消息
            string tempChat ="你对"+m_HisName+"说:" + message;

            //加入到ListBox
            this.lbChat.Items.Add(tempChat);

            //发送完毕,清空发送栏
            this.rtbChat.Text = "";
        }

        //退出按钮事件

        private void btnQuit_Click(object sender, EventArgs e)
        {

            //如果MethodsList对象不为空,则调用Close方法
            if (m_Ml != null)
            {
                m_Ml.Close();
            }
            Application.Exit();
        }

        //界面左下按钮“连接”事件

        private void btnConnect_Click(object sender, EventArgs e)
        {

            //重新显示登录界面
            this.panel3.Visible = true;

            //相关控件设置为不可用

            this.btnConnect.Enabled = false;
            this.btnDisConnect.Enabled = false;
            this.rtbChat.Enabled = false;
            this.btnSend.Enabled = false;
            this.lbChat.Enabled = false;
        }

        //界面左下按钮“断开”事件

        private void btnDisConnect_Click(object sender, EventArgs e)
        {

            //如果MethodsList不为空,则调用Close方法
            if (m_Ml != null)
            {
                m_Ml.Close();
            }
        }
    }

    这样,一个简单的以P2P思想编写的聊天程序就可以用了,不足的地方有很多,比如,我连韩贱人,我把我林大少名字一输,韩贱人IP地址一输,然后就想跟他说话,不好意思,他要是不连我,他收不到消息;另外一点,信息发送会很慢很慢,就算IPX也得等个十几二十秒,这跟协议本身有关;还有一个问题就是,注意Send方法,每按一下Send按钮,就要产生一个指向对方IP地址的TcpClient,老师看了我的代码以后让我把这改了,并指出,浪费系统资源是可耻的,我怀着强烈的罪恶感,改来改去也没改出个所以然来,所以我下一篇准备写一下如何改进这个P2P项目:1.自动连接 2.指向对方的TcpClient的处理。

    另外,接收信息就是接收信息,发送信息就是发送信息,如果你想写P2P的文件传输,那发送就是发送,接收就是接收,不要混一起写,如果一个方法中出现StreamReader以及StreamWriter,那显然是不规范的,程序大了就乱套了,参见上面的Run()方法以及Send()方法。

    还有一点,很多人喜欢在按钮里加各种判断以避免发生异常,我的想法是这么做可以,对个人对程序的理解很有好处,但是对于用户来说,他并不想看到过多的提示信息,比如用户还没登陆就按发送按钮发消息,你啪弹一窗口出来:“你小子还没登录,发啥消息?”,然后摆一确定按钮让他按,按多了把人家按毛了程序一卸大家都别吃饭了,我的建议是你直接把那些不该让他按的按钮给屏蔽了,让他明明白白的知道什么可以按,什么不可以按,也起到一个引导作用。

    先这样,回头改进好了再发上来,轮廓是这个轮廓,需要处理的细节很多。