内容提示:TCP的网络编程中一些典型的问题,以及一些分析和解决方案
TCP通讯故障
TCP通道发送和接受数据时通常会出现许多情况
1,数据缓冲区处理不当
2,网络上的消息处理不当
在现实世界中,与为知远程服务器或客户机进行通讯时,我们可能不知道流入数据的类型和尺寸,如果缓冲区中受到不一的消息
会发生什么情况?当多于事先定义的数据量到达数据缓冲区时该如何处理?
一,适当使用数据缓冲区
由于数据是通过TCP来接受的因此存放在系统的内部缓冲区中。对Receive()方法的每一个
调用都试图从TCP缓冲区中取走数据。Receive(Data)方法读取的数据量受Sizeof(Data)影响,
如果缓冲区不够大,在Receive()中无法装载所有TCP缓冲区数据,余下的数据则要等待下一个Receive()调用。
缓冲区太小回导致消息的不配比,缓冲区过大则导致消息的混合(TCP进行网络通讯时必须记住TCP是不考虑消息的边界问题)
Receive()函数同时返回一个整型,表示实际读取的数据量(Send()函数类同)。
消息故障解决方案
对于TCP通讯的无防卫的消息边界问题,必须使用一定的技术区分远程系统中的不同消息
A,一直发送固定长度的消息
B,将消息尺寸与消息一起发送
C,使用标记系统分割消息
1, 固定尺寸消息,最简单但是有可能是最昂贵的解决TCP消息问题的方案,接受到多个消息时,
能够根基消息中字节数将他们区分开。
{
int total=0;
int size=data.Length;
int dataleft=size;
int sent;
while(total<size)
{
sent=s.Send(data,total,dataleft,SocketFlags.None);
total+=sent;
dataleft-=sent;
}
return total;
}
响应的接受函数
{
int total=0;
int dataleft=size;
byte[] data=new byte[size];
int recv;
while(total<size)
{
recv=s.Receive(data,total,dataleft,0);
if(recv==0)
{
data=Encoding.ASCII.GetBytes("exit ");
break;
}
total+=recv;
dataleft-=recv;
}
return data;
}
虽然这种方法能够解决消息边界的问题,但是这种方法缺乏灵活性,必须保证所有的消息都恰好是同一长度,
这就意味着必须将短消息加长,这将造成网络带宽资源的浪费。
对这个问题最简单的解决方案是允许使用可变长度的消息,唯一的不足就是接受端的远程设备必须了解
每一个变长消息的确切尺寸。在消息包里包含消息大小的方法非常多,最简单的方法就是创建一个文本表示
消息的大小,并把它追加在消息的前面。
当然这种解决方案也有不足之处,客户机和服务器程序都必须了解用于表示消息的尺寸消息的字节数。
显然,对一个大型的数据包,仅用一个字符肯定不够,而是用3个到4个字符来表示尺寸的消息,对于较小的
数据包来说这又是一种资源的浪费。
如何解决这个问题呢?除了使用文本来表示信息尺寸外,我们还可以直接使用消息尺寸实际的整型数值。
这就需要将整型数转换为字节数组。
2,发送变长的信息
SendVarData()方法是由SendData()方法改进而来的,在这个方法中消息的尺寸信息用一个4个字节的整型数
来表示,附加在每一个消息的开始位置。使用4个字节的?整型数值,只要是小于65KB的消息都能使用相同的编码
方式来表示大小:
{
int total=0;
int size=data.length;
int dataleft=size;
int sent;
byte[] datasize=new byte[4];
datasize=BitConver.GetBytes(size);
sent=s.Send(datasize);
}
对应的接受函数
{
int total=0;
int recv;
byte[] datasize=new byte[4];
recv=s.Receive(datasize,0,4,0);
int size=BitConverter.ToInt32(datasize);
int dataleft=size;
byte[] data=new byte[size];
while(total<size)
{
recv=s.Receive(data,total,dataleft,0);
if(recv==0)
{
data=Encoding.ASCII.GetBytes("exit ");
break;
}
total+=recv;
dataleft-=recv;
}
return data;
}
/*/注意:在使用BitConverter()时,整型数到字节数组的转换是根基本地机器的字节顺序进行的,只要是
使用Intel芯片,Windows操作系统的机器,都没有问题。如果使用的是其他系统,采用不同的字节顺序,
麻烦就有拉。解决方案下次提
3,使用消息的标记
因为消息是从套接字中接受的,挨个字符对数据进行检测,当检测到一个标记时,就认为是一个完整的
消息,并将这个消息发送到应用中。标记后面的数据是一个消息的开始
但缺点之一就是:所接受到的数据流中的每一个字符必须都要检查以便确定是否为标记。对于大消息
来说,就可能导致系统性能的下降。而且某一些字符必须设计为标记,这些字符要与普通的字符相互区分。
除了设计自己专用的消息标记外,C#语言提供了一些类,能够用于简化这个过程。
TCP通讯中使用流(NetworkStream类)
ReadLine()方法和WriteLine()方法都是于数据流中换行符为止,为此我们可以使用换行符作为消息标记来
区分不同的消息
流服务器(StreamTcpSrvr.cs)
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
class StreamTcpSrvr
{
public static void Main()
{
string data;
IPEndPoint ipep=new IPEndPoint(IPAddress.Any,9050);
Socket newsock=new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
newsock.Bind(ipep);
newsock.Listen(10);
Console.WriteLine("Waiting for a client");
Socket client=newsock.Accept();
IPEndPoint newclient=(IPEndPoint)client.ReomoteEndPoint;
Console.WriteLine("Connected with {0} at port {1}",newclient.Address,newclient.Port);
NetWorkStream ns=new NetworkStream(client);
StreamReader sr=new StreamReader(ns);
StreamWriter sw=new StreamWriter(ns);
String welcome="Welcome to my test server";
sw.WriteLine(welcome);
sw.Flush();
while(True)
{
try
{
data=sr.ReadLine();
}
catch(IOException)
{
break;
}
Console.WriteLine(data);
sw.WriteLine(data);
sw.Flush();
}
Console.WriteLine("Disconnected from {0}",newclient.Address);
sw.Close();
sr.Close();
ns.Close();
}
}
StreamTcpSrvr如何了解远程连接是否断开的方式,因为ReadLine()方法是对数据流进行操作,而不是
对套接字进行操作。在远程连接关闭的时候该方法不返回0,相反,如果没有底层的套接字,该方法将
生成一个异常故障,为此,必须捕捉这个错误,同时给出响应的处理。
相应的流客户机(SteamTcpClient.cs)
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
class SteamTcpClient
{
public static void Main()
{
string data;
string input;
IPEndPoint ipep=new IPEndPoint(IPAddress.Parse("127.0.0.1"),9050);
Socket server=new Socket(SddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
try
{
server.Connect(ipep);
}
catch(SocketException e)
{
Console.WriteLine("Unable to connect to server.");
Console.WriteLine(e.ToString());
return;
}
NetworkStream ns=new NetworkStream(server);
StreamReader sr=new StreamReader(ns);
StreamWriter sw=new StreamWrite(ns);
data=sr.ReadLine();
Console.WriteLine(data);
while(true)
{
input=Console.ReadLine();
if(input=="exit")
break;
sw.WriteLine(input);
sw.Flush();
data=sr.ReadLine();
Console.WriteLine("Disconnecting from server");
sr.Close();
sw.Close();
ns.Close();
server.Shutdown(SocketShutdown.Both);
server.Close();
}
}
}
小结:因为TCP套接字是面向连接的,在数据传输只前,发送和接受的双方都必须建立好连接。数据以
流的形式发送到远程机器上,因此在TCP会话中消息是没有边界的。
在这种使用TCP协议的环境中,不能保护消息的边界,因此必须考虑如何识别单个消息。