在与服务端的链接创建之后,咱们就能够经过此链接来发送和接收数据。端口与端口之间以流(Stream)的形式传输数据,由于几乎任何对象均可以保存到流中,因此实际上能够在客户端与服务端之间传输任何类型的数据。对客户端来讲,往流中写入数据,即为向服务器传送数据;从流中读取数据,即为从服务端接收数据。对服务端来讲,往流中写入数据,即为向客户端发送数据;从流中读取数据,即为从客户端接收数据。html
咱们如今考虑这样一个任务:客户端打印一串字符串,而后发往服务端,服务端先输出它,而后将它改成大写,再回发到客户端,客户端接收到之后,最后再次打印一遍它。咱们将它分为两部分:一、客户端发送,服务端接收并输出;二、服务端回发,客户端接收并输出。编程
咱们能够在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、UTF八、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 {
// 获取一个链接,中断方法
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块中。
咱们接着再进行进一步处理,服务端将收到的字符串改成大写,而后回发,客户端接收后打印。此时它们的角色和上面彻底进行了一下对调:对于服务端来讲,就好像刚才的客户端同样,将字符串写入到流中;而客户端则同服务端同样,接收并打印。除此之外,咱们最好对流的读写操做加上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#网络编程有了一个初步的认识,能够说是刚刚入门了,后面的路还很长。本章的全部操做都是同步操做,像上面的代码也只是做为一个入门的范例,实际当中,一个服务端只能为一个客户端提供服务的状况是不存在的,下面就让咱们来看看上面所说的第四种状况,如何进行异步的服务端编程。
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: 甲乙丙丁
大致上能够得出这么几个结论:
出处:http://www.cnblogs.com/JimmyZhang/archive/2008/09/07/1286301.html