C#网络编程基本概念和操作Part3.docx
- 文档编号:2471068
- 上传时间:2023-05-03
- 格式:DOCX
- 页数:14
- 大小:122.53KB
C#网络编程基本概念和操作Part3.docx
《C#网络编程基本概念和操作Part3.docx》由会员分享,可在线阅读,更多相关《C#网络编程基本概念和操作Part3.docx(14页珍藏版)》请在冰点文库上搜索。
C#网络编程基本概念和操作Part3
C#网络编程(异步传输字符串)-Part.3
这篇文章我们将前进一大步,使用异步的方式来对服务端编程,以使它成为一个真正意义上的服务器:
可以为多个客户端的多次请求服务。
但是开始之前,我们需要解决上一节中遗留的一个问题。
消息发送时的问题
这个问题就是:
客户端分两次向流中写入数据(比如字符串)时,我们主观上将这两次写入视为两次请求;然而服务端有可能将这两次合起来视为一条请求,这在两个请求间隔时间比较短的情况下尤其如此。
同样,也有可能客户端发出一条请求,但是服务端将其视为两条请求处理。
下面列出了可能的情况,假设我们在客户端连续发送两条“WelcometoT!
”,则数据到达服务端时可能有这样三种情况:
NOTE:
在这里我们假设采用ASCII编码方式,因为此时上面的一个方框正好代表一个字节,而字符串到达末尾后为持续的0(因为byte是值类型,且最小为0)。
上面的第一种情况是最理想的情况,此时两条消息被视为两个独立请求由服务端完整地接收。
第二种情况的示意图如下,此时一条消息被当作两条消息接收了:
而对于第三种情况,则是两条消息被合并成了一条接收:
如果你下载了上一篇文章所附带的源码,那么将Client2.cs进行一下修改,不通过用户输入,而是使用一个for循环连续的发送三个请求过去,这样会使请求的间隔时间更短,下面是关键代码:
string msg= "WelcometoTraceFact.Net!
";
for (int i=0;i<=2;i++){
byte[]buffer= Encoding.Unicode.GetBytes(msg); //获得缓存
try {
streamToServer.Write(buffer,0,buffer.Length); //发往服务器
Console.WriteLine("Sent:
{0}",msg);
} catch (Exception ex){
Console.WriteLine(ex.Message);
break;
}
}
运行服务端,然后再运行这个客户端,你可能会看到这样的结果:
可以看到,尽管上面将消息分成了三条单独发送,但是服务端却将后两条合并成了一条。
对于这些情况,我们可以这样处理:
就好像HTTP协议一样,在实际的请求和应答内容之前包含了HTTP头,其中是一些与请求相关的信息。
我们也可以订立自己的协议,来解决这个问题,比如说,对于上面的情况,我们就可以定义这样一个协议:
[length=XXX]:
其中xxx是实际发送的字符串长度(注意不是字节数组buffer的长度),那么对于上面的请求,则我们发送的数据为:
“[length=25]WelcometoTraceFact.Net!
”。
而服务端接收字符串之后,首先读取这个“元数据”的内容,然后再根据“元数据”内容来读取实际的数据,它可能有下面这样两种情况:
NOTE:
我觉得这里借用“元数据”这个术语还算比较恰当,因为“元数据”就是用来描述数据的数据。
∙“[“”]”中括号是完整的,可以读取到length的字节数。
然后根据这个数值与后面的字符串长度相比,如果相等,则说明发来了一条完整信息;如果多了,那么说明接收的字节数多了,取出合适的长度,并将剩余的进行缓存;如果少了,说明接收的不够,那么将收到的进行一个缓存,等待下次请求,然后将两条合并。
∙“[”“]”中括号本身就不完整,此时读不到length的值,因为中括号里的内容被截断了,那么将读到的数据进行缓存,等待读取下次发送来的数据,然后将两次合并之后再按上面的方式进行处理。
接下来我们来看下如何来进行实际的操作,实际上,这个问题已经不属于C#网络编程的内容了,而完全是对字符串的处理。
所以我们不再编写服务端/客户端代码,直接编写处理这几种情况的方法:
public class RequestHandler {
private string temp= string.Empty;
public string[]GetActualString(string input){
return GetActualString(input, null);
}
private string[]GetActualString(string input,List
if (outputList== null)
outputList= new List
if (!
String.IsNullOrEmpty(temp))
input=temp+input;
string output= "";
string pattern=@"(?
<=^\[length=)(\d+)(?
=\])";
int length;
if (Regex.IsMatch(input,pattern)){
Matchm= Regex.Match(input,pattern);
//获取消息字符串实际应有的长度
length=Convert.ToInt32(m.Groups[0].Value);
//获取需要进行截取的位置
int startIndex=input.IndexOf(']')+1;
//获取从此位置开始后所有字符的长度
output=input.Substring(startIndex);
if (output.Length==length){
//如果output的长度与消息字符串的应有长度相等
//说明刚好是完整的一条信息
outputList.Add(output);
temp= "";
} else if (output.Length //如果之后的长度小于应有的长度, //说明没有发完整,则应将整条信息,包括元数据,全部缓存 //与下一条数据合并起来再进行处理 temp=input; //此时程序应该退出,因为需要等待下一条数据到来才能继续处理 } else if (output.Length>length){ //如果之后的长度大于应有的长度, //说明消息发完整了,但是有多余的数据 //多余的数据可能是截断消息,也可能是多条完整消息 //截取字符串 output=output.Substring(0,length); outputList.Add(output); temp= ""; //缩短input的长度 input=input.Substring(startIndex+length); //递归调用 GetActualString(input,outputList); } } else { //说明“[”,“]”就不完整 temp=input; } return outputList.ToArray(); } } 这个方法接收一个满足协议格式要求的输入字符串,然后返回一个数组,这是因为如果出现多次请求合并成一个发送过来的情况,那么就将它们全部返回。 随后简单起见,我在这个类中添加了一个静态的Test()方法和PrintOutput()帮助方法,进行了一个简单的测试,注意我直接输入了length=13,这个是我提前计算好的。 public static void Test(){ RequestHandler handler= new RequestHandler(); string input; //第一种情况测试-一条消息完整发送 input= "[length=13]明天中秋,祝大家节日快乐! "; handler.PrintOutput(input); //第二种情况测试-两条完整消息一次发送 input= "明天中秋,祝大家节日快乐! "; input= String.Format ("[length=13]{0}[length=13]{0}",input); handler.PrintOutput(input); //第三种情况测试A-两条消息不完整发送 input= "[length=13]明天中秋,祝大家节日快乐! [length=13]明天中秋"; handler.PrintOutput(input); input= ",祝大家节日快乐! "; handler.PrintOutput(input); //第三种情况测试B-两条消息不完整发送 input= "[length=13]明天中秋,祝大家"; handler.PrintOutput(input); input= "节日快乐! [length=13]明天中秋,祝大家节日快乐! "; handler.PrintOutput(input); //第四种情况测试-元数据不完整 input= "[leng"; handler.PrintOutput(input); //不会有输出 input= "th=13]明天中秋,祝大家节日快乐! "; handler.PrintOutput(input); } //用于测试输出 private void PrintOutput(string input){ Console.WriteLine(input); string[]outputArray=GetActualString(input); foreach (string output in outputArray){ Console.WriteLine(output); } Console.WriteLine(); } 运行上面的程序,可以得到如下的输出: OK,从上面的输出可以看到,这个方法能够满足我们的要求。 对于这篇文章最开始提出的问题,可以很轻松地通过加入这个方法来解决,这里就不再演示了,但在本文所附带的源代码含有修改过的程序。 在这里花费了很长的时间,接下来让我们回到正题,看下如何使用异步方式完成上一篇中的程序吧。 异步传输字符串 在上一篇中,我们由简到繁,提到了服务端的四种方式: 服务一个客户端的一个请求、服务一个客户端的多个请求、服务多个客户端的一个请求、服务多个客户端的多个请求。 我们说到可以将里层的while循环交给一个新建的线程去让它来完成。 除了这种方式以外,我们还可以使用一种更好的方式――使用线程池中的线程来完成。 我们可以使用BeginRead()、BeginWrite()等异步方法,同时让这BeginRead()方法和它的回调方法形成一个类似于while的无限循环: 首先在第一层循环中,接收到一个客户端后,调用BeginRead(),然后为该方法提供一个读取完成后的回调方法,然后在回调方法中对收到的字符进行处理,随后在回调方法中接着调用BeginRead()方法,并传入回调方法本身。 由于程序实现功能和上一篇完全相同,我就不再细述了。 而关于异步调用方法更多详细内容,可以参见 C#中的委托和事件(续)。 1.服务端的实现 当程序越来越复杂的时候,就需要越来越高的抽象,所以从现在起我们不再把所有的代码全部都扔进Main()里,这次我创建了一个RemoteClient类,它对于服务端获取到的TcpClient进行了一个包装: public class RemoteClient { private TcpClient client; private NetworkStream streamToClient; private const int BufferSize=8192; private byte[]buffer; private RequestHandler handler; public RemoteClient(TcpClientclient){ this.client=client; //打印连接到的客户端信息 Console.WriteLine("\nClientConnected! {0}<--{1}", client.Client.LocalEndPoint,client.Client.RemoteEndPoint); //获得流 streamToClient=client.GetStream(); buffer= new byte[BufferSize]; //设置RequestHandler handler= new RequestHandler(); //在构造函数中就开始准备读取 AsyncCallback callBack= new AsyncCallback(ReadComplete); streamToClient.BeginRead(buffer,0,BufferSize,callBack, null); } //再读取完成时进行回调 private void ReadComplete(IAsyncResultar){ int bytesRead=0; try { lock (streamToClient){ bytesRead=streamToClient.EndRead(ar); Console.WriteLine("Reading data,{0}bytes...",bytesRead); } if (bytesRead==0) throw new Exception("读取到0字节"); string msg= Encoding.Unicode.GetString(buffer,0,bytesRead); Array.Clear(buffer,0,buffer.Length); //清空缓存,避免脏读 string[]msgArray=handler.GetActualString(msg); //获取实际的字符串 //遍历获得到的字符串 foreach (string m in msgArray){ Console.WriteLine("Received: {0}",m); string back=m.ToUpper(); //将得到的字符串改为大写并重新发送 byte[]temp= Encoding.Unicode.GetBytes(back); streamToClient.Write(temp,0,temp.Length); streamToClient.Flush(); Console.WriteLine("Sent: {0}",back); } //再次调用BeginRead(),完成时调用自身,形成无限循环 lock (streamToClient){ AsyncCallback callBack= new AsyncCallback(ReadComplete); streamToClient.BeginRead(buffer,0,BufferSize,callBack, null); } } catch(Exception ex){ if(streamToClient! =null) streamToClient.Dispose(); client.Close(); Console.WriteLine(ex.Message); //捕获异常时退出程序 } } } 随后,我们在主程序中仅仅创建TcpListener类型实例,由于RemoteClient类在构造函数中已经完成了初始化的工作,所以我们在下面的while循环中我们甚至不需要调用任何方法: class Server { static void Main(string[]args){ Console.WriteLine("Server is running..."); IPAddress ip= new IPAddress(new byte[]{127,0,0,1}); TcpListener listener= new TcpListener(ip,8500); listener.Start(); //开始侦听 Console.WriteLine("StartListening..."); while (true){ //获取一个连接,同步方法,在此处中断 TcpClient client=listener.AcceptTcpClient(); RemoteClient wapper= new RemoteClient(client); } } } 好了,服务端的实现现在就完成了,接下来我们再看一下客户端的实现: 2.客户端的实现 与服务端类似,我们首先对TcpClient进行一个简单的包装,使它的使用更加方便一些,因为它是服务端的客户,所以我们将类的名称命名为ServerClient: public class ServerClient { private const int BufferSize=8192; private byte[]buffer; private TcpClient client; private NetworkStream streamToServer; private string msg= "WelcometoTraceFact.Net! "; public ServerClient(){ try { client= new TcpClient(); client.Connect("localhost",8500); //与服务器连接 } catch (Exception ex){ Console.WriteLine(ex.Message); return; } buffer= new byte[BufferSize]; //打印连接到的服务端信息 Console.WriteLine("ServerConnected! {0}-->{1}", client.Client.LocalEndPoint,client.Client.RemoteEndPoint); streamToServer=client.GetStream(); } //连续发送三条消息到服务端 public void SendMessage(string msg){ msg= String.Format("[length={0}]{1}",msg.Length,msg); for (int i=0;i<=2;i++){ byte[]temp= Encoding.Unicode.GetBytes(msg); //获得缓存 try { streamToServer.Write(temp,0,temp.Length); //发往服务器 Console.WriteLine("Sent: {0}",msg); } cat
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- C#网络编程基本概念和操作 Part3 C# 网络 编程 基本概念 操作
![提示](https://static.bingdoc.com/images/bang_tan.gif)