简介:
C#网络编程API包含在System.Net和System.Net.Sockets命名空间下,大部分网络操作都可以在其中找到相应的类来实现;包括Socket的创建和连接,网络流收发方法的封装,而且还封装了服务端类和客户端类,提供创建服务端和客户端的快速通道;
(一)Socket类
Socket类在System.Net.Sockets命名空间下,是最基本的网络操作类,其中封装了网络连接的创建和关闭,数据的收发,以及网络状态监控等一系列有用的功能;
示例(TCP):
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
class Test_Tcp
{
private Socket socket;
private void Server()
{
var server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 绑定服务端IP和端口,客户端通过这个地址连入
server.Bind(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9356));
// 开启服务监听,参数为最大挂起队列的连接数(并发),已连接的不计
server.Listen(1);
// 这是一个阻塞方法,接收客户端接入,返回客户端连接Socket
socket = server.Accept();
Console.WriteLine("Local : {0}\nRemote : {1}", socket.LocalEndPoint.ToString(), socket.RemoteEndPoint.ToString());
// 数据接收:这是一个异步过程
ReceiveAsync();
// 数据发送:这是一个阻塞方法
Write();
// 关闭客户端连接Socket和服务Socket
socket.Close();
server.Close();
}
}
Server()是一个简易的基于TCP连接的服务端开启方法,使用这个方法需要用到System.Net.Sockets和System.Net两个命名空间;
服务端开启分为4步:创建Socket、绑定IP和端口Bind()、开启监听Listen()、接入客户端Accept();
其中还有两个自定义方法,ReceiveAsync()和Write(),它们分别是异步数据接收、数据发送,它们的定义在后面可以看到。
另外,Socket的Accept()接入客户端连接方法也有异步版本BeginAccept(),它的用法类似于后面的BeginReceive(),多客户端系统一般都是用这种异步接入方式,在BeginAccept方法的回调方法中,维护一个客户端连接容器;
class Test_Tcp { private void Client() { socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.Connect("127.0.0.1", 9356); Console.WriteLine("{0}\n{1}", socket.LocalEndPoint.ToString(), socket.RemoteEndPoint.ToString()); ReceiveAsync(); Write(); socket.Close(); } }
Client()方法是相应的客户端开启方法,相对服务端来说代码更简单明了,而且不需绑定IP,端口也是自动分配,不过需注意Connect()方法中的IP和端口是服务端绑定的服务地址;ReceiveAysnc()、Write()和服务端中的两个方法是一样的;
using System.Text; class Test_Tcp { private const int BuffSize = 1024; private void ReceiveAsync() { if (socket == null || !socket.Connected) return; var Buff = new byte[BuffSize]; socket.BeginReceive(Buff, 0, BuffSize, SocketFlags.None, OnReceived, Buff); } private void OnReceived(IAsyncResult result) { if (socket == null || !socket.Connected) return; byte[] data = (byte[])result.AsyncState; int recLength = socket.EndReceive(result); Console.WriteLine("Length = {0}", recLength); if (recLength <= 0) return; ReceiveAsync(); string msg = Encoding.Default.GetString(data, 0, recLength); Console.WriteLine("Receive : {0}\n", msg); } private void Write() { while (socket != null && socket.Connected) { string msg = Console.ReadLine(); if (msg == "exit") break; byte[] buff = Encoding.Default.GetBytes(msg); socket.Send(buff); } } }
这3个方法是数据异步接收、数据接收回调、和聊天数据发送的方法,这里的字符串 - 字节序列转换,需要用到System.Text命名空间;
ReceiveAysnc()是开启异步数据接收方法,其中Socket.BeginReceive方法是Socket类中的异步接收方法,它需要一个字节缓冲区和一个回调方法作为参数;
OnRecieve()数据接收回调方法,在这个方法中,主要工作是开启新的异步接收方法RecieveAsync(),以及数据处理;这里的数据处理只是简单地转换为字符串,并打印到控制台,而一般在实际应用中,这里就收的数据data会用一个容器储存起来(一般是队列Queue),然后在其它地方从容器中取出数据,并进行复杂的处理;
Write()方法:在网络连接可用状态下,不断从控制台等待读取一行字符串,并将其转换为字节序列发送到socket,服务器在异步接收线程中会接收到数据,并触发回调方法OnRecieve(),控制台会看到打印的字符串;输入exit终结循环,Write()方法返回,接着关闭socket;
Socket.Send发送数据方法是一个阻塞方法,它同样也有异步版本BeginSend(),用法和BeginReceive()类似;
在实际项目应用中,数据的发送和接收会分别维护一个发送队列和接收队列,这样,应用层在调用数据发送方法时,只是把数据加入到发送队列,而不用等待发送完成,特别是数据量大的时候,等待发送的时间会影响到应用层性能;真正的数据发送用一个专门的线程不断从发送队列里面取数据并发送;数据接收也是类似维护一个接收队列。
程序入口:
class Test_Tcp { public void Run() { Console.WriteLine("Input 'c' or 's' :"); var key = Console.ReadKey(); Console.WriteLine(); if (key.Key == ConsoleKey.C) { Client(); } else if (key.Key == ConsoleKey.S) { Server(); } } } class Program { static int Main(string[] args) { new Test_Tcp().Run(); return 0; } }
(二)NetworkStream网络流
完全限定名System.Net.Sockets.NetworkStream网络流类,继承于Stream类,简单地理解就是对Socket读写网络数据的封装,用NetworkStream网络流封装Socket,可以简化数据的接收和发送;
NetworkStream的构造方法需要传入一个可用的网络Socket实例,然后就可以用流的方式替代Socket进行数据的读写;
var netStream = new NetworkStream(socket);
这里的socket必须网络连接成功(服务端连入客户端,客户端连接远程成功),才能用NetworkStream进行数据流的读写;
(三)UDP和心跳包
相比于TCP的可靠传输,UDP是一种非连接、不可靠的数据传输协议,它不需要建立连接,也就是说,服务端不需要监听Listen、接受连入Accept,而且Socket.Connected也不能用;Udp发送数据不需确定对方是否存在,网路是否可通,当然也无法确定对方是否收到(但可以手动发送返回包来通知对方),但是Udp相对Tcp的消耗也小;
Socket的构造方法第二、三个参数分别要设置为SocketType.Dgram、ProtocolType.Udp;
相比于TCP连接,UDP的客户端差别不大,但是在UDP服务端,由于没有客户端连接,数据的发送应该使用SendTo,这个方法要求传入一个客户端地址结构EndPoint表示目标终端,那么,在接收数据时,就应该保存好数据的来源地址;
那么,接收数据也应该用另一个版本ReceiveFrom,这个方法可以得到一个数据来源地址EndPoint,这时就可以保存要用到的EndPoint,这个EndPoint可以用一个容器来维护;
这两个方法在可靠连接Tcp中也可以用(但一般不这么用),另外它们都也有各自的异步版本,Begin开头的便是;
心跳包,顾名思义是一种在通信双方,定时发送一个特定的数据序列,一般3-10s,用来通知对方网络通信是正常的,一般这个数据序列短小、固定的;心跳包在Udp非连接协议中非常地必要,因为Udp没办法在没有数据接收的情况下确定网络状态;在Tcp中心跳包不是必要的,但是要求较高的项目中依然会应用心跳包;心跳超时(一般大于2倍的间隔时间),就表示网络通信失联;