我们知道C#和C++的差异之一,就是他本身没有类库,所使用的类库是.Net框架中的类库--.Net FrameWork SDK。在.Net FrameWork SDK中为网络编程提供了二个名称空间:"System.Net"和"System.Net.Sockets"。C#就是通过这二个名称空间中封装的类和方法实现网络通讯的。
首先我们解释一下在网络编程时候,经常遇到的几个概念:同步(synchronous)、异步(asynchronous)、阻塞(Block)和非阻塞(Unblock):
所谓同步方式,就是发送方发送数据包以后,不等接受方响应,就接着发送下一个数据包。异步方式就是当发送方发送一个数据包以后,一直等到接受方响应后,才接着发送下一个数据包。而阻塞套接字是指执行此套接字的网络调用时,直到调用成功才返回,否则此套节字就一直阻塞在网络调用上,比如调用StreamReader 类的Readlin ( 方法读取网络缓冲区中的数据,如果调用的时候没有数据到达,那么此Readlin ( 方法将一直挂在调用上,直到读到一些数据,此函数调用才返回;而非阻塞套接字是指在执行此套接字的网络调用时,不管是否执行成功,都立即返回。同样调用StreamReader 类的Readlin ( 方法读取网络缓冲区中数据,不管是否读到数据都立即返回,而不会一直挂在此函数调用上。在Windows网络通信软件开发中,最为常用的方法就是异步非阻塞套接字。平常所说的C/S(客户端/服务器)结构的软件采用的方式就是异步非阻塞模式的。
其实在用C#进行网络编程中,我们并不需要了解什么同步、异步、阻塞和非阻塞的原理和工作机制,因为在.Net FrameWrok SDK中已经已经把这些机制给封装好了。下面我们就用C#开一个具体的网络程序来说明一下问题。
一.本文中介绍的程序设计及运行环境
(1).微软视窗2000 服务器版
(2)..Net Framework SDK Beta 2以上版本
二.服务器端程序设计的关键步骤以及解决办法:
在下面接受的程序中,我们采用的是异步阻塞的方式。
(1).首先要要在给定的端口上面创建一个"tcpListener"对象侦听网络上面的请求。当接收到连结请求后通过调用"tcpListener"对象的"AcceptSocket"方法产生一个用于处理接入连接请求的Socket的实例。下面是具体实现代码:
//创建一个tcpListener对象,此对象主要是对给定端口进行侦听
tcpListener = new TcpListener ( 1234 ;
//开始侦听
tcpListener.Start ( ;
//返回可以用以处理连接的Socket实例
socketForClient = tcpListener.AcceptSocket ( ;
(2).接受和发送客户端数据:
此时Socket实例已经产生,如果网络上有请求,在请求通过以后,Socket实例构造一个"NetworkStream"对象,"NetworkStream"对象为网络访问提供了基础数据流。我们通过名称空间"System.IO"中封装的二个类"StreamReader"和"StreamWriter"来实现对"NetworkStream"对象的访问。其中"StreamReader"类中的ReadLine ( 方法就是从"NetworkStream"对象中读取一行字符;"StreamWriter"类中的WriteLine ( 方法就是对"NetworkStream"对象中写入一行字符串。从而实现在网络上面传输字符串,下面是具体的实现代码:
try
{
//如果返回值是"true",则产生的套节字已经接受来自远方的连接请求
if ( socketForClient.Connected
{
ListBox1.Items.Add ( "已经和客户端成功连接!" ;
while ( true
{
//创建networkStream对象通过网络套节字来接受和发送数据
networkStream = new NetworkStream ( socketForClient ;
//从当前数据流中读取一行字符,返回值是字符串
streamReader = new StreamReader ( networkStream ;
string msg = streamReader.ReadLine ( ;
ListBox1.Items.Add ( "收到客户端信息:" + msg ;
streamWriter = new StreamWriter ( networkStream ;
if ( textBox1.Text != ""
{
ListBox1.Items.Add ( "往客户端反馈信息:" + textBox1.Text ;
//往当前的数据流中写入一行字符串
streamWriter.WriteLine ( textBox1.Text ;
//刷新当前数据流中的数据
streamWriter.Flush ( ;
}
}
}
}
catch ( Exception ey
{
MessageBox.Show ( ey.ToString ( ;
}
(3).最后别忘了要关闭所以流,停止侦听网络,关闭套节字,具体如下:
//关闭线程和流
networkStream.Close ( ;
streamReader.Close ( ;
streamWriter.Close ( ;
_thread1.Abort ( ;
tcpListener.Stop ( ;
socketForClient.Shutdown ( SocketShutdown.Both ;
socketForClient.Close ( ;
Socket是一种特殊的I/O。常用的Socket类型有两种:
流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。
Visual C#中操作Socket: 虽然Visual C#可以使用NetworkStream来传送、接收数据,但NetworkStream在使用中有很大的局限性,利用NetworkStream只能传送和接收字符类型的数据,如果要传送的是一些复杂的数据如:二进制数据等,它就显得能力有限了。但使用NetworkStream在处理自身可操作数据时,的确要比Socket方便许多。Socket(套接字)几乎可以处理任何在网络中需要传输的数据类型。
表01和表02是Socket类中的常用属性和方法及其简要说明。
(比MSDN上翻译的容易明白多了,嘿嘿)
属性 说明
AddressFamily 获取Socket的地址族。
Available 获取已经从网络接收且可供读取的数据量。
Blocking 获取或设置一个值,该值指示Socket是否处于阻塞模式。
Connected 获取一个值,该值指示Socket是否已连接到远程资源。
Handle 获取Socket的操作系统句柄。
LocalEndPoint 获取本地终结点。
ProtocolType 获取Socket的协议类型。
RemoteEndPoint 获取远程终结点。
SocketType 获取Socket的类型。
表01:Socket类的常用属性及其说明
方法 说明
Accept 创建新的Socket以处理传入的连接请求。
BeginAccept 开始一个异步请求,以创建新的Socket来接受传入的连接请求。
BeginConnect 开始对网络设备连接的异步请求。
BeginReceive 开始从连接的Socket中异步接收数据。
BeginReceiveFrom 开始从指定网络设备中异步接收数据。
BeginSend 将数据异步发送到连接的
BeginSendTo 向特定远程主机异步发送数据。
Bind 使Socket与一个本地终结点相关联。
Close 强制Socket连接关闭。
Connect 建立到远程设备的连接。
EndAccept 结束异步请求以创建新的Socket来接受传入的连接请求
EndConnect 结束挂起的异步连接请求。
EndReceive 结束挂起的异步读取。
EndReceiveFrom 结束挂起的、从特定终结点进行异步读取。
EndSend 结束挂起的异步发送
EndSendTo 结束挂起的、向指定位置进行的异步发送。
GetSocketOption 返回Socket选项的值。
IOControl 为Socket设置低级别操作模式
Listen 将Socket置于侦听状态。
Poll
Receive 接收来自连接Socket的数据。
ReceiveFrom 接收数据文报并存储源终结点。
Select 确定一个或多个套接字的状态。
Send 将数据发送到连接的
SendTo 将数据发送到特定终结点。
SetSocketOption 设置Socket选项。
Shutdown 禁用某Socket上的发送和接收。
表02:Socket类的常用方法及其说明
其中“BeginAccept”和“EndAccept”、“BeginConnect”和“EndConnect”、
“BeginReceive”和“EndReceive”、“BeginReceiveFrom”和“EndReceiveFrom”、
“BeginSend”和“EndSend”、“BeginSendTo”和“EndSendTo”是六组异步方法,
其功能分别相当于“Accept”、“Connect”、“Receive”、“ReceiveFrom”、
“Send”和“SendTo”方法。
下面就通过一个具体的示例,来介绍Visual C#中如何通过托管Socket实现数据传送和接收的具体方法。
本文示例其实是由二部分组成,也可以看成是客户机程序和服务器程序。客户机程序功能是通过 Socket向服务器程序创建连接,并在连接完成后,向服务器发送数据;服务器程序通过侦听端口,接受网络的Socket的连接请求,并在连接完成后,接收从客户机发送来的数据,并显示出来。
Visual C#在使用Socket来介绍网络传送来的数据时,要解决下面三个问题,也是完成Visual C#使用Socket来接收数据的三个步骤:
1.侦听网络,接受网络连接申请;
2..获得用以接收数据的Socket实例,并以此实例接收远程主机发送来的数据;
3.根据远程主机发送来的控制码,断开网络连接,并清除资源。
此处接收接收数据,是上面介绍的【利用Socket来传送数据】传送来的数据。
下面就是利用Socket来接收数据的具体实现步骤:
1.启动Viisual Studio .Net,并新建一个Visual C#项目,项目名称为【利用Socket来接收数据】。
2.把Visual Studio .Net的当前窗口切换到【Form1.cs(设计)】窗口,并从【工具箱】中的【Windows窗体组件】选项卡中往Form1窗体中拖入下列组件,并执行相应操作:
一个ListBox组件,用以显示接收的数据。
一个StausBar组件,用以显示接收端程序的运行状况。
一个Button组件,名称为button1,并在这个组件被拖入窗体后,双击它,则系统会在Form1.cs文件中自动产生其Click事件对应的处理代码。
3.【解决方案资源管理器】窗口中,双击Form1.cs文件,进入Form1.cs文件的编辑界面。
4.以下面代码替代系统产生的InitializeComponent过程::
private void InitializeComponent ( ) { this.button1 = new System.Windows.Forms.Button ( ) ; this.listBox1 = new System.Windows.Forms.ListBox ( ) ; this.statusBar1 = new System.Windows.Forms.StatusBar ( ) ; this.SuspendLayout ( ) ; this.button1.FlatStyle = System.Windows.Forms.FlatStyle.Flat ; this.button1.Location = new System.Drawing.Point ( 96 , 16 ) ; this.button1.Name = "button1" ; this.button1.Size = new System.Drawing.Size ( 80 , 34 ) ; this.button1.TabIndex = 0 ; this.button1.Text = "监听" ; this.button1.Click += new System.EventHandler ( this.button1_Click ) ; this.listBox1.ItemHeight = 12 ; this.listBox1.Location = new System.Drawing.Point ( 16 , 68 ) ; this.listBox1.Name = "listBox1" ; this.listBox1.Size = new System.Drawing.Size ( 258 , 172 ) ; this.listBox1.TabIndex = 1 ; this.statusBar1.Location = new System.Drawing.Point ( 0 , 251 ) ; this.statusBar1.Name = "statusBar1" ; this.statusBar1.Size = new System.Drawing.Size ( 292 , 22 ) ; this.statusBar1.TabIndex = 2 ; this.statusBar1.Text = "无连接" ; this.AutoScaleBaseSize = new System.Drawing.Size ( 6 , 14 ) ; this.ClientSize = new System.Drawing.Size ( 292 , 273 ) ; this.Controls.AddRange ( new System.Windows.Forms.Control[] { this.statusBar1 , this.listBox1 , this.button1} ) ; this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle ; this.MaximizeBox = false ; this.Name = "Form1" ; this.Text = "利用Socket来接收数据" ; this.ResumeLayout ( false ) ; } |
至此【利用Socket来接收数据】项目设计后的界面就完成了,具体如图02所示:
![](http://tech.ccidnet.com/col/attachment/2003/11/262551.jpg)
图02:【利用Socket来接收数据】项目的设计界面
5.把Visual Studio .Net的当前窗口切换到Form1.cs的代码编辑窗口,并在Form1.cs文件的开头,用下列导入命名空间代码替代系统缺省的导入命名空间代码。
using System ; using System.Drawing ; using System.Collections ; using System.ComponentModel ; using System.Windows.Forms ; using System.Data ; using System.Net.Sockets ; //使用到TcpListen类 using System.Net ; using System.Threading ; //使用到线程 |
6.在Form1.cs中的class代码区中添加下列代码,下列代码的作用是定义全局变量和创建全局使用的实例:
int port = 8000 ; //定义侦听端口号 private Thread thThreadRead ; //创建线程,用以侦听端口号,接收信息 private TcpListener tlTcpListen ; //侦听端口号 private bool blistener = true ; //设定标示位,判断侦听状态 private Socket stRead ; |
7.在Form1.cs中的Main函数之后,添加下列代码,下列代码的作用是定义过程“Listen”,此过程的功能是监听“8000”端口号,接收网络中连接请求,建立连接,并获取接收数据时使用的Socket实例,并以Socket实例来接收客户机程序发送来的数据。并根据客户机发送来控制码来断开网络连接,释放资源:
{ try { tlTcpListen = new TcpListener ( port ) ; //以8000端口号来初始化TcpListener实例 tlTcpListen.Start ( ) ; //开始监听网络的连接请求 statusBar1.Text = "正在监听..." ; stRead = tlTcpListen.AcceptSocket ( ) ; //通过连接请求,并获得接收数据时使用的Socket实例 EndPoint tempRemoteEP = stRead.RemoteEndPoint ; IPEndPoint tempRemoteIP = ( IPEndPoint ) tempRemoteEP ; //获取请求的远程计算机名称 IPHostEntry host = Dns.GetHostByAddress ( tempRemoteIP.Address ) ; string sHostName = host.HostName ; statusBar1.Text = "已经连接!" ; //循环侦听 while ( blistener ) { string sTime = DateTime.Now.ToShortTimeString ( ) ; //获取接收数据时的时间 Byte [ ] byRead =new Byte [ 80 ] ; int iRead = stRead.ReceiveFrom ( byRead , ref tempRemoteEP ) ; //获得接收的字节数目 Byte [ ] byText = new Byte [ iRead ] ; //并根据接收到的字节数目来定义字节数组 Array.Copy ( byRead , 0 , byText , 0 , iRead ) ; string sTemp = System.Text.Encoding.Default. GetString ( byText ) ; //判断是否为断开连接控制码 if ( sTemp.Trim ( ) == "STOP" ) { stRead.Close ( ) ; tlTcpListen.Stop ( ) ; //关闭侦听 statusBar1.Text = "连接已经关闭!" ; thThreadRead.Abort ( ) ; //中止线程 return ; } else listBox1.Items.Add ( sTime + " " + sTemp ) ; } catch ( System.Security.SecurityException ) { MessageBox.Show ( "侦听失败!" , "错误" ) ; } } |
8.用下列代码替换Form1.cs中的button1组件的Click事件对应的处理代码,下面代码是以刚刚定义“Listen”过程来初始化线程实例thThreadRead,并启动这个线程:
private void button1_Click ( object sender , System.EventArgs e ) { thThreadRead = new Thread ( new ThreadStart ( Listen ) ) ; //以Listen过程来初始化Thread实例 thThreadRead.Start ( ) ; //启动线程 button1.Enabled = false ; } |
至此,在保存上述的所有修改步骤之后。一个基于TCP协议的、利用Socket接收端程序的就完成了。图03和图04分别是上述二个示例运行后的界面
图03:【利用Socket来传送数据】的运行界面
图04:【利用Socket来接收数据】的运行界面
六.总结:
.Net FrameWork SDK中的Socket类的功能是非常强大的,要十分详细的介绍它,非一篇文章所能达到,本文所窥探的也只是其中的很小的一部分。但本文中介绍的二个示例在功能上虽不复杂,但在结构上却非常典型,Socket类的其他方面的应用在结构上大都也是如此。
在上面介绍的内容中,不仅介绍在Visual C#实现端口侦听、网络连接申请、数据发送、数据接收的具体方法,还介绍了在使用Visual C#实现网络功能是所必然要涉及到的如线程创建、线程销毁、资源回收和利用控制码控制程序运行状态等的实现方法。了解并掌握这些对我们编写功能更强、结构更复杂的网络应用程序是非常有帮助的。
最后要和诸位朋友谈一点小体会,就是在编写网络应用程序时,要非常细心,对应用程序在执行的各个环节都要考虑到位,因为在网络中会出现很多意想不到的问题,就是网络状态非常良好,也存在很多使用者方面的问题。如果在编写的程序中缺乏对这些意外的处理,就可能会导致整个应用程序出错,甚至崩溃。