第一章概述
- ISO/OSI模型
- 共分为7层:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层;
- 物理层:实现网络连接,按比特流传送数据信息;
- 数据链路层:建立相邻节点之间的数据链路,按照数据帧的格式组织数据,控制帧的传输,进行差错控制,以及提供数据链路通路的建立、维持和释放;
- 网络层:接收来自其他计算机的数据包,或发送数据包;
- 传输层:提供独立于具体通信协议的数据传输服务,在计算机之间建立通信通道;
- 会话层:在计算机之间组织会话;
- 表示层:处理数据的表示方法并进行转换,以消除不同的语义差异;
- 应用层:专门针对网络通信应用程序提供服务;
- 网络协议
- 每个层都有一个或者多个协议;
- 物理层常用协议是RS-232,RJ-45;
- 数据链路层常用的协议是PPP;
- 网络层常用的协议是IP;
- 传输层常用的协议是TCP和UDP;
应用层常用的协议是HTTP、HTTPS和FTP; - 还包括自定义的各种协议;
- TCP/IP协议(传输控制协议/网间协议)
- 是网络通信协议中应用最广泛的协议,互联网采用的就是TCP/IP协议;
- 是一种简化的ISO模型;
- 这是属于一个协议体系,并不是只有TCP和IP协议,是网络协议体系标准;
- 分为四层:链路层、网络层、传输层、应用层;
- 链路层:也叫网络接口层,是最底层,TCP/IP没有严格定义该层,只要求主机必须使用某种协议与网络连接,以便能在其上传递IP分组
- 网络层:俗称IP层,它处理机器之间的通信,它接收来自传输层的请求,传输某个具有目的地址的分组并封装到数据报中;将分组分配到IP数据报中。IP地址由NIC统一进行管理,并进一步由地区性的NIC负责某个范围内的具体分层管理;
- 传输层:提供应用层之间的通信,即端到端的通信。管理信息流,提供可靠的传输服务,让数据无差错的按序到达;
- 应用层:负责发送和机收数据,根据选择的传输服务类型,按协议的格式传送给传输层;
- TCP是一种面向连接的,保证可靠传输协议;UDP是一种无连接的协议,每个数据包都是一个独立的信息,包括完整的源地址和目标地址,不能保证正确性;UDP传输数据时,每个数据包必须限制在64KB之内,TCP没有这个限制;
java的输入和输出
- java中的输入和输出的类型有:常规的标准输入和输出、文件输入和输出、内存数据结构输入和输出、网络数据流的输入和输出;
- 字节流抽象类:InputStream和OutputStream类;
- 字符流抽象类:Reader和Writer类;
- 流类概览
- InputStream分支:字节数组输入流(ByteArray)、文件字节输入流(File)、对象字节输入流(Object)、管道字节输入流(Piped)、序列输入流(Sequence)、过滤字节输入流(Filter)、缓冲字节输入流(Buffered)、基本数据字节输入流(Data)、回退字节输入流(Pushback)
- OutputStream分支:前七个都是对应的有个独有的打印字节流(PrintStream)封装其他流类,格式化的进行输出
- Reader和Writer分支类别都差不多;
- IO异常
- IO方法基本都会有异常,巨大部分异常都是IOException类或者子类;
- IO异常有两种处理方式:一是使用try catch,二是抛出到上一级,由调用者处理;
- InputStream常用方法
方法 | 定义 |
---|---|
int available | 返回下一次能够读取 不受阻塞或跳过 的字节数 |
void close | 关闭输入流,并释放相关的所有系统资源 |
void mark(int) | 标记输入流的当前位置,通常和reset结合使用,不是所有流都支持,可以通过markSupported方法判断是否支持mark |
abstract int read | 从输入流中读取一个字节 |
int read(byte[]) | 从输入流中读取一定数量的字节存入字节数组,并返回读取的字节数 |
int read(byte[] b, int off, int len) | 从输入流中读取最多len的字节存入字节数组b,读取的第一个字节存入b[off]中 |
void reset | 将流重新置位为上次mark的位置 |
long skip(long n) | 跳过输入流中的指定数量n字节 |
- OutputStream常用方法
方法 | 定义 |
---|---|
abstract void write(int b) | 将整型b的低字节写出入到输出流,低8位,其余24位忽略 |
void write(byte[] b) | 将字节数组b的数据写入到输出流 |
void write(byte[] b, int off, int len) | 把字节字节数组b中从off开始的len字节写入到输出流 |
flush | 有时写到输出流的字节并没有被真正的发送出去,而是被缓存,达到一定的量时才会发出去,最后总会遗留一点数据,使用此方法可以将缓冲区的数据强行执行写操作 |
void close | 关闭输出流,并释放相关的所有系统资源 |
- 文件流代码
static void fileReaderInput() {
try {
File file = new File("/Users/hansen/Desktop/项目练习/javaProjects/java考试/src/com/company/Person.java");
FileReader reader = new FileReader(file);
FileWriter writer = new FileWriter("Person(1).java");
char b[] = new char[100];
int length;
while ((length = reader.read(b)) != -1) {
writer.write(b, 0, length);
}
reader.close();
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
static void fileInputTest() {
try {
FileInputStream fileInputStream = new FileInputStream("/Users/hansen/Desktop/项目练习/javaProjects/java考试/src/com/company/Person.java");
FileOutputStream outputStream = new FileOutputStream("/Users/hansen/Desktop/项目练习/javaProjects/Person.java");
System.out.println("文件长度:" + fileInputStream.available());
byte bytes[] = new byte[100];
int count = 0;
while ((count = fileInputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, count);
}
fileInputStream.close();
outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
- 数组流
- 可以将其他的流写入数组流,数组流的特点是可以直接操作数组,并可以标记和重置某个位置,实现更为精细的控制;
- 使用的类:ByteArrayInputStream、ByteArrayOutputStream 、CharArrayInputStream、CharArrayOutputStream;
- 输出流可以不调用flush;
8.基本数据类型流 - DataInputStream和DataOutputStream能读写java 的基本数据类型,每种类型所占用的控件都是固定的,与机器无关;
- 缓冲流
- 缓冲流BufferedInputStream和BufferedReader类增加了缓存功能,内部创建了一个缓冲区支持mark和reset功能;
- 输出流BufferedOutputStream和BufferedWriter类可以将多字节批量徐写入基本流类,而不必每个字节都调用底层的系统IO;
- 对象流
- 对象流ObjectInputStream和ObjectOutputStream,能够对基本数据类型进行读写也能对java类对象进行读写;
- 类对象必须是经过序列化的对象,也就是必须实现Serializable或者Externalizable接口;
- 管道流
- 管道Pipe提供了在同一个进程中对两个线程通信的能力,就像一个管道连接接数据源和目的地,管道一边是输入流,另一边是输出流。但是管道不能让两个不同进程的线程通信;
- 管道流的类包括:PipedInputStream和PipedOutputStream、PipedReader、PipedWriter类;
- 实现方式:使用PipdInput和PipedOutput一起来建立一个管道。数据通过一个线程写入PipedOutput,再通过另一个线程使用PipedInput读出数据;
- 管道里可以传输任意数据类型;
static void pipedTest() throws IOException {
PipedOutputStream outputStream = new PipedOutputStream();
PipedInputStream inputStream = new PipedInputStream(outputStream);
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
outputStream.write("hello another".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
int receives;
while ((receives = inputStream.read()) != -1) {
System.out.println("从管道中读取到数据:" + (char)receives);
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
thread1.start();
thread2.start();
}
- 字节流和字符流之间的桥梁流
- InputStreamReader和OutputStreamWriter可以将输入输出的字节流转换成字符流
- 标准输入和输出
- 由System类提供System.in 默认键盘输入,system.out。默认表示屏幕输出, system.err 默认表示错误输出;
- Scanner类 把数据分解成各种基本数据类型和字符串,可以跟system.in配合,读取输入的指定类型数据;
- 压缩流类
- GZIP文件的压缩和解压缩类是 GZIPOutputStream 和 GZIPInputStream;
- ZIP文件的压缩和解压缩类是 ZipOutputStream 和 ZipInputStream;
- zip文件可能含有多个文件或目录,每一个目录和每一个文件都称为一个入口,使用ZipEntry类封装;在读或者写的时候可以获取或者生成entry,在写的是顺便要把生成的entry通过方法putNextEntry放进输出流;
第三章 IP地址和URL
- 主机和端口
- 连接到Internet或局域网的设备称为主机,一般指的是网络中的计算机;
- 一般一个主机只有一个网络接口,就是Internet地址,通常叫做IP地址;
- IP地址是分配给每一个网络接口的逻辑地址,物理地址就是MAC地址,它是设备唯一的;
- 一台主机上的服务被分为65536个端口,端口port是一个抽象的概念。端口用以区分不用的应用协议,也叫做协议端口。其中0~1023端口是系统所用的端口。在平时自己通信时,应该选择大于1023以上的端口;
- 任何一台主机的网络接口在通信之前都要配置正确的IP地址和掩码。
- 由于IP地址难以记忆,所以可以给主机取一个主机名(域名),主机名可以设置成255个字符以内的一个字符串,一个主机可以设置多个主机名;
- 通过主机名可以解析出IP地址,在电脑上可以使用ping工具解析域名;
- 还可以通过DNS解析域名,如果解析出现故障时,可以通过nsllokup命令判断是否DNS解析出现问题;NDS解析出域名后,会把IP地址缓存起来到到本地的一个hosts文件中保存,下一次解析会优先从缓存中寻找;
- IP地址
- IP层传输的数据是数据报。每个网络接口都被分配一个IP地址,为了表示IP所在网段还需要加上掩码。如果两个节点需要通信,还需要指明网关的IP地址,网关可以看做是两个网段之间必经的交汇点,在通信过程中起到路由的作用;
- 通过DCHP服务可以自动获取IP地址、子网掩码、默认网关;
- IP的传输方式分为三种:
1、单播,一个网络接口指定一个单播地址,当两个网络节点通信的时候,通信双方根据IP地址所在的网段和路由关系建立数据通道,进行点对点通信;
2.、 广播,数据包会发送给给网段内所有的网络接口,无论网段内的其他主机是否希望接收,都会收到。但是广播只会在广播域内有效,因为路由器会阻隔广播;
3、组播,如果有多台主机希望同时获得相同的信息,可以加入同一个组,这个组的标识就是多播地址;送到该组播地址的数据包将会发送给组内的所有网络接口;
- IP地址分类:A、B、C、D、E类地址。IPv4的地址的4个字节被分为两部分:网络地址和主机地址。网络地址就电话号码中的区号,主机地址则像一个地区内的某个固定电话的号码;
1、A类地址:网络地址占1个字节,且最高位时0,所以最大值是127,主机地址占3个字节。适用于大型网络,其中网络地址的10作为内部地址使用;
2、B类地址:网络地址占前两个字节,且最高位两位固定是10,那么B类网络地址的首字节取值范围是128~191。
3、C类地址:网络地址占前三个字节,最高三位固定是110,那么网络地址的首字节取值范围是192~223;C类地址适合中小型网络,其中192.168网段一般作为内部地址使用;
4、D类地址:是一类特殊的地址,用作组播地址。D地址的后28位不区分网络地址和主机地址,所以也没有掩码。发送到组播组的主机不一定需要加入到该组,组员也可以自由加入和退出。D类地址的首字节最高4位为1110,所有首字节的取值范围是224~239、所以D类地址的取址范围是224.0.0.0 ~ 239.255.255.255;
5、E类地址:也是一种特殊的地址,保留用于实验和未来使用。E类地址不标识网络地址,所以也不需要掩码;E类地址的首字节的最高5位固定是11110,首字节取值范围:240~247;
通过IP地址的首字节的值就可以判断,ip属于哪类地址。0 ~ 127 是A类;128 ~ 191是B类;192 ~ 223是C类; 224~239是D类;240~247是E类;
- 划分子网
1、子网是将A、B、C类中主机地址,从高位开始的几位拿出来作为子网号,以进一步划分子网。如果拿前三位作为子网号,那么后5位就是主机号了,在子网号要去除000和111的情况;
2、那么如何判定IP中的主机地址的几位是用于划分子网的呢?这个就需要子网掩码来判断。
假如IP地址是192.168.1.33,子网掩码是255.255.255.224。主机地址变成二进制就是00100001,子网掩码的最后的224就是11100000。通过子网掩码的224就可以看出主机地址使用的前三位作为子网号。前三位可以划分为6种子网,分别是001,010,011,100,110,101。本来是8种因为去掉了全是0和全是1的情况。所以192.168.1.33的子网号是001,那么该子网的主机地址取址范围是192.168.1.33 ~ 192.168.1.62,主机地址二进制就是00100001 ~ 00111110;
- 特殊的IP地址,即全0和全1的情况
1、全0对应当前主机,成为通配地址。可以接本机上收任一网卡的数据;
2、 全1意味着所有,它指的是网络中所有主机,也通常被称为广播地址;
3、 127.0.0.1是常用的环回地址,它指向主机内部的环回网络接口,这个接口允许主机给自己发送数据报,通常用来测试本地的TCP/IP协议是否安装完成,网络接口是配置成功;
4、 169.254.x.x,有时候网络中配置了DHCP(动态主机配置协议)服务器,为客户端分配IP地址。当DCHP分配失败后,客户端会采用169.254.x.x这样的默认地址,就是链路本地地址;
- 网络的连通性
- 使用ping命令可以模拟发送数据包,如果t数据包返回时间值ime是小于1ms证明是内网。丢失率为0代表网络连通性也比较好;
- 在使用ping的时候,每个数据包的ttl代表的是生存周期,当数据包的ttl为0时,那么这个数据包就会被丢弃。因为一个数据包从一个网络接口到另一个网络接口中,需要经过很多网络设备或者接口组成的路径,这个路径很复杂很有可能形成环路。为了避免数据包一直在网络传输路径中,就为每个数据包设置了一个生命周期值,数据包每经过一个节点,这个值就会减1,减到0就把数据包丢弃,防止对网络造成影响;
- 如果域名无法解析,就会提示”unknown host“错误信息;
- 如果网络地址存在,因为某些原因无法访问就会出现“无法访问目标主机”的错误提示;
在mac下使用ping
ping www.baidu.com
PING www.a.shifen.com (183.232.231.174): 56 data bytes
64 bytes from 183.232.231.174: icmp_seq=0 ttl=54 time=22.111 ms
64 bytes from 183.232.231.174: icmp_seq=1 ttl=54 time=23.262 ms
64 bytes from 183.232.231.174: icmp_seq=2 ttl=54 time=23.951 ms
64 bytes from 183.232.231.174: icmp_seq=3 ttl=54 time=23.570 ms
64 bytes from 183.232.231.174: icmp_seq=4 ttl=54 time=22.897 ms
64 bytes from 183.232.231.174: icmp_seq=5 ttl=54 time=23.951 ms
64 bytes from 183.232.231.174: icmp_seq=6 ttl=54 time=23.214 ms
64 bytes from 183.232.231.174: icmp_seq=7 ttl=54 time=22.972 ms
64 bytes from 183.232.231.174: icmp_seq=8 ttl=54 time=22.502 ms
64 bytes from 183.232.231.174: icmp_seq=9 ttl=54 time=23.083 ms
64 bytes from 183.232.231.174: icmp_seq=10 ttl=54 time=23.501 ms
64 bytes from 183.232.231.174: icmp_seq=11 ttl=54 time=22.926 ms
^C
--- www.a.shifen.com ping statistics ---
12 packets transmitted, 12 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 22.111/23.162/23.951/0.520 ms
- InetAddress类
- 通过该类的可以获得域名的地址和主机名,该类没有构造方法,都是通过静态方法生成对象的;
- 调用getByName获取某个域名的ip地址;
- 调用getAllByname获取一个域名的多个ip地址,因为有时候为了实现负载均衡,一个域名可以设置对应多个ip地址。
- getLoopbackAddress获取环回地址;
- 还可以调用方法判断地址是否为通配地址、链路连接地址、环回地址、组播地址;
- SocketAddress类
- 网络程序中,经常会使用套接字来定位和连接某台服务器提供的某个服务,套接字可以看做一个组合,由IP地址+端口组成;
- SocketAddress是抽象类,通常使用InetSocketAddress,创建对象时 默认ip是通配地址。在使用的静态方法createUnresolved实例化,实例化时不会解析DNS
- URI
- URI是统一资源标识符,每一个网络资源都有一个唯一的URI;在WWW规范中,URI的定义格式是:
:// ? #fragment; - scheme是协议头,也叫方案,决定了一个资源是如何被指定的,常用的有http、ftp、mailto、file、data;
- authority是授权信息,可以定义为[user:passowrd@]host[:port]。user和password请求host和port服务;
- path是路径,以“/”符号分隔的目录层次结构;
- query 指以“?”符号开始的查询串,之后是以&符号分隔的若干“属性=值”格式的序列;
- fragment 以符号“#”开始,指向某二级资源的段标识符,常用在html页面中。
- URI类就可以操作和设置标识符的各个部位;
- 生成URI对象的异常是URISyntaxException;
7.URL - URL是特殊形式的URI,除了标识网络资源,还提供定位该资源的方法和访问机制;
- 生成URL对象的异常是MalformedURLException;
- 用uri对象生成url对象时,会对特殊字符进行处理,如空格会转换成“%20”
- 可以有三种获取数据方式:1、通过URL对象获得输入流可以读资源数据,2、获得网络连接,然后获得输入和输出流。3、获取资源的内容返回一个对象,一般是一个输入流对象。
- URLConnection类
- 是一个抽象类,表示应用与网络资源的连接。主要功能是与远程资源服务器的交互,包括查询服务器的属性,设置服务器连接的参数等。另一个功能就是获取URL资源;
- 有两个直接子类HttpURLConnection和JarURLCOnnection;
- 对于基于HTTP协议的资源,有些通过GET操作无法获取文件长度,此时设置为POST操作就可以了;
- 当程序需要向服务器输出一些数据时,需要先调用connection的setDoOutput方法,否则在写入的时候会触发异常ProtocolException;
来个下载图片的例子
static void getImageTest() {
try {
URL url = new URL("https://pics1.baidu.com/feed/4d086e061d950a7be45c7a2636d79cd1f3d3c9f2.jpeg?token=af070391e468a6f441b15805ef6c0ea0");
BufferedInputStream inputStream = new BufferedInputStream(url.openStream());
FileOutputStream outputStream = new FileOutputStream("logo_test.png");
int i;
while ((i = inputStream.read()) != -1) {
outputStream.write(i);
}
inputStream.close();
outputStream.flush();
outputStream.close();
} catch (MalformedURLException e) {
} catch (IOException e) {
e.printStackTrace();
}
}
- URLStreamHandler
- 是一个抽象类,负责定义协议处理器,协议处理器能够根据不同的网络协议类型创建网络连接,如HTTP、FTP。会创建协议相关的URLConnection;
- 在URL的构造方法中可以指定流协议处理器;
- URLStreamHandler对象可以通过工厂类URLStreamHandlerFactory接口类创建,createURLStreamHandler(string protocol);
第4章 基于TCP的通信
- TCP传输层的数据格式称为segment报文段,UDP的数据结构称为Datagram数据报;
- TCP协议的包格式包括TCO首部和TCP数据部分,首部里面放了源端口和目的端口,还包括给每个字节编号的序号字段,数据部分则是传输的内容,最大报文段长度有限制。
- TCP通过三次握手建立连接,通过不同可变窗口大小进行流量控制,能够进行拥塞控制,发生异常时,会终止连接;
- socket
- 在通信程序中称为“套接字”,表示两台计算机之间的通信连接;
- 一个服务器必须必须绑定一个固定的端口,不同的端口表示不同的服务,服务器会一直监听是否由客户端发起连接请求。使用ServerSocket类创建服务器;
- socket = IP + port,0 ~ 1023是系统保留端口,1024 ~ 49151是用户端口,49152 ~ 65535 是动态端口,也叫私有端口。其中用户端口由IANA互联网数字分配机构注册和分配;
- 当服务器收到请求,就会在端口服务上创建一个Socket,这个socket有一个本地地址和外部地址,外部地址就是客户端的ip地址;
- mac通过命令netstat 查看本地socket连接情况;
- java使用SOCK_STREAM流模式来通信,一个socket可以同时保持两个流,即输出流和输入流。Socket流的连接请求形成一个队列,如果服务端忙于处理一个连接,别的连接请求将一直等待该连接处理完毕;
- Socket对象创建时,通常只指定远程主机的IP地址和服务端口,本地socket使用本地的ip地址和操作系统自动分配的端口;也可以绑定本地IP和端口;
- 关闭socket的输入或输出流调用socket方法shutdownInput和shutdownOutput;再调用close方法关闭流。当关闭输入流或者输出流其中一个的时候,另外一个也会失效;
- 在判断socket是否是链接状态时,应使用组合判断条件socket.isConnected() && socket.isClosed == false。因为socket关闭后isConnected的状态不会受到影响;
- socket可以设置不要延迟和缓存,调用方法setTcpNoDelay(boolean on),默认是有缓存的,数据会达到一定量之后再发送;
- socket在调用close方法关闭后,状态不会立即变成已关闭,而是进入time_wait,此时会处理未发送的数据等,处理完毕之后,才会真正断开连接;通过调用方法setSoLinger(boolean on, int outtime),设置是否立即强制真正关闭socket。这样做的目的可以减少time_wait的socket数量,因为每一个socket都会占用一个本地端口;
- 设置socket缓冲区的大小,调用serReceiveBufferSize(int size)。增加缓冲区可以增加体网络I/O的效率,减小缓冲区有助于减少输入数据的积压。接收和输入缓冲区的默认值是8KB,如果设置值大于64KB应该socket调用connect方法之前;‘
- 还可以发送一字节的TCP紧急数据,调用方法setOOBInline(boolean on),必须服务器和客户端同时设置为true才有用,发送紧急数据的方法为:sendUrgentData(int data);
- ServerSocket
- 绑定一个端口后,到服务器的连接请求会存在一个队列当中,队列的最大长度是50.如果队列满了在收到连接请求会直接拒绝;如果不设置IP默认的则是通配地址,通配地址就会在支持电脑的任意网络接口地址的访问,一般一个计算机设备只有一个网络接口,也就是网卡;
- 在构造对象的时候可以设置队列的最大长度backlog;
- 在关闭socket时,如果队列中有尚未accept的客户请求,则会触发SocketException异常;
- 调用accept方法监听客户端的连接请求并且接收请求,从队列中获得请求后,会返回与该客户端连接的Socket对象,如果没有请求该方法会一直阻塞,等待连接请求;
- 当给ServerSocket设置超时时间为0 的时候accept方法就会无限等待,如果大于0,就会阻塞到一定的时间后触发SocketTimeException异常,但是ServerSocket服务仍然有效;
- 设置性能偏好,调用方法serPerformancePreferences,设置连接时间、延迟、带宽的优先级。三个参数那个值大,哪个性能偏好就越好,客户端socket也同样有此设置;
- 服务端的多线程处理:因为服务端的socket的accept方法和socket方法都会阻塞线程,所以为了让多个客户端的之间的读写相互没有影响,服务端为每一个客户端socket创建一个线程去处理读写;
class DateServerSocket {
public static void main(String[] args) {
testServerSocket();
}
static void testServerSocket() {
try {
// 端口 队列长度 绑定网络接口ip地址
ServerSocket serverSocket = new ServerSocket(9999,100,null);
System.out.println("Date 服务器开始运行..." + "port == " + serverSocket.getLocalPort() + " ip == "+ serverSocket.getLocalSocketAddress());
while (true) {
Socket socket = serverSocket.accept();
new ClientSocket(socket).start();
}
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
static class ClientSocket extends Thread {
private Socket socket;
private DataInputStream inputStream;
ClientSocket(Socket socket) {
this.socket = socket;
System.out.println("收到一个请求:port = " + socket.getLocalPort() + "ip = " + socket.getLocalAddress() + "net address = " + socket.getInetAddress() + "net port" + socket.getPort());
}
@Override
public void run() {
try {
inputStream = new DataInputStream(socket.getInputStream());
System.out.println("收到客户端消息:" + inputStream.readUTF());
inputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
第五章基于UDP的通信
1。 UDP协议
- UDP是传输层另一种协议,应用之间传递的数据是数据报,数据是独立的不依赖于任何网络连接,但是数据报能否发送或者时间都是不能确定的,所以数据传输的延迟会大大减少。
- UDP适用于那些对出错不太敏感,但需要及时进行大批量数据传输的应用;
- UDP在java的使用涉及到三个类:DatagramSocket、DatagramPacket、MulticastSocket;
- DatagramPacket用于描述数据报,就想一个信件;
- DatagramSocket用于接收和发送数据报;
- MulticastSocket是DatagramSocket的子类,用于基于组播组的通信;
- TCP的端口和UDP的端口是不冲突的,它们之间没有任何关系;
- DatagramSocket类
- UDP之间可以建立固定通信关系,就是一个DatagramSocket绑定一个固定的地址和端口,这样UDP只会发送或者接收绑定的地址和端口的消息;使用connect(InetSocketAddress address)方法绑定;
- 如果需要支持发送广播地址(255.255.255.255),调用方法setBroadcast方法;
- 设置超时时间是设置的接收数据报的时间,默认为0,接收方法会一直阻塞下去;
class UDPService {
private DatagramSocket datagramSocket;
public static void main(String[] args) {
UDPService service = new UDPService();
service.service();
}
UDPService() {
try {
datagramSocket = new DatagramSocket(9999);
System.out.println("UDP 服务启动");
} catch (SocketException e) {
e.printStackTrace();
}
}
void service() {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
byte[] buffer = new byte[100];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
try {
datagramSocket.receive(packet);
String message = new String(buffer,0,packet.getLength());
System.out.println("receive client message from " + packet.getAddress() + ":" + packet.getPort() + "is " + message);
if (message.equalsIgnoreCase("date")) {
SimpleDateFormat format = new SimpleDateFormat("yyyy 年 MM 月 dd 日");
packet.setData(("Now date is " + format.format(new Date())).getBytes());
} else if (message.equalsIgnoreCase("bye")) {
packet.setData("bye".getBytes());
}
datagramSocket.send(packet);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
class UDPClient {
public static void main(String[] args) {
try {
DatagramSocket socket = new DatagramSocket();
InetAddress address = InetAddress.getByName("localhost");
BufferedReader inputStream = new BufferedReader(new InputStreamReader(System.in));
System.out.println("请输入发送的内容:");
String msg = null;
while ((msg = inputStream.readLine()) != null) {
byte [] bytes = msg.getBytes();
DatagramPacket sendPacket = new DatagramPacket(bytes, bytes.length,address,9999);
socket.send(sendPacket);
DatagramPacket receivePacket = new DatagramPacket(new byte[100], 100);
socket.receive(receivePacket);
String message = new String(receivePacket.getData(), 0 , receivePacket.getLength());
if (message.equalsIgnoreCase("byte")) {
break;
}
System.out.println("receive service a message from " + receivePacket.getAddress() + "port " +receivePacket.getPort() + "is " + message );
}
} catch (SocketException e) {
e.printStackTrace();
} catch ( UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
第六章NIO和NIO2
1. NIO
- NIO比IO的优势在于,增加了缓冲区Buffer和通道操作更灵活;
- NIO使用缓冲区buffer和通道channel来传输数据,读取数据的时候,程序从channel读入数据到buffer;写出数据的时候把buffer的数据写入到channel。程序从原来的直接操作输入和输入流变成读写缓冲区;
- NIO可以实现非阻塞的IO,就是要求channel把数据读入buffer,在channel在读取数据的时候,线程可以做别的事情,等数据读取完毕,再回来继续执行;
- Buffers:缓冲区Buffers用来装载一定数量的数据。读写的时候都是操作缓冲区;
- Charsets:字符集,以及字符的编码器和解码器;
- Channels:指各种类型的数据通道。在NIO中输入通过channel读入缓冲区,输出通过缓冲区写入channel,是数据流的必经通道;
- Selectors: 提供复用的非阻塞的IO功能。它允许一个线程监控多个通道。多个通道注册到Selectors,当某个通道准备好IO操作时,就会被选中而进行读写操作;
2. 缓冲区Buffer
- Buffer是一个抽象类,用于存放基本类型的数据,它通过3个属性确定缓冲区当前的状态:容量capacity、位置position、限制limit
- 容量是缓冲区所能容纳元素的数量,定义后不能再改变;
- 位置:就是下一个可以读写元素的位置,它不会超过限制limit-。当向一个缓冲区写入一个基本类型的值,position就会向后移动若干个字节,字节的长度就是数据类型的长度。最小值为0,最大值为capacity - 1;在读或者写的过程中,position会自动移动位置。当写和读互换时,position会被重置;
- 限制:限定一个位置,从这个位置开始的元素不能被读或者写。限制的最大值不能超过capacity;
- 缓冲区并不是线程安全的,如果多个线程同时访问需要加锁;
- 除了布尔型,其他的基本数据类型对应一个Buffer,如ByteBuffer、IntBuffer等;
- 在字符集GBK中,一般中文占两个字节,字母占用一个字节;
- 调用静态方法初始化,allocate和allocateDirect,后者会初始化每一个字节为0,position为0,limit等于capacity,还会有一个类型一致的备份数组,改变其中一个的内容,另一个也会随之变化;
- 缓冲区重置:会重置缓冲区的位置为标记位置,如果没有标记,就会触发异常InValidMarkException,调用方法reset返回一个buffer。标记的位置不能大于position;
反转缓冲区:调用flip,会把limit设置为当前位置position,把position置为0,并且清除标记。通过此方法可以将写操作变成读操作,读的数据恰恰是刚写入的数据,也可以将读转为写; - 重读缓冲区:调用rewind方法,重置position为0,mark清空,其他不变;
- 写操作:调用方法put;读操作:调用方法:get;
- 压缩缓冲区:调用方法compact,会将position到limit之间的数据复制到缓冲区的开头,也就是未读的数据,如果有mark就清除,
- 判断该两个缓冲区是否相等:1. 相同的元素类型。2. 剩余的元素数量相等。3.剩余的元素逐个相等;
- 缓冲区类型转换:调用as....方法进行转换,需要注意的java采用的是大端存储,即高字节在低地址;
2. 选择器selector
- 它是用来检测java的多个通道是否有事件发生。通过selector可以使用一个线程管理多个通道,因此被称作是SelectableChannel的复用器;
- 一个selector就像一个餐馆的服务员,每个餐桌相当于一个通道,那个餐桌需要服务,服务员就服务哪个;
- 当selector调用select方法,select方法处于阻塞状态直到对应的channel中有时间发生。一旦select方法返回,意味着当前线程可以对事件进行相应和处理;
- 一个channel如何关联到selector呢?
通过调用selectablechannel的register方法,把channel注册到选择器,会得到一个selectionKey对象。一旦注册成功,注册状态会持续到取消注册或者选择器关闭;通道取消注册必须选取消selectionkey对象; - 创建Selector:Selector.open(),selector会一直处于打开的状态,直到调用selector.close关闭;
- 在注册的时候可以选择SelectionKey的属性,读操作(OP_Read)、写操作(OP_Write)、接受套接字操作(OP_ACCEPT)、套接字连接操作(OP_Connect)
- 与Selector一起使用时,Channel必须处于非阻塞模式下;
- 在选择那些已经准备好进行IO的通道时,调用select方法会阻塞执行,阻塞直到有一个通道准备好才返回,也可以使用selectNow方法选择非阻塞的方式,选择通道后返回的是选择键的个数,拿到个数后再通过Iterator对象来遍历所有选中的选择键。
例如:
aSelewctor.select();
Iterator it = selector.selectedKeys().iterator();
虽然select方法是阻塞的,但可以通过wakeup方法可以让它立刻返回,如果当前没有线程阻塞在select方法中,而另一个线程调用了wakeup方法,那下一个调用select方法的线程会立即返回;
3. Channel接口
- 通道时双向的,流是单向的;通道可以以一部的方式进行读写,流以阻塞的方式进行读写;通道和缓冲区相连,流和程序相连;
- NIO中常用的网络相关的通道类有:SocketChannel、ServerSocketChannel、DatagramChannel、FileChannel;
- SocketChannel
以非阻塞的方式读取socket,使用一个线程和多个连接进行通信,通过把多个SocketChannel通道注册到selector,之后再循环中使用selector的select方法,一旦有时间发生就会得到通知,进行相应的处理;
调用静态方法open创建SocketChannel对象;调用connect对象方法连接服务;一旦SocketChannel建立了远程连接,就会一直能保持连接直到关闭。
已经连接的SocketChannel必须使用close方法关闭; - ServerSocketChannel
通过open静态方法创建;
再调用bind方法绑定到本地地址,以便监听连接请求;
调用accept方法监听连接请求,同样是阻塞的,可以放到一个whil循环中不断监听连接请求; - DatagramChannel
以非阻塞方式发送和接受UDP数据报。也是SelectableChannel的子类;因为UDP不是面向连接,所以没有读写数据,只有发送和接收数据;和DatagramSocket一样,只是操作的是缓冲区;
也可以实现读写数据报,前提条件是需要connect到某个固定的远程主机; - FIleChannel
实现读写、匹配和处理文件的通道,文件通道工作在阻塞模式下;
NIO服务器示例代码
class NIODateServer {
private ServerSocketChannel serverSocketChannel;
private Selector selector;
public static void main(String[] args) {
NIODateServer server = new NIODateServer();
try {
server.init();
server.service();
} catch (IOException e) {
e.printStackTrace();
}
}
void init() {
try {
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
// 设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress("localhost",9999));
System.out.println("服务器开始启动");
// 将server通道注册到selector 分类为可接受连接的
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
void service() throws IOException {
// 循环监听请求
while (true) {
selector.select();
Iterator iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = (SelectionKey) iterator.next();
if (key.isAcceptable()) {
// 处理连接请求
dealAccept(key);
} else if (key.isReadable()) {
// 处理接收客户端数据
dealRead(key);
}
// 删除处理过的元素
iterator.remove();
}
}
}
// 处理接受套接字的channel
void dealAccept(SelectionKey key) throws IOException {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = channel.accept();
System.out.println("收到连接请求:" + "\nip " + socketChannel.getRemoteAddress() + "\nport " + socketChannel.socket().getPort());
socketChannel.configureBlocking(false);
// 将SocketChannel注册到selector
socketChannel.register(selector, SelectionKey.OP_READ);
}
// 处理通道读操作
void dealRead(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel)key.channel();
// 创建字节缓冲区
ByteBuffer buffer = ByteBuffer.allocateDirect(100);
try {
int count;
count = socketChannel.read(buffer);
if (count > 0) {
System.out.println("收到消息:" + socketChannel.toString());
dealMessageBuffer(buffer, socketChannel);
} else if (count == -1) {
// 当客户端断开 selector会一直收到该socket的可读操作
System.out.println("客户端已断开,取消选择键关闭socket");
key.cancel();
socketChannel.close();
}
} catch (IOException e) {
System.out.println("消息内容写入缓冲区出现错误--------");
e.printStackTrace();
// 如果读取数据出现异常就关闭socket
socketChannel.close();
}
// 使用完的buffer要清空 不然这里会循环收到消息 不太理解
buffer.clear();
}
// 处理收到的客户端消息
void dealMessageBuffer(ByteBuffer buffer, SocketChannel socket) throws IOException {
// 先将缓冲区进行反转 从写变成读
buffer.flip();
byte[] bytes = new byte[100];
buffer.get(bytes,0, buffer.limit());
String message = new String(bytes,0,buffer.limit());
System.out.println("the message content is " + message);
// 清空缓冲区 准备重新写入数据
buffer.clear();
if (message.equalsIgnoreCase("date")) {
SimpleDateFormat format = new SimpleDateFormat("yyyy - MM - dd");
buffer.put(("date is " + format.format(new Date())).getBytes());
} else if (message.equalsIgnoreCase("time")) {
SimpleDateFormat format = new SimpleDateFormat("HH - mm - ss");
buffer.put(("time is " + format.format(new Date())).getBytes());
} else {
buffer.put(message.getBytes());
}
// 再反转写为读 limit = position position = 0
buffer.flip();
socket.write(buffer);
}
}
NIO客户端示例代码
class NIODateClient {
private SocketChannel socketChannel;
private Selector selector;
public static void main(String[] args) throws IOException {
NIODateClient client = new NIODateClient();
client.init();
client.service();
}
void init () throws IOException {
selector = Selector.open();
socketChannel = SocketChannel.open(new InetSocketAddress("localhost",9999));
socketChannel.configureBlocking(false);
System.out.println(socketChannel.isConnected() ? "连接成功" : "连接失败" );
socketChannel.register(selector, SelectionKey.OP_READ);
}
void service() throws IOException {
doWirte();
doRead();
}
// 从键盘输入消息内容
private void doWirte() {
new NIOWriteThread(socketChannel).start();
}
private void doRead() throws IOException {
while (true) {
selector.select();
Iterator iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = (SelectionKey) iterator.next();
if (key.isReadable()) {
SocketChannel socket = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocateDirect(100);
socket.read(buffer);
buffer.flip();
byte[] bytes = new byte[100];
buffer.get(bytes, 0, buffer.limit());
String message = new String(bytes, 0,buffer.limit());
System.out.println("收到服务器消息:" + message);
buffer.clear();
}
// 删除处理过的元素
iterator.remove();
}
}
}
// 开启一个子线程处理输入 并将内容发送出去
class NIOWriteThread extends Thread {
private SocketChannel socketChannel;
NIOWriteThread(SocketChannel channel) {
socketChannel = channel;
}
@Override
public void run() {
ByteBuffer buffer = ByteBuffer.allocateDirect(100);
try {
while (!Thread.currentThread().isInterrupted()) {
// 第二次输入 前 先清空缓冲区
buffer.clear();
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String string = reader.readLine();
if (string != null) {
if (string.equalsIgnoreCase("quit")) {
System.exit(0);
}
buffer.put(string.getBytes());
buffer.flip();
socketChannel.write(buffer);
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
buffer.clear();
}
}
}
}
NIO.2
- NIO.2 相比NIO是提供了异步通道,进行网络连接、读取、写入等操作,
- AsynchronousChannel接口支持异步非阻塞IO操作,异步就是在调用IO的时候不必将进程挂起等待数据结果,而是里返回去做别的事情,之后进程会收到通知IO的操作结果;
- 异步通道的IO操作会返回一个Future对象,还可以在IO操作方法中设置参数CompletionHandler来处理异步IO。异步通道对于多线程是线程安全的,允许同时进行读写,视具体的异步通道类型而定;
- 实现AsynchronousChannel接口的类有AsynchronousServerSocketChannel、AsynchronousSocketChannel、AsynchronousFileChannel、AsynchronousByteChannel(byte异步读写)
- 服务端使用bind绑定本地地址并监听端口,客户端使用connect连接远程服务
- AsynchronousChannelGroup是一组异步通道,目的是进行资源共享,其内有一个相关的线程池来处理IO时间,并分配组内异步操作结果的CompletionHandler;
- 一个AsynchronousChannelGroup通过调用withFixedThreadPool或者withCachedThreadPool方法生成对象;
第七章 多线程和并发
进程是具有独立功能的程序针对一个数据集合的运行活动。它是操作系统分配资源的最小单位;
线程是比进程更小的概念,一个进程可以包含若干个线程,一个进程的多个线程可以共享进程资源;
1. 创建线程
- java虚拟机允许一个用户拥有多个并发的线程,每个线程都有一个优先级,高优先级的线程优先执行;
- 创建线程的方法有两种:1是继承Thread类,2是实现Runbable接口。都要实现run方法。只是后者在创建的时候需要通过Thread构造创建;最后通过start方法启动线程;run方法执行完成后,线程终止;
2. 线程的状态
- 线程一共有六种状态:New、Runnable、Blocked、Waiting、Timed_waiting、Terminated;
new 线程对象已创建,但是没有调用start启动时的状态
Runnable 线程已经运行的状态
Blocked 线程被阻塞,等待监控锁的状态
Waiting 等待状态,线程等待另外一个线程执行某个动作,通过notify或者notifyAll方法通知等待状态的线程重新运行
Timed_waiting 线程等待一段时间,等待另一个线程的执行
Terminated 线程推出之后的状态 - 线程的状态是有声明周期的,线程的状态由Java虚拟机调度,程序员也可以通过一些方法来控制线程状态的转换。
通过调用start,线程进入Runnable状态
通过调用wait方法或者sleep,线程会进入等待状态
处在等待状态的线程通过其他线程地阿偶用notify或notifyAll唤醒时,会重新进行可运行状态
处在可运行状态的线程,因线程同步或者IO阻塞的出现,会进入阻塞状态,当IO阻塞解除或者线程获得锁时,线程重新进行可运行状态 - 暂停线程:调用静态方法sleep
- 中断线程:调用对象方法interrupt,如果一个线程正阻塞在wait方法、join方法或者sleep方法的调用中,会触发InterruptedException异常
- 等待线程结束:调用线程对象方法join,如果想要当前线程等待另一个线程执行完毕,就需要调用那个对象的join方法
class ThreadTest implements Runnable {
private String threadName;
ThreadTest(String name) {
threadName = name;
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new ThreadTest("1"));
Thread thread2 = new Thread(new ThreadTest("2"));
thread2.start();
thread2.join();
thread1.start();
System.out.println("Waiting");
// 到这里的时候线程的任务已经执行完
while (thread2.isAlive()) {
System.out.println("Still waiting");
thread2.join(1000);
}
System.out.println("End.");
}
@Override
public void run() {
try {
for (int i = 0; i <5 ; i++) {
Thread.sleep(1000);
System.out.format("thread name: %s -- %d \n", Thread.currentThread().getName(), i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3. 同步Synchronization
- 运行在同一个JVM中的多个线程共享进程的堆内存,所以多个线程可以访问同一个对象。每个线程都有自己的栈内寸,栈内寸中的变量时线程安全的。但是堆内存中的变量不是,需要进行同步处理Synchronization;
- 同步使用synchronized关键字,它将一个方法或者代码块作为同步块,java通过监控锁强制对synchronized的方法进行互斥的访问;
- 当一个线程调用某个对象synchronized方法,该线程就获得了对这个对象的监控锁。如果监控锁属于别的线程,该线程就会等待直到监控锁可用;
- 当线程退出了synchronized方法,就会释放监控锁;
class SynchronizeObj {
int count = 0;
public synchronized void increase() {
for (int i = 0; i < 5; i++) {
count++;
System.out.println(Thread.currentThread().getName() + "加 1 == " + count);
}
}
public synchronized void decrease() {
for (int i = 0; i < 5; i++) {
count--;
System.out.println(Thread.currentThread().getName() + "减 1 == " + count);
}
}
}
class SynchronizedTestThread extends Thread {
SynchronizeObj obj;
SynchronizedTestThread(SynchronizeObj obj) {
this.obj = obj;
}
@Override
public void run() {
synchronized (obj) {
obj.increase();
System.out.println("change to another operation");
obj.decrease();
}
}
public static void main(String[] args) {
SynchronizeObj obj = new SynchronizeObj();
SynchronizedTestThread thread1 = new SynchronizedTestThread(obj);
SynchronizedTestThread thread2 = new SynchronizedTestThread(obj);
thread1.start();
thread2.start();
}
}
4. 线程间的协调
- 当两个线程同时操作一个共享资源时,常常由于一个获得监控锁的进程在运行时由于条件的限制无法顺利完成,而另一个线程因为无法获得监控锁而不能运行,造成程序阻塞在某处;
- notify方法唤醒一个等待共享对象监控锁的线程,并且该线程调用了wait方法等待共享对象监控锁的释放。该方法只能由拥有共享对象监控锁的线程执行;notifyAll唤醒所有等待共享对象监控锁的线程;
- wait方法会释放共享对象的监控锁;
5. 死锁
- 死锁是指多个线程互相等待引起的永久阻塞的现象。当多个线程以不同的顺序想要获得同一个锁时很容易发生死锁;
6. 并发
Lock接口 同样可以实现同步,Lock是管理多线程访问共享资源的一种工具,只有获得锁的线程才可以访问共享资源,而且某一时刻只有一个线程可以获得锁;
ReadWriterLock允许对共享资源并发访问;
ReentrantLock是重入锁,当一个线程用有锁,只要还没有释放,调用lock的线程就可以重新获得该锁;
调用lock对象方法lock使线程获得锁,获得锁之后的操作通常放在try-catch结构中,处理完之后要主动的调用unlock释放锁,通常在finally中;
Future接口表示异步操作执行的结果集。Future定义的方法包括检查操作是否结束、等待操作结束、查询操作结果;
get方法查询结果,等待操作任务结束的过程处于阻塞状态
cancel任务,尝试取消任务,如果任务已经结束了,或者已经被取消或者由于某种原因无法被取消,就会返回false。通过参数mayInterruptIfRunning可以中断正在执行的任务。Callable接口表示一个任务,可以返回任务的结果并抛出异常。和Runnable接口很像只是RUnnable不会返回结果和抛出异常;Callable接口只有一个方法call
Excecutor接口用来管理线程,执行Runnable任务,它替代Thread的start方法启动执行线程,允许执行异步任务
有两个子接口,分别是:ExecutorService和ScheduleExecutorService;
ExecutorService,定义了停止任务、提交任务、执行任务并返回结果等方法,通常使用线程池的工厂方法来创建 如: Executors.newCachedThreadPool() 返回ExecutorService对象;如果不shutdown,service 执行完任务一分钟后,会关闭所有线程。创建的线程池会重用线程,如果线程超过1分钟没有被使用,将会从缓存中删除;
shutdown,关闭线程池,关闭提交的任务,已提交的任务继续运行
shutdownNow 尝试停止所有正在运行的任务,并放回等待任务的列表
awaitTermination 阻塞等待所有的任务完成执行
submit 提交一个Runnable任务并返回Future对象,也可以提交Callable,表示任务的结果
invokeAny 执行任务集合中的一个,只要集合一种一个任务运行完毕或者抛出异常,其他的任务都会取消
invokeAll 执行集合中的所有任务
7. CountDownLatch类
- 是一个同步辅助类,允许一个或多个线程等待直到其他线程完成一些操作;
- 初始化的时候可以指定一个数字,每调用一次countDown方法,数值就会减1,直到为0时,等待中的线程才会被释放;
- 调用CountDownLatch对象的await方法的线程会进入等待状态;