您的位置:首页 > 理论基础 > 计算机网络

c#网络编程学习笔记02_Tcp编程(中)_简单的同步tcp聊天程序

2015-10-19 13:13 846 查看
/*

写一个同步tcp程序,功能为,客户端发送一个字符串给服务器,服务器将字符串打印,服务器再将字符串全部转化为大写字母,发送给客户端,然后客户端接受打印

*/

参考大牛地址:http://www.tracefact.net/CSharp-Programming/Network-Programming-Part2.aspx

回忆一下编写服务端的一般步骤:

1.取得服务器的ip和端口号,创建TcpListener对象,调用Start方法开始监听。

2.利用TcpListener的AcceptTcpClient方法得到监听的客户端TcpClient对象,利用GetStream得到NetStream对象,即字节流。然后可选取其他流处理方法进行字符或者字节流处理。

3.进行通信。

4. 关闭流。关闭监听。

上代码:

服务器端:

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;
Console.Write("Server is running!");
IPAddress ip = new IPAddress(new byte[] { 127, 0, 0, 1 });
TcpListener listener = new TcpListener(ip, 8500);
listener.Start();
Console.WriteLine("服务器开始侦听");
TcpClient remoteClient = listener.AcceptTcpClient();
Console.WriteLine("Client conneted! {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退出
Console.WriteLine("按下Q键退出");
ConsoleKey key;
do
{
key = Console.ReadKey(true).Key;
} while (key != ConsoleKey.Q);
}
}
}


客户端程序1:测试连接:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;

namespace Client
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Client is running:");
TcpClient client = new TcpClient();

try
{
client.Connect("localhost", 8500);
}
catch (Exception e)
{
Console.WriteLine(e.Message);

throw;
}
Console.WriteLine("Server Conneted!{0} --- > {1}", client.Client.LocalEndPoint.ToString(), client.Client.RemoteEndPoint.ToString());
Console.Read();
}
}
}


先运行服务器,再运行客户端发现:



连接成功!!,但是,,我们发现服务器端并没有执行流处理,即int bytesRead = streamToClient.Read(buffer, 0, BufferSize);说明Read方法是一个同步方法,这里没有接受字符串,发生了阻塞。我们接下来开始发送字符串,完善客户端程序。

客户端程序二:完善版

