码迷,mamicode.com
首页 > 其他好文 > 详细

同步传输字符串

时间:2016-04-19 19:46:25      阅读:300      评论:0      收藏:0      [点我收藏+]

标签:


同步传输字符串

 

接下来考虑着一种情况,完成一个简单的文本通信:

(1).客户端将字符串发送到服务端,服务端接受字符串并显示

(2).服务端将字符串由英文的小写转换为大写,然后发回给客户端,客户端接受并显示.

 

客户端发送,服务端接受并输出

 

1.服务端程序

 

可以在TcpClient上调用GetStream()方法来获得连接到远程计算机的网络流NetworkStream.当在客户端调用时,它获得连接服务端的流;当在服务端调用时,它获得连接客户端的流.

 

先看服务端的代码实现:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
using System.Net;
using System.Net.Sockets;
namespace Server
{
    class Program
    {
        static void Main(string[] args)
        {
            const int BufferSize = 8192;//缓存大小,8192字节
 
            Console.WriteLine("Server is running...");
            IPAddress ip = new IPAddress(new byte[] { 192, 168, 3, 19 });
            TcpListener listener = new TcpListener(ip,1621);
 
            listener.Start();//开始监听
 
            Console.WriteLine("Start Listening...");
 
            //获取一个连接,中断方法
            TcpClient remoteClient = listener.AcceptTcpClient();
 
            //打印连接到的客户端信息
            Console.WriteLine("Client Connected ! Local:{0}<-- Client:{1}",remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint);
 
            //获得流,并写入buffer中
 
            NetworkStream streamToClient = remoteClient.GetStream();
            byte[] buffer = new byte[BufferSize];
            int byteRead = streamToClient.Read(buffer,0,BufferSize);
 
            //获得请求的字符串
            string msg = Encoding.Unicode.GetString(buffer,0,byteRead);
            Console.WriteLine("Received: {0} [{1}bytes]",msg,byteRead);
 
            //按Q退出
            Console.WriteLine("\n\n按Q退出\n\n");
            ConsoleKey key;
            do
            {
                key = Console.ReadKey(true).Key;
            } while (key!=ConsoleKey.Q);
        }
    }
}


等一下,这里楼主有个问题需要问一下,在使用netstat命令查看端口时,你知道根据端口的状态判断哪些端口是可以用来监听的,那些端口不能用来监听吗?


 

这段程序的上半部分上一次说过了,remoteClient.GetStream()方法获取到了连接至客户端的流,然后从流中读出数据并保存在了buffer,随后使用Encoding.Unicode.GetString()方法,从缓存中获取到实际的字符串.最后将字符串显示在了控制台中.这段代码有个地方需要注意:如果能够读取的字符串的总字节数大于BufferSize,就会出现字符串截断现象,只能读取到不完整的字符串.这是因为缓存的字节数是有限的,在本例中是8192.如果传递的数据字节数比较大,例如图片,音频,文件,则必须采用”分次读取然后转存”的方式,代码如下:

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


咱们没有使用”分次读取转存”的方式,为啥呢?因为:楼主的水平有限,不想误人子弟.

 

说实话,8192已经很多了.当使用Unicode编码时,8192字节可以保存4096个汉字和英文字符.使用不同的编码方式,占用的字节数有很大的差异.

 

现在不对客户端在任何修改,先调试运行服务器,在运行客户端,会发现服务端在打印完”Client Connected ! Local:192.168.3.19:1621<-- Client:192.168.3.19:4044”之后,再次被阻塞了,没有继续运行,也没有输出”Reading data,{0} bytes...”等任何字符.可见,AcceptTcpClient()方法类似,Read()方法也是同步的,只有当客户端发送数据的时候,服务端才会读取数据,执行此方法,否则会一直等待下去.

 

2.客户端程序

 

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

        static void Main(string[] args)
        {
            #region MyRegion
            /*
 
            Console.WriteLine("Client is running...");
            TcpClient client;
            for (int i = 0; i <= 2; i++)
            {
                try
                {
                    client = new TcpClient();
                    //与服务器建立连接
                    client.Connect(IPAddress.Parse("192.168.3.19"), 1621);
 
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                    return;
                }
                //打印连接到的服务端信息
                Console.WriteLine("Server Connected ! Local:{0} -->Server:{1}", client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
            }
            //按Q退出
            Console.WriteLine("\n\n输入\"Q\"键退出. ");
            ConsoleKey key;
            do
            {
                key = Console.ReadKey(true).Key;
            } while (key != ConsoleKey.Q);*/
            #endregion
            Console.WriteLine("Client is running...");
            TcpClient client;
            try
            {
                client = new TcpClient();
                //与服务器建立连接
                client.Connect(IPAddress.Parse("192.168.1.120"),1621);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                return;
            }
 
            //打印连接到的服务端信息
            Console.WriteLine("Server Connected! Local:{0}-->Server:{1}",client.Client.LocalEndPoint,client.Client.RemoteEndPoint);
 
            string msg = "Hello,readers!";
 
            NetworkStream streamToServer = client.GetStream();
 
            byte[] buffer = Encoding.Unicode.GetBytes(msg);//获得缓存
            streamToServer.Write(buffer,0,buffer.Length);
 
            Console.WriteLine("Sent: {0}",msg);
 
            //按Q退出
            Console.WriteLine("\n\n按Q退出");
            ConsoleKey key;
            do
            {
                key = Console.ReadKey(true).Key;
            } while (key!=ConsoleKey.Q);
 
        }


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

