.net网络编程之一:Socket编程
在.net下进行网络编程其实也相对比较简单,因为在.net类库已经提供了大量封装好的类。在.net下网络编程比较底层的类是System.Net.Sockets.Socket类,这个类提供了丰富的方法和属性,并且还提供了异步数据传输支持。
对Socket类做一个简单的介绍,它有如下常见方法:
public Socket Accept ():为新建连接创建新的 Socket。
public void Bind (EndPoint localEP):使 Socket 与一个本地终结点相关联。
public void Close ():关闭 Socket 连接并释放所有关联的资源。注意这个方法有冲载方法。
public void Connect (EndPoint remoteEP):建立与远程主机的连接。注意这个方法有重载方法。
public void Disconnect (bool reuseSocket):关闭套接字连接并是否允许重用套接字。
public void Listen (int backlog):将 Socket 置于侦听状态。
public int Receive (byte[] buffer):接收来自绑定的 Socket 的数据。注意这个方法有重载方法。
public int ReceiveFrom (byte[] buffer,ref EndPoint remoteEP):接收数据报并存储源终结点。注意这个方法有重载方法。
public int Send (byte[] buffer):将数据发送到连接的 Socket。注意这个方法有重载方法。
public void SendFile (string fileName):将文件和可选数据异步发送到连接的 Socket。注意这个方法有重载方法。
public int SendTo (byte[] buffer,EndPoint remoteEP):将数据发送到特定终结点。注意这个方法有重载方法。
public void Shutdown (SocketShutdown how):禁用某 Socket 上的发送和接收。
说明:
因为在网络传输时传输的数据都是二进制形式的(表现为字节数组),所以如果要传输类似于中文这样的双字节字符就需要在传输之前用合适的编码转换成字节数组,然后接收方按照发送方的编码将接收到字节数组转换成字符串。另外,注意接收数据的时候是先声明了一个字节数组,然后将接收到的数据保存到字节数组中,这个方法有个返回值表示实际接收了多少字节数据。这是因为数组是不可变的,假如声明了一个1024字节大小的数组,而发送方仅发送1字节数据,而接收方并不直到发送方发送的数据量把整个1024字节当作发送发送的数据的话,最终会发生错误。
要实现一个服务器端的面向连接的Socket用于接收客户端的请求的话,有如下步骤:
(1)首先根据IP地址和端口号实例化一个Socket,注意端口要要大于1024并且不要使用特殊端口号,要大于1024的原因是1024以下的端口号已经被指派了,而1433、3306这样的端口号已经被用作SQL Server和MySQL的默认端口号了,若指定为这些端口号容易发生冲突。
(3)接着调用Bind()方法进行绑定,然后再调用Listen()方法用于监听,Listen()方法的参数用于指定监听的队列大小,也就是最多可容纳的等待接受的传入连接数。
(4)再调用Accept()方法,调用这个方法之后会是程序处于阻塞状态,直至有客户端连接为止。当有客户端连接,这个方法将会返回一个新的Socket,使用这个Socket与客户端进行通讯。
(5)使用Accept()方法返回的新Socket的Send()方法就可以向客户端发送数据了,还可以使用这个新Socket的Receive()接收客户端的数据。
(6)最后终止与客户端会话时,注意使用ShutDown()方法关闭Socket连接,并且使用Close()方法释放所占用的资源。
使用Socket类编写客户端的Socket程序步骤如下:
(1)首先指定远程主机和端口号实例化Socket类,注意连接的端口号一定要与服务器监听的端口号一致。
(2)接着调用Connect()方法连接远程主机。
(3)连接到远程主机之后就可以调用Send()方法向服务器发送请求了,然后可以调用Receive()方法接收服务器响应数据,注意如果是发送的类似于中文这样的双字节字符串的话,还需要按照服务器响应的字符串编码将字节数组转换成字符串。
最后终止与客户端会话时,注意使用ShutDown()方法关闭Socket连接,并且使用Close()方法释放所占用的资源。
需要特别说明的是上面是建立连接式Socket(如建立TCP协议Socket)的一般步骤,如果是建立非连接式Socket(如UDP协议Socket)就不需要进行监听端口了,直接使用ReceiveFrom()方法就能接收来自指定主机端口的数据,用SendTo()方法就能向直接主机端口发送数据。
下面一个例子来演示Socket的编程,在这个例子里服务器端可以处理多个客户端请求并响应,具体的做法是每接收到一个客户端请求就新建一个线程来与客户端通讯,主程序依然继续监听,在例子中新建的与客户端通讯的Socket类仅处理客户端的日期时间请求(为了演示,这里做了简化),根据客户端的请求的不同发送日期时间中响应信息到客户端,客户端接收到数据之后在控制台显示。
服务器端主要由两个类组成:ServerSocket类和SocketThread类,ServerSocket类用于监听客户端连接请求,SocketThread类用于接收日期时间显示请求并做出应答。
ServerSocket类代码如下(ServerSocket类和SocketThread类位于同一个类库项目中):
- using System;
- using System.Net;
- using System.Net.Sockets;
- using System.Text;
- namespace ServerSocket
- {
- /// <summary>
- /// Socket监听服务器,用于监听客户端连接请求
- /// 作者:周公
- /// 编写时间:2009-03-18
- /// </summary>
- public class ServerSocket:IDisposable
- {
- Socket listener = null;
- /// <summary>
- /// 开始监听指定端口
- /// </summary>
- public void StartListening(int port)
- {
- // Data buffer for incoming data.
- byte[] bytes = new Byte[1024];
- //以运行服务器端程序所在的机器为服务器监听客户端连接
- IPHostEntry ipHostInfo = Dns.Resolve(Dns.GetHostName());
- IPAddress ipAddress = ipHostInfo.AddressList[0];
- IPEndPoint localEndPoint = new IPEndPoint(ipAddress, port);
- //创建一个TCP/IP Socket用于监听客户端连接
- Socket listener = new Socket(AddressFamily.InterNetwork,
- SocketType.Stream, ProtocolType.Tcp);
- try
- {
- //先绑定要监听的主机和端口
- listener.Bind(localEndPoint);
- //再开始监听,并且指定监听队列的最大长度
- listener.Listen(10);
- //开始监听连接
- while (true)
- {
- Console.WriteLine("等待客户端连接...");
- //线程将一直阻塞直到有新的客户端连接
- Socket handler = listener.Accept();
- //启用一个新的线程用于处理客户端连接
- //这样主线程还可以继续接受客户端连接
- SocketThread socketThread = new SocketThread(handler);
- }
- }
- catch (Exception e)
- {
- Console.WriteLine(e.ToString());
- }
- }
- public static int Main(String[] args)
- {
- ServerSocket server = new ServerSocket();
- server.StartListening(11000);
- return 0;
- }
- #region IDisposable 成员
- public void Dispose()
- {
- if (listener != null)
- {
- listener.Shutdown(SocketShutdown.Both);
- listener.Close();
- }
- }
- #endregion
- }
- }
SocketThread类代码如下:
- using System;
- using System.Collections.Generic;
- using System.Text;
- using System.Threading;
- using System.Net.Sockets;
- namespace ServerSocket
- {
- /// <summary>
- /// 用于处理客户端请求的Socket
- /// 作者:周公
- /// 编写时间:2009-03-18
- /// </summary>
- public class SocketThread:IDisposable
- {
- private Socket socket;
- private Thread thread;
- private bool isListening = false;
- private string text;
- /// <summary>
- /// 构造方法
- /// </summary>
- /// <param name="socket">用于处理客户端应答的Socket</param>
- public SocketThread(Socket socket)
- {
- this.socket = socket;
- isListening = true;
- thread = new Thread(new ThreadStart(Work));
- thread.Start();
- }
- public void Work()
- {
- byte[] buffer=new byte[1024];
- while (isListening)
- {
- int receivedLength = socket.Receive(buffer);
- text=System.Text.Encoding.UTF8.GetString(buffer,0,receivedLength);
- //<EOF>是自定义的协议,表示中止消息交流
- if (text.IndexOf("<EOF>") > -1)
- {
- isListening=false;
- socket.Send(new byte[] { 0 });
- }
- else
- {
- //Console.WriteLine("接收到的数据:" + text);
- //根据客户端的请求获取相应的响应信息
- string message = GetMessage(text);
- //将响应信息以字节的方式发送到客户端
- socket.Send(Encoding.UTF8.GetBytes(message));
- }
- }
- }
- private string GetMessage(string request)
- {
- string message = string.Empty;
- //Console.WriteLine("Message=" + request);
- switch (request)
- {
- case "date": message = "服务器日期:"+DateTime.Now.ToString("yyyy-MM-dd"); break;
- case "time": message ="服务器时间:"+ DateTime.Now.ToString("HH:mm:ss"); break;
- case "datetime": message = "服务器日期时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); break;
- case "year": message = "服务器年份:" + DateTime.Now.Year.ToString(); break;
- case "month": message = "服务器月份:" + DateTime.Now.Month.ToString(); break;
- case "day": message = "这是本月第" + DateTime.Now.Day.ToString()+"天"; break;
- default: message = "不正确的参数"; break;
- }
- return message;
- }
- #region IDisposable 成员
- public void Dispose()
- {
- isListening = false;
- if (thread != null)
- {
- if (thread.ThreadState != ThreadState.Aborted)
- {
- thread.Abort();
- }
- thread = null;
- }
- if (socket != null)
- {
- socket.Shutdown(SocketShutdown.Both);
- socket.Close();
- }
- }
- #endregion
- }
- }
客户端请求的Socket代码如下(与前面两个类位于不同的控制台项目中):
- using System;
- using System.Net;
- using System.Net.Sockets;
- using System.Text;
- namespace ServerSocket
- {
- /// <summary>
- /// 客户端请求的Socket包装类
- /// 作者:周公
- /// 编写时间:2009-03-18
- /// </summary>
- public class ClientSocket:IDisposable
- {
- private Socket sender = null;
- private bool isListening = false;
- //定义用于接收服务器响应的存储区.
- private byte[] bytes = new byte[1024];
- //用于终止Soket的消息
- private string shutDownMessage = "<EOF>";
- public ClientSocket()
- {
- try
- {
- //设置要连接的主机信息并使用11000作为监听端口.
- IPHostEntry ipHostInfo = Dns.GetHostEntry(Dns.GetHostName());
- IPAddress ipAddress = ipHostInfo.AddressList[0];
- IPEndPoint remoteEP = new IPEndPoint(ipAddress, 11000);
- // 创建一个TCP/IP协议的socket连接
- sender = new Socket(AddressFamily.InterNetwork,
- SocketType.Stream, ProtocolType.Tcp);
- sender.Connect(remoteEP);
- isListening = true;
- }
- catch (Exception ex)
- {
- Console.WriteLine(ex.ToString());
- }
- }
- public void StartClient()
- {
- // 连接到远程主机,并捕获所有信息
- try
- {
- Console.WriteLine("连接到主机{0}",
- sender.RemoteEndPoint.ToString());
- OutParameters();
- string consoleMessage = Console.ReadLine();
- while (isListening&&!string.IsNullOrEmpty(consoleMessage))
- {
- consoleMessage = consoleMessage.ToLower();
- if (consoleMessage == "bye")
- {
- SendShutDownMessage();
- }
- else
- {
- string resultMessage=SendMessage(consoleMessage);
- Console.WriteLine(resultMessage);
- OutParameters();
- consoleMessage = Console.ReadLine();
- //Console.WriteLine("consoleMessage=" + consoleMessage);
- }
- }
- }
- catch (ArgumentNullException ane)
- {
- Console.WriteLine("参数异常 : {0}", ane.ToString());
- }
- catch (SocketException se)
- {
- Console.WriteLine("出现Socket异常: {0}", se.ToString());
- }
- catch (Exception e)
- {
- Console.WriteLine("出现了异常 : {0}", e.ToString());
- }
- }
- /// <summary>
- /// 向远程主机发送信息
- /// </summary>
- /// <param name="message">要发送的信息</param>
- public string SendMessage(string message)
- {
- byte[] buffer = Encoding.UTF8.GetBytes(message);
- sender.Send(buffer);
- int count=sender.Receive(bytes);
- return Encoding.UTF8.GetString(bytes, 0, count);
- }
- /// <summary>
- /// 向服务器发送关闭Socket信息,并中止与服务器的连接
- /// </summary>
- public void SendShutDownMessage()
- {
- SendMessage(shutDownMessage);
- Console.WriteLine("已经关闭与服务器的连接");
- isListening = false;
- Environment.Exit(0);
- }
- private void OutParameters()
- {
- Console.WriteLine("参数说明:");
- Console.WriteLine("获取服务器日期:date");
- Console.WriteLine("获取服务器时间:time");
- Console.WriteLine("获取服务器日期时间:datetime");
- Console.WriteLine("获取服务器年份:year");
- Console.WriteLine("获取服务器月份:month");
- Console.WriteLine("获取服务器天数:day");
- Console.WriteLine("关闭连接:bye");
- Console.WriteLine("请输入你要进行的操作:");
- }
- public static int Main(String[] args)
- {
- ClientSocket client = new ClientSocket();
- client.StartClient();
- return 0;
- }
- #region IDisposable 成员
- public void Dispose()
- {
- isListening = false;
- if (sender != null)
- {
- sender.Shutdown(SocketShutdown.Both);
- sender.Close();
- }
- }
- #endregion
- }
- }
上面的三个类位于两个控制台项目中ServerSocket类和SocketThread类位于同一个类库项目中,其中ClientSocket类位于另一个控制台项目中,编译之后会生成两个exe文件,运行的时候首先运行服务器端exe程序以便监听,再运行客户端exe程序。这个程序运行的效果如下:
注意,在终止程序的时候首先在客户端向服务器发送一个自定义的字符串“<EOF>”,在客户端使用命令行参数“bye”就是发送这个字符串的,这样就会正确终止服务器端响应Socket及正确关闭客户端请求的Socket,否则有可能抛出异常。
以上仅仅是演示了如何使用Socket进行编程,如果要传输文件的话,可能需要更多类似于关闭Socket的自定义字符串用以控制了。另外使用Socket来传输数据是比较高效的,但是传输控制相对来说要麻烦一点,针对网络中一些特殊场合的数据传输,可以使用特定的传输协议,在这个系列的后续文章中会继续介绍网络编程的有关知识。程序代码稍后整理之后提供下载。
目前有关在.net下进行网络编程的文章较少,而偶尔有需要进行网络编程的项目需求,所以打算研究一下.net下的网络编程,抱着交流技术的态度,整理成文章与大家分享。不过在网络编程方面,本人经验几乎没有经验,如果大家发现有不当之处,望不吝赐教。
此文中的源代码可以到:http://download.csdn.net/source/1117938下载。
来源http://blog.csdn.net/zhoufoxcn/archive/2009/03/18/4000301.aspx