一.简介Socket:
Socket诞生于上个世纪80年代初,美国政府的高级研究工程机构(ARPA)给加利福尼亚大学伯克力(Berkeley)分校提供了资金,委托他们在UNIX操作系统下实现TCP/IP协议的开发接口。于是研发人员就为TCP/IP网络通信开发了一个API(应用程序接口)。这个API称为Socket接口(套接字)。所以有时候说,Socket是TCP/IP上的API。
到了上个世纪90年代,当时的一些网络开发商,包括现在地Sun和Microsoft公司等,共同制定了一套WINDOWS下的网络编程接口,即WindowsSockets规范,简称WinSock规范。WinSock规范是一套开放的、支持多种协议的Windows下的网络编程接口。从1991年的1.0版到1995年的2.0.8版,经过不断完善并在Intel、Microsoft、Sun等公司的大力支持下,已成为Windows网络编程的事实上的标准。目前,WinSock规范主要有1.1版和2.0版。两者的最重要区别是1.1版只支持TCP/IP协议,而2.0版可以支持多协议。2.0版有良好的向后兼容性,任何使用1.1版的源代码,二进制文件,应用程序都可以不加修改地在2.0规范下使用。
可见Socket接口其实是TCP/IP网络的API接口函数,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(套接字)几乎可以处理任何在网络中需要传输的数据类型。
我们知道Visual C#和Visual C++的区别之一,就是Visual C#没有属于自己的类库,而Visual C++却是有的,Visual C#使用的类库是.Net框架为所有开发.Net平台程序语言提供的一个公用的类库——.Net FrameWork SDK。Visual C#主要网络功能主要使用.Net FrameWork SDK中的提供的二个命名空间“System.Net.Sockets”和“System.Net”。而实现Socket使用的是命名空间“System.Net.Sockets”中的Socket类。Visual C#通过创建Socket类的实例来实现Socket的托管版本。在Visual C#中创建完Socket实例后,可以通过此Socket实例的Bind方法绑定到网络中指定的终结点,也可以通过其Connect方法向指定的终结点建立的连接。连接创建完毕,就可以使用其Send或SendTo方法将数据发送到Socket;同样使用其的Receive或ReceiveFrom方法从Socket中读取数据。在Socket使用完毕后,请使用其的Shutdown方法禁用Socket,并使用Close方法关闭Socket。表01和表02是Socket类中的常用属性和方法及其简要说明。
属性 说明
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实现客户机程序的具体方法。
三.本文介绍程序的设计、调试、运行的软件环境:
(1).微软公司视窗2000服务器版
(2).Visual Studio .Net正式版,.Net FrameWork SDK版本号3705
四.利用Socket来传送数据:
Visual C#在使用Socket传送数据时要注意下列问题的解决方法:
1.创建Socket实例,使用此实例创建和远程终结点的连接,并判断连接是否成功建立。
2.发送数据到Socket,实现数据传送。
这些问题解决方法都可以在下面介绍代码中找到相对应的部分。由于下面的代码都有详细的注解,
这里就不详细介绍。下面是利用Socket传送数据的具体实现步骤:
1.启动Visual Studio .Net,并新建一个Visual C#项目,
项目名称为【利用Socket来发送数据】。
2.把Visual Studio .Net的当前窗口切换到【Form1.cs(设计)】窗口,
并从【工具箱】中的【Windows窗体组件】选项卡中往Form1窗体中拖入下列组件,并执行相应操作:
二个TextBox组件,一个用以输入远程主机的IP地址,一个用以输入往远程主机传送的数据。
一个StausBar组件,用以显示程序的运行状况。
一个ListBox组件,用以显示程序已传送的数据信息。
三个Label组件。
二个Button组件,名称分别为button1、button2,并在这二个组件被拖入窗体后,分别双击它们,则系统会在Form1.cs文件中自动产生这二个组件的Click事件对应的处理代码。
3.【解决方案资源管理器】窗口中,双击Form1.cs文件,进入Form1.cs文件的编辑界面。
4.以下面代码替代系统产生的InitializeComponent过程:
private void InitializeComponent ( )
{
this.label1 = new System.Windows.Forms.Label ( ) ;
this.textBox1 = new System.Windows.Forms.TextBox ( ) ;
this.button1 = new System.Windows.Forms.Button ( ) ;
this.label2 = new System.Windows.Forms.Label ( ) ;
this.textBox2 = new System.Windows.Forms.TextBox ( ) ;
this.listBox1 = new System.Windows.Forms.ListBox ( ) ;
this.statusBar1 = new System.Windows.Forms.StatusBar ( ) ;
this.label3 = new System.Windows.Forms.Label ( ) ;
this.button2 = new System.Windows.Forms.Button ( ) ;
this.SuspendLayout ( ) ;
this.label1.Location = new System.Drawing.Point ( 24 , 20 ) ;
this.label1.Name = "label1" ;
this.label1.Size = new System.Drawing.Size ( 74 , 30 ) ;
this.label1.TabIndex = 0 ;
this.label1.Text = "IP地址:" ;
this.textBox1.BorderStyle = System.Windows.
Forms.BorderStyle.FixedSingle ;
this.textBox1.Location = new System.Drawing.Point ( 94 , 18 ) ;
this.textBox1.Name = "textBox1" ;
this.textBox1.Size = new System.Drawing.Size ( 166 , 21 ) ;
this.textBox1.TabIndex = 1 ;
this.textBox1.Text = "" ;
this.button1.FlatStyle = System.Windows.Forms.FlatStyle.Flat ;
this.button1.Location = new System.Drawing.Point ( 280 , 14 ) ;
this.button1.Name = "button1" ;
this.button1.Size = new System.Drawing.Size ( 62 , 28 ) ;
this.button1.TabIndex = 2 ;
this.button1.Text = "连接" ;
this.button1.Click += new System.EventHandler ( this.button1_Click ) ;
this.label2.Location = new System.Drawing.Point ( 16 , 64 ) ;
this.label2.Name = "label2" ;
this.label2.TabIndex = 3 ;
this.label2.Text = "发送信息:" ;
this.textBox2.BorderStyle = System.Windows.
Forms.BorderStyle.FixedSingle ;
this.textBox2.Location = new System.Drawing.Point ( 94 , 58 ) ;
this.textBox2.Name = "textBox2" ;
this.textBox2.Size = new System.Drawing.Size ( 166 , 21 ) ;
this.textBox2.TabIndex = 4 ;
this.textBox2.Text = "" ;
this.listBox1.ItemHeight = 12 ;
this.listBox1.Location = new System.Drawing.Point ( 20 , 118 ) ;
this.listBox1.Name = "listBox1" ;
this.listBox1.Size = new System.Drawing.Size ( 336 , 160 ) ;
this.listBox1.TabIndex = 6 ;
this.statusBar1.Location = new System.Drawing.Point ( 0 , 295 ) ;
this.statusBar1.Name = "statusBar1" ;
this.statusBar1.Size = new System.Drawing.Size ( 370 , 22 ) ;
this.statusBar1.TabIndex = 7 ;
this.statusBar1.Text = "无连接" ;
this.label3.Location = new System.Drawing.Point ( 14 , 94 ) ;
this.label3.Name = "label3" ;
this.label3.Size = new System.Drawing.Size ( 128 , 23 ) ;
this.label3.TabIndex = 8 ;
this.label3.Text = "已经发送的信息:" ;
this.button2.FlatStyle = System.Windows.Forms.FlatStyle.Flat ;
this.button2.Location = new System.Drawing.Point ( 280 , 54 ) ;
this.button2.Name = "button2" ;
this.button2.Size = new System.Drawing.Size ( 62 , 28 ) ;
this.button2.TabIndex = 9 ;
this.button2.Text = "发送" ;
this.button2.Click += new System.EventHandler
( this.button2_Click ) ;
this.AutoScaleBaseSize = new System.Drawing.Size ( 6 , 14 ) ;
this.ClientSize = new System.Drawing.Size ( 370 , 317 ) ;
this.Controls.AddRange ( new System.Windows.Forms.Control[] {
this.button2 ,
this.statusBar1 ,
this.listBox1 ,
this.textBox2 ,
this.label2 ,
this.button1 ,
this.textBox1 ,
this.label1 ,
this.label3} ) ;
this.FormBorderStyle = System.
Windows.Forms.FormBorderStyle.FixedSingle ;
this.MaximizeBox = false ;
this.Name = "Form1" ;
this.Text = "利用Socket来发送数据" ;
this.ResumeLayout ( false ) ;
}
至此【利用Sokcet来传送数据】项目设计后的界面就完成了
5.在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 ;
6.在Form1的class代码区中加入下列代码,下列代码的作用是定义全局变量和创建全局使用的实例:
int port = 8000 ;
//定义侦听端口号
private TcpClient tcpc ;
//对服务器端创建TCP连接
private Socket stSend ;
//创建发送数据套接字
private bool tcpConnect = false ;
//定义标识符,用以表示TCP连接是否建立
7.用下列代码替换Form1.cs中的button1组件的Click事件对应的处理代码,下列代码的功能是初始化以创建的Socket实例,并向远程终结点提出连接申请,并判断连接是否建立:
private void button1_Click
( object sender , System.EventArgs e )
{
//以下代码是判断是否和远程终结点成功连接
try
{
stSend = new Socket ( AddressFamily.InterNetwork ,
SocketType.Stream , ProtocolType.Tcp ) ;
//初始化一个Socket实例
IPEndPoint tempRemoteIP = new IPEndPoint
( IPAddress.Parse ( textBox1.Text ) , port ) ;
//根据IP地址和端口号创建远程终结点
EndPoint epTemp = ( EndPoint ) tempRemoteIP ;
stSend.Connect ( epTemp ) ;
//连接远程主机的8000端口号
statusBar1.Text = "成功连接远程计算机!" ;
tcpConnect = true ;
button1.Enabled = false ;
button2.Enabled = true ;
}
catch ( Exception )
{
statusBar1.Text = "目标计算机拒绝连接请求!" ;
}
}
8.用下列代码替换Form1.cs中button2组件的Click事件对应的处理代码,下列代码的功能是通过已建立的连接,利用Socket来传送数据到远程主机。
private void button2_Click
( object sender , System.EventArgs e )
{
int iLength = textBox2.Text.Length ;
//获取要发送的数据的长度
Byte [ ] bySend = new byte [ iLength ] ;
//根据获取的长度定义一个Byte类型数组
bySend = System.Text.Encoding.Default.GetBytes
( textBox2.Text ) ;
//按照指定编码类型把字符串指定到指定的Byte数组
int i = stSend.Send ( bySend ) ;
//发送数据
listBox1.Items.Add ( textBox2.Text ) ;
}
9.用下列代码替换Form1.cs中“清理所有正在使用的资源。”对应的代码。其作用是在程序退出之前,判断连接状态,如果没有退出,则向远程主机发送控制码“STOP”,用以断开和远程主机的连接,并清除相应资源。所谓控制码就是网络应用程序之间彼此交换信息的一种自定义码子,应用程序通过接收、发送这些码子,可以明确网络应用程序的行为,保证执行的一致性,也就少了很多出错的几率。控制码在编写远程控制方面的应用程序时使用比较多。之所以要有这一步是因为在用Visual C#编写网络应用程序的时候,很多人都遇到这样的情况。当程序退出后,通过Windows的“资源管理器”看到的是进程数目并没有减少。这是因为程序中使用的线程可能并没有有效退出。虽然Thread类中提供了“Abort”方法用以中止进程,但并不能够保证成功退出。因为进程中使用的某些资源并没有回收。可见在某些情况下,依靠Visual C#的垃圾回收器也不能保证完全的回收资源,这时就需要我们自己手动回收资源的。下面就是手动回收资源采用的一种方法:
protected override void Dispose ( bool disposing )
{
if ( tcpConnect )
{
Byte [ ] bySend = new byte [ 4 ] ;
//根据字符串“STOP”长度来定义Byte数组
bySend = System.Text.Encoding.
Default.GetBytes ( "STOP" ) ;
int i = stSend.Send ( bySend ) ;
//发送控制码
stSend.Close ( ) ;
//关闭套接字
}
if ( disposing )
{
if ( components != null )
{
components.Dispose ( ) ;
}
}
base.Dispose ( disposing ) ;
}
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#实现网络功能是所必然要涉及到的如线程创建、线程销毁、资源回收和利用控制码控制程序运行状态等的实现方法。了解并掌握这些对我们编写功能更强、结构更复杂的网络应用程序是非常有帮助的。
最后要和诸位朋友谈一点小体会,就是在编写网络应用程序时,要非常细心,对应用程序在执行的各个环节都要考虑到位,因为在网络中会出现很多意想不到的问题,就是网络状态非常良好,也存在很多使用者方面的问题。如果在编写的程序中缺乏对这些意外的处理,就可能会导致整个应用程序出错,甚至崩溃。
至此在上述步骤都正确执行后,【利用Socket来传送数据】就全部完成了。