基本概念
协议:
程序间达成的这种包含了信息交换的形式和意义的共识称为协议
IP:主机之间的通信,不负责具体的应用程序
TCP/UDP: 实现端到端的传输,应用程序到应用程序,加入端口寻址
IP地址:
每个互联网地址代表了一台主机与底层的通信信道的连接,也叫网络接口(network interface)
一台主机可以有多个接口
IP地址实际上是分配给了主机与网络之间的连接,而不是主机本身
套接字
基本类
InetAddress
:表示一个网络地址,包括主机名和数字类型的地址信息
NetworkInterface
:提供了访问主机所有接口的信息的功能
TCP类
一个Socket实例代表了TCP连接的一端。一个TCP连接(TCP connection)是一条抽象的双向信道,两端分别由IP地址和端口号确定。类似于电话系统。在开始通信之前,要建立一个TCP连接,这需要先由客户端TCP向服务器端TCP发送连接请求。ServerSocket实例则监听TCP连接请求,并为每个请求创建新的Socket实例。
Socket:
构建TCP链接,提供IO流操作信息
ServerSocket
:负责服务器端侦听链接
注意:
- Socket类的shutdownInput()和shutdownOutput()方法能够将输入输出流相互独立地关闭
- 从socket中读取信息时需要循环读取,即使对方只write了一次信息,为什么不只用一个read方法呢?TCP协议并不能确定在read()和write()方法中所发送信息的界限,也就是说,虽然我们只用了一个write()方法来发送字符串,服务器也可能从多个块(chunks)中接受该信息。即使字符串在服务器上存于一个块中,在返回的时候,也可能被TCP协议分割成多个部分。对于初学者来说,最常见的错误就是认为由一个write()方法发送的数据总是会由一个read()方法来接收。
UDP类
一旦被创建,UDP套接字就可以用来连续地向不同的地址发送信息,或从任何地址接收信息。类似于邮局系统。
DatagramPacket: 数据报文的承载体,拥有自己的缓存和内部长度,偏移量 (length offset)。除传输的信息本身外,每个实例中还附加了地址和端口信息。
内部长度:在输入时,用来指定接收到的将被复制到缓冲区的消息的最长字节数,在返回时,用来指示实际存入缓冲区的字节数。
DatagramSocket: 接受或者发送报文,进行通信。由于数据报文可能丢失,在传输或者等待响应的时候需要加上超时限制和重传限制。
注意:
DP处理了接收到的消息后,数据包的内部长度将设置为刚处理过的消息的长度,而这可能比缓冲区的原始长度短,此时需要重新显示设置缓存区长度。
UDP协议没有提供从网络错误中恢复的机制,因此,并不对可能需要重传的数据进行缓存
消息从网络到达后,其所包含数据被read()方法或receive()方法返回前,数据存储在一个先进先出(first-in, first-out,FIFO)的接收数据队列中。对于已连接的TCP套接字来说,所有已接收但还未传送的字节都看作是一个连续的字节序列(见第6章)。然而,对于UDP套接字来说,接收到的数据可能来自于不同的发送者。一个UDP套接字所接收的数据存放在一个消息队列中,每个消息都关联了其源地址信息。每次receive()调用只返回一条消息。然而,如果receive()方法在一个缓存区大小为n的DatagramPacket实例中调用,而接收队列中的第一条消息长度大于n,则receive()方法只返回这条消息的前n个字节。超出部分的其他字节都将自动被丢弃,而且对接收程序也没有任何消息丢失的提示!
一个DatagramPacket实例中所运行传输的最大数据量为65507字节,即UDP数据报文所能负载的最多数据。因此,使用一个有65600字节左右缓存数组的数据包总是安全的。
对于新手的另一个潜在的问题根源是DatagramPacket类的getData()方法,该方法总是返回缓冲区的原始大小,忽略了实际数据的内部偏移量和长度信息。正确使用应该配合offset取用缓存区需要的数据段。
IO流
输入输出 | 类型 |
---|---|
Buffered[Input/Output]Stream | 性能 |
Checked[Input/Output]Stream | 维护 |
Cipher[Input/Output]Stream | 加密/解密 |
Data[Input/Output]Stream | 数据处理 |
Digest[Input/Output]Stream | 维护 |
GZIP[Input/Output]Stream | 压缩/解压缩 |
Object[Input/Output]Stream | 数据处理 |
注:当没有数据可读,而又没有检测到流结束标记时,InputStream的read()方法都将阻塞等待,直到至少有一个字节可读
信息发送和接受
TCP/IP协议的唯一约束是,信息必须在块(chunks)中发送和接收,而块的长度必须是8位的倍数,因此,我们可以认为在TCP/IP协议中传输的信息是字节序列。鉴于此,我们可以进一步把传输的信息看作数字序列或数组,每个数字的取值范围是0到255
信息编码
TCP/IP协议传输的是字节序列,那么Java中的其他类型Int Short等则需要显示转换为字节类型,才能在IO流中传送。
整数编码
字节大小:
- Int 32位 4个字节来表示
- Long 64位 8个字节表示
字节顺序:
- 对于需要超过一个字节来表示的数据类型,我们必须知道这些字节的发送顺序。显然有两种选择:从整数的右边开始,由低位到高位地发送,即little-endian顺序;或从左边开始,由高位到低位发送,即big-endian顺序。
是否是有符号数:
- 有符号数都用补码表示 无符号数?
将消息的正确值存入字节数组(编码:位操作)
public static int encodeIntBigEndian(byte[] dst, long val, int offset, int size) {
for (int i = 0; i < size; i++) {
dst[offset++] = (byte) (val >> ((size - i - 1) * Byte.SIZE));
}
return offset;
}
赋值语句的右边,首先将数值向右移动,以使我们需要的字节处于该数值的低8位中。然后,将移位后的数转换成byte型,并存入字节数组的适当位置。在转换过程中,除了低8位以外,其他位都将丢弃。这个过程将根据给定数值所占字节数迭代进行。该方法还将返回存入数值后字节数组中新的偏移位置,因此我们不必做额外的工作来跟踪偏移量
将消息从字节数组取出来(解码)
public static long decodeIntBigEndian(byte[] val, int offset, int size)
{
long rtn = 0;
for (int i = 0; i < size; i++)
rtn = (rtn << Byte.SIZE) | ((long) val[offset + i] & BYTEMASK);
return rtn;
}
根据给定数组的字节大小进行迭代,通过每次迭代的左移操作,将所取得字节的值累积到一个long型整数中。
理解上述操作过程,具体操作中可以使用工具类 Data(Input/Output)Stream,它提供了writeByte(),writeShort(),writeInt(),以及writeLong()方法
字符串和文本编码
每个String实例都对应了一个字符序列(数组,char[]类型)。一个字符在Java内部表示为一个整数。例如,字符”a”,即字母”a”的符号,与整数97对应。
在一组符号与一组整数之间的映射称为编码字符集(coded character set.) ASCII、Unicode(Java采用)等
发送者与接收者必须在符号与整数的映射方式上达成共识,才能使用文本信息进行通信。此外,对于每个整数值都比255小的一小组字符,则不需要其他信息,因为其每个字符都能够作为一个单独的字节进行编码。而大于255的则需要多个字节编码,如何排列这些字节顺序称为编码方案。 编码字符集和字符的编码方案结合起来称为字符集(charset)
Java中可以使用getBytes(字符集名称)的方法来获得一个字符串在不同字符集上的编码表示。
布尔值编码(位图)
位图的主要思想是整型数据中的每一位都能够对一个布尔值编码–通常是0表示false,1表示true。例如,我们将int中的各位从0到31进行编号,其中0代表最低位。一般来说,如果一个int值在第i位值为1,其他位都为0的话,该int型整数的值就是 2i
final int BIT5 = (1<<5); //左移5位
final int BIT7 = 0x80;
final int BITS2AND3 = 12; // 8+4
//操作某一位时,需要将该位图与特定位对应的掩码进行或与等操作 | & ~(求补码)
成帧与解析
帧
成帧(Framing)技术则解决了接收端如何定位消息的首尾位置的问题。无论信息是编码成了文本、多字节二进制数、或是两者的结合,应用程序协议必须指定消息的接收者如何确定何时消息已完整接收。
UDP协议拥有完整的消息界限,DatagramPacket 负载的数据有一个确定的长度,接收者能够准确地知道消息的结束位置。然而,如果通过TCP套接字来发送消息,情况将变得更复杂,因为TCP协议中没有消息边界的概念。有如下两种解决方案:
基于定界符(Delimiter-based):消息的结束由一个唯一的标记(unique marker,)指出,即发送者在传输完数据后显式添加的一个特殊字节序列。这个特殊标记不能在传输的数据中出现。
显式长度(Explicit length):在变长字段或消息前附加一个固定大小的字段,用来指示该字段或消息中包含了多少字节。
构建协议
不同的应用程序有着自己的业务需求,传递不同格式的数据,在实现客户端和服务器通信的时候需要保持双方对数据格式编码都保持一致,这就是协议的作用。
例如有一个投票程序,能够查询候选人票数和投票,构建协议的时候可以考虑一下几点:
通信数据编码表示:
基于文本的表示方法:
该协议指定使用US-ASCII字符集对文本进行编码。消息的开头是一个所谓的”魔术字符串”,即一个字符序列,用于接收者快速将投票协议的消息和网络中随机到来的垃圾消息区分开。投票/查询布尔值被编码成字符形式,’v’表示投票消息,’i’表示查询消息。消息的状态,即是否为服务器的响应等待。
二进制表示方法:
二进制格式使用固定大小的消息。每条消息由一个特殊字节开始,该字节的最高六位为一个”魔术”值010101。这一点少量的冗余信息为接收者收到适当的投票消息提供了一定程度的保证。该字节的最低两位对两个布尔值进行了编码。消息的第二个字节总是0,第三、第四个字节包含了candidateID值。只有响应消息的最后8个字节才包含了选票总数信息
使用何种通信技术 TCP/UDP
多线程
- 一个客户端一个线程
- 线程池技术
- Java自带的线程调度工具 Executor接口
多接收者
只有UDP协议允许多播或者广播
广播
- IPv4的本地广播地址(255.255.255.255)将消息发送到在同一广播网络上的每个主机。本地广播信息决不会被路由器转发
- 并不存在可以向网络范围内所有主机发送消息的广播地址
- 本地广播功能还是非常有用的,它通常用于在网络游戏中处于同一本地(广播)网络的玩家之间交换状态信息。
- 单播和广播代码是类似的,只用把单播地址改成广播地址
多播
- 多播与单播之间的一个主要区别是地址的形式。一个多播地址指示了一组接收者,这个组也叫多播组,任何加入多播组的主机都可以接受到发送到该多播组的消息。
- Java中使用MulticastSocket来实现多播功能。
互联网广播的范围是限定在一个本地广播网络之内的,并对广播接收者的位置进行了严格的限制。多播通信可能包含网络中任何位置的接收者,因此多播有个好处就是它能够覆盖一组分布在各处的接收者。IP多播的不足在于接收者必须知道要加入的多播组的地址。而接收广播信息则不需要指定地址信息。在某些情况下,广播是一个比多播更好更易于发现的机制。所有主机在默认情况下都可以接收广播
NIO
- 多线程处理多任务,在服务器监听端口时,无法知道哪个线程或接口将发起链接,因此服务器需要一直轮询这些接口,而不能做其他事情,造成忙等状态,十分浪费系统资源。
- 程序员几乎不能对什么时候哪个线程将获得服务进行控制。
- NIO:非阻塞式IO 我们需要一种方法来一次轮询一组客户端,以查找哪个客户端需要服务。这正是NIO中将要介绍的Selector和Channel抽象的关键点。NIO中将介绍的另一个主要特性是Buffer类。就像selector和channel为一次处理多个客户端的系统开销提供了更高级的控制和可预测性,Buffer则提供了比Stream抽象更高效和可预测的I/O。使用Buffer有两个主要好处。
第一,与读写缓冲区数据相关联的系统开销暴露给了程序员。
第二,一些对Java对象的特殊Buffer映射操作能够直接操作底层平台的资源(例如,操作系统的缓冲区)