目录
简介
成帧与解析
成帧技术案例
在程序中使用套接字向其他程序提供信息或者使用其他程序提供的信息,这就需要任何需要交换信息的程序间在信息编码方式上达成共识(包含了信息交换的形式和意义),称为协议,用来实现特定的应用程序的协议叫应用程序协议。大部分应用程序协议是根据字段序列组成的离散信息定义的,而每个字段有包含了一段位序列编码的特定信息。应用程序协议中明确定义了信息发送者应该怎么排列和解释这些信息,同时定义接收者应该怎样解析。TCP/IP协议中信息必须在块(8位的倍数)中发送和接收,所以TCP/IP协议传输的信息是字节序列或者数组。
传输信息时,对于一个TCP套接字而言,通过套接字将字节信息写到一个与Socket关联的OutputStream实例中。而UDP套接字会将信息封装到DatagramPacket实例中,然后通过DatagramSocket发送。可见传输的信息的数据是字节和字节数组。Java是强类型语言,需要将其他数据类型转换成字节数组。为了完整传输字节信息,需要发送端和接收端达成一些共识:
1)传输的每个整数字节大小(size)。如Java中int数据类型由32位表示,所以使用4个字节传输int型的变量或者常量。
2)对多字节的整数,使用的是big-endian顺序还是little-endian顺序。big-endian从高位到低位发送,而little-endian是低位到高位发送。
3)传送的数值是有符号的还是无符号的。Java中4种基本整型都是有符号的,编码和解码无符号数需要掩码。掩码是整数值,其中一位或者多位是1,其他为0,与掩码进行“位操作”清空特定一位或者得到特定的一位。
4)明确符号与整数的映射方式(即编码方式),才能使用文本信息通信。发送者如果和接收者采用不同的字符集,可能会造成乱码的情况,Java中传输文本信息-->字节/字节数组,可以调用getBytes()方法,并可以将字符集名称传递getBytes()方法。
发送端按照规定格式传输数据,接收端必须将接收到的字节序列还原成原始信息,应用程序协议通常处理的是由一组字段组成的离散信息,无论信息以文本、多字节二进制或者两者结合传输,必须指定消息的接收者如何确定何时消息已完整接收。成帧技术解决了接收端如何定位消息的首位位置问题。
1.使用UDP套接字发送数据,消息负载到DatagramPacket中发送,由于DatagramPacket负载的数据有一个确定的长度,接收者能够准确地知道消息的结束位置。
2.对于TCP套接字,没有消息边界的概念。如果每个消息有固定数量字段组成,所有字段又有固定的长度,消息的长度就能确定,接收端可将消息长度对应的字节数读取到字节缓冲区中。对于可变消息长度,需要确定消息以及字段的边界问题。
主要有两个技术使接收者能够准确地找到消息的结束位置。
1)基于定界符:消息的结束由一个唯一的标记指出,即发送者在传输完数据后显式添加的一个特殊字节序列。但要求消息本身不能存在定界符。
2)显式长度:在变长字段或消息前附加一个固定大小的字段。用来指示该字段或消息包含了多少字节。
需要注意的一点是:基于定界符使用在TCP连接上传输的最后一个消息上,发送完这个消息后,发送者就简单地关闭(使用shutdownOutput()或者close()方法)发送端的TCP连接,接收者读取万这条消息的最后一个字节后,将接收到一个流结束标记(即read()方法返回-1),该编辑指示出已经读取到达了消息的末尾。
下面的案例分别使用两中成帧技术来发送消息,接口Framer定义了两个方法,frameMsg()方法定义使用成帧技术将消息添加到流中,而nextMsg()通过判断成帧技术判断消息末尾,读取下一条消息。
public interface Framer {
void frameMsg(byte[] message, OutputStream out) throws Exception;
byte[] nextMsg() throws IOException;
}
1.基于定界符实现了成帧技术的类DelimerFramer。
下面的类基于定界符的成帧方法,定界符为“换行”符('\n').frameMsg()方法并没有实现填充,当成帧的字节序列中包含了定界符时,简单地抛出了异常。nextMsg()方法扫描流,知道读取到定界符,并返回定界符前面的所有的字符。如果流为空则返回null,如果累积了一个消息的不少字符,但知道流结束也没有知道定界符,程序将抛出一个异常来只是成帧错误。
public class DelimerFramer implements Framer {
private InputStream in;
private static final byte DELIMITER = '\n';
public DelimerFramer(InputStream in) {
this.in = in;
}
@Override
// 基于定界符的成帧技术
public void frameMsg(byte[] message, OutputStream out) throws IOException {
for (byte b : message) {
// 消息本身不能包含有定界符
if (b == DELIMITER) {
throw new IOException("message contains delimiter");
}
}
// 写入消息以及定界符
out.write(message);
out.write(DELIMITER);
out.flush();
}
@Override
public byte[] nextMsg() throws IOException {
ByteArrayOutputStream messageBuffer = new ByteArrayOutputStream();
int nextByte;
while ((nextByte = in.read()) != DELIMITER) {
// 流已经结束,没有出现定界符
if (nextByte == -1) {
if (messageBuffer.size() == 0) {
return null;
} else {
// 读取到的消息不为空,则抛出无定界符非空消息异常。
throw new EOFException("Non-empty message without delimiter");
}
}
messageBuffer.write(nextByte);
}
return messageBuffer.toByteArray();
}
}
2.基于长度的成帧技术的LengthFramer类。
基于长度的成帧方法,适用于长度小于65535字节的消息,发送者首先给出指定消息的长度小于65535字节的消息,发送者首先给出指定消息的长度,并将长信息以big-endian顺序存入两个字节的整数中,再将这两个字节放在完整的消息内容前,连同消息一起写入输出流。在接收端,我们使用DataInputStream读取整型的长度信息,readFully()方法将阻塞等待,直到给定数组完全填满。这种成帧方法,发送者不需要检查成帧的消息内容,只需要检查消息的长度是否超出了限制。
public class LengthFramer implements Framer {
private static final int MAXMESSSAGELENGTH = 65535;
private static final int BYTEMASK = 0xff;
private static final int SHORTMASK = 0xffff;
private static final int BYTESHIFT = 8;
private DataInputStream in;
public LengthFramer(InputStream in) {
this.in = new DataInputStream(in);
}
@Override
public void frameMsg(byte[] message, OutputStream out) throws Exception {
//消息长度不能超过65535,即两个字节
if (message.length > MAXMESSSAGELENGTH) {
throw new IOException("message too long");
}
//基于显式长度的帧消息,前两个字节写入消息长度。
out.write((message.length >> BYTESHIFT) & BYTEMASK);
out.write(message.length & BYTEMASK);
out.write(message);
out.flush();
}
@Override
public byte[] nextMsg() throws EOFException, IOException {
int length;
try {
//读取前两个字节是消息的长度。
length = in.readUnsignedShort();
} catch (EOFException e) {
return null;
}
byte[] bytes = new byte[length];
in.readFully(bytes);
return bytes;
}
}