同步传输字符串
接下来考虑着一种情况,完成一个简单的文本通信:
(1).客户端将字符串发送到服务端,服务端接受字符串并显示
(2).服务端将字符串由英文的小写转换为大写,然后发回给客户端,客户端接受并显示.
客户端发送,服务端接受并输出
可以在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()方法也是同步的,只有当客户端发送数据的时候,服务端才会读取数据,执行此方法,否则会一直等待下去.
接下来编写客户端想服务端发送字符串的代码,与服务端类似,先获取连接到服务端的流,将字符串保存到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块中.
到现在为止,客户端已经能发送字符串到服务端,服务端能接受并显示.接下来,再进行进一步处理,使服务端将字符串有英文小写改为英文大写,然后发回给客户端,客户端接受并显示.此时服务端和客户端的角色和上面进行了一下对调:对于服务端来说,就好像刚才的客户端一样,将字符串写入到流中,而客户端则同服务端一样,接受并显示.
服务端:
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); } } }
上面的代码大家应该很熟悉了,主要的变化是转换字母大小写,并写入到流中的操作.
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缓存导致的文本边界问题.