//服务端:
Server is running...
Start Listening...
Client Connected ! Local:192.168.1.120:1621<-- Client:192.168.1.120:5465
Received: Hello,readers! [28bytes]
//客户端:
Client is running...
Server Connected! Local:192.168.1.120:5465-->Server:192.168.1.120:1621
Sent: Hello,readers!


 

可以看到,已经成功的发送和接受了一串字符串,是不是有点成就感了?QQ不过如此了了的事.但不要高兴的太早,对于即时通信程序来说,在客户端和服务端之间,应该是可以不间断的接受和发送消息的.但是上面的代码只能接受客户端发送的一条消息,因为代码已经运行完毕,控制外也已经输出了”按Q退出”.无法再继续执行任何的后续动作.此时如果在开启一个客户端,那么出现的情况是:客户端可以与服务器建立连接,也就是”netstat -a”显示为ESTABLISHED,这是操作系统所知道的;但是由于服务端的程序已经执行到了最后一步,只能输入Q退出,无法再执行任何动作.

 

回想一下,前面说过,当一个服务端需要接受多个客户端连接时,所采用的处理办法是:AcceptTcpClient()方法放在一个while循环中.类似的,当服务端需要同一个客户端的多次请求进行处理时,可以将Read()方法也放入到一个do/while循环中.

 

综合起来就只有四种情况:

 

第一种:如果不使用do/while循环,服务端只有一个listener.AcceptTcpClient()方法和一个TcpClient.GetStream().Read()方法,则服务端只能处理来自一个客户端的一条请求.

 

