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


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()方法也是同步的,只有当客户端发送数据的时候,服务端才会读取数据、运行此方法,否则它便会一直等待。

客户端程序

接下来我们编写客户端向服务器发送字符串的代码,与服务端类似,它先获取连接服务器端的流,将字符串保存到 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 块中。

版权声明:本文为YES开发框架网发布内容,转载请附上原文出处连接
张国生
上一篇:Part2
评论列表

发表评论

评论内容
昵称:
关联文章