网络协议及编码

394 查看

TCP/IP协议族:IP协议、TCP协议、UDP协议

应用程序通过套接字API对UPD协议和TCP协议所提供的服务进行访问。

  • 底层由物理层:基础的通信信道构成,如以太网/WIFI或调制解调器拨号连接。

  • 网络层:完成将分组报文(packet)传输到它的目的地,即路由功能,一般采用IP协议。IP协议提供了一种数据服务:每组分组报文都有网络独立处理和分发,就像信件或包裹通过邮政系统发送一样。每个IP报文必须包含一个保存其目的地址的字段。

  • 传输层:提供了TCP协议和UDP协议,这两种协议都建立在IP层提供的服务之上,IP协议只是将分组报文分发到不同的主机,然后还需要更细粒度的寻址将报文发送到主机指定的应用程序端口,这个寻址功能是TCP或UDP要完成的,因此他们也成为端到端的传输协议。IP协议只是将数据从一个主机传到另一个主机。

TCP协议提供一个可信赖的字节流信道(处理报文丢失、重传及顺序混乱问题),一种面向连接的协议:在使用它进行通信之前,两个应用程序之间首先要建立一个TCP连接,这涉及到两台机器的TCP部件完成握手消息的交互。

UDP协议不尝试对IP层产生的错误进行修复,仅仅是简单地扩展IP协议,传输到端口。

网络层中的IP协议就像把邮件送到某个街道的某个楼的信箱,而传输层则是将信件送到该楼层的具体某个房间里头。

IP协议其实是单播协议,还有多播协议,广播到任意数量的地址。

什么是套接字(Socket)

一种抽象层,应用程序通过它来发送和接受数据,就像应用程序打开一个文件句柄,将数据读写到稳定的存储器上一样。使用socket可以将应用程序添加到网络中,并与处于同一个网络中的其他应用程序进行通信。一台计算机上的应用程序向socket写入的信息能够被另一台计算机上的另一个应用程序读取。

不同类型的socket与不同类型的底层协议族以及同一个协议族的不同协议栈相关联。

TCP/IP协议族中的主要socket类型为流套接字(stream scoket)和数据报套接字(datagram socket)。

流套接字将TCP作为其端对端协议,提供了一个可信赖的字节流服务。
数据报套接字使用UDP协议,提供了一个best-effort的数据报服务。

一个套接字抽象层可以被多个应用程序引用,每个使用了特定套接字的程序都可以通过那个套接字进行通信。每个端口都标识了一台主机上的一个应用程序。实际上,一个端口确定了一台主机上的一个套接字。

任何要交换信息的程序之间在信息的编码方式上必须达成共识(比如将信息表示为位序列),以及哪个程序发送消息,什么时候和怎么接受信息都将影响程序的行为。程序间达成的这种包含了信息交换的形式和意义的共识称为协议。用来实现特定应用程序的协议称为应用程序协议。

TCP/IP协议的唯一约束是,信息必须在块(chunk)中发送和接收,而块的长度必须是8位的倍数,因此我们可以认为TCP/IP协议中传输的信息是字节序列。

如果是自己设计和编写套接字的客户端和服务器端,则可以随心所欲地定义自己的应用程序协议。

对于需要超过一个字节来表示的数据类型,我们必须知道这些字节的发送顺序。
一种是从整数的右边开始,由低位到高位发送,即little-endian顺序;一种是从左边开始,由高位到低位发送,即big-endian顺序。

对于任何多字节的整数,发送者和接收者必须在使用big-endian顺序还是使用little-endian顺序上达成共识。

另外一个需要达成的共识是:所传输的数值是有符号的还是无符号的。
对于给定的k位,我们可以通过二进制补码来表示-2的k-1次方到2的k-1次方-1范围的值,如果使用无符号,则可以表示0到2的k次方-1之间的数值。

DataOutputStream允许你将基本数据类型按big-endian顺序进行编码,即将整数以适当大小的二进制补码的形式写到流中。

在一组符号与一组整数之间的映射称为编码字符集,比如ASCII(将英文字母、数字、标点符号以及一些特殊符号映射为0-127的整数),Unicode(映射到0~65535之间的的整数)。
编码方案:发送者和接收者需要对这些整数如何表示成字节序列达成一致。

字符集:charset,由编码字符集和字符的编码方法结合起来。

Java的输入输出流

Java里头内置了特定的序列化,隐藏了所有繁琐的参数编码解码细节。Serialization处理了将实际的Java对象转换成字节序列的工作,因此你可以在不同虚拟机之间传递Java对象实例。
缺点是:它们比较笼统,在通信开销上不能做到最高效,比如一个对象的序列化形式其包含的信息在JVM环境以外是毫无意义的;其次是Serializable和Externalizable接口不能用于已经定义了不同传输格式的情况;最后用户自定义的类必须自己实现序列化接口,容易出错。

自定义协议实例:

1、基于文本的编码方式

public byte[] toWire(VoteMsg msg) throws IOException {
        String msgString = MAGIC + DELIMSTR + (msg.isInquiry() ? INQSTR : VOTESTR)
                + DELIMSTR + (msg.isResponse() ? RESPONSESTR + DELIMSTR : "")
                + Integer.toString(msg.getCandidateID()) + DELIMSTR
                + Long.toString(msg.getVoteCount());
        byte data[] = msgString.getBytes(CHARSETNAME);
        return data;
    }

文本方式通常使用一个魔数来开头。

2、基于二进制的编码方式

二进制格式使用固定大小的消息,每条消息由一个特殊字节开始(魔数)。

/* Wire Format
 *                                1  1  1  1  1  1
 *  0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
 * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 * |     Magic       |Flags|       ZERO            |
 * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 * |                  Candidate ID                 |
 * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 * |                                               |
 * |         Vote Count (only in response)         |
 * |                                               |
 * |                                               |
 * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
 */
public byte[] toWire(VoteMsg msg) throws IOException {
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        DataOutputStream out = new DataOutputStream(byteStream); // converts ints
        short magicAndFlags = MAGIC;
        if (msg.isInquiry()) {
            magicAndFlags |= INQUIRE_FLAG;
        }
        if (msg.isResponse()) {
            magicAndFlags |= RESPONSE_FLAG;
        }
        out.writeShort(magicAndFlags);
        // We know the candidate ID will fit in a short: it's > 0 && < 1000
        out.writeShort((short) msg.getCandidateID());
        if (msg.isResponse()) {
            out.writeLong(msg.getVoteCount());
        }
        out.flush();
        byte[] data = byteStream.toByteArray();
        return data;
    }

TCP协议是一个基于流的服务,因而需要提供字节的帧。

// Create an inquiry request (2nd arg = true)
        VoteMsg msg = new VoteMsg(false, true, CANDIDATEID, 0);
        byte[] encodedMsg = coder.toWire(msg);
        // Send request
        System.out.println("Sending Inquiry (" + encodedMsg.length + " bytes): ");
        System.out.println(msg);
        framer.frameMsg(encodedMsg, out);
这里采用了基于显示长度的方式来标识帧大小:LengthFarmer类为每条消息添加一个长度前缀。
public void frameMsg(byte[] message, OutputStream out) throws IOException {
        if (message.length > MAXMESSAGELENGTH) {
            throw new IOException("message too long");
        }
        // write length prefix
        out.write((message.length >> BYTESHIFT) & BYTEMASK);
        out.write(message.length & BYTEMASK);
        // write message
        out.write(message);
        out.flush();
    }