Visual C#托管Socket的实现方法(一)
Socket就是套接字,它是网络编程中最常用遇到的概念和工具。在TCP/IP网络中,传送和接收数据就会经常使用到Socket,由于使用Socket能够在网络上处理复杂数据,所以在各种网络应用程序中,涉及到数据传送和接收,一般都会使用Socket,可见要掌握网络编程,精通Socket是非常重要。由于Socket本身的复杂性,决定了掌握它是比较困难的。Visual C#是微软公司推荐的开发.Net平台应用程序的主要语言,随着.Net的深入人心,目前很多有远见的公司都把以前的软件转向了.Net平台。掌握网络编程始终是学习一种开发语言的重点,这一点对于Visual C#也同样如此。Visual C#实现网络功能其关键也是掌握托管Socket的使用方法。本文就来详细介绍Visual C#中利用托管Socket实现网络数据传送和接收的实现方法及其注意事项。
一.简介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 | 将Socket置于侦听状态 |
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传送数据时要注意下列问题的解决方法:
- 创建Socket实例,使用此实例创建和远程终结点的连接,并判断连接是否成功建立。
- 发送数据到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来传送数据】项目设计后的界面就完成了,具体如图01所示:
图01:【利用Sokcet来传送数据】项目的设计界面
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);
}
至此在上述步骤都正确执行后,【利用Socket来传送数据】就全部完成了。
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 ;