您的位置:首页 > 理论基础 > 计算机网络

【转载】 基于UDP、TCP协议的C#网络编程

2009-06-12 15:26 806 查看
写这篇之前,先简单介绍一下TCP、UDP协议,深的讲不出来,有不明白的请问我秘书Dawnh同学。 TCP(传输控制协议)是
TCP/IP 协议栈中的传输层协议,它通过序列确认以及包重发机制,提供可靠的数据流发送和到应用程序的虚拟连接服务。与IP协议相结合,
TCP组成了因特网协议的核心。
UDP(用户数据报协议)是ISO参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。 UDP协议基本上是 IP 协议与上层协议的接口。UDP协议适用端口分辨运行在同一台设备上的多个应用程序。
C#
中,已将TCP,UDP,SMTP等协议封装为相应的类型库,提供了一系列方法供程序员进行操作,可以简单的理解为,基于TCP的编程就好象通电话,我拨
打贱人甲电话,贱人甲必须按下接听键,我们之间才能建立起有效的连接,而基于UDP的编程就好象是收音机广播,我这头只管播,对面谁在听或者是不是收到我
并不关心。TCP、UDP同属于高层协议,复杂程度是大大不如Socket编程的。
下面我准备写两个例子,一个用UDP,一个用TCP,TCP比较好理解,UDP实际上也不麻烦,但是从网上找资料看你会看的非常晕,MSDN的各种Sample也统统放到一个类里写,效果并不好,我稍微一总结,先写个基于UDP的例子。
示例一:UDP

Form1做为服务器端,按下Send,将文本框的值发送出去,Form1做为客户端,接收信息并加入到ListBox控件中。
Form1:
public partial class Form1 : Form
{
UdpClient uc; //声明UDPClient
public Form1()
{
uc = 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端口发送数据
uc.Send(b, b.Length,Dns.GetHostName(),8888);
}
}
Form2:
public partial class Form2 : Form
{
UdpClient uc = null; //声明UDPClient
public Form1()
{
//屏蔽跨线程改控件属性那个异常
CheckForIllegalCrossThreadCalls = false;
InitializeComponent();
//注意此处端口号要与发送方相同
uc = new UdpClient(8888);
//开一线程
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协议的网络编程基础的东西就这些,写法很固定,但是需要很多的技巧,前几天试着写一个聊天室程序,差点没吐血,果然不是一般的麻烦。

原文地址链接:http://blog.chinahr.com/blog/dingyi2008/post/120900
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: