客户端发送数据
1. 服务端的实现
我们还是将一个问题分成两部分来处理,先是发送数据,然后是接收数据。我们先看发送数据部分的服务端。如果你从第一篇文章看到 了现在,那么我觉得更多的不是技术上的问题而是思路,所以我们不再将重点放到代码上,这些应该很容易就看懂了。
class Server {
static void Main(string[] args) {
Console.WriteLine("Server is running ... ");
IPAddress ip = IPAddress.Parse("127.0.0.1");
TcpListener listener = new TcpListener(ip, 8500);
listener.Start(); // 开启对控制端口 8500 的侦听
Console.WriteLine("Start Listening ...");
while (true) {
// 获取一个连接,同步方法,在此处中断
TcpClient client = listener.AcceptTcpClient();
RemoteClient wapper = new RemoteClient(client);
wapper.BeginRead();
}
}
}
public class RemoteClient {
private TcpClient client;
private NetworkStream streamToClient;
private const int BufferSize = 8192;
private byte[] buffer;
private ProtocolHandler handler;
public RemoteClient(TcpClient client) {
this.client = client;
// 打印连接到的客户端信息
Console.WriteLine("\nClient Connected!{0} <-- {1}",
client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
// 获得流
streamToClient = client.GetStream();
buffer = new byte[BufferSize];
handler = new ProtocolHandler();
}
// 开始进行读取
public void BeginRead() {
AsyncCallback callBack = new AsyncCallback(OnReadComplete);
streamToClient.BeginRead(buffer, 0, BufferSize, callBack, null);
}
// 再读取完成时进行回调
private void OnReadComplete(IAsyncResult ar) {
int bytesRead = 0;
try {
lock (streamToClient) {
bytesRead = streamToClient.EndRead(ar);
Console.WriteLine("Reading data, {0} bytes ...", bytesRead);
}
if (bytesRead == 0) throw new Exception("读取到 0 字节");
string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
Array.Clear(buffer,0,buffer.Length); // 清空缓存,避免脏读
// 获取 protocol 数组
string[] protocolArray = handler.GetProtocol(msg);
foreach (string pro in protocolArray) {
// 这里异步调用,不然这里可能会比较耗时
ParameterizedThreadStart start =
new ParameterizedThreadStart(handleProtocol);
start.BeginInvoke(pro, null, null);
}
// 再次调用 BeginRead(),完成时调用自身,形成无限循环
lock (streamToClient) {
AsyncCallback callBack = new AsyncCallback(OnReadComplete);
streamToClient.BeginRead(buffer, 0, BufferSize, callBack, null);
}
} catch(Exception ex) {
if(streamToClient!=null)
streamToClient.Dispose();
client.Close();
Console.WriteLine(ex.Message); // 捕获异常时退出程序
}
}
// 处理 protocol
private void handleProtocol(object obj) {
string pro = obj as string;
ProtocolHelper helper = new ProtocolHelper(pro);
FileProtocol protocol = helper.GetProtocol();
if (protocol.Mode == FileRequestMode.Send) {
// 客户端发送文件,对服务端来说则是接收文件
receiveFile(protocol);
} else if (protocol.Mode == FileRequestMode.Receive) {
// 客户端接收文件,对服务端来说则是发送文件
// sendFile(protocol);
}
}
private void receiveFile(FileProtocol protocol) {
// 获取远程客户端的位置
IPEndPoint endpoint = client.Client.RemoteEndPoint as IPEndPoint;
IPAddress ip = endpoint.Address;
// 使用新端口号,获得远程用于接收文件的端口
endpoint = new IPEndPoint(ip, protocol.Port);
// 连接到远程客户端
TcpClient localClient;
try {
localClient = new TcpClient();
localClient.Connect(endpoint);
} catch {
Console.WriteLine("无法连接到客户端 --> {0}", endpoint);
return;
}
// 获取发送文件的流
NetworkStream streamToClient = localClient.GetStream();
// 随机生成一个在当前目录下的文件名称
string path =
Environment.CurrentDirectory + "/" + generateFileName(protocol.FileName);
byte[] fileBuffer = new byte[1024]; // 每次收 1KB
FileStream fs = new FileStream(path, FileMode.CreateNew, FileAccess.Write);
// 从缓存 buffer 中读入到文件流中
int bytesRead;
int totalBytes = 0;
do {
bytesRead = streamToClient.Read(buffer, 0, BufferSize);
fs.Write(buffer, 0, bytesRead);
totalBytes += bytesRead;
Console.WriteLine("Receiving {0} bytes ...", totalBytes);
} while (bytesRead > 0);
Console.WriteLine("Total {0} bytes received, Done!", totalBytes);
streamToClient.Dispose();
fs.Dispose();
localClient.Close();
}
// 随机获取一个图片名称
private string generateFileName(string fileName) {
DateTime now = DateTime.Now;
return String.Format(
"{0}_{1}_{2}_{3}", now.Minute, now.Second, now.Millisecond, fileName
);
}
}
这里应该没有什么新知识,需要注意的地方有这么几个:
- 在 OnReadComplete()回调方法中的 foreach 循环,我们使用委托异步调用了 handleProtocol()方法,这是因为 handleProtocol 即将执行的是一个读取或接收文件的操作,也就是一个相对耗时的操作。
- 在 handleProtocol()方法中,我们深切体会了定义 ProtocolHelper 类和 FileProtocol 结构的好处。如果没有定义它们,这里将 是不堪入目的处理 XML 以及类型转换的代码。
- handleProtocol()方法中进行了一个条件判断,注意 sendFile()方法我屏蔽掉了,这个还没有实现,但是我想你已经猜到它将 是后面要实现的内容。
- receiveFile()方法是实际接收客户端发来文件的方法,这里没有什么特别之处。需要注意的是文件存储的路径,它保存在了当 前程序执行的目录下,文件的名称我使用 generateFileName()生成了一个与时间有关的随机名称。
2. 客户端的实现
我们现在先不着急实现客户端 S1、R1 等用户菜单,首先完成发送文件这一功能,实际上,就是为上一节 SendMessage()加一个姐妹 方法 SendFile()。
class Client {
static void Main(string[] args) {
ConsoleKey key;
ServerClient client = new ServerClient();
string filePath = Environment.CurrentDirectory + "/" + "Client01.jpg";
if(File.Exists(filePath))
client.BeginSendFile(filePath);
Console.WriteLine("\n\n 输入\"Q\"键退出。");
do {
key = Console.ReadKey(true).Key;
} while (key != ConsoleKey.Q);
}
}
public class ServerClient {
private const int BufferSize = 8192;
private byte[] buffer;
private TcpClient client;
private NetworkStream streamToServer;
public ServerClient() {
try {
client = new TcpClient();
client.Connect("localhost", 8500); // 与服务器连接
} catch (Exception ex) {
Console.WriteLine(ex.Message);
return;
}
buffer = new byte[BufferSize];
// 打印连接到的服务端信息
Console.WriteLine("Server Connected!{0} --> {1}",
client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
streamToServer = client.GetStream();
}
// 发送消息到服务端
public void SendMessage(string msg) {
byte[] temp = Encoding.Unicode.GetBytes(msg); // 获得缓存
try {
lock (streamToServer) {
streamToServer.Write(temp, 0, temp.Length); // 发往服务器
}
Console.WriteLine("Sent: {0}", msg);
} catch (Exception ex) {
Console.WriteLine(ex.Message);
return;
}
}
// 发送文件 - 异步方法
public void BeginSendFile(string filePath) {
ParameterizedThreadStart start =
new ParameterizedThreadStart(BeginSendFile);
start.BeginInvoke(filePath, null, null);
}
private void BeginSendFile(object obj) {
string filePath = obj as string;
SendFile(filePath);
}
// 发送文件 -- 同步方法
public void SendFile(string filePath) {
IPAddress ip = IPAddress.Parse("127.0.0.1");
TcpListener listener = new TcpListener(ip, 0);
listener.Start();
// 获取本地侦听的端口号
IPEndPoint endPoint = listener.LocalEndpoint as IPEndPoint;
int listeningPort = endPoint.Port;
// 获取发送的协议字符串
string fileName = Path.GetFileName(filePath);
FileProtocol protocol =
new FileProtocol(FileRequestMode.Send, listeningPort, fileName);
string pro = protocol.ToString();
SendMessage(pro); // 发送协议到服务端
// 中断,等待远程连接
TcpClient localClient = listener.AcceptTcpClient();
Console.WriteLine("Start sending file...");
NetworkStream stream = localClient.GetStream();
// 创建文件流
FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
byte[] fileBuffer = new byte[1024]; // 每次传 1KB
int bytesRead;
int totalBytes = 0;
// 创建获取文件发送状态的类
SendStatus status = new SendStatus(filePath);
// 将文件流转写入网络流
try {
do {
Thread.Sleep(10); // 为了更好的视觉效果,暂停 10 毫秒:-)
bytesRead = fs.Read(fileBuffer, 0, fileBuffer.Length);
stream.Write(fileBuffer, 0, bytesRead);
totalBytes += bytesRead; // 发送了的字节数
status.PrintStatus(totalBytes); // 打印发送状态
} while (bytesRead > 0);
Console.WriteLine("Total {0} bytes sent, Done!", totalBytes);
} catch {
Console.WriteLine("Server has lost...");
}
stream.Dispose();
fs.Dispose();
localClient.Close();
listener.Stop();
}
}
接下来我们来看下这段代码,有这么两点需要注意一下:
- 在 Main()方法中可以看到,图片的位置为应用程序所在的目录,如果你跟我一样处于调试模式,那么就在解决方案的 Bin 目 录下的 Debug 目录中放置三张图片 Client01.jpg、Client02.jpg、Client03.jpg,用来发往服务端。
- 我在客户端提供了两个 SendFile()方法,和一个 BeginSendFile()方法,分别用于同步和异步传输,其中私有的 SendFile()方 法只是一个辅助方法。实际上对于发送文件这样的操作我们几乎总是需要使用异步操作。
- SendMessage()方法中给 streamToServer 加锁很重要,因为 SendFile()方法是多线程访问的,而在 SendFile()方法中又调用 了 SendMessage()方法。
- 我另外编写了一个 SendStatus 类,它用来记录和打印发送完成的状态,已经发送了多少字节,完成度是百分之多少,等等。 本来这个类的内容我 是直接写入在 Client 类中的,后来我觉得它执行的工作已经不属于 Client 本身所应该执行的领域之内 了,我记得这样一句话:当你觉得类中的方法与类的名称不符的时候,那么就应该考虑重新创建一个类。我觉得用在这里非 常恰当。
下面是 SendStatus 的内容:
// 即时计算发送文件的状态
public class SendStatus {
private FileInfo info;
private long fileBytes;
public SendStatus(string filePath) {
info = new FileInfo(filePath);
fileBytes = info.Length;
}
public void PrintStatus(int sent) {
string percent = GetPercent(sent);
Console.WriteLine("Sending {0} bytes, {1}% ...", sent, percent);
}
// 获得文件发送的百分比
public string GetPercent(int sent){
decimal allBytes = Convert.ToDecimal(fileBytes);
decimal currentSent = Convert.ToDecimal(sent);
decimal percent = (currentSent / allBytes) * 100;
percent = Math.Round(percent, 1); //保留一位小数
if (percent.ToString() == "100.0")
return "100";
else
return percent.ToString();
}
}
3. 程序测试
接下里我们运行一下程序,来检查一下输出,首先看下服务端:
接着是客户端,我们能够看到发送的字节数和进度,可以想到如果是图形界面,那么我们可以通过扩展 SendStatus 类来创建一个进度 条:
最后我们看下服务端的 Bin\Debug 目录,应该可以看到接收到的图片:
本来我想这篇文章就可以完成发送和接收,不过现在看来没法实现了,因为如果继续下去这篇文章就太长了,我正尝试着尽量将文章控 制在 15 页以内。那么我们将在下篇文章中再完成接收文件这一部分。
版权声明:本文为YES开发框架网发布内容,转载请附上原文出处连接
Socket 张国生