网络 I/O 编程实战:从基础到异步处理
1. 网络 I/O 及异步处理概述
在实际应用中,传统的网络 I/O 处理方式可扩展性不佳。大多数服务器每分钟需要处理数千甚至数万个连接。为了应对高并发连接,应用程序采用异步 I/O 技术。其工作原理是:应用程序接受客户端的连接请求,创建一个与客户端连接的套接字,之后原始的监听器继续监听下一个客户端的连接请求。这样,应用程序就能处理大量的连接,每次接受一个连接时都会创建一个新的套接字。客户端并不知道新套接字的创建过程,在客户端看来,它已经成功连接到了所请求的 IP 地址和端口。需要注意的是,这种方式与使用无连接协议的 UDP 不同,TCP/IP 建立连接后,客户端和服务器可以直接通信,无需为每个数据包重新寻址。
2. 创建网络流式服务器
要创建一个用于 TCP/IP 流式传输的网络服务器,可按以下步骤操作:
1.
创建 TcpListener 对象
:选择一个 TCP/IP 端口进行监听,这里选择端口 65000。
IPAddress localAddr = IPAddress.Parse("127.0.0.1");
TcpListener tcpListener = new TcpListener(localAddr, 65000);
-
启动监听
:调用
Start()方法开始监听客户端的连接请求。
tcpListener.Start();
-
等待客户端连接
:使用
AcceptSocket()方法等待客户端的连接请求,该方法会返回一个代表 Berkeley 套接字接口的Socket对象,并且该对象绑定到了特定的端点。AcceptSocket()是一个同步方法,在收到连接请求之前不会返回。
Socket socketForClient = tcpListener.AcceptSocket();
-
发送文件到客户端
:创建
NetworkStream对象和StreamWriter对象,将文件内容逐行发送给客户端。
NetworkStream networkStream = new NetworkStream(socketForClient);
System.IO.StreamWriter streamWriter = new System.IO.StreamWriter(networkStream);
以下是完整的服务器代码示例:
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace NetworkStreamingServer
{
public class NetworkIOServer
{
public static void Main()
{
NetworkIOServer app = new NetworkIOServer();
app.Run();
}
private void Run()
{
IPAddress localAddr = IPAddress.Parse("127.0.0.1");
TcpListener tcpListener = new TcpListener(localAddr, 65000);
tcpListener.Start();
for (; ; )
{
Socket socketForClient = tcpListener.AcceptSocket();
Console.WriteLine("Client connected");
SendFileToClient(socketForClient);
Console.WriteLine("Disconnecting from client...");
socketForClient.Close();
Console.WriteLine("Exiting...");
break;
}
}
private void SendFileToClient(Socket socketForClient)
{
NetworkStream networkStream = new NetworkStream(socketForClient);
System.IO.StreamWriter streamWriter = new System.IO.StreamWriter(networkStream);
System.IO.StreamReader streamReader = new System.IO.StreamReader(@"C:\test\source\myTest.txt");
string theString;
do
{
theString = streamReader.ReadLine();
if (theString != null)
{
Console.WriteLine("Sending {0}", theString);
streamWriter.WriteLine(theString);
streamWriter.Flush();
}
}
while (theString != null);
streamReader.Close();
networkStream.Close();
streamWriter.Close();
}
}
}
3. 创建流式网络客户端
客户端的创建相对简单,主要步骤如下:
1.
创建 TcpClient 对象
:连接到服务器的指定端口。
TcpClient socketForServer;
socketForServer = new TcpClient("localHost", 65000);
- 创建 NetworkStream 和 StreamReader 对象 :从服务器读取数据。
NetworkStream networkStream = socketForServer.GetStream();
System.IO.StreamReader streamReader = new System.IO.StreamReader(networkStream);
- 读取服务器发送的数据 :逐行读取服务器发送的数据并输出到控制台。
string outputString;
do
{
outputString = streamReader.ReadLine();
if (outputString != null)
{
Console.WriteLine(outputString);
}
}
while (outputString != null);
以下是完整的客户端代码示例:
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
namespace NetworkStreamingClient
{
public class Client
{
static public void Main(string[] Args)
{
TcpClient socketForServer;
try
{
socketForServer = new TcpClient("localHost", 65000);
}
catch
{
Console.WriteLine("Failed to connect to server at {0}:65000", "localhost");
return;
}
NetworkStream networkStream = socketForServer.GetStream();
System.IO.StreamReader streamReader = new System.IO.StreamReader(networkStream);
try
{
string outputString;
do
{
outputString = streamReader.ReadLine();
if (outputString != null)
{
Console.WriteLine(outputString);
}
}
while (outputString != null);
}
catch
{
Console.WriteLine("Exception reading from Server");
}
networkStream.Close();
}
}
}
4. 测试与注意事项
为了测试上述服务器和客户端代码,创建了一个简单的测试文件
myTest.txt
,内容如下:
This is line one
This is line two
This is line three
This is line four
运行服务器和客户端代码后,服务器和客户端的输出如下:
| 服务器输出 | 客户端输出 |
| — | — |
| Client connected
Sending This is line one
Sending This is line two
Sending This is line three
Sending This is line four
Disconnecting from client…
Exiting… | This is line one
This is line two
This is line three
This is line four
Press any key to continue |
如果在同一台机器上进行测试,需要在不同的命令窗口或开发环境实例中分别运行服务器和客户端代码,并且要先启动服务器,否则客户端会连接失败。如果不在同一台机器上运行,需要将代码中的
127.0.0.1
和
localhost
替换为运行服务器的机器的 IP 地址。如果使用 Windows XP Service Pack 2 且采用默认设置,会弹出 Windows 安全警报,询问是否要解除对端口的阻止。
5. 处理多个连接
前面的示例在处理多个客户端连接时可扩展性不佳,每个客户端都会占用服务器的全部资源。为了解决这个问题,创建一个新的服务器
AsynchNetworkServer
,并在其中嵌套一个新的类
ClientHandler
。当服务器接收到客户端连接时,会实例化一个
ClientHandler
对象,并将套接字传递给该对象。
ClientHandler
类的构造函数会创建套接字的副本和缓冲区,并在该套接字上打开一个新的
NetworkStream
。然后,使用重叠 I/O 技术异步地读取和写入套接字。为了实现异步 I/O,
ClientHandler
类定义了两个委托方法
OnReadComplete()
和
OnWriteComplete()
,用于管理客户端发送的字符串的重叠 I/O 操作。
以下是服务器的主要代码逻辑:
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace AsynchNetworkServer
{
public class AsynchNetworkServer
{
class ClientHandler
{
private byte[] buffer;
private Socket socket;
private NetworkStream networkStream;
private AsyncCallback callbackRead;
private AsyncCallback callbackWrite;
public ClientHandler(Socket socketForClient)
{
socket = socketForClient;
buffer = new byte[256];
networkStream = new NetworkStream(socketForClient);
callbackRead = new AsyncCallback(this.OnReadComplete);
callbackWrite = new AsyncCallback(this.OnWriteComplete);
}
public void StartRead()
{
networkStream.BeginRead(buffer, 0, buffer.Length, callbackRead, null);
}
private void OnReadComplete(IAsyncResult ar)
{
int bytesRead = networkStream.EndRead(ar);
if (bytesRead > 0)
{
string s = System.Text.Encoding.ASCII.GetString(buffer, 0, bytesRead);
Console.Write("Received {0} bytes from client: {1}", bytesRead, s);
networkStream.BeginWrite(buffer, 0, bytesRead, callbackWrite, null);
}
else
{
Console.WriteLine("Read connection dropped");
networkStream.Close();
socket.Close();
networkStream = null;
socket = null;
}
}
private void OnWriteComplete(IAsyncResult ar)
{
networkStream.EndWrite(ar);
Console.WriteLine("Write complete");
networkStream.BeginRead(buffer, 0, buffer.Length, callbackRead, null);
}
}
public static void Main()
{
AsynchNetworkServer app = new AsynchNetworkServer();
app.Run();
}
private void Run()
{
IPAddress localAddr = IPAddress.Parse("127.0.0.1");
TcpListener tcpListener = new TcpListener(localAddr, 65000);
tcpListener.Start();
for (; ; )
{
Socket socketForClient = tcpListener.AcceptSocket();
Console.WriteLine("Client connected");
ClientHandler handler = new ClientHandler(socketForClient);
handler.StartRead();
}
}
}
}
客户端代码相对简单,主要步骤如下:
1.
创建 TcpClient 对象
:连接到服务器的指定端口。
TcpClient tcpSocket = new TcpClient("localhost", 65000);
- 创建 NetworkStream 对象 :获取与服务器的网络流。
NetworkStream streamToServer = tcpSocket.GetStream();
-
发送消息到服务器
:创建
StreamWriter对象,将消息发送到服务器。
System.IO.StreamWriter writer = new System.IO.StreamWriter(streamToServer);
string message = "Hello Programming C#";
writer.WriteLine(message);
writer.Flush();
-
读取服务器的响应
:创建
StreamReader对象,读取服务器的响应并输出到控制台。
System.IO.StreamReader reader = new System.IO.StreamReader(streamToServer);
string strResponse = reader.ReadLine();
Console.WriteLine("Received: {0}", strResponse);
以下是完整的客户端代码示例:
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
namespace AsynchNetworkClient
{
public class AsynchNetworkClient
{
private NetworkStream streamToServer;
static public int Main()
{
AsynchNetworkClient client = new AsynchNetworkClient();
return client.Run();
}
AsynchNetworkClient()
{
string serverName = "localhost";
Console.WriteLine("Connecting to {0}", serverName);
TcpClient tcpSocket = new TcpClient(serverName, 65000);
streamToServer = tcpSocket.GetStream();
}
private int Run()
{
string message = "Hello Programming C#";
Console.WriteLine("Sending {0} to server.", message);
System.IO.StreamWriter writer = new System.IO.StreamWriter(streamToServer);
writer.WriteLine(message);
writer.Flush();
System.IO.StreamReader reader = new System.IO.StreamReader(streamToServer);
string strResponse = reader.ReadLine();
Console.WriteLine("Received: {0}", strResponse);
streamToServer.Close();
return 0;
}
}
}
6. 异步网络文件流式传输
结合异步文件读取和异步网络流式传输的技能,可以实现一个按需向客户端提供文件的程序。服务器的工作流程如下:
1.
异步读取客户端请求
:服务器在套接字上进行异步读取,等待客户端发送文件名。
2.
异步读取文件
:获取文件名后,在服务器上启动对该文件的异步读取操作。
3.
异步写入客户端
:每当文件的一个缓冲区数据可用时,开始向客户端进行异步写入操作。
4.
循环处理
:当向客户端的异步写入操作完成后,再次启动文件的读取操作,如此循环,直到文件全部读取并传输到客户端。
客户端只需要从服务器读取数据流,在示例中,客户端将文件内容输出到控制台,也可以将其异步写入到一个新文件中,从而实现基于网络的文件复制程序。
以下是服务器的主要代码逻辑:
// 部分代码省略,与前面的 AsynchNetworkServer 类似
class ClientHandler
{
private byte[] buffer;
private Socket socket;
private NetworkStream networkStream;
private AsyncCallback callbackRead;
private AsyncCallback callbackWrite;
private AsyncCallback myFileCallBack;
private System.IO.FileStream inputStream;
public ClientHandler(Socket socketForClient)
{
socket = socketForClient;
buffer = new byte[256];
networkStream = new NetworkStream(socketForClient);
callbackRead = new AsyncCallback(this.OnReadComplete);
callbackWrite = new AsyncCallback(this.OnWriteComplete);
myFileCallBack = new AsyncCallback(this.OnFileCompletedRead);
}
public void StartRead()
{
networkStream.BeginRead(buffer, 0, buffer.Length, callbackRead, null);
}
private void OnReadComplete(IAsyncResult ar)
{
int bytesRead = networkStream.EndRead(ar);
if (bytesRead > 0)
{
string fileName = System.Text.Encoding.ASCII.GetString(buffer, 0, bytesRead);
inputStream = System.IO.File.OpenRead(fileName);
inputStream.BeginRead(buffer, 0, buffer.Length, myFileCallBack, null);
}
else
{
Console.WriteLine("Read connection dropped");
networkStream.Close();
socket.Close();
networkStream = null;
socket = null;
}
}
private void OnFileCompletedRead(IAsyncResult ar)
{
int bytesRead = inputStream.EndRead(ar);
if (bytesRead > 0)
{
networkStream.BeginWrite(buffer, 0, bytesRead, callbackWrite, null);
}
else
{
networkStream.Close();
socket.Close();
networkStream = null;
socket = null;
}
}
private void OnWriteComplete(IAsyncResult ar)
{
networkStream.EndWrite(ar);
Console.WriteLine("Write complete");
inputStream.BeginRead(buffer, 0, buffer.Length, myFileCallBack, null);
}
}
客户端代码如下:
using System;
using System.Net.Sockets;
using System.Threading;
using System.Text;
public class AsynchNetworkClient
{
private const int BufferSize = 256;
private NetworkStream streamToServer;
static public int Main()
{
AsynchNetworkClient client = new AsynchNetworkClient();
return client.Run();
}
AsynchNetworkClient()
{
string serverName = "localhost";
Console.WriteLine("Connecting to {0}", serverName);
TcpClient tcpSocket = new TcpClient(serverName, 65000);
streamToServer = tcpSocket.GetStream();
}
private int Run()
{
string message = @"C:\test\source\AskTim.txt";
Console.Write("Sending {0} to server.", message);
System.IO.StreamWriter writer = new System.IO.StreamWriter(streamToServer);
writer.Write(message);
writer.Flush();
bool fQuit = false;
while (!fQuit)
{
char[] buffer = new char[BufferSize];
System.IO.StreamReader reader = new System.IO.StreamReader(streamToServer);
int bytesRead = reader.Read(buffer, 0, BufferSize);
if (bytesRead == 0)
fQuit = true;
else
{
string theString = new String(buffer);
Console.WriteLine(theString);
}
}
streamToServer.Close();
return 0;
}
}
通过以上步骤和代码示例,我们可以实现从基础的网络流式服务器和客户端到异步处理多个连接以及异步网络文件流式传输的功能。在实际开发中,还需要注意异常处理、资源管理等问题,以提高程序的稳定性和性能。
下面是一个简单的 mermaid 流程图,展示了异步网络文件流式传输服务器的工作流程:
graph TD;
A[启动服务器监听] --> B[等待客户端连接];
B --> C[接受客户端连接,创建 ClientHandler];
C --> D[开始异步读取客户端请求];
D --> E{是否收到文件名};
E -- 是 --> F[异步读取文件];
F --> G{文件是否读取完};
G -- 否 --> H[异步写入客户端];
H --> I[写入完成,继续读取文件];
I --> G;
G -- 是 --> J[关闭连接];
E -- 否 --> D;
通过这个流程图,可以更清晰地理解服务器在异步网络文件流式传输过程中的工作流程。
网络 I/O 编程实战:从基础到异步处理(续)
7. 代码优化与注意事项
在实际应用中,上述代码还可以进行一些优化,同时需要注意一些潜在的问题。
7.1 服务器端优化
-
线程管理
:在
AsynchNetworkServer中,OnReadComplete()和OnWriteComplete()方法中向控制台写入信息可能会阻塞线程。在生产环境中,应避免在这些方法中进行阻塞操作,因为使用的是线程池中的线程,阻塞操作可能会导致线程池添加更多线程,影响性能和可扩展性。可以将日志记录等操作放到单独的线程中进行。 -
异常处理
:在实际应用中,网络操作可能会出现各种异常,如连接中断、文件读取失败等。应在代码中添加更完善的异常处理逻辑,确保程序在出现异常时能够正确处理,避免程序崩溃。例如,在
ClientHandler类的各个方法中添加try-catch块。
private void OnReadComplete(IAsyncResult ar)
{
try
{
int bytesRead = networkStream.EndRead(ar);
if (bytesRead > 0)
{
string s = System.Text.Encoding.ASCII.GetString(buffer, 0, bytesRead);
Console.Write("Received {0} bytes from client: {1}", bytesRead, s);
networkStream.BeginWrite(buffer, 0, bytesRead, callbackWrite, null);
}
else
{
Console.WriteLine("Read connection dropped");
networkStream.Close();
socket.Close();
networkStream = null;
socket = null;
}
}
catch (Exception ex)
{
Console.WriteLine($"Exception in OnReadComplete: {ex.Message}");
networkStream.Close();
socket.Close();
networkStream = null;
socket = null;
}
}
7.2 客户端优化
-
缓冲区管理
:在客户端代码中,每次循环都创建一个新的
StreamReader对象可能会影响性能。可以在循环外创建StreamReader对象,避免重复创建。 -
超时处理
:在客户端连接服务器和读取数据时,应添加超时处理机制,避免长时间等待无响应的服务器。例如,在
TcpClient连接时设置连接超时时间。
TcpClient tcpSocket = new TcpClient();
try
{
IAsyncResult result = tcpSocket.BeginConnect("localhost", 65000, null, null);
bool success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(5));
if (!success)
{
Console.WriteLine("Connection timed out");
return;
}
tcpSocket.EndConnect(result);
}
catch (Exception ex)
{
Console.WriteLine($"Connection error: {ex.Message}");
return;
}
8. 性能测试与调优
为了确保网络 I/O 程序的性能,需要进行性能测试,并根据测试结果进行调优。
8.1 性能测试工具
可以使用一些专业的性能测试工具,如 Apache JMeter、LoadRunner 等,对服务器和客户端进行压力测试。这些工具可以模拟大量的并发连接,测试服务器在高负载下的性能表现。
8.2 调优策略
- 调整缓冲区大小 :根据实际情况调整缓冲区大小,以提高数据传输效率。如果缓冲区太小,会导致频繁的读写操作;如果缓冲区太大,会占用过多的内存资源。
- 优化线程池配置 :合理配置线程池的大小,避免线程过多或过少。线程过多会导致上下文切换频繁,影响性能;线程过少会导致任务排队等待,降低并发处理能力。
9. 总结与展望
通过前面的介绍,我们从基础的网络流式服务器和客户端开始,逐步深入到异步处理多个连接以及异步网络文件流式传输。网络 I/O 编程在现代应用中至关重要,特别是在高并发场景下,异步 I/O 技术能够显著提高程序的性能和可扩展性。
在未来的发展中,网络 I/O 编程可能会朝着以下方向发展:
-
更高效的异步模型
:随着技术的不断进步,可能会出现更高效的异步编程模型,进一步提高网络 I/O 的性能。
-
分布式系统集成
:网络 I/O 编程将更多地与分布式系统集成,实现跨节点的数据传输和处理。
-
安全性增强
:在网络传输过程中,数据的安全性越来越重要。未来的网络 I/O 编程将更加注重数据加密、身份验证等安全机制。
下面是一个表格,总结了不同网络 I/O 实现方式的特点:
| 实现方式 | 优点 | 缺点 | 适用场景 |
| — | — | — | — |
| 同步网络 I/O | 代码简单,易于理解 | 可扩展性差,处理大量连接时性能低 | 连接数较少的场景 |
| 异步网络 I/O | 可扩展性好,能处理大量并发连接 | 代码复杂,调试难度大 | 高并发场景 |
同时,为了更清晰地展示客户端在异步网络文件流式传输过程中的工作流程,给出以下 mermaid 流程图:
graph TD;
A[连接服务器] --> B[发送文件名];
B --> C[开始循环读取数据];
C --> D{是否收到数据};
D -- 是 --> E[处理数据(如输出到控制台)];
E --> C;
D -- 否 --> F[关闭连接];
通过以上内容,我们对网络 I/O 编程有了更全面的了解,从基础实现到高级应用,再到性能优化和未来展望。在实际开发中,应根据具体需求选择合适的实现方式,并不断优化代码,以提高程序的性能和稳定性。
超级会员免费看
616

被折叠的 条评论
为什么被折叠?



