[转]C#网络编程(异步传输字符串) – Part.3

正文转自:http://www.tracefact.net/CSharp-Programming/Network-Programming-Part3.aspx

眼看首文章我们拿前进一大步,使用异步的道来针对服务端编程,以使她变成一个确意义上的服务器:可以啊多个客户端的数请求服务。但是开始前,我们要缓解上同一节被留的一个问题。

消息发送时的题材

本条题材就是:客户端分两次为流动着形容副数据(比如字符串)时,我们主观上以随即有限破写副视为两破呼吁;然而服务端有或以立刻半赖联合起来就是等同长长的告,这在有限独请求间隔时间比较紧缺的情形下进一步如此。同样,也来或客户端起同样漫漫告,但是服务端将其就是两条告处理。下面列有了或的景,假而我们于客户端连接发送两长长的“Welcome
to Tracefact.net!”,则数达服务端时可能发生如此三种状态:

图片 1

NOTE:在此间我们设以ASCII编码方式,因为这时点的一个四方正好代表一个字节,而字符串到达最终后呢不断的0(因为byte是值类型,且最好小为0)。

方的率先种状况是无比优良之状,此时点滴修消息被视为两只单身请求由服务端完整地吸收。第二栽情形的示意图如下,此时同等长长的消息被用作两长长的信息接收了:

图片 2

如果于第三种植情形,则是简单漫长信息被合并成为了平漫漫吸收:

图片 3

假如您生充斥了齐一致首文章所附带的源码,那么用Client2.cs进行一下改,不经用户输入,而是采取一个for循环连续的殡葬三单请求过去,这样见面使求的间隔时间更短,下面是着重代码:

 

string msg = "Welcome to TraceFact.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;
     }
 }

 

 

 

运作服务端,然后还运行此客户端,你也许会见看到这么的结果:

图片 4

图片 5

可以看,尽管地方用信息分成了三修单独发送,但是服务端却以晚少久合并成了同等长达。对于这些情况,我们可以这么处理:就仿佛HTTP协议一样,在实际上的乞求与答内容前面包含了HTTP头,其中凡有的以及请求相关的音。我们呢可以签订自己的磋商,来化解这题目,比如说,对于地方的状态,我们虽好定义这样一个协议:

[length=XXX]:其中xxx是实际上发送的字符串长度(注意勿是配节数组buffer的长度),那么对于地方的求,则我们发送的多寡为:“[length=25]Welcome
to
TraceFact.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<string> outputList) {
         if (outputList == null)
             outputList = new List<string>();

         if (!String.IsNullOrEmpty(temp))
             input = temp + input;

         string output = "";
         string pattern = @"(?<=^\[length=)(\d+)(?=\])";
         int length;

         if (Regex.IsMatch(input, pattern)) {

             Match m = 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 < 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();
 }

 

运作方面的主次,可以收获如下的出口:

图片 6

OK,从者的出口可以看出,这个方法能够满足我们的渴求。对于这篇稿子最初步提出的问题,可以很自在地由此在者主意来化解,这里虽不再演示了,但于本文所附带的源代码含有修改了之先后。在此地花费了非常丰富之时间,接下吃咱回去正题,看下什么运用异步方式成就达同首被的次吧。

异步传输字符串

当直达同一首被,我们是因为简到繁,提到了劳务端的季种植方法:服务一个客户端的一个要、服务一个客户端的大都独请求、服务多独客户端的一个请、服务多单客户端的差不多单请求。我们说交得用里层的while循环交给一个新建的线程去让它们来就。除了这种艺术外,我们还好使相同种植更好之计――使用线程池中的线程来成功。我们得以采取BeginRead()、BeginWrite()等异步方法,同时被这BeginRead()方法与她的回调方法形成一个接近于while的尽循环:首先以首先叠循环中,接收至一个客户端后,调用BeginRead(),然后呢该措施供一个读取完成后底回调方法,然后以回调方法中对收取的字符进行处理,随后于回调方法被就调用BeginRead()方法,并传到回调方法本身。

由于程序实现功能与落得平等首完全相同,我就算不再细述了。而关于异步调用方法更多详细内容,可以参见
C#遭遇的委托以及事件(续)。

1.服务端的落实

public class RemoteClient {
     private TcpClient client;
     private NetworkStream streamToClient;
     private const int BufferSize = 8192;
     private byte[] buffer;
     private RequestHandler handler;

     public RemoteClient(TcpClient client) {
         this.client = client;

         // 打印连接到的客户端信息
        Console.WriteLine("\nClient Connected!{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(IAsyncResult ar) {
         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("Start Listening ...");

         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 = "Welcome to TraceFact.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("Server Connected!{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);
             } catch (Exception ex) {
                 Console.WriteLine(ex.Message);
                 break;
             }
         }

         lock (streamToServer) {
             AsyncCallback callBack = new AsyncCallback(ReadComplete);
             streamToServer.BeginRead(buffer, 0, BufferSize, callBack, null);
         }
     }

     public void SendMessage() {
         SendMessage(this.msg);
     }

     // 读取完成时的回调方法
    private void ReadComplete(IAsyncResult ar) {
         int bytesRead;

         try {
             lock (streamToServer) {
                 bytesRead = streamToServer.EndRead(ar);
             }
             if (bytesRead == 0) throw new Exception("读取到0字节");

             string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
             Console.WriteLine("Received: {0}", msg);
             Array.Clear(buffer, 0, buffer.Length);      // 清空缓存,避免脏读

            lock (streamToServer) {
                 AsyncCallback callBack = new AsyncCallback(ReadComplete);
                 streamToServer.BeginRead(buffer, 0, BufferSize, callBack, null);
             }
         } catch (Exception ex) {
             if(streamToServer!=null)
                 streamToServer.Dispose();
             client.Close();

             Console.WriteLine(ex.Message);
         }
     }
 }

在上面的SendMessage()方法中,我们让它连续发送了三条同样的消息,这么仅仅是为了测试,因为异步操作同样会出现上面说过的:服务器将客户端的请求拆开了的情况。最后我们在Main()方法中创建这个类型的实例,然后调用SendMessage()方法进行测试:

class Client {
     static void Main(string[] args) {
         ConsoleKey key;

         ServerClient client = new ServerClient();
         client.SendMessage();

         Console.WriteLine("\n\n输入\"Q\"键退出。");
         do {
             key = Console.ReadKey(true).Key;
         } while (key != ConsoleKey.Q);
     }
 }

是不是感觉很清爽?因为良好的代码重构,使得程序在复杂程度提高的情况下依然可以在一定程度上保持良好的阅读性。

 

3.序测试

说到底一步,我们事先运行服务端,接着连续运行两单客户端,看看它的出口分别是啊:

图片 7

图片 8

图片 9

世家好看,在服务端,我们得以连接多独客户端,同时为其服务;除本条之外,由接收的字节数发现,两单客户端都发生星星点点只请求被服务端合并成为了千篇一律长长的告,因为咱们以中在了特别的商,所以在服务端可以对这种场面开展完美的拍卖。

当客户端,我们并未以类似的拍卖,所以当客户端收到回复时,仍然会来请求合并的动静。对于这种场面,我怀念大家就明白该怎么处理了,就不再多废话了。

采取这种概念协议的办法发出其的长,但缺点也殊显著,如果客户了解了之协议,有意地输入[length=xxx],但是后面的尺寸也无兼容,此时次就算会拧。可摘的解决办法是本着“[”和“]”进行编码,当客户端有意输入这片单字符时,我们以它替换成“\[”和“\]”或者别的字符,在读取后再用它过来。

关于这个范例就到此结束了,剩下的星星单范例都以使异步传输的章程,并且会投入更多的情商内容。下一致篇我们将介绍如何向服务端发送或收到文件。

 

相关文章