第二种:如果使用一个do/while循环,并将listener.AcceptTcpClient(0方法和TcpClient.GetStream().Read()方法放在循环中,那么服务端将可以处理多个客户端的一条请求.

 

第三种:使用一个do/while循环,并将listener.AcceptTcpClient()方法放在循环内,TcpClient.GetStream().Read()方法放在循环外,那么可以处理一个客户端的多条请求.

 

第四种:使用两个do/while循环,对它们分别进行嵌套,那么结果是啥?你肯定会说,可以处理多个客户端的多个请求.事实上不是这样的.因为内层的do/while循环总是在为一个客户端服务,它会中断在TcpClient.GetStream().Read()方法的位置,而无法执行完毕.即时可以通过某种方式让内层循环退出,例如,当客户端向服务端发送”exit”字符串时,服务端也只能挨个对客户端提供服务.如果服务端想并行的对多个客户端的多个请求进行服务,那么服务端就需要采用多线程.主线程,即执行外层的do/while循环的线程,它在AcceptTcpClient()获取到一个TcpClient之后,必须将内层的do/while循环交给其他的线程去处理,然后主线程快速的重新回到listener.AcceptTcpClient()的位置,来响应其他的客户端?明白了吗?是不是有点晕,楼主也有点晕,没关系.咱们一个一个讲解.

 

咱们先来看第二种和第三种情况.

对于第二种情况,按照上面描述的那些对代码做一些改动,服务端代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
using System.Net;
using System.Net.Sockets;
using System.IO;
 
namespace Server
{
    class Program
    {
        static void Main(string[] args)
        {
            const int BufferSize = 8192;//缓存大小,8192字节
 
            Console.WriteLine("Server is running...");
            IPAddress ip = new IPAddress(new byte[] { 192, 168, 1, 120 });
            TcpListener listener = new TcpListener(ip,1621);
 
            listener.Start();//开始监听
 
            Console.WriteLine("Start Listening...");
            do
            {
                //获取一个连接,中断方法
                TcpClient remoteClient = listener.AcceptTcpClient();
                //打印连接到的客户端信息
                Console.WriteLine("Client Connected ! Local:{0}<-- Client:{1}", remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);
 
                //获得流,并写入buffer中
                NetworkStream streamToClient = remoteClient.GetStream();
                byte[] buffer = new byte[BufferSize];
                int bytesRead = streamToClient.Read(buffer,0,BufferSize);
 
                //获得请求的字符串
                string msg = Encoding.Unicode.GetString(buffer,0,bytesRead);
                Console.WriteLine("Received: {0} [{1}bytes]",msg,bytesRead);
 
            } while (true);
            /*
            //获取一个连接,中断方法
            TcpClient remoteClient = listener.AcceptTcpClient();
 
            //打印连接到的客户端信息
            Console.WriteLine("Client Connected ! Local:{0}<-- Client:{1}",remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint);
 
            //获得流,并写入buffer中
 
            NetworkStream streamToClient = remoteClient.GetStream();
            byte[] buffer = new byte[BufferSize];
            int byteRead = streamToClient.Read(buffer,0,BufferSize);
 
            //获得请求的字符串
            string msg = Encoding.Unicode.GetString(buffer,0,byteRead);
            Console.WriteLine("Received: {0} [{1}bytes]",msg,byteRead);
 
            //按Q退出
            Console.WriteLine("\n\n按Q退出\n\n");
            ConsoleKey key;
            do
            {
                key = Console.ReadKey(true).Key;
            } while (key!=ConsoleKey.Q);
            */
           /* //获取字符串
            byte buffer = new byte[BufferSize];
            int bytesRead;
            MemoryStream ms = new MemoryStream();
            do
            {
                bytesRead = streamToClient.Read(buffer,0,BufferSize);
            } while (bytesRead>0);*/
        }
    }
}


然后启动多个客户端程序,在服务端可以看到这样的情况:

Server is running...
Start Listening...
Client Connected ! Local:192.168.1.120:1621<-- Client:192.168.1.120:6737
Received: Hello,readers! [28bytes]
Client Connected ! Local:192.168.1.120:1621<-- Client:192.168.1.120:6742
Received: Hello,readers! [28bytes]
Client Connected ! Local:192.168.1.120:1621<-- Client:192.168.1.120:6754
Received: Hello,readers! [28bytes]


 

现在将第二种情况变为第三种情况,只需要将do向下挪动几行就可以了:

            //获取一个连接,中断方法
            TcpClient remoteClient = listener.AcceptTcpClient();
            //打印连接到的客户端信息
            Console.WriteLine("Client Connected ! Local:{0}<-- Client:{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);
 
                //获得请求的字符串
                string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
                Console.WriteLine("Received: {0} [{1}bytes]", msg, bytesRead);
 
            } while (true);


然后再改动一下客户端,让它可以连续发送多个字符串到到服务器.当按下回车的时候发送字符串,输入”Q”的时候,跳出循环:

            Console.WriteLine("Client is running...");
            TcpClient client;
            try
            {
                client = new TcpClient();
                //与服务器建立连接
                client.Connect(IPAddress.Parse("192.168.1.120"), 1621);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                return;
            }
 
            //打印连接到的服务端信息
            Console.WriteLine("Server Connected! Local:{0}-->Server:{1}", client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
 
 
 
            NetworkStream streamToServer = client.GetStream();
 
            string msg;
            do
            {
                Console.Write("Sent:");
                msg = Console.ReadLine();
                if (!String.IsNullOrEmpty(msg) && msg != "Q")
                {
                    byte[] buffer = Encoding.Unicode.GetBytes(msg);
                    try
                    {
                        //发往服务器
                        streamToServer.Write(buffer, 0, buffer.Length);
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message);
                        return;
                    }
                }
            } while (msg != "Q");


接下来先运行服务端,再运行客户端,输入以下字符串来进行测试,应该能够看到预期的结果.

 

注意:如果再开启一个客户端,该客户端虽然可以成功的连接服务端,也可以发送字符串,但是服务端不会做任何的处理和显示.

 

这里有一点需要注意:当客户端在TcpClient实例上调用Close()方法,或者在流上调用Didpose()方法时,服务端的streamToClient.Read()方法会持续返回0,但是不抛出异常,所以会产生一个无限循环.服务端不断的刷新显示”Received: [0 bytes]”,因此,服务端在调用streamToClient.Read()方法后,应加上一个如下判断:

                int bytesRead = streamToClient.Read(buffer, 0, BufferSize);
                if (bytesRead==0)
                {
                    Console.WriteLine("Client offline");
                    break;
                }


如果直接关闭掉客户端,或者客户端执行完毕但没有调用stream.Dispose()或者TcpClient.Close(),切服务端此时仍阻塞在Read()方法处,则会在服务端抛出异常:未经处理的异常:  System.IO.IOException: 无法从传输连接中读取数据:远程主机强迫关闭了一个现有的连接。

 

因此,服务端的streamToClient.Read()方法需要写在一个try/catch.下面是改进的服务端代码:

            do
            {
                try
                {
                    byte[] buffer = new byte[BufferSize];
                    int bytesRead = streamToClient.Read(buffer, 0, BufferSize);
                    if (bytesRead == 0)
                    {
                        Console.WriteLine("Client offline");
                        break;
                    }
                    //获得请求的字符串
                    string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
                    Console.WriteLine("Received: {0} [{1}bytes]", msg, bytesRead);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                    break;
                }
            } while (true);


同样的,如果服务端在连接到客户端之后调用remoteClient.Close(),则客户端在调用streamToServer.Write()时也会抛出异常.因此,他们的读写操作都必须放入try/catch块中.

 

 

服务端发送,客户端接受并显示

 

1.服务端程序

 

到现在为止,客户端已经能发送字符串到服务端,服务端能接受并显示.接下来,再进行进一步处理,使服务端将字符串有英文小写改为英文大写,然后发回给客户端,客户端接受并显示.此时服务端和客户端的角色和上面进行了一下对调:对于服务端来说,就好像刚才的客户端一样,将字符串写入到流中,而客户端则同服务端一样,接受并显示.

 

服务端:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
using System.Net;
using System.Net.Sockets;
using System.IO;
 
namespace Server
{
    class Program
    {
        static void Main(string[] args)
        {
            const int BufferSize = 8192;//缓存大小,8192字节
 
            Console.WriteLine("Server is running...");
            IPAddress ip = new IPAddress(new byte[] { 192,168,1,120});
 
            TcpListener listener = new TcpListener(ip, 1621);
            //开始监听
            listener.Start();
            Console.WriteLine("Start Listening...");
 
            //获取一个连接,中断方法
            TcpClient remoteClient = listener.AcceptTcpClient();
 
            //打印连接到的客户端信息
            Console.WriteLine("Client Connected! Local:{0}<--Client:{1}",remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint);
 
            //获得流
            NetworkStream streamToClient = remoteClient.GetStream();
 
            do
            {
                try
                {
                    byte[] buffer = new byte[BufferSize];
                    int bytesRead = streamToClient.Read(buffer,0,BufferSize);
                    if (bytesRead==0)
                    {
                        Console.WriteLine("Client offline");
                        break;
                    }
                    //获得请求的字符串
                    string msg = Encoding.Unicode.GetString(buffer,0,bytesRead);
                    Console.WriteLine("Received: {0} [{1} bytes]",msg,bytesRead);
 
                    //转换为大写
 
                    msg = msg.ToUpper();
                    buffer = Encoding.Unicode.GetBytes(msg);
                    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();
 
            //按Q退出
            Console.WriteLine("\n\n按Q退出");
            ConsoleKey key;
            do
            {
                key = Console.ReadKey(true).Key;
            } while (key!=ConsoleKey.Q);
        }
    }
}


上面的代码大家应该很熟悉了,主要的变化是转换字母大小写,并写入到流中的操作.

 

2.客户端代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
 
namespace Client
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("CLient is running...");
            TcpClient client;
            const int BufferSize = 8192;
 
            try
            {
                client = new TcpClient();
                //与服务器建立连接
                client.Connect(IPAddress.Parse("192.168.3.19"), 9322);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                return;
            }           
            //打印连接到的服务端信息
            Console.WriteLine("Server Connected! Local: {0} --> Server: {1}",client.Client.LocalEndPoint,client.Client.RemoteEndPoint);
 
            NetworkStream streamToServer = client.GetStream();
 
            string msg;
 
            do
            {
                Console.Write("Sent:");
                msg = Console.ReadLine();
 
                if (!string.IsNullOrEmpty(msg)&&msg!="Q")
                {
                    byte[] buffer = Encoding.Unicode.GetBytes(msg);
                    try
                    {
                        //发往服务器
                        streamToServer.Write(buffer,0,buffer.Length);
                        int bytesRead;
                        buffer = new byte[BufferSize];
 
                        //接受并显示服务器回传的字符串
                        bytesRead = streamToServer.Read(buffer,0,BufferSize);
 
                        if (bytesRead==0)
                        {
                            Console.WriteLine("Server offline");
                            break;
                        }
                        msg = Encoding.Unicode.GetString(buffer,0,bytesRead);
                        Console.WriteLine("Received: {0} [{1}bytes]",msg,bytesRead);
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message);
                        break;
                    }
                }
            } while (msg!="Q");
            streamToServer.Dispose();
            client.Close();
        }
    }
}
 


先运行一下服务端,然后运行客户端,就会看到相应的输出.

 

 

这样大家应该会对C#的网络编程有了一定得了解,当然了,这是只是网络编程中的皮毛.因为到目前为止,咱们的操作都是同步操作,上面的代码只能作为入门使用.在实际中,一个服务端只能为一个客户端提供服务的情况几乎不存在.

 

下面咱们要看异步的网络编程之前,先学习一下在不同的编码方式中英文的大小,以及TCP缓存导致的文本边界问题.


同步传输字符串

标签:

原文地址:http://blog.csdn.net/shanyongxu/article/details/51189335

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!