本文转自:http://www.tracefact.net/CSharp-Programming/Network-Programming-Part2.aspx

服务端客户端通信

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

同步传输字符串

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

.1服务端程序

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

class Server {
static void Main(string[] args) {
const int BufferSize = ; // 缓存大小,8192字节 Console.WriteLine("Server is running ... ");
IPAddress ip = new IPAddress(new byte[] { , , , });
TcpListener listener = new TcpListener(ip, ); 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, , BufferSize);
Console.WriteLine("Reading data, {0} bytes ...", bytesRead); // 获得请求的字符串
string msg = Encoding.Unicode.GetString(buffer, , 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, , BufferSize);
msStream.Write(buffer, , bytesRead);
} while (bytesRead > ); buffer = msStream.GetBuffer();
string msg = Encoding.Unicode.GetString(buffer); 这里我没有使用这种方法,一个是因为不想关注在太多的细节上面,一个是因为对于字符串来说,8192字节已经很多了,我们通常不会传递这么多的文本。当使用Unicode编码时,8192字节可以保存4096个汉字和英文字符。使用不同的编码方式,占用的字节数有很大的差异,在本文最后面,有一段小程序,可以用来测试Unicode、UTF8、ASCII三种常用编码方式对字符串编码时,占用的字节数大小。 现在对客户端不做任何修改,然后运行先运行服务端,再运行客户端。结果我们会发现这样一件事:服务端再打印完“Client Connected!127.0.0.1: <-- 127.0.0.1:xxxxx”之后,再次被阻塞了,而没有输出“Reading data, {} 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", ); // 与服务器连接
} 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, , buffer.Length); // 发往服务器
Console.WriteLine("Sent: {0}", msg); // 按Q退出
}
} 现在再次运行程序,得到的输出为: // 服务端
Server is running ...
Start Listening ...
Client Connected!127.0.0.1: <-- 127.0.0.1:
Reading data, bytes ...
Received: "Welcome To TraceFact.Net"
输入"Q"键退出。
// 客户端
Client Running ...
Server Connected!127.0.0.1: --> 127.0.0.1:
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, , BufferSize);
Console.WriteLine("Reading data, {0} bytes ...", bytesRead); // 获得请求的字符串
string msg = Encoding.Unicode.GetString(buffer, , bytesRead);
Console.WriteLine("Received: {0}", msg);
} while (true); 然后启动多个客户端,在服务端应该可以看到下面的输出(客户端没有变化): Server is running ...
Start Listening ...
Client Connected!127.0.0.1: <-- 127.0.0.1:
Reading data, bytes ...
Received: "Welcome To TraceFact.Net"
Client Connected!127.0.0.1: <-- 127.0.0.1:
Reading data, 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, , BufferSize);
Console.WriteLine("Reading data, {0} bytes ...", bytesRead); // 获得请求的字符串
string msg = Encoding.Unicode.GetString(buffer, , 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, , buffer.Length); // 发往服务器
Console.WriteLine("Sent: {0}", msg);
}
} while (key != ConsoleKey.X); 接下来我们先运行服务端,然后再运行客户端,输入一些字符串,来进行测试,应该能够看到下面的输出结果: // 服务端
Server is running ...
Start Listening ...
Client Connected!127.0.0.1: <-- 127.0.0.1:
Reading data, bytes ...
Received: 欢迎访问我的博客:TraceFact.Net
Reading data, bytes ...
Received: 我们一起进步!
//客户端
Client Running ...
Server Connected!127.0.0.1: --> 127.0.0.1:
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服务端程序 我们接着再进行进一步处理,服务端将收到的字符串改为大写,然后回发,客户端接收后打印。此时它们的角色和上面完全进行了一下对调:对于服务端来说,就好像刚才的客户端一样,将字符串写入到流中;而客户端则同服务端一样,接收并打印。除此以外,我们最好对流的读写操作加上lock,现在我们直接看代码,首先看服务端: class Server {
static void Main(string[] args) {
const int BufferSize = ; // 缓存大小,8192Bytes
ConsoleKey key; Console.WriteLine("Server is running ... ");
IPAddress ip = new IPAddress(new byte[] { , , , });
TcpListener listener = new TcpListener(ip, ); 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, , BufferSize);
}
if (bytesRead == ) throw new Exception("读取到0字节");
Console.WriteLine("Reading data, {0} bytes ...", bytesRead); // 获得请求的字符串
string msg = Encoding.Unicode.GetString(buffer, , bytesRead);
Console.WriteLine("Received: {0}", msg); // 转换成大写并发送
msg = msg.ToUpper();
buffer = Encoding.Unicode.GetBytes(msg);
lock(streamToClient){
streamToClient.Write(buffer, , 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 = ; try {
client = new TcpClient();
client.Connect("localhost", ); // 与服务器连接
} 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, , buffer.Length); // 发往服务器
}
Console.WriteLine("Sent: {0}", msg); int bytesRead;
buffer = new byte[BufferSize];
lock(streamToServer){
bytesRead = streamToServer.Read(buffer, , BufferSize);
}
msg = Encoding.Unicode.GetString(buffer, , 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: --> 127.0.0.1:
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: <-- 127.0.0.1:
Reading data, 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 = ; i <= ; i++) {
if (i == ) {
buffer = Encoding.ASCII.GetBytes(str);
back = Encoding.ASCII.GetString(buffer, , buffer.Length);
mode = "ASCII";
} else if (i == ) {
buffer = Encoding.UTF8.GetBytes(str);
back = Encoding.UTF8.GetString(buffer, , buffer.Length);
mode = "UTF8";
} else {
buffer = Encoding.Unicode.GetBytes(str);
back = Encoding.Unicode.GetString(buffer, , buffer.Length);
mode = "Unicode";
} Console.WriteLine("Mode: {0}, String: {1}, Buffer.Length: {2}",
mode, str, buffer.Length); Console.WriteLine("Buffer:");
for (int j = ; j <= buffer.Length - ; j++) {
Console.Write(buffer[j] + " ");
} Console.WriteLine("\nRetrived: {0}\n", back);
}
}
} 输出为: Mode: ASCII, String: b, Buffer.Length:
Buffer:
Retrived: b Mode: UTF8, String: b, Buffer.Length:
Buffer:
Retrived: b Mode: Unicode, String: b, Buffer.Length:
Buffer:
Retrived: b Mode: ASCII, String: abcd, Buffer.Length:
Buffer:
Retrived: abcd Mode: UTF8, String: abcd, Buffer.Length:
Buffer:
Retrived: abcd Mode: Unicode, String: abcd, Buffer.Length:
Buffer:
Retrived: abcd Mode: ASCII, String: 乙, Buffer.Length:
Buffer:
Retrived: ? Mode: UTF8, String: 乙, Buffer.Length:
Buffer:
Retrived: 乙 Mode: Unicode, String: 乙, Buffer.Length:
Buffer:
Retrived: 乙 Mode: ASCII, String: 甲乙丙丁, Buffer.Length:
Buffer:
Retrived: ???? Mode: UTF8, String: 甲乙丙丁, Buffer.Length:
Buffer:
Retrived: 甲乙丙丁 Mode: Unicode, String: 甲乙丙丁, Buffer.Length:
Buffer:
Retrived: 甲乙丙丁 大体上可以得出这么几个结论:
•ASCII不能保存中文(貌似谁都知道=_-`)。
•UTF8是变长编码。在对ASCII字符编码时,UTF更省空间,只占1个字节,与ASCII编码方式和长度相同;Unicode在对ASCII字符编码时,占用2个字节,且第2个字节补零。
•UTF8在对中文编码时需要占用3个字节;Unicode对中文编码则只需要2个字节。

[转]C#网络编程(同步传输字符串) - Part.2的更多相关文章

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

    服务端客户端通信 在与服务端的连接建立以后,我们就可以通过此连接来发送和接收数据.端口与端口之间以流(Stream)的形式传输数据,因为几乎任何对象都可以保存到流中,所以实际上可以在客户端与服务端之间 ...

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

    本文转自:http://www.tracefact.net/CSharp-Programming/Network-Programming-Part3.aspx 这篇文章我们将前进一大步,使用异步的方式 ...

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

    这篇文章我们将前进一大步,使用异步的方式来对服务端编程,以使它成为一个真正意义上的服务器:可以为多个客户端的多次请求服务.但是开始之前,我们需要解决上一节中遗留的一个问题. 消息发送时的问题 这个问题 ...

  4. Python网络编程02 /基于TCP、UDP协议的socket简单的通信、字符串转bytes类型

    Python网络编程02 /基于TCP.UDP协议的socket简单的通信.字符串转bytes类型 目录 Python网络编程02 /基于TCP.UDP协议的socket简单的通信.字符串转bytes ...

  5. 猫哥网络编程系列:HTTP PEM 万能调试法

    注:本文内容较长且细节较多,建议先收藏再阅读,原文将在 Github 上维护与更新. 在 HTTP 接口开发与调试过程中,我们经常遇到以下类似的问题: 为什么本地环境接口可以调用成功,但放到手机上就跑 ...

  6. python select网络编程详细介绍

    刚看了反应堆模式的原理,特意复习了socket编程,本文主要介绍python的基本socket使用和select使用,主要用于了解socket通信过程 一.socket模块 socket - Low- ...

  7. C++11网络编程

    Handy是一个简洁优雅的C++11网络库,适用于linux与Mac平台.十行代码即可完成一个完整的网络服务器. 下面是echo服务器的代码: #include <handy/handy.h&g ...

  8. Python Socket 网络编程

    Socket 是进程间通信的一种方式,它与其他进程间通信的一个主要不同是:它能实现不同主机间的进程间通信,我们网络上各种各样的服务大多都是基于 Socket 来完成通信的,例如我们每天浏览网页.QQ ...

  9. python网络编程-socket编程

     一.服务端和客户端 BS架构 (腾讯通软件:server+client) CS架构 (web网站) C/S架构与socket的关系: 我们学习socket就是为了完成C/S架构的开发 二.OSI七层 ...

随机推荐

  1. oracle RAC的VIP和scan

    我们都知道Oracle RAC中每个节点都有一个虚拟IP,简称VIP,与公网IP在同一个网段. 没有VIP时,Oracle客户端是靠"TCP/IP协议栈超时"来判断服务器故障.而T ...

  2. 解决html表格中内容超出不强制换行和超出宽度自动隐藏并显示省略号

    在表格布局中经常会遇到因为表格内容长短的变化导致布局混乱的情况,这个时候我们可能会有为了布局稳定把单元格宽度写死的情况:但是我们设置了宽度却发现超出了宽度之后会自动变大,用css定义元素的overfl ...

  3. ID卡和IC卡

    1.ID卡 ID卡就是一种身份识别卡,卡内除了卡号之外,无任何加密功能. ID卡的工作原理:它是由卡.读卡器.后台控制器组成的. (1)读卡器通过天线发射射频信号 (2)当卡进入信号范围内后卡被激活 ...

  4. arcgis批量处理mxd定义服务中的路径

     >>> from arcpy import env... env.workspace=r"c:\165mxd"... out = r"c:\166mx ...

  5. EXCEL快速自动填充方法集锦

    EXCEL快速自动填充方法集锦 原文地址,转载请注明:http://www.cnblogs.com/croso/p/5396841.html 方法一: 名称框输入a1:a1000回车,1, ctrl+ ...

  6. AWS EC2 复制实例后,自定义指标无法显示数据

    从一个实例创建了一个AMI,然后通过这个AMI创建新的EC2实例,结果发票自定义指标不会显示: 系统一直在邮件中提示: print() on closed filehandle MDATA at Cl ...

  7. [Android]使用RecyclerView替代ListView(一)

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/4232560.html RecyclerView是一个比List ...

  8. Android Content Provider Guides

    Android Content Provider Guides Content Providers管理对结构化数据集的访问.它们包装数据,并且提供一种定义数据安全的机制. Content provid ...

  9. PHP学习之登录以及后台商品展示

    1.3用户登录 用户登录成功后跳转到商品显示页面 1.3.1设计界面 1.新建一个login.php页面,用来做用户的登录 2.登录业务原理 通过输入的用户名和密码查询对应的记录,表示登陆成功,否则登 ...

  10. UI控件(复习一下)

    如何修改控件状态• 可见,确实需要经常修改控件状态• 那如何去修改控件的状态呢?方法很简单➢ 每一个UI控件都是一个对象➢ 修改UI控件的状态,其实就是修改控件对象的属性➢ 比如修改UILabel显示 ...