<span style="white-space:pre">		</span>const int BufferSize = 8192;
Console.Write("Server is running!");
IPAddress ip = new IPAddress(new byte[] { 127, 0, 0, 1 });
TcpListener listener = new TcpListener(ip, 8500);
listener.Start();
Console.WriteLine("服务器开始侦听");
TcpClient remoteClient = listener.AcceptTcpClient();
Console.WriteLine("Client conneted! {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退出
Console.WriteLine("按下Q键退出");
ConsoleKey key;
do
{
key = Console.ReadKey(true).Key;
remoteClient.Close();
streamToClient.Close();
} while (key != ConsoleKey.Q);


运行结果:



恩。。看上去挺爽了,但是我在这里要强调两个问题!!!

问题1:

记得在程序结束的时候关闭流,关闭连接。

重要的重要的重要的问题2:

我在写这段代码的时候脑袋中突然出现一个问题,在服务器和客户单进行连接后,服务器和客户端的代码都在向下执行,为什么客户端写入了数据后,服务器一定就能接收到呢?为什么不是服务器先接受然后为空?过了几分钟,我明白了,NetStream中的write和read都是同步方法,都会阻塞的。。(沃日...),严格来说,与AcceptTcpClient()方法类似,这个Read()方法也是同步的,只有当客户端发送数据的时候,服务端才会读取数据、运行此方法,否则它便会一直等待。。

问题三:这个程序只能实现一个客户端,并且只能发送一条信息。

这明显是不行的,我们如何实现一个客户端,多条消息呢?

当我们需要一个服务端对同一个客户端的多次请求服务时,可以将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 conneted! {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);


然后启动多个客户端,结果:



下面说第三种,要稍微改动下客户端和服务器的代码:
服务端:
do
{
remoteClient = listener.AcceptTcpClient();
Console.WriteLine("Client conneted! {0}----{1}", remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);
//获得流,并且写到buffer中
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);


客户端:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;

namespace Client3
{
class Program
{
static void Main(string[] args)
{
TcpClient client=null;
NetworkStream streamToServer = null;
ConsoleKey key;
do
{
key = Console.ReadKey(true).Key;
try
{
client = new TcpClient();
client.Connect("localhost", 8500);
streamToServer = client.GetStream();

if (key == ConsoleKey.S)
{
Console.WriteLine("Input the message : ");
string msg = Console.ReadLine();
byte[] buffer = Encoding.Unicode.GetBytes(msg);
streamToServer.Write(buffer, 0, buffer.Length);
Console.WriteLine("send : {0}", msg);
}

}
catch (Exception e)
{
Console.WriteLine(e.Message);
throw;
}

} while (key!=ConsoleKey.K);

}
}
}

运行结果:



这里还需要注意一点,当客户端在TcpClient实例上调用Close()方法,或者在流上调用Dispose()方法,服务端的streamToClient.Read()方法会持续地返回0,但是不抛出异常,所以会产生一个无限循环;而如果直接关闭掉客户端,或者客户端执行完毕但没有调用stream.Dispose()或者TcpClient.Close(),如果服务器端此时仍阻塞在Read()方法处,则会在服务器端抛出异常:“远程主机强制关闭了一个现有连接”。因此,我们将服务端的streamToClient.Read()方法需要写在一个try/catch中。同理,如果在服务端已经连接到客户端之后,服务端调用remoteClient.Close(),则客户端会得到异常“无法将数据写入传输连接:
您的主机中的软件放弃了一个已建立的连接。”;而如果服务端直接关闭程序的话,则客户端会得到异常“无法将数据写入传输连接: 远程主机强迫关闭了一个现有的连接。”。因此,它们的读写操作必须都放入到try/catch块中。
/*-----------------------------------------------无敌分割线--------------------------------------------------*/
回到开始,完成那个程序。
客户端发送到服务端是搞定了,接下来搞定字符处理,并发送回客户端打印。
具体做法就是和客户端发送给服务器一样。除此之外,我们最好对流的操作加上lock(这是个什么东西)。
服务器:
const int bufferSize = 8192;
TcpListener listener;
TcpClient client;
NetworkStream stream;
IPAddress ip = new IPAddress(new byte[] { 127, 0, 0, 1 });

try
{
listener = new TcpListener(ip, 8500);
listener.Start();
Console.WriteLine("开始监听");
byte[] buffer = new byte[bufferSize];
client = listener.AcceptTcpClient();
stream = client.GetStream();
lock (stream)
{
int readnum = stream.Read(buffer, 0, bufferSize);
}

string msg = Encoding.Unicode.GetString(buffer);
Console.WriteLine("Msg = {0}", msg);

msg = msg.ToUpper();
buffer = Encoding.Unicode.GetBytes(msg);
lock (stream)
{
stream.Write(buffer, 0, bufferSize);
}

}
catch (Exception)
{

throw;
}
stream.Close();
client.Close();
ConsoleKey key;
do
{
key = Console.ReadKey(true).Key;
} while (key!=ConsoleKey.Q);

客户端:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;

namespace Client
{
class Program
{
//接下来我们编写客户端向服务器发送字符串的代码,与服务端类似,
//他先获取连接服务器端的流,将字符串保存在缓存中,写入流这一个过程,相当于将消息发送服务端
static void Main(string[] args)
{
Console.WriteLine("Client is running:");
TcpClient client =null;

try
{
client = new TcpClient();
client.Connect("localhost", 8500);
}
catch (Exception e)
{
Console.WriteLine(e.Message);

throw;
}
//打印连接到服务器的信息
Console.WriteLine("Server Conneted!{0} --- > {1}", client.Client.LocalEndPoint.ToString(), client.Client.RemoteEndPoint.ToString());

string msg = "\"What a Big Shit\"";
NetworkStream streamToServer = client.GetStream();

byte[] buffer = Encoding.Unicode.GetBytes(msg);
lock (streamToServer)
{
streamToServer.Write(buffer, 0, buffer.Length);
}

Console.WriteLine("Sent: {0}", msg);

lock (streamToServer)
{
streamToServer.Read(buffer, 0, buffer.Length);
}
msg = Encoding.Unicode.GetString(buffer);
Console.WriteLine("转换后: {0}", msg);

//按下q退出
ConsoleKey key;
Console.WriteLine("按下q退出");
do
{
key = Console.ReadKey(true).Key;
client.Close();
streamToServer.Close();
} while (key != ConsoleKey.Q);
}
}
}


运行结果:



这种一对一的方式,在实际开发中几乎是不存在,这只是一个概念,一个流程,接下来会有些难度了。恩恩,今天就这样了~~~
地方、
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: