0%

一. DNS概念

通常我们对一个主机的标识是域名的形式,这中形式更易于记忆,如www.baidu.com等,而路由器并不知道www.baidu.com对应的是哪一台服务器,在他们眼里只有IP地址才是有效地址,这种定长的结构化表示也更易于解析和通信。为了解决域名和IP地址之间的映射关系,DNS站了出来。DNS(domain name system)是域名系统的缩写,他负责将我们看到的域名解析为ip地址。

顶级域名、二级域名

顶级域名(一级域名)如:.com、.net、.edu、.gov、.cn等。N级域名就是在N-1级域名前追加一级。比如.baidu.com就是一个二级域名,.siba.com.cn是一个三级域名。这里又一个误点,有很多人吧www也当作一个域名,这种理解其实是不恰当的,www其实是表示该主机所提供的服务为www服务,即万维网服务。具体大家可以参考顶级域名 一级域名 二级域名 三级域名什么区别?中的高赞回答。

DNS层次关系

upload successful
如图,DNS层次关系通常是树状结构的,从上往下分别为根DNS服务器、顶级DNS服务器、权威DNS服务器。

  • 1.根DNS服务器:负责提供顶级域名服务器的IP地址,也就是根DNS服务器负责维护一张顶级DNS的映射表,一个顶级域名对应一个顶级DNS服务器的IP。
  • 2.顶级DNS服务器:对于每个顶级域(如com、org、net、edu等)和国家顶级域都有顶级域服务器(TLD服务器),TLD服务器负责提供权威DNS服务器的IP地址。
  • 3.权威DNS服务器:在因特网上具有公共可访问的主机的每个组织机构必须提供公共可访问的DNS记录,这些记录将这些主机的名字映射为IP地址。

除此之外还有一个本地DNS服务器,这个服务器通常与主机再同一个局域网当中。当主机发出DNS请求时,该请求被发往本地的DNS服务器,他起着代理的作用,并将该请求转发到DNS服务器层次结构中。他可以起到一个DNS缓存的作用,第一次DNS服务器在接收到返回的域名映射的IP地址,会将主机名和IP地址进行缓存,之后的一定时间内,如果客户端再次访问相同的主机时,本地DNS服务器会立刻返回对应的IP地址,减少其他DNS服务器的压力。

二. DNS解析

当我们在浏览器输入一个URL的时候,通常第一步就是进行DNS解析,如www.baidu.com,会有很多步骤需要做。

upload successful

  1. 查找浏览器缓存:因为浏览器一般会缓存DNS记录一段时间,不同浏览器的时间可能不一样,一般2-30分钟不等,浏览器去查找这些缓存,如果有缓存,直接返回IP,否则下一步。如chrome采用了预提DNS记录,在本地建立DNS缓存的方法,加快网站的连接速度。

  2. 查找系统缓存:浏览器缓存中找不到IP之后,浏览器会查看本地硬盘的 hosts 文件,看看其中有没有和这个域名对应的规则,如果有的话就直接使用 hosts 文件里面的 ip 地址。

  3. 如果在本地的 hosts 文件没有能够找到对应的 ip 地址,浏览器会发出一个 DNS请求到本地DNS服务器 。

  4. 查询你输入的网址的DNS请求到达本地DNS服务器之后,本地DNS服务器会首先查询它的缓存记录,如果缓存中有此条记录,就可以直接返回结果,此过程是递归的方式进行查询。如果没有,本地DNS服务器还要向DNS根服务器进行查询。

  5. 根DNS服务器没有记录具体的域名和IP地址的对应关系,而是告诉本地DNS服务器,你可以到顶级DNS服务器上去继续查询,并给出相应的顶级DNS服务器的地址。这种过程是迭代的过程。

  6. 本地DNS服务器继续向顶级DNS服务器发出请求,在这个例子中,请求的对象是.com域服务器。.com域服务器收到请求之后,也不会直接返回域名和IP地址的对应关系,而是告诉本地DNS服务器,你的域名的权威DNS服务器的地址。

  7. 最后,本地DNS服务器向域名的权威DNS服务器发出请求,这时就能收到一个域名和IP地址对应关系,本地DNS服务器不仅要把IP地址返回给用户电脑,还要把这个对应关系保存在缓存中,以备下次别的用户查询时,可以直接返回结果,加快网络访问。

查询方式

DNS进行解析的方式有两种,一种是迭代的方法,也就是我们上面用到的,一种是递归的方法。

递归解析
upload successful
本地DNS服务器自己负责向其他DNS服务器进行查询,一般是先向根DNS服务器查询,再由根DNS服务器一级级向下查询。最后得到的查询结果返回给局部DNS服务器,再由局部DNS服务器返回给客户端。

迭代解析
upload successful
当本地DNS服务器自己不能回答客户机的DNS查询时,也可以通过迭代查询的方式进行解析,如图所示。本地DNS服务器不是自己向其他DNS服务器进行查询,而是把能解析该域名的其他DNS服务器的IP地址返回给客户端DNS程序,客户端DNS程序再继续向这些DNS服务器进行查询,直到得到查询结果为止。也就是说,迭代解析只是帮你找到相关的服务器而已,而不会帮你去查。

实际使用中,主机向本地dns服务器的查询是一种递归查询方式,本地dns服务器向其他dns服务器查询使用的迭代方式。

DNS劫持

DNS劫持就是通过劫持了DNS服务器,通过某些手段取得某域名的解析记录控制权,进而修改此域名的解析结果,导致对该域名的访问由原IP地址转入到修改后的指定IP,其结果就是对特定的网址不能访问或访问的是假网址,从而实现窃取资料或者破坏原有正常服务的目的。DNS劫持通过篡改DNS服务器上的数据返回给用户一个错误的查询结果来实现的。

DNS劫持症状:在某些地区的用户在成功连接宽带后,首次打开任何页面都指向ISP提供的“电信互联星空”、“网通黄页广告”等内容页面。还有就是曾经出现过用户访问Google域名的时候出现了百度的网站。这些都属于DNS劫持。

解决方法:
1.加强本地计算机病毒检查,开启防火墙等,防止恶意软件,木马病毒感染计算机
2.改变路由器默认密码,防止攻击者修改路由器的DNS配置指向恶意的DNS服务器
3.企业的话可以准备两个以上的域名,一旦一个域名挂掉,还可以使用另一个
4.用HTTP DNS 代替 Local DNS

参考文献

浅谈DNS缓存机制–浏览器和OS篇
浏览器输入URL后发生了啥

一. HTTP简介

今天看了一些http协议的一些相关的概念,准备在博客中记录一下。
HTTP是客户端和服务端用于通信的协议,可以用来传输文本、图片、文件。他是基于TCP连接,所以说HTTP协议是一个可靠的协议,当HTTP客户端发起一个与服务端的TCP连接,一旦连接建立起来,该浏览器和服务器进程就可以通过套接字接口访问TCP。

二. HTTP特性

1. HTTP的无状态

HTTP是一种无状态的协议,我们如何去理解无状态协议呢。当浏览器向服务端连续发送相同的请求时,服务器不会因为两次请求的相同而返回不同的响应,而是从新去发送请求的对象,就像服务器完全忘记之前做过的事情一样,换一句话说就是每一次请求都是完全独立的。因为HTTP服务器不会保存关于客户的任何相关信息,所以说HTTP是一个无状态的。

那如果我们需要使用HTTP协议去维持一个有状态的场景应该怎么办呢。比如说我们登陆网站,我们会对服务端发送一个登陆请求,登陆上以后我们想浏览网站,就会对服务端发送其他请求,我们想要服务端记住我们已经登陆的状态,这种情况下我们应该如何去处理问题呢?为了帮助HTTP变得有状态,我们通常使用Session和Cookie,我这里先卖一个关子,后面为大家详细介绍。

2. HTTP的长连接和短连接

HTTP的短连接就是,当客户端向服务端每发送一次请求是就要建立一次TCP连接然后传输数据然后立刻关闭连接,这样当客户端向服务端请求的web页面含有多个对象时如图片、视频等,每多一个对象,客户端就需要重新和服务端建立一次连接,这样无疑会增加网页加载的时间。

而从HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头加入这行代码:
Connection:keep-alive

在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接需要客户端和服务端都支持长连接。

我们都说HTTP长连接和短连接本质上是TCP的长连接和短连接,应为HTTP是应用层上的协议,它的底层是基于TCP,应为TCP协议支持长连接,能够保持TCP在请求结束后不立刻关闭,所以说HTTP支持长连接,如果TCP协议在请求结束后立刻关闭,我们说这样的HTTP是短连接的。

当我们请求的一个网页含有大量的图片、CSS、JS等静态资源,长连接能够帮助我们只建立一个TCP通道就可以完成全部的请求,这就省下了大量的资源消耗。

三. HTTP格式

应为HTTP不仅用于发送请求服务端,还用于响应客户端。所以HTTP包含了请求报文格式和响应报文格式。

1. 请求报文格式

upload successful
请求报文包括,请求行、请求头部、请求体。
请求行

  • 请求方法:有GET、POST、PUT、DELET。GET请求参数可以放在URL后面,而POST请求的数据参数只能放在请求数据里面,和GET请求相比POST请求更安全一些,并且应为POST请求的参数是放在请求数据内,POST可以传输的数据也更多一些。PUT和DELET使用的更少一些,以我的使用经验来说,这些往往适合REST风格配合使用。

  • URL:是指请求的资源

  • 协议版本:指HTTP协议的版本,如HTTP/1.1

请求头部
请求头部由头部字段名:值的格式构成,看起来像JSON格式,我介绍一些比较常用的。

  • user-agent: 这的是用户使用浏览器版本。例如:user-agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36。这个是chrome的引擎。

  • Cookie:用于在客户端存储少量的信息,通常用于实现session功能。

  • Host:请求的资源在哪个主机的端口上。

请求数据
请求数据又叫请求体,使用是用于传输html表单中的信息。通常来说使用POST方法会将参数放到请求体中,而使用GET方法一般不会放在请求体中。

2. 响应报文格式

upload successful
响应报文协议是当服务端进行处理返回给客户端的信息,这里面同样包含了3个部分,响应行、响应头、响应正文。
响应行
由3部分组成,分别为:协议版本,状态码,状态码描述,之间由空格分隔。

状态代码为3位数字,200~299的状态码表示成功,300~399的状态码指资源重定向,400~499的状态码指客户端请求出错,500~599的状态码指服务端出错(HTTP/1.1向协议中引入了信息性状态码,范围为100~199)

这里举出一些常见的:
upload successful

响应头
与请求头部类似,为响应报文添加了一些附加信息
upload successful

我们前面提到了http是无状态的。这简化了服务器的设计,并且可以允许程序员去开发可以同时处理数以千计的TCP连接的高新能Web服务器。然而一个Web站点经常希望能够识别用户。为此,HTTP使用了cookie。cookie在HTTP应用中有四个部分:

    1. http请求头部包含了cookie字段
    1. http响应报文包含了Set-Cookie字段
    1. 服务端数据库会保存cookie的相关信息。
    1. 客户端会保存cookie信息的文件。

我们举一个例子,假设张三晚上想上淘宝,当他第一次上淘宝时,我们假定他以前访问过淘宝,当请求报文到达淘宝的时候,淘宝的服务器会生成一个唯一的识别码,并以此作为索引在他的后端数据库产生一个表项,接下来淘宝的服务器会用一个包含了set-cookie首部的http响应报文对于张三的浏览器进行响应,其中set-cookie就含有该识别码。可能是

  • Set-cookie:1678
    当张三的浏览器收到了该HTTP响应报文时,他会看到set-cookie:首部。该浏览器在他管理的cookie文件中会添加一行,该行包含服务器的主机名和在set-cookie:首部的识别码。当张三继续浏览淘宝时,其浏览器就会查询该cookie文件并抽取他对这个网站的识别码,并放到http请求报的识别码的cookie首部行中。之后每一次发往淘宝的cookie的请求报文中就包含了一下首部行:
  • cookie:1678
    在这样的方式下,淘宝就可以跟踪张三的浏览记录,并为张三提供购物车服务,即淘宝可以维护张三希望购买的物品列表,这样张三在结束回话时可以一起为他们支付。

如果张三再次访问该网站的时候,比如说是一个星期以后,她的浏览器会在其请求报文中继续放入首部行cookie:1678.淘宝将根据张三过去访问过的页面向她推荐产品,如果张三在淘宝网注册过,可以将邮件地址、银行卡相关联,这就可以解释淘宝等购物网站的点击购物功能,即在点击购买的时候不需要重新提供信用卡和姓名。

web缓存

Web缓存器也叫做代理服务器,它能够代表初始的服务器来满足http请求的网络实体,并在存储空见保存最近请求过的对象的副本。
举一个例子,如果有一个浏览器想访问淘宝。

    1. 浏览器首先会向web缓存器发送http请求。
    1. 缓存服务器会查询存储空间中是否含有请求对象的副本,如果含有的话,立刻返回副本数据。
    1. 如果web缓存器中没有该对象,他就打开一个与该对象的初始服务器的TCP连接,web缓存器则向淘宝网发送http请求,在收到请求后,淘宝的服务器会返回一个响应报文。
    1. 当web缓存起接收到该对象是,他在本地存储空间会存储一份副本,并向客户端浏览器发送该副本。

这样以后在去访问该对象时,就可以快速返回。但是这会有一个问题,如果代理服务器备份的副本时间过长,对象已经被修改了,这样web缓存器返回回来的对象就是错误的。
我们使用条件GET可以解决这个问题,条件GET是指在请求报文中,我们使用GET方法,并且在请求头中包含了“If-Modified-Since”字段,这是由web缓存起发送的,缓存器在备份副本的时候会记录修改的时间,当浏览器再次请求相同的对象时,web缓存器会先向服务端发送一个包含If-Modified-Since的GET请求,如果没有被修改则返回一个304响应报文。这样虽然会影响一些效率,但是增加了安全性和可靠性。

参考文献

HTTP请求、响应报文格式
浅谈HTTP中GET、POST用法以及它们的区别
计算机网络 自顶向下方法

一. 观察者模式模式简介

文章转发自设计模式(五)观察者模式

定义

观察者模式(又被称为发布-订阅(Publish/Subscribe)模式,属于行为型模式的一种,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。

观察者模式结构图

upload successful

在观察者模式中有如下角色:

  • Subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象。

  • ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。

  • Observer:抽象观察者,是观察者者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己。

  • ConcrereObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。

二. 观察者模式简单实现

观察者模式这种发布-订阅的形式我们可以拿微信公众号来举例,假设微信用户就是观察者,微信公众号是被观察者,有多个的微信用户关注了程序猿这个公众号,当这个公众号更新时就会通知这些订阅的微信用户。好了我们来看看用代码如何实现:

抽象观察者(Observer)
微信用户是观察者,里面实现了更新的方法:

1
2
3
4
public interface Observer {
public void update(String message);
}

具体观察者(ConcrereObserver)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class WeixinUser implements Observer {
// 微信用户名
private String name;
public WeixinUser(String name) {
this.name = name;
}
@Override
public void update(String message) {
System.out.println(name + "-" + message);
}


}

抽象被观察者(Subject)
抽象主题,提供了attach、detach、notify三个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface Subject {
/**
* 增加订阅者
* @param observer
*/
public void attach(Observer observer);
/**
* 删除订阅者
* @param observer
*/
public void detach(Observer observer);
/**
* 通知订阅者更新消息
*/
public void notify(String message);
}

具体被观察者(ConcreteSubject)
微信公众号是具体主题(具体被观察者),里面存储了订阅该公众号的微信用户,并实现了抽象主题中的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SubscriptionSubject implements Subject {
//储存订阅公众号的微信用户
private List<Observer> weixinUserlist = new ArrayList<Observer>();

@Override
public void attach(Observer observer) {
weixinUserlist.add(observer);
}

@Override
public void detach(Observer observer) {
weixinUserlist.remove(observer);
}

@Override
public void notify(String message) {
for (Observer observer : weixinUserlist) {
observer.update(message);
}
}
}

客户端调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Client {
public static void main(String[] args) {
SubscriptionSubject mSubscriptionSubject=new SubscriptionSubject();
//创建微信用户
WeixinUser user1=new WeixinUser("杨影枫");
WeixinUser user2=new WeixinUser("月眉儿");
WeixinUser user3=new WeixinUser("紫轩");
//订阅公众号
mSubscriptionSubject.attach(user1);
mSubscriptionSubject.attach(user2);
mSubscriptionSubject.attach(user3);
//公众号更新发出消息给订阅的微信用户
mSubscriptionSubject.notify("刘望舒的专栏更新了");
}
}

结果

1
2
3
杨影枫-刘望舒的专栏更新了
月眉儿-刘望舒的专栏更新了
紫轩-刘望舒的专栏更新了

三. 使用观察者模式的场景和优缺点

使用场景

  • 关联行为场景,需要注意的是,关联行为是可拆分的,而不是“组合”关系。
  • 事件多级触发场景。
  • 跨系统的消息交换场景,如消息队列、事件总线的处理机制。

优点

解除耦合,让耦合的双方都依赖于抽象,从而使得各自的变换都不会影响另一边的变换。

缺点

在应用观察者模式时需要考虑一下开发效率和运行效率的问题,程序中包括一个被观察者、多个观察者,开发、调试等内容会比较复杂,而且在Java中消息的通知一般是顺序执行,那么一个观察者卡顿,会影响整体的执行效率,在这种情况下,一般会采用异步实现。

计算机网络(2)——网络编程BIO、NIO

本来在学习nginx,想写一些nginx的blog,但是在看一写blog的时候,看到了io多路复用,epoll等概念,确认过眼神,是没学过的概念,于是由转而看起了网络编程多线程相关的一些知识,也是学到了很多新的知识。

一. 同步、异步、阻塞、非阻塞

同步、异步是两种通信机制,同步是指在调用函数的时候,必须要有返回结果才能够继续往下执行;异步则是指,调用在发出的时候,调用者不会立刻得到结果,而是在调用发出之后,被调用者通过状态、通知来通知调用者,或者通过回调函数处理这个调用。
upload successful
上图比较清晰的给我们解释了什么事同步IO,什么是异步IO,同步IO是当系统调用发生时,内核会将程序挂起,CPU会从用户空间切换到内核空间进行数据交换,完成系统调用时,CPU返回用户空间并返回结果,进程由挂起状态转换为就绪状态,准备执行下面的命令。而异步IO是当系统调用发生时,系统会立即给程序发生一个结果,好让程序继续往下执行,当系统调用结束时,系统通通知调用进程。(设置一个用户空间特殊的变量值或者触发一个signal或者产生一个软中断 或者调用应用程序的回调函数)

而阻塞和非阻塞是种状态,它们关注的是程序在等待结果时的状态,阻塞是指程序在调用结果返回直线,当前线程会被挂起,只有在得到结果之后才能返回。这种被挂起的状态被称为阻塞。非阻塞是指线程在结果返回之前不会被挂起,该调用会执行其他的事情。

同步IO可以分为阻塞IO和非阻塞IO,我们之后谈论的BIO就是阻塞IO,NIO是非阻塞IO,而aio就是异步io。

二. BIO模型

BIO又叫做blocking IO,阻塞模型,下面我们来说BIO流程,注意下面所说的都是针对多线程的。

upload successful
采用BIO的服务端会用一个socket去监听窗口,当有客户端连接进来时,socket会创建一个新的线程用于和客户端进行交互,通过输出流返回应答给客户端,线程销毁,这是典型的 请求——应答 模型。
下面上代码:
BIOServer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package bio;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class BIOServer {
private static int PORT = 12345;
private static ServerSocket server;
public static void start() throws IOException {
start(PORT);
}

public synchronized static void start(int port) throws IOException{
if(server != null) return;
try {
server = new ServerSocket(port);
System.out.println("服务器已启动, 端口号:" + port);

while (true){
Socket socket = server.accept();
new Thread(new BIOServerHandler(socket)).start();
}
}finally {
if(server != null){
System.out.println("服务器关闭");
server.close();
server = null;
}
}
}

}

BIO服务端交互代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package bio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class BIOServerHandler implements Runnable{

private Socket socket;
public BIOServerHandler(Socket socket){
this.socket = socket;
}

@Override
public void run() {
BufferedReader in = null;
PrintWriter out = null;
try {
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
String expression;
String result;
while (true){
//通过BufferedReader读取一行
//如果已经读到输入流尾部,返回null,退出循环
//如果得到非空值,就尝试计算结果并返回
if((expression = in.readLine()) == null) break;
System.out.println("服务器收到消息:" + expression);
try {
result = "to do!";
}catch (Exception e){
result = "计算错误" + e.getMessage();
}
out.println(result);

}
}catch (Exception e){
e.printStackTrace();
}finally {
if(in != null){
try {
in.close();
}catch (IOException e){
e.printStackTrace();
}
}
if(out != null){
out.close();
out = null;
}
if(socket!= null){
try {
socket.close();
}catch (IOException e){
e.printStackTrace();
}
socket = null;
}
}
}
}


BIOClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package bio;

import javax.print.DocFlavor;
import java.io.*;
import java.net.Socket;

public class BIOClient {
private static int PORT = 12345;
private static String IP = "localhost";
public static void send(String expression){
send(PORT, expression);
}

public static void send(int port, String expression){
System.out.println("发送消息为:" + expression);
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try{
socket = new Socket(IP, PORT);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(),true);
out.println(expression);
System.out.println("结果为:"+ in.readLine());
}catch (Exception e){
e.printStackTrace();
}finally {
if(in != null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
in = null;
}
if(out != null){
out.close();
out = null;
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
socket = null;
}
}
}

}


测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package bio;

import java.io.IOException;
import java.net.ServerSocket;
import java.util.Random;

public class Test {
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
try {
BIOServer.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
Thread.sleep(100);
char operators[] = {'+','-','*','/'};
Random random = new Random(System.currentTimeMillis());
new Thread(new Runnable() {
@Override
public void run() {
while (true){
String expression = random.nextInt(10)+""+operators[random.nextInt(4)]+(random.nextInt(10)+1);
BIOClient.send(expression);
try {
Thread.currentThread().sleep(random.nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}

BIO的只适用与小并发量且是一问一答的程序,因为每有一个客户端连接进来,服务端就会创建一个新的线程用于对接,线程也会占用内存资源,并且创建线程需要进行系统调用,当线程数量快速膨胀后,cpu需要不断切换线程,这样上下文切换将会占用cpu大量的时间。最重要的缺点是accept是阻塞的,这是无法解决根本问题的,而NIO作为非阻塞模型,就可以很好解决这个问题。

三. NIO模型

Nio官方叫法new io,我们习惯称之为no-block io应为非阻塞是nio的最大的特点。对于NIO,如果TCP RecvBUffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则返回0,永远不会阻塞。在NIO中用户最关心的是,“我可以读了”。本质上来说,NIO模型中,socket主要的读写、注册、接收函数,在等待就绪阶段是非堵塞的,真正的IO操作是同步阻塞的。
我们回忆BIO模型,之所以需要多线程,是因为在进行IO操作的时候,一是没有办法知道到底能不能写,能不能读,只能等待,即使通过各种估算,算出来的操作系统没有能力进行读写,也没有办法在socket.read()和socket.write()函数中返回,这两个函数无法进行有效的中断,所以除了多开线程另其炉灶,没有好的办法利用CPU。NIO的读写函数可以立即返回,这就给我们不开线程,这就给我们不开线程利用CPU的最好机会,如果一个连接不能读写,我们可以吧这件事记下来,通常的方式是在selector上注册标记位,然后切换到其他就绪的连接继续进行读写。

NIO最重要的三个基本概念是channel、buffer、selector。

1.channel

channel 我们通过翻译可以看出,它是一个通道,可以与硬件如网卡、硬盘进行连接,当通道被被打开时,我们可以向通道中读写数据。和流的概念类似但是通道是双向的,而流只具有单向性,并且通道可以支持多线程的读写,比较重要的的一些通道如FileChannel、ServerSocketChannel、SocketChannel都是现在比较常用的类,再多线程中发挥着重要作用,由于我们这只将网络编程,所以我下面重点介绍ServerSocketChannel、SocketChannel。

ServerSocketChannel

ServerSocketChannel 是用于TCP服务端的通道,他的作用和标准IO中的ServerSocket类似,可以绑定端口进行监听。和ServerSocket相比,ServerSocketChannel最大的优势是可以设置为非阻塞的状态,当没有客户端连接进来时,ServerSocketChannel可以立刻返回null,如果有客户端连接时,就会和ServerSocket一样返回一个Socket,除此之外ServerSocketChannel是多线程安全的,支持多线程并发。

SocketChannel

SocketChannel 是用于TCP客户端的通道,它同样可以设置非阻塞模式对服务端进行连接,以及对通道内的数据进行读写。SocketChannel还有一个特点就是支持异步关闭,如果SocketChannel在一个线程上read阻塞,另一个线程对该SocketChannel调用shutdownInput,则读阻塞的线程将返回-1表示没有读取任何数据;如果SocketChannel在一个线程上write阻塞,另一个线程对该SocketChannel调用shutdownOutput,则写阻塞的线程将抛出AsynchronousCloseException。

2.buffer

Buffer通常与Channel进行交互,数据从通道读入缓冲区,或者从缓冲区写入通道中,缓冲区本质上是一个可以写入数据的数组,之后可以读取数据,Buffer对象包装了此内存块,可以更轻松的使用内存块。

使用buffer时通常遵循以下几个步骤:

  1. 将数据写入缓冲区
  2. 调用buffer.flip()反转读写模式
  3. 从缓冲区读取数据
  4. 调用buffer.clear()或者buffer.compact()清除缓冲区的内容。

将数据写入Buffer 时,Buffer 会跟踪写入的数据量。 当需要读取数据时,就使用 flip() 方法将缓冲区从写入模式切换到读取模式。 在读取模式下,缓冲区允许读取写入缓冲区的所有数据。

读完所有数据之后,就需要清除缓冲区,以便再次写入。 可以通过两种方式执行此操作:通过调用 clear() 或调用 compact() 。区别在于 clear() 是方法清除整个缓冲区,而 compact() 方法仅清除已读取的数据,未读数据都会移动到缓冲区的开头,新数据将在未读数据之后写入缓冲区。

buffer有一些基本概念:

  1. capacity:指的时缓冲区的容量,是他所包含的元素的数量,不能为负并且不能够更改。
  2. position:缓冲去的位置, 是下一个要读取或者要写入的元素的索引。不能为负,并且不能够大于limit
  3. limit:缓冲区的限制,缓冲区的限制不能为负,并且不能够大于capacity

另外还有标记 mark ,
标记、位置、限制和容量值遵守以下不变式:
0 <= mark<= position <= limit<= capacity

position和limit的含义取决于Buffer是处于读取还是写入模式。无论缓冲模式如何,capacity总是一样表示容量。

以下是写入和读取模式下的容量,位置和限制的说明:
upload successful

capacity

作为存储器块,缓冲区具有一定的固定大小,也称为“容量”。 只能将 capacity 多的 byte,long,char 等写入缓冲区。 缓冲区已满后,需要清空它(读取数据或清除它),然后才能将更多数据写入。

position

将数据写入缓冲区时,可以在某个位置执行操作。 position 初始值为 0 ,当一个 byte,long,char 等已写入缓冲区时,position 被移动,指向缓冲区中的下一个单元以插入数据。 position 最大值为 capacity -1

从缓冲区读取数据时,也可以从给定位置开始读取数据。 当缓冲区从写入模式切换到读取模式时,position 将重置为 0 。当从缓冲区读取数据时,将从 position 位置开始读取数据,读取后会将 position 移动到下一个要读取的位置。

limit

在写入模式下,Buffer 的 limit 是可以写入缓冲区的数据量的限制,此时 limit=capacity。

将缓冲区切换为读取模式时,limit 表示最多能读到多少数据。 因此,当将 Buffer 切换到读取模式时,limit被设置为之前写入模式的写入位置(position ),换句话说,你能读到之前写入的所有数据(例如之前写写入了 6 个字节,此时 position=6 ,然后切换到读取模式,limit 代表最多能读取的字节数,因此 limit 也等于 6)。

分配缓冲区
buffer 在一开始需要使用allocate来分配缓冲区的大小,缓冲区的大小决定了capacity的大小。例子如下

1
ByteBuffer buffer = ByteBuffer.allocate(48); //创建容量为48字节的缓冲区

将数据写入缓冲区
可以通过两种方式将数据写入 Buffer:

  1. 将数据从通道写入缓冲区
  2. 通过缓冲区的 put() 方法,自己将数据写入缓冲区。
    这是一个示例,显示了 Channel 如何将数据写入 Buffer:
1
2
int data = fileChannel.read(buffer); // 将 Channel 的数据读入缓冲区,返回读入到缓冲区的字节数
buffer.put(127); // 此处的 127 是 byte 类型

flip() 切换缓冲区的读写模式
flip() 方法将 Buffer 从写入模式切换到读取模式。 调用 flip() 会将 position 设置回 0,并将 limit 的值设置为切换之前的 position 值。换句话说,limit 表示之前写进了多少个 byte、char 等 —— 现在能读取多少个 byte、char 等。

从缓冲区读取数据
有两种方法可以从 Buffer 中读取数据:

  1. 将数据从缓冲区读入通道。
  2. 使用 get() 方法之一,自己从缓冲区读取数据。
    以下是将缓冲区中的数据读入通道的示例:
1
2
int bytesWritten = fileChannel.write(buffer);
byte aByte = buffer.get();

和 put() 方法一样,get() 方法也有许多其他版本,允许以多种不同方式从 Buffer 中读取数据。有关更多详细信息,请参阅JavaDoc以获取具体的缓冲区实现。

rewind() 倒带
Buffer对象的 rewind() 方法将 position 设置回 0,因此可以重读缓冲区中的所有数据, limit 则保持不变。

clear() 和 compact()
如果调用 clear() ,则将 position 设置回 0 ,并将 limit 被设置成 capacity 的值。换句话说,Buffer 被清空了。 但是 Buffer 中的实际存放的数据并未清除。

如果在调用 clear() 时缓冲区中有任何未读数据,数据将被“遗忘”,这意味着不再有任何标记告诉读取了哪些数据,还没有读取哪些数据。

如果缓冲区中仍有未读数据,并且想稍后读取它,但需要先写入一些数据,这时候应该调用 compact() ,它会将所有未读数据复制到 Buffer 的开头,然后它将 position 设置在最后一个未读元素之后。 limit 属性仍设置为 capacity ,就像 clear() 一样。 现在缓冲区已准备好写入,并且不会覆盖未读数据。

mark() 和 reset()
以通过调用 Buffer 对象的 mark() 方法在 Buffer 中标记给定位置。 然后,可以通过调用 Buffer.reset() 方法将位置重置回标记位置,就像在标准 IO 中一样。

1
2
3
4
buffer.mark();
// 调用 buffer.get() 等方法读取数据...

buffer.reset(); // 设置 position 回到 mark 位置。

使用buffer的一些好处

当我们使用buffer和channel共同进行数据传输的时候,channel解决了阻塞的问题,那么buffer解决了什么问题?下面我们看一下buffer和channel的流程图

upload successful
乍一看内核空间和用户空间进行交互的时候,还需要现将数据放入到buffer然后在通过read取出来不是多此一举吗?其实不然,我们设想以下下面的情况,
ByteBuffer buffer = ByteBuffer.allocate(1024 * 4);
……
channel.read(buffer);
……
while(true){
byte[] buf = new byte[32];
buffer.read(buf);
//handle
}
如果有10244 个byte需要读取,若我们使用字节流读取的话就需要进行10244/32次IO操作,若我们提前讲数据放入buffer,我们就只需要一次IO操作,其他的操作都是在内存中进行,效率就会高很多。

3.selector

Selector是NIO中的一个组件,它负责监控和管理多个Channel从而管理多个网络连接。并可以使用轮询的方式确定那些通道可以读写。通道必须处于非阻塞的模式才能够和选择器一起使用,这意味着无法将FileChannel与Selector一起使用,因为FileChannel无法切换到非阻塞模式。套接字通道则支持。

通常selector通过register()的方法来管理通道,他可以监听四种不同的事件,

  • Connect 连接
  • Accept 接收
  • Read 读
  • Write 写

一个“发起事件”的通道也被称为“已就绪”事件。 因此,已成功连接到另一台服务器的通道是“连接就绪”。 接受传入连接的服务器套接字通道是“接收就绪”。 准备好要读取的数据的通道“读就绪”。 准备好写入数据的通道称为“写就绪”。

下面结合一个具体的例子我们来操作NIO
NIOServer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package nio;

public class NioServer {
private static int PORT = 12345;
private static NIOServerHandle serverhandle;
public static void start(){
start(PORT);
}
public static synchronized void start(int port){
if(serverhandle!=null)
serverhandle.stop();
serverhandle = new NIOServerHandle(port);
new Thread(serverhandle,"Server").start();
}
public static void main(String[] args){
start();
}
}

NIOServerHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
package nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class NIOServerHandle implements Runnable{
private Selector selector;
private ServerSocketChannel serverSocketChannel;
private volatile boolean started;

public NIOServerHandle(int port){
try{
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(port),1024);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
started = true;
System.out.println("服务器已启动, 端口号"+ port);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}


public void stop(){started = false;}

@Override
public void run() {
while (started){
try {
selector.select(1000);
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while (it.hasNext()){
key = it.next();
it.remove();
try {
hanleInput(key);
}catch (Exception e){
if(key != null){
key.cancel();
if(key.channel() != null){
key.channel().close();
}
}
}
}

} catch (IOException e) {
e.printStackTrace();
}
}
if(selector != null){
try {
selector.close();
}catch (Exception e){
e.printStackTrace();
}
}
}

public void hanleInput(SelectionKey key) throws IOException{
if(key.isValid()){
if(key.isAcceptable()){
ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
sc.register(selector,SelectionKey.OP_READ);
}
if(key.isReadable()){
SocketChannel sc = (SocketChannel)key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(buffer);
if(readBytes >0){
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String expression = new String(bytes,"UTF-8");
System.out.println("服务器收到消息:" + expression);
String result = "to do";
doWrite(sc, result);
}
else if(readBytes<0){
key.cancel();
sc.close();
}
}
}
}

private void doWrite(SocketChannel channel, String response) throws IOException {
byte[] bytes = response.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
channel.write(writeBuffer);
}

}


NIOClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package nio;

public class NIOClient {
private static String HOST = "localhost";
private static int PORT = 12345;
private static NIOClientHandle clientHandle;
public static void start(){
start(HOST,PORT);
}

private static synchronized void start(String host, int port) {
if(clientHandle != null)
clientHandle.stop();
clientHandle = new NIOClientHandle(host,port);
new Thread(clientHandle,"Server").start();
}

//向服务器发送消息
public static boolean sendMsg(String msg) throws Exception{
if(msg.equals("q")) return false;
clientHandle.sendMsg(msg);
return true;
}
public static void main(String[] args){
start();
}

}


NIOClientHandle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
package nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class NIOClientHandle implements Runnable{
private String host;
private int port;
private Selector selector;
private SocketChannel socketChannel;
private volatile boolean started;

public NIOClientHandle(String host, int port) {
this.host = host;
this.port = port;
try{
//创建选择器
selector = Selector.open();
//打开监听通道
socketChannel = SocketChannel.open();
//如果为 true,则此通道将被置于阻塞模式;如果为 false,则此通道将被置于非阻塞模式
socketChannel.configureBlocking(false);//开启非阻塞模式
started = true;
}catch(IOException e){
e.printStackTrace();
System.exit(1);
}

}

public void stop() {
started = false;
}

@Override
public void run() {
try {
doConnect();
}catch (IOException e){
e.printStackTrace();
System.exit(1);
}
while (started){
try {
selector.select(1000);
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while(it.hasNext()){
key = it.next();
it.remove();
try{
handleInput(key);
}catch(Exception e){
if(key != null){
key.cancel();
if(key.channel() != null){
key.channel().close();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
System.exit(1);

}
}
if(selector != null)
try{
selector.close();
}catch (Exception e) {
e.printStackTrace();
}

}

private void doConnect() throws IOException {
if(socketChannel.connect(new InetSocketAddress(host,port)));
else socketChannel.register(selector, SelectionKey.OP_CONNECT);
}

public void sendMsg(String msg) throws IOException {
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel, msg);
}
private void doWrite(SocketChannel channel,String request) throws IOException{
//将消息编码为字节数组
byte[] bytes = request.getBytes();
//根据数组容量创建ByteBuffer
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
//将字节数组复制到缓冲区
writeBuffer.put(bytes);
//flip操作
writeBuffer.flip();
//发送缓冲区的字节数组
channel.write(writeBuffer);
//****此处不含处理“写半包”的代码
}

private void handleInput(SelectionKey key) throws IOException {
if(key.isValid()){
SocketChannel sc = (SocketChannel) key.channel();
if(key.isConnectable()){
if(sc.finishConnect());
else System.exit(1);
}
//读消息
if(key.isReadable()){
//创建ByteBuffer,并开辟一个1M的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取请求码流,返回读取到的字节数
int readBytes = sc.read(buffer);
//读取到字节,对字节进行编解码
if(readBytes>0){
//将缓冲区当前的limit设置为position=0,用于后续对缓冲区的读取操作
buffer.flip();
//根据缓冲区可读字节数创建字节数组
byte[] bytes = new byte[buffer.remaining()];
//将缓冲区可读字节数组复制到新建的数组中
buffer.get(bytes);
String result = new String(bytes,"UTF-8");
System.out.println("客户端收到消息:" + result);
}
//没有读取到字节 忽略
// else if(readBytes==0);
//链路已经关闭,释放资源
else if(readBytes<0){
key.cancel();
sc.close();
}
}
}

}
}




Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package nio;

import java.util.Scanner;

public class Test {
public static void main(String[] args) throws Exception{
//运行服务器
NioServer.start();
//避免客户端先于服务器启动前执行代码
Thread.sleep(100);
//运行客户端

NIOClient.start();
while(NIOClient.sendMsg(new Scanner(System.in).nextLine()));
}

}


参考文献

怎样理解阻塞非阻塞与同步异步的区别?
Java 网络IO编程总结(BIO、NIO、AIO均含完整实例代码)
BIO/NIO底层原理分析
清华大牛权威讲解nio,epoll,多路复用,更好的理解redis-netty-Kafka等热门技术
Java NIO浅析
SocketChannel简述
Java NIO 学习笔记(一)—-概述,Channel/Buffer

socket编程

最近做项目,需要了解一些socket编程的相关知识,因为本身在大学的时候也接触过socket编程,但是时间已经很久了,并且当时学的不够用心,今天下午看了一些博客,对socket有了一些新的认识,在这里记录一下我学到的一些知识。

一.什么是socket

什么socket,这里我们要先从osi模型和TCP/IP模型开始讲起,OSI一共包含了7层,其中应用层、表示层和会话层是面向用户的,程序员可以在此基础上进行开发,如http、ftp、SMTP(邮件)等协议就是基于应用层建立的,下面的四层协议是由系统内核封装,对用户不可见的。
upload successful

OSI模型表达的过于复杂,为了简化模型,人们又提出了TCP/IP模型,这种模型将上面的3层模型统一的表示成应用层。
upload successful

为了使数据分组从源传送到目的地,源端OSI模型的每一层都必须与目的端的对等层进行通信,这种通信方式称为对等层通信。在每一层通信过程中,使用本层自己协议进行通信。在进行网络通信的时候如打开一个网页,数据在发送端(这里指客户端)会从上到下进行封装,然后传输到服务端后,数据会自下而上进行解封装。最后获取上层数据。

我们在进行网络通信的过程中,可以使用http这样应用层的协议,但是当没有协议满足我们的需求的时候,我们需要自主与TCP/UDP进行交互,这时候就有了socket,这是建立在传输层上的一个抽象层,帮助我们与tcp/ip建立连接进行通信,我们可以把它看作两个主机进行双向通信的端点。socket主要有3个参数:通信的目的IP地址、使用的传输层协议(TCP或UDP)和使用的端口号。

二. socket工作流程

upload successful
socket是一种打开——读/写——关闭的模式的实现,使用TCP协议进行通讯为例。一共分为服务端socket和客户端socket。服务端socket负责监听客户端连接请求,当客户端发送请求时,两端进行通信。具体流程如下:

  • 1.服务端根据地址类型、socket类型、协议创建socket。
  • 2.服务端为socket绑定ip地址和端口号。
  • 3.服务器socket监听端口号请求,随时准备接受客户端发来的请求,这时候服务器的socket并没有被打开。
  • 4.客户端创建socket
  • 5.客户端打开socket,根据服务器的ip地址和端口号试图连接服务器socket
  • 6.服务器socket接收到客户端socket请求,被动打开,开始接收客户端请求,直到客户端返回连接信息。这时候socket进入阻塞状态,所谓阻塞即accept()方法一直到客户端返回连接信息后才返回,开始接收下一个客户端谅解请求。
  • 7.客户端连接成功,向服务端发送连接状态信息。
  • 8.服务器accept方法返回,连接成功
  • 9.客户端向socket写入信息
  • 10.服务器读取信息
  • 11.客户端关闭
  • 12.服务器关闭
1.三次握手

在客户端和服务端建立TCP连接的过程中会发生有名的三次握手

upload successful

第一次握手:客户端会尝试连接服务器,向服务器发送syn包,syn=j,客户端进入syn_send状态等待服务器确认。

第二次握手:服务器接收客户端syn包并确认(ack=j+1),同时向客户端发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态。

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(已确认),完成三次握手。

值得强调的是accept()这个方法,内核会创建两个队列,SYN队列和accept队列,其中accept队列的长度由backlog指定。服务器在调用accept之后,阻塞,等待accept队列有元素,当三次握手结束后服务器会把客户端从syn队列转移到accept队列,而accept()被唤醒,从accept队列中取出请求方,重新建立一个socket用于准备发送和接收数据,原来的socket还在监听哪个端口。换一句话说,socket()返回的套接字用于监听(listen)和接受(accept)客户端请求,这个套接字不能用于与客户端之间发送和接受数据。accept()接受一个客户端的连接请求,并返回一个新的套接字。所谓“新的”就是说这个套接字与socket()返回的套接字不是同一个socket。这个新的套接字用于与这次接受的客户端之间的通信。

2.四次握手

当客户端发送信息完毕之后,客户端会与服务端断开连接,此时会发生4次挥手
upload successful

第一次挥手:先由客户端向服务器端发送一个FIN,请求关闭数据传输。

第二次挥手:当服务器接收到客户端的FIN时,向客户端发送一个ACK,其中ack的值等于FIN+SEQ,此时客户端不再向服务端发送消息,但是服务端还可以向客户端发送信息。

第三次挥手:服务端向客户端发送一个FIN,去告诉客户端关闭应用。

第四次挥手:当客户端收到服务端的FIN时,发送一个ACK给服务器。其中ACK的值等于FIN+SEQ。

三. socket实例

demo1 同步实例

这里我抛出来一些简单的socket的demo,先贴出服务端的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from socket import *
from time import ctime

HOST = ''
PORT = 11113
BUFSIZE = 4096
ADDR = (HOST, PORT)

tcpServer = socket(AF_INET, SOCK_STREAM)
tcpServer.bind(ADDR)
tcpServer.listen(10)

while 1:
print('waiting for connection...')
tcpClient, addr = tcpServer.accept()
print(addr)

while 1:
data = tcpClient.recv(BUFSIZE)
print(data.decode())

if not data:
print('---------')
break;

buf = '[' + ctime() + ']' + data.decode()
tcpClient.send(buf.encode())

tcpClient.close()
tcpServer.close()

服务端会一直监听11113端口,直到有客户端连接进来,会创建一个新的socket用于交换信息。
下面是客户端的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from socket import *
import json

HOST = 'localhost'
PORT = 11113
BUFSIZE = 4096
ADDR = (HOST, PORT)

tcpClient = socket(AF_INET, SOCK_STREAM)
tcpClient.connect(ADDR)



while 1:
data = input('> ')

if not data:
break
tcpClient.send(data.encode())

data = tcpClient.recv(BUFSIZE)
print(data.decode())
if not data:
break

tcpClient.close()

这个demo基本上涵盖了socket通信的整个流程,但是demo中有一些缺点,因为socket.recv()是阻塞的,所以当服务端执行到recv()的时候,会一直等待,直到客户端发送消息才能够继续往下执行代码。这样的结构对于一些实时性要求比较高的场景很不友好。比如说,我们在用实时的视频流进行监控和一些异常计算,当有异常现象的时候,我们会与巡逻小车进行socket进行通信来处理异常,这就要求我们既要能够实时接收摄像头传来的视频流,又要接巡逻小车发来的指令信号,使用上述的demo作为框架就显得不那么合适了。

2. demo2 异步实例

有一种解决方法就是使用异步通信来解决,这样我们可以对传来的数据进行监听,当监听到有客户端传来请求时,我们会对传来的消息进行解析处理;当客户端没有传来请求时,服务端就会处理自己的事情。在python中可以使用select完成异步通信,下面有一个demo,这里我只抛出来客户端的demo,服务端的异步处理同客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from socket import *
import json
import time
import select

HOST = 'localhost'
PORT = 6688
BUFSIZE = 4096
ADDR = (HOST, PORT)

tcpClient = socket(AF_INET, SOCK_STREAM)

tcpClient.connect(ADDR)

input = [tcpClient]
# tcpClient.setblocking(False)
print("["+time.ctime()+"]" +"发现异常")
data_exchange = {'Position':0, 'CarArrived':False, 'ResetCarPosition':False, 'CarPatrol':False}
data = json.dumps(data_exchange)
tcpClient.send(data.encode())
tcpClient.settimeout(5)

while 1:
rs, ws, es = select.select(input, [], [], 1)
for indata in rs:
if indata == tcpClient:
data = tcpClient.recv(BUFSIZE)
if data:
car_recv = data.decode()
data_exchange = json.loads(car_recv)
if data_exchange['CarPatrol'] == True:
print("["+time.ctime()+"]" +"小车已经就绪,重置实验")
data_exchange = {'Position':0, 'CarArrived':False, 'ResetCarPosition':False, 'CarPatrol':False}
elif data_exchange['CarArrived'] == True:
print("["+time.ctime()+"]" +"小车已经到达异常点,开始处理异常")
time.sleep(3)
print("["+time.ctime()+"]" +"处理异常结束,小车归位")
data_exchange['ResetCarPosition'] = True
data = json.dumps(data_exchange)
tcpClient.send(data.encode())
print("1")


tcpClient.close()

参考文献

网络OSI七层模型、TCP/IP模型以及数据发送封装与解封装过程

简单理解Socket

socket中accept()函数的理解

Dockerfile

今天带来dockerfile的第二篇文档————Dockerfile,Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明。我们可以用java的源码和class文件进行类比,如果我们将image文件比做为一个个类文件,那么Dockerfile就可以看成是源码文件。当我们在Dockerfile文件中写好我们希望构建的镜像之后,可以使用docker build来读取Dockerfile中的指令构建我们的镜像。

一. Dockerfile相关指令以及build指令

  • FORM 基础镜像,当前新镜像是基于哪个镜像的
  • MAINTAINER 镜像维护者的姓名和邮箱
  • RUN 容器构建时需要运行的命令
  • EXPOSE 当前容器对外暴露出的端口
  • WORKDIR 指定在创建容器后,终端默认登陆进来的工作目录,一个落脚点
  • ENV 用来在构建镜像过程中设置环境变量
  • ADD 将宿主机目录下的文件拷贝进镜像且ADD命令会自动处理URL和解压tar压缩包
  • COPY 类似ADD,拷贝文件和目录到镜像中,注意这个不会解压
  • VOLUME 容器数据卷,用于数据保存和持久化操作
  • CMD 指定一个容器启动时要运行的命令,dockerfile可以有多个cmd,但是只有最后一个生效
  • ENTRYPOINT 指定一个容器启动时要运行的命令,不会被覆盖,会追加
  • ONBUILD 当构建一个被继承的Dockerfile时运行命令,父镜像在被子继承后父镜像的onbuild会被触发,类似于一个触发器

build指令解释如下:

1
docker build [OPTIONS] PATH | URL | -

关于options有一些说明:

  • –build-arg=[] :设置镜像创建时的变量
  • –cpu-shares :设置 cpu 使用权重;
  • –cpu-period :限制 CPU CFS周期;
  • –cpu-quota :限制 CPU CFS配额;
  • –cpuset-cpus :指定使用的CPU id;
  • –cpuset-mems :指定使用的内存 id;
  • –disable-content-trust :忽略校验,默认开启;
  • -f :指定要使用的Dockerfile路径;
  • –force-rm :设置镜像过程中删除中间容器;
  • –isolation :使用容器隔离技术;
  • –label=[] :设置镜像使用的元数据;
  • -m :设置内存最大值;
  • –memory-swap :设置Swap的最大值为内存+swap,”-1”表示不限swap;
  • –no-cache :创建镜像的过程不使用缓存;
  • –pull :尝试去更新镜像的新版本;
  • –quiet, -q :安静模式,成功后只输出镜像 ID;
  • –rm :设置镜像成功后删除中间容器;
  • –shm-size :设置/dev/shm的大小,默认值是64M;
  • –ulimit :Ulimit配置。
  • –tag, -t: 镜像的名字及标签,通常 name:tag 或者 name 格式;可以在一次构建中为一个镜像设置多个标签。
  • –network: 默认 default。在构建期间设置RUN指令的网络模式

下面我们举一个例子来构建自己的dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
FROM contos

ENV MYPATH /usr/local
WORKDIR $MYPATH

RUN yum -y install vim
RUN yum -y install net-tools

CMD echo $MYPATH
CMD echo "success-------ok"
CMD /bin/bash

这里我们引入原始的centos镜像,然后设置工作路径为 /usr/local,并在这个linux系统中装入vim和net-tools组建,最后我们输出我们的工作路径和success。

使用build指令进行镜像构建

1
docker build -f /mydocker/DockerFile -t mycentos:1.3 .

这里我遇到了一个坑,就是我是在根目录下构建的images,就会出现问题

1
Error checking context: 'no permission to read from'/proc/sys/net/ipv4/route/flush''.

这里我们一定不要在根目录下进行build操作。

最终结果就是成功构建,我们使用docker images 可以看到:
upload successful

二. 参考文献

尚硅谷docker核心技术
docker官方文档
docker build 命令 菜鸟教程

docker基础学习(1)

今天在天池参加了一个比赛,里面涉及到了docker的容器技术的打包,之前一直没有接触过这些东西,所以乘着周末学习了一些docker基础知识。

一. 什么是docker

我们从官方文档里面可以看到,docker是为程序员提供一个可以构建、运行和移植的容器。这个容器可以帮助程序员们快速部署应用,这样的功能被称之为容器化。

容器化有以下几个特点:

  • 灵活:即使是很复杂的程序,也可以被放在容器中。
  • 轻量级:容器内的应用运行于主机的内核系统,并可以共享内核资源,这样就比虚拟机更轻量化,可以快速创建和启动。
  • 可移植性:容器可以在本地创建,在云端部署,并在任何地方运行。
  • 松耦合: 容器具有高度的封装性,可以在不破坏其他容器的前提下完成容器的升级或替换。
  • 可扩展: 我们可以自动扩充并自动部署容器副本。
  • 安全性:我们不需要手动配置任何参数,容器会帮助我们自动约束和隔离应用。

docker容器并不仅仅是一个正在运行的线程,它增加了一些封装的功能使得其与主机和其他的容器相互隔离,实现隔离功能的最主要的原因是容器可以和内部的镜像文件进行交互,一个镜像文件包括了程序运行的一切代码或二进制文件、依赖项、以及所需的任何其他的文件系统对象。

二. docker和VM的对比

由下图我们可以看到,docker在linux本地运行,并与其他容器共享主机的内核,当他运行一个独立的进程的时候,不会占用其他可执行文件的内存,容器内部只有程序以及所依赖的二进制文件和依赖哭,并与其他容器相隔离,这种轻巧的特性使得容器可以在几秒内启动。
upload successful

相比之下虚拟机的虚拟的是整个操作系统,包括硬件的设置,这样的方式使得VM会产生大量的开销,超出了应用程序逻辑所消耗的的开销。

三. docker架构

在看架构图之前,我们先来了解一下docker的三个基本概念:镜像、容器、仓库。

  • 镜像:镜像是只读,里面有需要运行的文件。它通常是用来创建容器的,镜像可以创建多个容器;我们可以在本地创建镜像也可以通过docker pull从仓库中下载。
  • 容器:容器是镜像的运行的一个实例,我们在使用镜像创建容器的时候可以配置一定的参数,来约束容器的运行。容器的运行是相互独立的,容器之间是不会影响的,这样可以保证容器可以运行在一个相对安全的地方。
  • 仓库:仓库是用来管理、存储镜像的,最有名的仓库就是docker hub,其他的比如国内阿里云的镜像仓库、网易晕的镜像仓库都是用来管理镜像的远程仓库,他们为我们提供下载上传渠道。
    upload successful

如上图,docker架构一共分为三个部分,docker 客户端, docker服务端,和docker注册中心。docker客户端用于发送docker指令,并与docker的守护进程进行通信,守护进程会对指令进行分析,并执行指令,注册中心就是仓库,它用来共享和管理dicker镜像,当我们使用docker pull的时候,docker守护进程会从指定的仓库中拉取镜像并放到本地,这样我们在使用docker run创建容器的时候,就可以直接使用本地的镜像直接创建容器。

四. docker常用指令

docker 有一些比较常用的指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
1. 从仓库拉取镜像
docker pull [image name:tag]
2. 查看本地的所有镜像
docker images
3. 查看运行的容器
docker ps
4. 查看所有的容器
docker ps -a
5. 删除镜像
docker rmi [image name:tag]
6. 启动、停止、重启容器命令
docker start container_name/container_id
docker stop container_name/container_id
docker restart container_name/container_id
7. -i 以交互模式运行容器
-t 为容器重新分配一个伪输入终端
-d 在后台运行
docker run -i -t container_name/container_id
8. 删除容器的命令:
docker rm container_name/container_id
9. 查看docker信息
docker info
10. 使用镜像创建docker容器
docker run container_name/container_id
11. 查看docker 日志
-t 打印时间
-f 持续打印
-tail 显示日志总数
docker logs
12. 进入后台
docker attach container_id
13. 进入后台,退出容器不会停止
docker exec -it container_id /bin/bash

更多的指令我们可以在docker –help中查看。

四. 参考文献

尚硅谷docker核心技术
docker官方文档

今天在使用SpringBoot 2.2.6 配置@ConfigurationProperties注解是,发生错误,看一些博客也没有解决问题,在这里记录一下。

首先查找blog,发现在pom添加 spring-boot-configuration-processor配置包就可以了,于是去maven依赖包中查找,代码如下:

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>

但是maven始终无法解析这个依赖包,可能还未上传的缘故,解决方法,将依赖包的版本往前退一个,改为2.1.6就可以了

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.1.6.RELEASE</version>
</dependency>

之前我们说过反射机制,今天我们聊一聊反射机制相关的一个设计模式————代理模式,代理模式中的动态代理就是应用了我们反射来动态生成代理对象完成方法调用。

一. 代理模式概念

代理模式是给某一个对象提供一个代理,并由代理对象来控制对真实对象的访问。

当我们想对一个对象增加一些附加的功能,例如权限的验证、增加日志功能,同时我们又不想破坏对象本身的结构,我们就可以创建一个和这个对象功能相同的代理对象,通过对代理对象的方法调用,间接调用被代理的对象,还可以在方法调用的前后增加一些附加的功能,以达到增强对象的目的。

代理模式一共有三种,静态代理模式、动态代理模式、gclib代理模式。

二. 静态代理模式

upload successful

如图,我们在有一个ITeacherDao接口,然后我们又创建一个类TeacherDao实现了ITeacherDao接口,当我们在使用这个类的时候,我们希望对类中实现的teache方法做一些增强。这里我们创建TeacherProxy代理类同样实现了ITeacherDao接口,并通过构造函数,传入接口参数来间接对TeacherDao形成依赖关系。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//接口类
public interface ITeacherDao {
String teach(String name);
}


//实现方法
public class TeacherDao implements ITeacherDao{
@Override
public String teach(String name) {
System.out.println("老师教学生" + name);
return name;
}
}

//代理类
public class TeacherProxy implements ITeacherDao{
private ITeacherDao target;

//利用接口来代理目标乐居
public TeacherProxy(ITeacherDao target) {
this.target = target;
}

@Override
public String teach(String name) {
System.out.println("代理开始");
String stu = target.teach(name);
System.out.println("代理结束");
return stu;
}
}

我们向TeacherProxy传入ITeacherDao对象target,并通过构造函数传入被代理对象,在实现teach方法的过程中,通过调用被代理类的teach方法达到方法增强的效果。在实现过程中我们用到了泛型操作,这使得我们可以创建新的ITeacherDao实现类,而无需修改代理类的代码。

静态代理的优点:

  • 易于理解和实现
  • 代理类和真实类的关系是编译期静态决定的,和下文马上要介绍的动态代理比较起来,执行时没有任何额外开销。

静态代理的缺点:
对于每一个接口都需要一个创建新的代理类,当一个工程创建的接口增多时,我们需要创建和维护的代理类也会增多,这无疑是增加工程的代码量和复杂度,不易于管理和维护。

三. 动态代理模式

动态代理模式也叫做JDK代理模式,它是通过调用JDK的Proxy类newProxyInstance()来实现对象代理,他会使代理类在运行时动态的生成。
JDK中生成代理对象的API:

代理类所在包:java.lang.reflect.Proxy
JDK实现代理只需要使用newProxyInstance方法,但是该方法需要接收三个参数,完整的写法是:

1
static Object newProxyInstance(ClassLoader loader, Class [] interfaces, InvocationHandler handler)

这是一个静态方法,且接收的三个参数依次为:

  • ClassLoader loader: 指定当前被委托对象使用类加载器,用null表示默认类加载
  • Class [] interfaces: 指定被委托对象实现的接口。
  • InvocationHandler handler: 调用处理器,执行目标对象的方法时,会触发调用处理器的方法,从而把当前执行目标对象的方法作为参数传入

java.lang.reflect.InvocationHandler:这是调用处理器接口,它自定义了一个 invoke 方法,用于集中处理在动态代理类对象上的方法调用,通常在该方法中实现对委托类的代理访问。

1
2
3
4
5
// 该方法负责集中处理动态代理类上的所有方法调用。
//第一个参数既是代理类实例,
//第二个参数是被调用的方法对象
// 第三个参数是调用方法的参数。
Object invoke(Object proxy, Method method, Object[] args)

当我们在调用代理类的方法的时候,代理类会转而进入到InvocationHandler的invoke方法中从而操作被代理对象调用相应的方法。这样我们就可以对被代理对象进行统一的处理,也可以更具参数的不同对被代理对象进行风别处理。

我们在使用JDK动态代理时,通过创建一个代理工厂生成代理对象,并动态执行被代理对象的方法调用,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ProxyFactory{
Object target;

public ProxyFactory(Object target) {
this.target = target;
}

public Object getNewInstance(){
Object o = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代理开始");
Object ObjectReturn = method.invoke(target, args);
System.out.println("代理结束");
return ObjectReturn;
}
});
return o;
}


}

我们点开newProxyInstance源码可以看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);

final Class<?>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}

/*
* Look up or generate the designated proxy class.
*/

//生成代理类
Class<?> cl = getProxyClass0(loader, intfs);

/*
* Invoke its constructor with the designated invocation handler.
*/
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}

//利用代理类生构造器
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
//利用构造器在生成实例
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}

我们可以看到newProxyInstance现为我们生成了一个代理类,再利用代理类生成一个构造器,最后用构造器生成代理对象。这里我们看到构造器传入了constructorParams,我们追踪一下源码。

1
2
private static final Class<?>[] constructorParams =
{ InvocationHandler.class };

发现constructorParams是一个包含了InvocationHandler.class的数组,其实我们最终生成代理类会包含一个InvocationHandler对象h,当我们调用代理对象的方法的时候,实际上调用调用的是h的invoke()方法进而调用实际的方法。我们接下来用idea的反编译看一下生成的class类文件,反编译的操作是在VM options中添加:-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package com.sun.proxy;

import com.mjj.proxy.JDKproxy.ITeacherDao;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements ITeacherDao {
private static Method m1;
private static Method m2;
private static Method m3;
private static Method m0;

public $Proxy0(InvocationHandler var1) throws {
super(var1);
}

public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}

public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final String teacher(String var1) throws {
try {
return (String)super.h.invoke(this, m3, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}

public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m3 = Class.forName("com.mjj.proxy.JDKproxy.ITeacherDao").getMethod("teacher", Class.forName("java.lang.String"));
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}

这里我们可以清晰的看到代理类在初始化的时候通过反射加载方法,在调用方法的时候实际上是调用我们传入的InvocationHandler的方法。本质上代理类和被代理类都实现了相同的接口,拥有相似的结构,而在方法的调用上都是通过反射实现对代理类的调用。

我们维护的动态代理类生成器ProxyFactory无需实现接口,具有可维护、易扩展的特点,具体的源代码可以参考这个链接(JDK动态代理实现原理(jdk8))但是JDK动态代理有一个无法避免的缺点就是,被代理类必须实现接口。若我们想代理一个没有实现接口的类,我们就不能使用这种方法,而只能使用我们下面介绍的cglib代理。

三. cglib代理模式

cglib代理模式不需要被代理的对象实现接口,其底层是通过ASM字节码框架生成类的字节码,达到动态创建类的目的。它利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。下面我借用一下别人的uml类图来说明一下:

upload successful
cglib通过先创建了代理类并实例化proxy,proxy通过继承关系继承被代理类,然后通过对父类方法的重写以达到业务增强的效果。
实例代码如下:

创建目标类:Target:方法简单输出一句话

1
2
3
4
5
public class Target {
public void request() {
System.out.println("执行目标类的方法");
}
}

创建目标类的方法增强拦截器:TargetMethodInterceptor:在拦截器内部,调用目标方法前进行前置和后置增强处理。

1
2
3
4
5
6
7
8
9
10
public class TargetMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
System.out.println("方法拦截增强逻辑-前置处理执行");
Object result = proxy.invokeSuper(obj, args);
System.out.println("方法拦截增强逻辑-后置处理执行");
return result;
}
}

生成代理类,并测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class CglibDynamicProxyTest {

public static void main(String[] args) {
Enhancer enhancer = new Enhancer();

// 设置生成代理类的父类class对象
enhancer.setSuperclass(Target.class);

// 设置增强目标类的方法拦截器
MethodInterceptor methodInterceptor = new TargetMethodInterceptor();
enhancer.setCallback(methodInterceptor);

// 生成代理类并实例化
Target proxy = (Target) enhancer.create();

// 用代理类调用方法
proxy.request();
}
}

测试输出:可以看到成功进行了业务增强的处理。

1
2
3
方法拦截增强逻辑-前置处理执行
执行目标类的方法
方法拦截增强逻辑-后置处理执行

四. 动态代理和cglib对比

  • 实现方式不同
    动态代理是利用相同接口实现和被代理类相同结构代理类,再利用反射机制实现方法代理,而cglib是利用asm框架操作字节码文件生层代理类,再利用继承关系实现代理。
  • 使用方式不同
    动态代理要求代理对象必须实现接口,而cglib虽然不需要被代理对象实现接口,但是由于使用了继承关系,所以cglib无法代理final对象。
  • 效率对比
    关于两者之间的性能的话,网上有人对于不通版本的jdk进行测试,经过多次试验,测试结果大致是这样的,在1.6和1.7的时候,JDK动态代理的速度要比CGLib动态代理的速度要慢,但是并没有教科书上的10倍差距,在JDK1.8的时候,JDK动态代理的速度已经比CGLib动态代理的速度快很多了,但是JDK动态代理和CGLIB动态代理的适用场景还是不一样的哈!

五. 参考文献

Java的三种代理模式
Java代理设计模式的四种具体实现:静态代理和动态代理
Java 动态代理作用是什么?
Java:聊聊JDK和CGLib动态代理实现和区别
设计模式(11)动态代理 JDK VS CGLIB面试必问

之前在看springIOC的视频,里面设计到了很多反射的知识,我对反射一直停留在会用的阶段,借着这个机会好好学习一下Java的反射机制。

一. 反射基本概念

反射机制是指在程序运行期间对于任意一个类可以知道它的全部属性和方法,对于任意一个对象可以调用他的所有方法。这种 动态的获取信息 以及 动态调用对象的方法 的功能称为 java 的反射机制。

一般来说我们可以通过new一个对象来达到正向创建对象的目的,这样创建出来的对象类型是编译期可知的,而我们可以借助反射机制在运行期间通过Class动态创建对象,这样的对象往往在编译期间是不可知的。

Java 反射主要提供以下功能:

  • 在运行时判断任意一个对象所属的类;(isInstance)
  • 在运行时构造任意一个类的对象;(newInstance)
  • 在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);
  • 在运行时调用任意一个对象的方法(invoke)

二. 反射的使用

Class类获取

提到反射的使用,就不得不提class类,class类是标示一个运行时类的具体信息,它由JVM在装载一个类的时候自动创建用于记录该类的相关信息并存放在虚拟机堆中。他的创建方式一共有三种

第一种,使用 Class.forName 静态方法。当你知道该类的全路径名时,你可以使用该方法获取 Class 类对象。

1
Class clz = Class.forName("java.lang.String");

第二种是使用 类.class直接获取类的Class对象

1
Class clz = String.class;

第三种是通过对象的getClass()方法获取Class对象

1
2
String str = new String("Hello");
Class clz = str.getClass()

这里第二种方式需要在编译期前就确定Class类,这与反射机制的编译时不可知,运行时可知的特点相矛盾,而第三种已经创建出类的实体,再利用getClass方法获取类对象,对于反射来说已经意义不大了,所以我们再利用反射的时候通常使用第一种方式,通过全类名的方式获取class类,也符合编译器不可知的特点。

获取对象实例

通过反射创建类对象主要有两种方式:通过 Class 对象的 newInstance() 方法、通过 Constructor 对象的 newInstance() 方法。

第一种:通过 Class 对象的 newInstance() 方法。

1
2
Class clz = Class.forName("java.lang.String");
Object object = clz.newInstance();

这种方式只适用于适用无参的构造函数,有参数的无法使用。

第二种:通过 Constructor 对象的 newInstance() 方法

1
2
3
Class clz = Class.forName("java.lang.String");
Constructor cons = clz.getConstructor(String.class);
Object object = constructor.newInstance("111");

通过 Constructor 对象创建类对象可以选择特定构造方法,而通过 Class 对象则只能使用默认的无参数构造方法。

获取类属性、方法

属性的获得一共有两种getField()和getDeclaredField()

第一种getField()

1
2
Class<?> bookclz = Class.forName("com.mjj.book.Book");
Field author = bookclz.getField("author");

这种方式可以获取类内以public方式声明的属性以及继承的public的属性,对于private和默认声明的属性无法获取。

第二种getDeclaredField()

1
2
3
4
Class<?> bookclz = Class.forName("com.mjj.book.Book");
Field author = bookclz.getDeclaredField("author");
author.setAccessible(true);
System.out.println(author);

这种属性的获取方式可以获取到本类private属性的值,但是当我们在使用这样私有属性的时候,我们需要设置setAccessible(true);才可以正确使用。

方法获取的方式一样都有两种分别是

1
2
3
4
5
6
7
//getDeclaredMethods 方法返回类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。
public Method[] getDeclaredMethods() throws SecurityException
//getMethods 方法返回某个类的所有公用(public)方法,包括其继承类的公用方法。
public Method getMethod(String name, Class<?>... parameterTypes)
//getMethod 方法返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class的对象
public Method[] getMethods() throws SecurityException

三. 参考文献

大白话说Java反射:入门、使用、原理
深入解析Java反射(1) - 基础