我正在寻找一种处理断开连接的方法,因为每次关闭客户端时,服务器都会停止工作。我收到一条错误消息,它是"无法读取超出流的末尾"在这一行:
string message = reader.ReadString();
此外,我还需要一种方法来从客户端列表中删除断开连接的客户端。 这是我的代码: 服务器
using System;
using System.Threading;
using System.Net.Sockets;
using System.IO;
using System.Net;
using System.Collections.Generic;
namespace Server
{
class Server
{
public static List<TcpClient> clients = new List<TcpClient>();
static void Main(string[] args)
{
IPAddress ip = IPAddress.Parse("127.0.0.1");
TcpListener ServerSocket = new TcpListener(ip, 14000);
ServerSocket.Start();
Console.WriteLine("Server started.");
while (true)
{
TcpClient clientSocket = ServerSocket.AcceptTcpClient();
clients.Add(clientSocket);
handleClient client = new handleClient();
client.startClient(clientSocket);
}
}
}
public class handleClient
{
TcpClient clientSocket;
public void startClient(TcpClient inClientSocket)
{
this.clientSocket = inClientSocket;
Thread ctThread = new Thread(Chat);
ctThread.Start();
}
private void Chat()
{
while (true)
{
BinaryReader reader = new BinaryReader(clientSocket.GetStream());
while (true)
{
string message = reader.ReadString();
foreach (var client in Server.clients)
{
BinaryWriter writer = new BinaryWriter(client.GetStream());
writer.Write(message);
}
}
}
}
}
}
客户端
using System;
using System.Net.Sockets;
using System.IO;
using System.Threading;
namespace Client
{
class Client
{
public static void Write()
{
TcpClient client = new TcpClient("127.0.0.1", 14000);
while (true)
{
string str = Console.ReadLine();
BinaryWriter writer = new BinaryWriter(client.GetStream());
writer.Write(str);
}
}
public static void Read()
{
TcpClient client = new TcpClient("127.0.0.1", 14000);
while (true)
{
BinaryReader reader = new BinaryReader(client.GetStream());
Console.WriteLine(reader.ReadString());
}
}
static void Main(string[] args)
{
Thread Thread = new Thread(Write);
Thread Thread2 = new Thread(Read);
Thread.Start();
Thread2.Start();
}
}
}
答案 0 :(得分:8)
每次关闭客户端时,服务器都会停止工作。我收到一条错误消息,它是&#34;无法读取超出流的结尾&#34;
从某种意义上说,这是完全正常的。也就是说,使用BinaryReader
时,其正常行为是在到达流末尾时抛出EndOfStreamException
。
为什么它会到达流末尾?好吧,因为客户端已断开连接,并且流会发生什么。在套接字级别,实际发生的是读取操作以0作为读取的字节数完成。这表明客户端已正常关闭套接字,并且不会再发送任何数据。
在.NET API中,这被转换为NetworkStream
的末尾,TcpClient
用于包装实际处理网络I / O的Socket
对象。而这个NetworkStream
对象又由BinaryReader
对象包裹。并且BinaryReader
在到达流的末尾时抛出该异常。
请注意,您的代码实际上并未为用户提供关闭客户端的优雅方式。他们必须使用 Ctrl + C ,或者直接终止进程。使用前者具有执行正常关闭套接字的偶然效果,但这只是因为.NET正在处理进程的终止并在对象上运行终结器,例如用于连接到服务器的TcpClient
对象,终结器调用Socket.Shutdown()
告诉服务器它正在关闭。
如果你要杀死这个过程(例如使用任务管理器),你会发现一个IOException
被抛出了。应始终准备好网络代码以查看IOException
;网络不可靠,确实发生了故障。您希望做一些合理的工作,例如从连接中删除远程端点,而不是让整个程序崩溃。
现在,所有这一切,只是因为EndOfStreamException
是&#34;正常&#34;,这并不意味着您发布的代码是,或者无论如何都是正确方法的一个例子网络编程。你有很多问题:
TcpClient
和NetworkStream
不直接公开此内容,但您可以使用TcpClient.Client
属性来使Socket
对象更好地关闭,即一个端点可以指示它已完成发送,并且仍然可以等到另一个端点也完成发送。TcpClient.Close()
方法断开连接就像是挂断电话而不说'再见&#34;。使用Socket.Shutdown()
就像打完一个有礼貌的电话一样,好吧,这就是我想说的一切......还有什么吗?&#34; BinaryReader
但未正确处理EndOfStreamException
。Socket
对象,该对象支持全双工通信。没有必要创建第二个连接只是为了进行读写。单个连接就足够了,并且更好,因为当您将发送和接收分成两个连接时,您还需要在协议中添加一些内容,以便服务器知道这两个连接代表一个客户端(您的代码执行不实际上。)Chat()
方法有额外的&#34;而(true)&#34;在它。我修改了您的原始示例,以解决上述所有问题,我在此处介绍过:
Server Program.cs:
class Program
{
private static readonly object _lock = new object();
private static readonly List<TcpClient> clients = new List<TcpClient>();
public static TcpClient[] GetClients()
{
lock (_lock) return clients.ToArray();
}
public static int GetClientCount()
{
lock (_lock) return clients.Count;
}
public static void RemoveClient(TcpClient client)
{
lock (_lock) clients.Remove(client);
}
static void Main(string[] args)
{
IPAddress ip = IPAddress.Parse("127.0.0.1");
TcpListener ServerSocket = new TcpListener(ip, 14000);
ServerSocket.Start();
Console.WriteLine("Server started.");
while (true)
{
TcpClient clientSocket = ServerSocket.AcceptTcpClient();
Console.WriteLine($"client connected: {clientSocket.Client.RemoteEndPoint}");
lock (_lock) clients.Add(clientSocket);
handleClient client = new handleClient();
client.startClient(clientSocket);
Console.WriteLine($"{GetClientCount()} clients connected");
}
}
}
服务器handleClient.cs:
public class handleClient
{
TcpClient clientSocket;
public void startClient(TcpClient inClientSocket)
{
this.clientSocket = inClientSocket;
Thread ctThread = new Thread(Chat);
ctThread.Start();
}
private void Chat()
{
BinaryReader reader = new BinaryReader(clientSocket.GetStream());
try
{
while (true)
{
string message = reader.ReadString();
foreach (var client in Program.GetClients())
{
BinaryWriter writer = new BinaryWriter(client.GetStream());
writer.Write(message);
}
}
}
catch (EndOfStreamException)
{
Console.WriteLine($"client disconnecting: {clientSocket.Client.RemoteEndPoint}");
clientSocket.Client.Shutdown(SocketShutdown.Both);
}
catch (IOException e)
{
Console.WriteLine($"IOException reading from {clientSocket.Client.RemoteEndPoint}: {e.Message}");
}
clientSocket.Close();
Program.RemoveClient(clientSocket);
Console.WriteLine($"{Program.GetClientCount()} clients connected");
}
}
Client Program.cs:
class Program
{
private static readonly object _lock = new object();
private static bool _closed;
public static void Write(TcpClient client)
{
try
{
string str;
SocketShutdown reason = SocketShutdown.Send;
while ((str = Console.ReadLine()) != "")
{
lock (_lock)
{
BinaryWriter writer = new BinaryWriter(client.GetStream());
writer.Write(str);
if (_closed)
{
// Remote endpoint already said they are done sending,
// so we're done with both sending and receiving.
reason = SocketShutdown.Both;
break;
}
}
}
client.Client.Shutdown(reason);
}
catch (IOException e)
{
Console.WriteLine($"IOException writing to socket: {e.Message}");
}
}
public static void Read(TcpClient client)
{
try
{
while (true)
{
try
{
BinaryReader reader = new BinaryReader(client.GetStream());
Console.WriteLine(reader.ReadString());
}
catch (EndOfStreamException)
{
lock (_lock)
{
_closed = true;
return;
}
}
}
}
catch (IOException e)
{
Console.WriteLine($"IOException reading from socket: {e.Message}");
}
}
static void Main(string[] args)
{
TcpClient client = new TcpClient("127.0.0.1", 14000);
Thread writeThread = new Thread(() => Write(client));
Thread readThread = new Thread(() => Read(client));
writeThread.Start();
readThread.Start();
writeThread.Join();
readThread.Join();
client.Close();
Console.WriteLine("client exiting");
}
}
请注意,在大多数情况下,我没有解决您在代码中使用的不一致和非常规命名。唯一的例外是客户端代码中的线程变量,因为我确实不喜欢与类型名称完全匹配的大写局部变量。
您还有其他一些问题,上面的代码修订版没有解决这些问题。其中包括:
BinaryReader
。这在很多方面都是令人烦恼的课程。我建议,特别是对于您仍然只处理文本的聊天服务器方案,您切换到使用StreamReader
/ StreamWriter
。Program
类具有服务器代码,服务器代码知道Program
类。将服务器和客户端实现封装到自己的类中,与程序的主要入口点分开,并进一步将顶级服务器代码与每个客户端数据结构分离(使用C#和#)会好得多。 39; s event
允许顶级服务器代码被通知重要事件,例如需要从列表中删除客户端,而不必让每个客户端数据结构实际知道顶部级别的服务器对象,别介意它的客户端列表。)通常情况下,我会说这些都超出了这样的答案范围,已经很长了。我已经解决了您的代码中的直接问题,然后解决了一些问题,这在名义上足够了。
但是,我已经写了几年前我写的basic network programming示例的更新版本,这是一种&#34;中间版&#34;例如,添加多个客户端支持,异步操作以及使用最新的C#功能(如async
/ await
)。所以,我继续前进并花了一些时间来做到这一点。我想我最终会把它发布到我的博客上......这是另一个项目。与此同时,这里有代码(请注意,这是一个完全从头开始的例子......这样做比尝试重新编写代码更有意义...)
此实现的大部分工作都在服务器和客户端共享的单个类中:
/// <summary>
/// Represents a remote end-point for the chat server and clients
/// </summary>
public sealed class ConnectedEndPoint : IDisposable
{
private readonly object _lock = new object();
private readonly Socket _socket;
private readonly StreamReader _reader;
private readonly StreamWriter _writer;
private bool _closing;
/// <summary>
/// Gets the address of the connected remote end-point
/// </summary>
public IPEndPoint RemoteEndPoint { get { return (IPEndPoint)_socket.RemoteEndPoint; } }
/// <summary>
/// Gets a <see cref="Task"/> representing the on-going read operation of the connection
/// </summary>
public Task ReadTask { get; }
/// <summary>
/// Connect to an existing remote end-point (server) and return the
/// <see cref="ConnectedEndPoint"/> object representing the new connection
/// </summary>
/// <param name="remoteEndPoint">The address of the remote end-point to connect to</param>
/// <param name="readCallback">The callback which will be called when a line of text is read from the newly-created connection</param>
/// <returns></returns>
public static ConnectedEndPoint Connect(IPEndPoint remoteEndPoint, Action<ConnectedEndPoint, string> readCallback)
{
Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
socket.Connect(remoteEndPoint);
return new ConnectedEndPoint(socket, readCallback);
}
/// <summary>
/// Asynchronously accept a new connection from a remote end-point
/// </summary>
/// <param name="listener">The listening <see cref="Socket"/> which will accept the connection</param>
/// <param name="readCallback">The callback which will be called when a line of text is read from the newly-created connection</param>
/// <returns></returns>
public static async Task<ConnectedEndPoint> AcceptAsync(Socket listener, Action<ConnectedEndPoint, string> readCallback)
{
Socket clientSocket = await Task.Factory.FromAsync(listener.BeginAccept, listener.EndAccept, null);
return new ConnectedEndPoint(clientSocket, readCallback);
}
/// <summary>
/// Write a line of text to the connection, sending it to the remote end-point
/// </summary>
/// <param name="text">The line of text to write</param>
public void WriteLine(string text)
{
lock (_lock)
{
if (!_closing)
{
_writer.WriteLine(text);
_writer.Flush();
}
}
}
/// <summary>
/// Initiates a graceful closure of the connection
/// </summary>
public void Shutdown()
{
_Shutdown(SocketShutdown.Send);
}
/// <summary>
/// Implements <see cref="IDisposable.Dispose"/>
/// </summary>
public void Dispose()
{
_reader.Dispose();
_writer.Dispose();
_socket.Close();
}
/// <summary>
/// Constructor. Private -- use one of the factory methods to create new connections.
/// </summary>
/// <param name="socket">The <see cref="Socket"/> for the new connection</param>
/// <param name="readCallback">The callback for reading lines on the new connection</param>
private ConnectedEndPoint(Socket socket, Action<ConnectedEndPoint, string> readCallback)
{
_socket = socket;
Stream stream = new NetworkStream(_socket);
_reader = new StreamReader(stream, Encoding.UTF8, false, 1024, true);
_writer = new StreamWriter(stream, Encoding.UTF8, 1024, true);
ReadTask = _ConsumeSocketAsync(readCallback);
}
private void _Shutdown(SocketShutdown reason)
{
lock (_lock)
{
if (!_closing)
{
_socket.Shutdown(reason);
_closing = true;
}
}
}
private async Task _ConsumeSocketAsync(Action<ConnectedEndPoint, string> callback)
{
string line;
while ((line = await _reader.ReadLineAsync()) != null)
{
callback(this, line);
}
_Shutdown(SocketShutdown.Both);
}
}
客户端程序将直接使用该类。服务器端封装在另一个类中,在上面的相同DLL中找到:
/// <summary>
/// Event arguments for the <see cref="ChatServer.Status"/> event
/// </summary>
public class StatusEventArgs : EventArgs
{
/// <summary>
/// Gets the status text
/// </summary>
public string StatusText { get; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="statusText">The status text</param>
public StatusEventArgs(string statusText)
{
StatusText = statusText;
}
}
/// <summary>
/// A server implementing a simple line-based chat server
/// </summary>
public class ChatServer
{
private readonly object _lock = new object();
private readonly Socket _listener;
private readonly List<ConnectedEndPoint> _clients = new List<ConnectedEndPoint>();
private bool _closing;
/// <summary>
/// Gets a task representing the listening state of the servdere
/// </summary>
public Task ListenTask { get; }
/// <summary>
/// Raised when the server has status to report
/// </summary>
public event EventHandler<StatusEventArgs> Status;
/// <summary>
/// Constructor
/// </summary>
/// <param name="port">The port number the server should listen on</param>
public ChatServer(int port)
{
_listener = new Socket(SocketType.Stream, ProtocolType.Tcp);
_listener.Bind(new IPEndPoint(IPAddress.Any, port));
_listener.Listen(int.MaxValue);
ListenTask = _ListenAsync();
}
/// <summary>
/// Initiates a shutdown of the chat server.
/// </summary>
/// <remarks>This method closes the listening socket, which will subsequently
/// cause the listening task to inform any connected clients that the server
/// is shutting down, and to wait for the connected clients to finish a graceful
/// closure of their connections.
/// </remarks>
public void Shutdown()
{
_listener.Close();
}
private async Task _ListenAsync()
{
try
{
while (true)
{
ConnectedEndPoint client = await ConnectedEndPoint.AcceptAsync(_listener, _ClientReadLine);
_AddClient(client);
_CleanupClientAsync(client);
}
}
catch (ObjectDisposedException)
{
_OnStatus("Server's listening socket closed");
}
catch (IOException e)
{
_OnStatus($"Listening socket IOException: {e.Message}");
}
await _CleanupServerAsync();
}
private async Task _CleanupServerAsync()
{
ConnectedEndPoint[] clients;
lock (_lock)
{
_closing = true;
clients = _clients.ToArray();
}
foreach (ConnectedEndPoint client in clients)
{
try
{
client.WriteLine("Chat server is shutting down");
}
catch (IOException e)
{
_OnClientException(client, e.Message);
}
client.Shutdown();
}
// Clients are expected to participate in graceful closure. If they do,
// this will complete when all clients have acknowledged the shutdown.
// In a real-world program, may be a good idea to include a timeout in
// case of network issues or misbehaving/crashed clients. Implementing
// the timeout is beyond the scope of this proof-of-concept demo code.
try
{
await Task.WhenAll(clients.Select(c => c.ReadTask));
}
catch (AggregateException)
{
// Actual exception for each client will have already
// been reported by _CleanupClientAsync()
}
}
// Top-level "clean-up" method, which will observe and report all exceptions
// In real-world code, would probably want to simply log any unexpected exceptions
// to a log file and then exit the process. Here, we just exit after reporting
// exception info to caller. In either case, there's no need to observe a Task from
// this method, and async void simplifies the call (no need to receive and then ignore
// the Task object just to keep the compiler quiet).
private async void _CleanupClientAsync(ConnectedEndPoint client)
{
try
{
await client.ReadTask;
}
catch (IOException e)
{
_OnClientException(client, e.Message);
}
catch (Exception e)
{
// Unexpected exceptions are programmer-error. They could be anything, and leave
// the program in an unknown, possibly corrupt state. The only reasonable disposition
// is to log, then exit.
//
// Full stack-trace, because who knows what this exception was. Will need the
// stack-trace to do any diagnostic work.
_OnStatus($"Unexpected client connection exception. {e}");
Environment.Exit(1);
}
finally
{
_RemoveClient(client);
client.Dispose();
}
}
private void _ClientReadLine(ConnectedEndPoint readClient, string text)
{
_OnStatus($"Client {readClient.RemoteEndPoint}: \"{text}\"");
lock (_lock)
{
if (_closing)
{
return;
}
text = $"{readClient.RemoteEndPoint}: {text}";
foreach (ConnectedEndPoint client in _clients.Where(c => c != readClient))
{
try
{
client.WriteLine(text);
}
catch (IOException e)
{
_OnClientException(client, e.Message);
}
}
}
}
private void _AddClient(ConnectedEndPoint client)
{
lock (_lock)
{
_clients.Add(client);
_OnStatus($"added client {client.RemoteEndPoint} -- {_clients.Count} clients connected");
}
}
private void _RemoveClient(ConnectedEndPoint client)
{
lock (_lock)
{
_clients.Remove(client);
_OnStatus($"removed client {client.RemoteEndPoint} -- {_clients.Count} clients connected");
}
}
private void _OnStatus(string statusText)
{
Status?.Invoke(this, new StatusEventArgs(statusText));
}
private void _OnClientException(ConnectedEndPoint client, string message)
{
_OnStatus($"Client {client.RemoteEndPoint} IOException: {message}");
}
}
大多数情况下,这就是你需要的一切。上面的DLL代码(在我的示例中)由两个不同的程序(服务器和客户端)引用。
这是服务器:
class Program
{
private const int _kportNumber = 5678;
static void Main(string[] args)
{
ChatServer server = new ChatServer(_kportNumber);
server.Status += (s, e) => WriteLine(e.StatusText);
Task serverTask = _WaitForServer(server);
WriteLine("Press return to shutdown server...");
ReadLine();
server.Shutdown();
serverTask.Wait();
}
private static async Task _WaitForServer(ChatServer server)
{
try
{
await server.ListenTask;
}
catch (Exception e)
{
WriteLine($"Server exception: {e}");
}
}
}
这是客户:
class Program
{
private const int _kportNumber = 5678;
static void Main(string[] args)
{
IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Loopback, _kportNumber);
ConnectedEndPoint server = ConnectedEndPoint.Connect(remoteEndPoint, (c, s) => WriteLine(s));
_StartUserInput(server);
_SafeWaitOnServerRead(server).Wait();
}
private static void _StartUserInput(ConnectedEndPoint server)
{
// Get user input in a new thread, so main thread can handle waiting
// on connection.
new Thread(() =>
{
try
{
string line;
while ((line = ReadLine()) != "")
{
server.WriteLine(line);
}
server.Shutdown();
}
catch (IOException e)
{
WriteLine($"Server {server.RemoteEndPoint} IOException: {e.Message}");
}
catch (Exception e)
{
WriteLine($"Unexpected server exception: {e}");
Environment.Exit(1);
}
})
{
// Setting IsBackground means this thread won't keep the
// process alive. So, if the connection is closed by the server,
// the main thread can exit and the process as a whole will still
// be able to exit.
IsBackground = true
}.Start();
}
private static async Task _SafeWaitOnServerRead(ConnectedEndPoint server)
{
try
{
await server.ReadTask;
}
catch (IOException e)
{
WriteLine($"Server {server.RemoteEndPoint} IOException: {e.Message}");
}
catch (Exception e)
{
// Should never happen. It's a bug in this code if it does.
WriteLine($"Unexpected server exception: {e}");
}
}
}
在我看来,上面提到的最重要的事情之一是ConnectedEndPoint
和ChatServer
类对使用的类具有零依赖性他们。通过使用回调委托和事件,依赖于这些类的代码能够双向交互,而这些支持类不必知道代码所在的类型(参见&#34;控制反转&#34;,这是一个变种。
您可以越多地使代码关系看起来像只有单向引用的树,编写代码就越容易,以后再进行维护。
注意:为了便于说明,我使用了事件和回调代理。这两种方法都可以自行运行。主要的权衡是复杂性与灵活性。使用事件使代码更加灵活 - 可以根据需要添加和删除事件处理程序 - 但是如果使用带有sender
和EventArgs
参数的方法签名的.NET约定来实现事件,则它是多一点&#34;重量级&#34;而不仅仅是在创建有问题的对象时传递一个简单的回调委托。我在代码中给出了每个示例,您可以在哪种情况下决定您喜欢哪种方法。
您还会注意到上述内容大量使用了C#的异步功能。起初,这可能会使代码看起来更难阅读。但事实上,使用这些功能让一切工作变得更容易,比起我尝试使用较旧的BeginXXX()
/ EndXXX()
方法,或天堂禁止,奉献一个线程到每个连接(随着客户端数量的增加,它很难扩展非常)。绝对值得习惯于以这种方式思考本质上异步的操作,例如网络I / O.