百度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协议的网络编程
窗体:
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没拷回来,做了个丑的,凑合看)
上来先输入用户名及你要连接的IP地址,连接上以后,聊天界面如下图,因为程序应用了P2P,因此不要试图开两个窗口测试程序,因为开一个就已经把端口占用了。我们以连本机为例对该程序做一个测试:
在该项目中,最好将所有的方法及对流的处理都新建一个类,在该类中做所有操作,当然,所有工作都在界面程序中做也可以。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()方法。
还有一点,很多人喜欢在按钮里加各种判断以避免发生异常,我的想法是这么做可以,对个人对程序的理解很有好处,但是对于用户来说,他并不想看到过多的提示信息,比如用户还没登陆就按发送按钮发消息,你啪弹一窗口出来:“你小子还没登录,发啥消息?”,然后摆一确定按钮让他按,按多了把人家按毛了程序一卸大家都别吃饭了,我的建议是你直接把那些不该让他按的按钮给屏蔽了,让他明明白白的知道什么可以按,什么不可以按,也起到一个引导作用。
先这样,回头改进好了再发上来,轮廓是这个轮廓,需要处理的细节很多。