zoukankan      html  css  js  c++  java
  • C#网络编程(同步传输字符串) Part.2

    C#网络编程(同步传输字符串) - Part.2

    服务端客户端通信

    在与服务端的连接建立以后,我们就可以通过此连接来发送和接收数据。端口与端口之间以流(Stream)的形式传输数据,因为几乎任何对象都可以保存到流中,所以实际上可以在客户端与服务端之间传输任何类型的数据。对客户端来说,往流中写入数据,即为向服务器传送数据;从流中读取数据,即为从服务端接收数据。对服务端来说,往流中写入数据,即为向客户端发送数据;从流中读取数据,即为从客户端接收数据。

    同步传输字符串

    我们现在考虑这样一个任务:客户端打印一串字符串,然后发往服务端,服务端先输出它,然后将它改为大写,再回发到客户端,客户端接收到以后,最后再次打印一遍它。我们将它分为两部分:1、客户端发送,服务端接收并输出;2、服务端回发,客户端接收并输出。

    1.客户端发送,服务端接收并输出

    1.1服务端程序

    我们可以在TcpClient上调用GetStream()方法来获得连接到远程计算机的流。注意这里我用了远程这个词,当在客户端调用时,它得到连接服务端的流;当在服务端调用时,它获得连接客户端的流。接下来我们来看一下代码,我们先看服务端(注意这里没有使用do/while循环):

    class Server {
        static void Main(string[] args) {
            const int BufferSize = 8192;    // 缓存大小,8192字节
                       
            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 ...");
           
            // 获取一个连接,中断方法
            TcpClient remoteClient = listener.AcceptTcpClient();
            // 打印连接到的客户端信息
            Console.WriteLine("Client Connected!{0} <-- {1}",
                remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);

            // 获得流,并写入buffer中
            NetworkStream streamToClient = remoteClient.GetStream();
            byte[] buffer = new byte[BufferSize];
            int bytesRead = streamToClient.Read(buffer, 0, BufferSize);
            Console.WriteLine("Reading data, {0} bytes ...", bytesRead);

            // 获得请求的字符串
            string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
            Console.WriteLine("Received: {0}", msg);

            // 按Q退出
        }
    }

    这段程序的上半部分已经很熟悉了,我就不再解释。remoteClient.GetStream()方法获取到了连接至客户端的流,然后从流中读出数据并保存在了buffer缓存中,随后使用Encoding.Unicode.GetString()方法,从缓存中获取到了实际的字符串。最后将字符串打印在了控制台上。这段代码有个地方需要注意:在能够读取的字符串的总字节数大于BufferSize的时候会出现字符串截断现象,因为缓存中的数目总是有限的,而对于大对象,比如说图片或者其它文件来说,则必须采用“分次读取然后转存”这种方式,比如这样:

    // 获取字符串
    byte[] buffer = new byte[BufferSize];
    int bytesRead;          // 读取的字节数
    MemoryStream msStream = new MemoryStream();
    do {
        bytesRead = streamToClient.Read(buffer, 0, BufferSize);
        msStream.Write(buffer, 0, bytesRead);
    } while (bytesRead > 0);

    buffer = msStream.GetBuffer();
    string msg = Encoding.Unicode.GetString(buffer);

    这里我没有使用这种方法,一个是因为不想关注在太多的细节上面,一个是因为对于字符串来说,8192字节已经很多了,我们通常不会传递这么多的文本。当使用Unicode编码时,8192字节可以保存4096个汉字和英文字符。使用不同的编码方式,占用的字节数有很大的差异,在本文最后面,有一段小程序,可以用来测试Unicode、UTF8、ASCII三种常用编码方式对字符串编码时,占用的字节数大小。

    现在对客户端不做任何修改,然后运行先运行服务端,再运行客户端。结果我们会发现这样一件事:服务端再打印完“Client Connected!127.0.0.1:8500 <-- 127.0.0.1:xxxxx”之后,再次被阻塞了,而没有输出“Reading data, {0} bytes ...”。可见,与AcceptTcpClient()方法类似,这个Read()方法也是同步的,只有当客户端发送数据的时候,服务端才会读取数据、运行此方法,否则它便会一直等待。

    1.2 客户端程序

    接下来我们编写客户端向服务器发送字符串的代码,与服务端类似,它先获取连接服务器端的流,将字符串保存到buffer缓存中,再将缓存写入流,写入流这一过程,相当于将消息发往服务端。

    class Client {
        static void Main(string[] args) {
            Console.WriteLine("Client Running ...");
            TcpClient client;

            try {
                client = new TcpClient();
                client.Connect("localhost", 8500);      // 与服务器连接
            } catch (Exception ex) {
                Console.WriteLine(ex.Message);
                return;
            }
            // 打印连接到的服务端信息
            Console.WriteLine("Server Connected!{0} --> {1}",
                client.Client.LocalEndPoint, client.Client.RemoteEndPoint);

            string msg = "\"Welcome To TraceFact.Net\"";
            NetworkStream streamToServer = client.GetStream();

            byte[] buffer = Encoding.Unicode.GetBytes(msg);     // 获得缓存
            streamToServer.Write(buffer, 0, buffer.Length);     // 发往服务器
            Console.WriteLine("Sent: {0}", msg);

            // 按Q退出
        }
    }

    现在再次运行程序,得到的输出为:

    // 服务端
    Server is running ...
    Start Listening ...
    Client Connected!127.0.0.1:8500 <-- 127.0.0.1:7847
    Reading data, 52 bytes ...
    Received: "Welcome To TraceFact.Net"
    输入"Q"键退出。
    // 客户端
    Client Running ...
    Server Connected!127.0.0.1:7847 --> 127.0.0.1:8500
    Sent: "Welcome To TraceFact.Net"
    输入"Q"键退出。

    再继续进行之前,我们假设客户端可以发送多条消息,而服务端要不断的接收来自客户端发送的消息,但是上面的代码只能接收客户端发来的一条消息,因为它已经输出了“输入Q键退出”,说明程序已经执行完毕,无法再进行任何动作。此时如果我们再开启一个客户端,那么出现的情况是:客户端可以与服务器建立连接,也就是netstat-a显示为ESTABLISHED,这是操作系统所知道的;但是由于服务端的程序已经执行到了最后一步,只能输入Q键退出,无法再采取任何的动作。

    回想一个上面我们需要一个服务器对应多个客户端时,对AcceptTcpClient()方法的处理办法,将它放在了do/while循环中;类似地,当我们需要一个服务端对同一个客户端的多次请求服务时,可以将Read()方法放入到do/while循环中

    现在,我们大致可以得出这样几个结论:

    • 如果不使用do/while循环,服务端只有一个listener.AcceptTcpClient()方法和一个TcpClient.GetStream().Read()方法,则服务端只能处理到同一客户端的一条请求。
    • 如果使用一个do/while循环,并将listener.AcceptTcpClient()方法和TcpClient.GetStream().Read()方法都放在这个循环以内,那么服务端将可以处理多个客户端的一条请求。
    • 如果使用一个do/while循环,并将listener.AcceptTcpClient()方法放在循环之外,将TcpClient.GetStream().Read()方法放在循环以内,那么服务端可以处理一个客户端的多条请求。
    • 如果使用两个do/while循环,对它们进行分别嵌套,那么结果是什么呢?结果并不是可以处理多个客户端的多条请求。因为里层的do/while循环总是在为一个客户端服务,因为它会中断在TcpClient.GetStream().Read()方法的位置,而无法执行完毕。即使可以通过某种方式让里层循环退出,比如客户端往服务端发去“exit”字符串时,服务端也只能挨个对客户端提供服务。如果服务端想执行多个客户端的多个请求,那么服务端就需要采用多线程。主线程,也就是执行外层do/while循环的线程,在收到一个TcpClient之后,必须将里层的do/while循环交给新线程去执行,然后主线程快速地重新回到listener.AcceptTcpClient()的位置,以响应其它的客户端。

    对于第四种情况,实际上是构建一个服务端更为通常的情况,所以需要专门开辟一个章节讨论,这里暂且放过。而我们上面所做的,即是列出的第一种情况,接下来我们再分别看一下第二种和第三种情况。

    对于第二种情况,我们按照上面的叙述先对服务端进行一下改动:

    do {
        // 获取一个连接,中断方法
        TcpClient remoteClient = listener.AcceptTcpClient();
        // 打印连接到的客户端信息
        Console.WriteLine("Client Connected!{0} <-- {1}",
            remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);

        // 获得流,并写入buffer中
        NetworkStream streamToClient = remoteClient.GetStream();
        byte[] buffer = new byte[BufferSize];
        int bytesRead = streamToClient.Read(buffer, 0, BufferSize);
        Console.WriteLine("Reading data, {0} bytes ...", bytesRead);

        // 获得请求的字符串
        string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
        Console.WriteLine("Received: {0}", msg);
    } while (true);

    然后启动多个客户端,在服务端应该可以看到下面的输出(客户端没有变化):

    Server is running ...
    Start Listening ...
    Client Connected!127.0.0.1:8500 <-- 127.0.0.1:8196
    Reading data, 52 bytes ...
    Received: "Welcome To TraceFact.Net"
    Client Connected!127.0.0.1:8500 <-- 127.0.0.1:8199
    Reading data, 52 bytes ...
    Received: "Welcome To TraceFact.Net"

    由第2种情况改为第3种情况,只需要将do向下挪动几行就可以了:

    // 获取一个连接,中断方法
    TcpClient remoteClient = listener.AcceptTcpClient();
    // 打印连接到的客户端信息
    Console.WriteLine("Client Connected!{0} <-- {1}",
        remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);
    // 获得流,并写入buffer中
    NetworkStream streamToClient = remoteClient.GetStream();

    do {
        byte[] buffer = new byte[BufferSize];
        int bytesRead = streamToClient.Read(buffer, 0, BufferSize);
        Console.WriteLine("Reading data, {0} bytes ...", bytesRead);

        // 获得请求的字符串
        string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
        Console.WriteLine("Received: {0}", msg);
    } while (true);

    然后我们再改动一下客户端,让它发送多个请求。当我们按下S的时候,可以输入一行字符串,然后将这行字符串发送到服务端;当我们输入X的时候则退出循环:

    NetworkStream streamToServer = client.GetStream();
    ConsoleKey key;
    Console.WriteLine("Menu: S - Send, X - Exit");
    do {
        key = Console.ReadKey(true).Key;

        if (key == ConsoleKey.S) {
            // 获取输入的字符串
            Console.Write("Input the message: ");
            string msg = Console.ReadLine();

            byte[] buffer = Encoding.Unicode.GetBytes(msg);     // 获得缓存
            streamToServer.Write(buffer, 0, buffer.Length);     // 发往服务器
            Console.WriteLine("Sent: {0}", msg);
        }
    } while (key != ConsoleKey.X);

    接下来我们先运行服务端,然后再运行客户端,输入一些字符串,来进行测试,应该能够看到下面的输出结果:

    // 服务端
    Server is running ...
    Start Listening ...
    Client Connected!127.0.0.1:8500 <-- 127.0.0.1:11004
    Reading data, 44 bytes ...
    Received: 欢迎访问我的博客:TraceFact.Net
    Reading data, 14 bytes ...
    Received: 我们一起进步!
    //客户端
    Client Running ...
    Server Connected!127.0.0.1:11004 --> 127.0.0.1:8500
    Menu: S - Send, X - Exit
    Input the message: 欢迎访问我的博客:TraceFact.Net
    Sent: 欢迎访问我的博客:TraceFact.Net
    Input the message: 我们一起进步!
    Sent: 我们一起进步!

    这里还需要注意一点,当客户端在TcpClient实例上调用Close()方法,或者在流上调用Dispose()方法,服务端的streamToClient.Read()方法会持续地返回0,但是不抛出异常,所以会产生一个无限循环;而如果直接关闭掉客户端,或者客户端执行完毕但没有调用stream.Dispose()或者TcpClient.Close(),如果服务器端此时仍阻塞在Read()方法处,则会在服务器端抛出异常:“远程主机强制关闭了一个现有连接”。因此,我们将服务端的streamToClient.Read()方法需要写在一个try/catch中。同理,如果在服务端已经连接到客户端之后,服务端调用remoteClient.Close(),则客户端会得到异常“无法将数据写入传输连接: 您的主机中的软件放弃了一个已建立的连接。”;而如果服务端直接关闭程序的话,则客户端会得到异常“无法将数据写入传输连接: 远程主机强迫关闭了一个现有的连接。”。因此,它们的读写操作必须都放入到try/catch块中。

    2.服务端回发,客户端接收并输出

    2.2服务端程序

    我们接着再进行进一步处理,服务端将收到的字符串改为大写,然后回发,客户端接收后打印。此时它们的角色和上面完全进行了一下对调:对于服务端来说,就好像刚才的客户端一样,将字符串写入到流中;而客户端则同服务端一样,接收并打印。除此以外,我们最好对流的读写操作加上lock,现在我们直接看代码,首先看服务端:

    class Server {
        static void Main(string[] args) {
            const int BufferSize = 8192;    // 缓存大小,8192Bytes
            ConsoleKey key;

            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 ...");

            // 获取一个连接,同步方法,在此处中断
            TcpClient remoteClient = listener.AcceptTcpClient();

            // 打印连接到的客户端信息
            Console.WriteLine("Client Connected!{0} <-- {1}",
                remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);

            // 获得流
            NetworkStream streamToClient = remoteClient.GetStream();
           
            do {
                // 写入buffer中
                byte[] buffer = new byte[BufferSize];
                int bytesRead;
                try {
                    lock(streamToClient){
                        bytesRead = streamToClient.Read(buffer, 0, BufferSize);
                    }
                    if (bytesRead == 0) throw new Exception("读取到0字节");
                    Console.WriteLine("Reading data, {0} bytes ...", bytesRead);

                    // 获得请求的字符串
                    string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
                    Console.WriteLine("Received: {0}", msg);

                    // 转换成大写并发送
                    msg = msg.ToUpper();                   
                    buffer = Encoding.Unicode.GetBytes(msg);
                    lock(streamToClient){
                        streamToClient.Write(buffer, 0, buffer.Length);
                    }
                    Console.WriteLine("Sent: {0}", msg);
                } catch (Exception ex) {
                    Console.WriteLine(ex.Message);
                    break;
                }                          
            } while (true);

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

    接下来是客户端:

    class Client {
        static void Main(string[] args) {
            Console.WriteLine("Client Running ...");
            TcpClient client;
            ConsoleKey key;
            const int BufferSize = 8192;

            try {
                client = new TcpClient();
                client.Connect("localhost", 8500);      // 与服务器连接
            } catch (Exception ex) {
                Console.WriteLine(ex.Message);
                return;
            }

            // 打印连接到的服务端信息
            Console.WriteLine("Server Connected!{0} --> {1}",
                client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
                       
            NetworkStream streamToServer = client.GetStream();         
            Console.WriteLine("Menu: S - Send, X - Exit");

            do {
                key = Console.ReadKey(true).Key;

                if (key == ConsoleKey.S) {
                    // 获取输入的字符串
                    Console.Write("Input the message: ");
                    string msg = Console.ReadLine();

                    byte[] buffer = Encoding.Unicode.GetBytes(msg);     // 获得缓存
                    try {
                        lock(streamToServer){
                            streamToServer.Write(buffer, 0, buffer.Length);     // 发往服务器
                        }
                        Console.WriteLine("Sent: {0}", msg);

                        int bytesRead;
                        buffer = new byte[BufferSize];                     
                        lock(streamToServer){
                            bytesRead = streamToServer.Read(buffer, 0, BufferSize);
                        }
                        msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
                        Console.WriteLine("Received: {0}", msg);

                    } catch (Exception ex) {
                        Console.WriteLine(ex.Message);
                        break;
                    }
                }
            } while (key != ConsoleKey.X);

            streamToServer.Dispose();
            client.Close();

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

    最后我们运行程序,然后输入一串英文字符串,然后看一下输出:

    // 客户端
    Client is running ...
    Server Connected!127.0.0.1:12662 --> 127.0.0.1:8500
    Menu: S - Send, X - Exit
    Input the message: Hello, I'm jimmy zhang.
    Sent: Hello, I'm jimmy zhang.
    Received: HELLO, I'M JIMMY ZHANG.

    // 服务端
    Server is running ...
    Start Listening ...
    Client Connected!127.0.0.1:8500 <-- 127.0.0.1:12662
    Reading data, 46 bytes ...
    Received: Hello, I'm jimmy zhang.
    Sent: HELLO, I'M JIMMY ZHANG.

    看到这里,我想你应该对使用TcpClient和TcpListener进行C#网络编程有了一个初步的认识,可以说是刚刚入门了,后面的路还很长。本章的所有操作都是同步操作,像上面的代码也只是作为一个入门的范例,实际当中,一个服务端只能为一个客户端提供服务的情况是不存在的,下面就让我们来看看上面所说的第四种情况,如何进行异步的服务端编程。

    附录:ASCII、UTF8、Uncicode编码下的中英文字符大小

    private static void ShowCode() {
        string[] strArray = { "b", "abcd", "乙", "甲乙丙丁" };
        byte[] buffer;
        string mode, back;

        foreach (string str in strArray) {

            for (int i = 0; i <= 2; i++) {
                if (i == 0) {
                    buffer = Encoding.ASCII.GetBytes(str);
                    back = Encoding.ASCII.GetString(buffer, 0, buffer.Length);
                    mode = "ASCII";
                } else if (i == 1) {
                    buffer = Encoding.UTF8.GetBytes(str);
                    back = Encoding.UTF8.GetString(buffer, 0, buffer.Length);
                    mode = "UTF8";
                } else {
                    buffer = Encoding.Unicode.GetBytes(str);
                    back = Encoding.Unicode.GetString(buffer, 0, buffer.Length);
                    mode = "Unicode";
                }

                Console.WriteLine("Mode: {0}, String: {1}, Buffer.Length: {2}",
                    mode, str, buffer.Length);

                Console.WriteLine("Buffer:");
                for (int j = 0; j <= buffer.Length - 1; j++) {
                    Console.Write(buffer[j] + " ");
                }

                Console.WriteLine("\nRetrived: {0}\n", back);
            }
        }
    }

    输出为:

    Mode: ASCII, String: b, Buffer.Length: 1
    Buffer: 98
    Retrived: b

    Mode: UTF8, String: b, Buffer.Length: 1
    Buffer: 98
    Retrived: b

    Mode: Unicode, String: b, Buffer.Length: 2
    Buffer: 98 0
    Retrived: b

    Mode: ASCII, String: abcd, Buffer.Length: 4
    Buffer: 97 98 99 100
    Retrived: abcd

    Mode: UTF8, String: abcd, Buffer.Length: 4
    Buffer: 97 98 99 100
    Retrived: abcd

    Mode: Unicode, String: abcd, Buffer.Length: 8
    Buffer: 97 0 98 0 99 0 100 0
    Retrived: abcd

    Mode: ASCII, String: 乙, Buffer.Length: 1
    Buffer: 63
    Retrived: ?

    Mode: UTF8, String: 乙, Buffer.Length: 3
    Buffer: 228 185 153
    Retrived: 乙

    Mode: Unicode, String: 乙, Buffer.Length: 2
    Buffer: 89 78
    Retrived: 乙

    Mode: ASCII, String: 甲乙丙丁, Buffer.Length: 4
    Buffer: 63 63 63 63
    Retrived: ????

    Mode: UTF8, String: 甲乙丙丁, Buffer.Length: 12
    Buffer: 231 148 178 228 185 153 228 184 153 228 184 129
    Retrived: 甲乙丙丁

    Mode: Unicode, String: 甲乙丙丁, Buffer.Length: 8
    Buffer: 50 117 89 78 25 78 1 78
    Retrived: 甲乙丙丁

    大体上可以得出这么几个结论:

    • ASCII不能保存中文(貌似谁都知道=_-`)。
    • UTF8是变长编码。在对ASCII字符编码时,UTF更省空间,只占1个字节,与ASCII编码方式和长度相同;Unicode在对ASCII字符编码时,占用2个字节,且第2个字节补零。
    • UTF8在对中文编码时需要占用3个字节;Unicode对中文编码则只需要2个字节。
  • 相关阅读:
    CSS浮动(float、clear)通俗讲解
    JAVA 类的加载
    数据库操作 delete和truncate的区别
    正则表达式 匹配相同数字
    Oracle EBS OM 取消订单
    Oracle EBS OM 取消订单行
    Oracle EBS OM 已存在的OM订单增加物料
    Oracle EBS OM 创建订单
    Oracle EBS INV 创建物料搬运单头
    Oracle EBS INV 创建物料搬运单
  • 原文地址:https://www.cnblogs.com/JimmyZhang/p/1286301.html
Copyright © 2011-2022 走看看