一些常见术语:
说到Java原生网络编程,肯定离不开Socket。Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
短连接:
连接->数据传输->关闭连接
传统http是无状态的,浏览器和服务器每次进行一次http请求,就奖励一次连接,但是任务结束后就中断。也可以说:短连接是指SOCKET连接后发送后接收完数据后马上断开连接
长链接:
连接->传输数据->保持连接->传输数据->.......->关闭连接
长连接至建立socket连接后不管是否使用都保持连接。
什么时候用长链接,短连接?
长连接多用于操作频繁,点对点的通信,而且连接数不能太多的情况。每个TCP连接都需要三次握手,这需要时间,如果每个操作都是线连接,在操作的话,那么处理速度会降低很多,所以每个操作完后都不断开,下次处理时直接以发送数据包就OK了,不用建立TCP连接
短连接是话,想web网站的http服务一般都是短连接,因为长连接对于服务器来说会耗费一定的资源,想web网站这种这么频繁是成千上万甚至上亿客户端的连接用短连接会更省一些资源
总之,长连接和短连接的选择要视情况而定。
原生JDK网络编程-BIO
BIO(Blocking IO)阻塞IO,传统的同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入、输出流进行同步阻塞式通信
package com.joung.bio;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException {
//服务端启动必备
ServerSocket serverSocket = new ServerSocket();
//表示我们服务器在哪个端口上监听
serverSocket.bind(new InetSocketAddress(10001));
System.out.println("Start server.......");
try {
while(true){
new Thread(new ServerTask(serverSocket.accept())).start();
}
} finally {
serverSocket.close();
}
}
private static class ServerTask implements Runnable{
private Socket socket;
public ServerTask(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try(ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream()))
{
/*接受客户端的输出,也就是服务器的输入*/
String userName = inputStream.readUTF();
System.out.println("Accetp client message:"+userName);
//处理各种实际的业务
/*服务器的输入的输出,也就是客户端的输入*/
outputStream.writeUTF("Hello,"+userName);
outputStream.flush();
}catch (Exception e){
e.printStackTrace();
}finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
package com.joung.bio;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws IOException {
Socket socket=null;
ObjectInputStream inputStream=null;
ObjectOutputStream outputStream=null;
InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1",10001);
try {
socket = new Socket();
socket.connect(socketAddress);
outputStream = new ObjectOutputStream(socket.getOutputStream());
inputStream = new ObjectInputStream(socket.getInputStream());
outputStream.writeUTF("joung");
outputStream.flush();
System.out.println(inputStream.readUTF());
}finally {
if (socket != null) {
socket.close();
}
if (outputStream!=null) outputStream.close();
if (inputStream!=null) inputStream.close();
}
}
}
BIO这种模型,每一次都要开一个线程处理客户端的任务,我们想一个,用线程来改善:ExecutorService executorService = Executors.newFixedThreadPool( Runtime.getRuntime().availableProcessors()*2);
Runtime.getRuntime().availableProcessors()=计算机逻辑处理数
原生JDK网络编程-NIO
什么是NIO?
NIO库在JDK1.4中引入的。NIO弥补原来的BIO的不足,它在标准JAVA代码中提供了高速的、面向块的I/O。NIO翻译成no-blocking io或者new io 都说得通
和BIO的主要区别
Java中NIO和IO之间第一个最大的区别就是IO是面向流的,NIO是面向缓冲区的。面向流的话意味着每次从流中读一个或者多个字节,直至读取所有字节,它们没有被缓冲在任何地方。此外,它不能前后移动流中的数据。Java NIO的缓存导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在换从区中前后移动。这就增加了处理过程的灵活性。但是,还需要检查是否该缓冲区中包含所有需要处理的数据。而且需要确保当更多的数据读入缓冲区是,不覆盖缓冲区里尚未处理的数据。
NIO三大核心组件:
Selector:可以理解为一个选择器或者叫轮询代理器,服务端或者客户端想关注什么事件就注册到Selector中。Selector放在一个单独 线程中来监视多个输入通道。
举个例子,在一家养生会所中,每次来客人时,客人A和前台小姐姐说,我想叫为技师做SPA;客人B和前台小姐姐说,我想叫个技师做按摩。然后前台小姐姐就会去不断去询问有没有技师空闲做SPA或者按摩,有技师空闲的话就告诉客人。这个前台小姐姐就是Selector。
Channels:通道,被建立的一个应用程序和操作系统交互事件、传递内容的渠道(注意是连接到操作系统)。那么既然是和操作系统进行内容的传递,那么说明应用程序可通过通道读取数据,也可以通过通道向操作系统写数据,而可以同时进行读写。
Buffer:Buffer用于和NIIO通过进行交互。数据是从通道读入缓冲区,从缓冲区写入到通道中 的。以写为例,应用程序都是将数据写入到缓冲,再通过通道把缓冲的数据发送出去,读也是一样,数据总是先从通道读到缓冲,应用程序再读取缓冲中的数据
缓冲区本质上是一块看图写入数据,然后可以从中读取数据的内存。这块内存被包装成 NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。
重要概念SelectionKey:
在向Selection对象注册感兴趣的事件时,Java NIO共定义了四种:OP_READ、OP_WRITE、
OP_CONNECT、OP_ACCEPT,分贝对应读、写、请求连接、接收连接等网络Socket操作。
老板就相当于ServerSocketChannel,前台相当于Selector,客户相当于SocketChannel,部长相当于Buffer。注册开始营业、SPA业务、按摩业务就是SelectionKey。
1)老板开始营业,注册监听客户发过来的请求,有客户连接前台告知老板
2)客户向前台注册感兴趣的服务,有技师空闲,前台告知客户
3)技师空闲了,客户和部长(Buffer)沟通后,技师给客户服务
4)技师的服务评分,客户通过部长告知老板
网络协议常见问题
那些应用比较适合用UDP
多播的信息移动要用udp实现,因为tcp只支持1对一通信。
udp适用场景:多播、信息简短、响应快、丢点包不碍事
tcp适用场景:完整性、可靠性
如果既要完整性、可靠性还要使用udp呢?那么只能靠上层应用自己指定规则了,比如UDT
DDOS攻击
DDOS攻击利用合理的服务请求占用过多的服务资源,使正常用户的请求无法得到响应。
常见的DDOS攻击有计算机网络带宽攻击和连通性攻击
带宽攻击,指以极大的通信量冲击网络,使得所有可用的网络资源都被消耗殆尽,最后导致合法的用户请求无法通过
连通性攻击,指用大量的连接请求冲击计算机,使得所有可用的网络资源都被消耗殆尽,最后计算机无法再处理合法的用户请求。比如TCP的第二次握手就是连通性攻击。
HTTP和HTTPS区别:
HTTPS协议:一般理解为http+SSL/TLS,通过SSL证书来验证服务器的身份,并未浏览器之间的通信进行加密。
1)HTTP的URL是以http://开头,HTTPS的URL是以https://开头
2)htts是不安全的,HTTPS是安全的
3)http的标准端口是80,HTTPS的标准端口是443
4)在OSI网络模型中,http工作于应用层,而HTTPS的安全传输机制工作在传输层
5)http无法加密,而HTTPS对传输的数据进行加密
6)http无需证书,而HTTPS需要CA机构颁发的SSL证书
HTTPS请求过程:
1)客户端发起HTTPS请求
2)服务端向CA机构获取证书
3)服务器响应请求,携带数字证书
4)客户端拿到证书后,进行验证。验证不通过,则抛出HTTPS警告;验证通过取出证书中的公钥A并生成随机码KEY,并使用公钥A对随机码KEY进行加密
5)客户端把加密后的随机码KEY发送给服务端
6)服务端获取加密后的随机码KEY,用私钥B进行解密,再用解密后的随机码KEY对数据加密
7)传输对称加密后的数据
8)之后一直通过随机码KEY进行对称加密传输
序列化概述:
序列化(编码)是将对象序列化为二进制形式(字节数组),主要用于网络传输、数据持久化等,而反序列化(解码)则是从网络、磁盘等读取的字节数组还原成原始对象,主要用于网络传输对象是解码,以便完成远程调用。
影响序列化性能的关键因素:序列化后的码流大小(网络带宽的占用)、序列化的性能(CPU资源占用)、是否支持夸语言(异构系统的对接和开发语言切换)。
Java默认提供的序列化:无法跨语言、序列化后的码流太大、序列化的心梗差
XML,优点:人机可读性好,可指定元素或特性的名称。缺点:序列化后的数据只包含数据本身以及类的结构,不包含类型标识和程序集信息;只能序列化公共属性和字段;不能序列化方法;文件庞大,文件格式复杂,传输占宽带。适用场景:当做配置文件存储数据,实时数据转换
JSON:是一种轻量级的数据交换格式,优点:兼容性高、数据格式简单,易于读写、序列化后数据较小,可扩展性好、兼容性好,与XML想必,其协议比较简单,解析速度快。缺点:数据的描述性比XML差,不适合性能要求为ms级别的情况、额外空间开销比较大。适用场景:跨防火墙、可调试性要求高、基于Web browser的Ajax请求、传输数据相对较小,实时性要求相对低(例如秒级别)的服务。
FastJson:采用一种“嘉定有序快速匹配”的算法。优点:接口简单易用、目前Java语言中最快的json库。缺点:过于注重快,而偏离了“标准”及功能性、代码质量不高、文档不全,安全漏洞较多。适用场景:协议交互、Web输出、Android客户端。
select、poll、epoll的区别?
select、poll、epoll都是操作系统实现IO多路复用的机制。我们知道,IO多路复用就是通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作,那么这三种机制有什么区别呢
1、支持一个进程所能打开的最大连接数
2、FD剧增后带来的IO效率问题
3、消息传递方式
总结:
综上所述,在选择select、poll、epoll时要个根据具体的使用场景以及这三种方式的自身特点
1)表面上看epoll的性能最好,但是在连接数少,并且连接都十分活跃的情况下,select、poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调
2)select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可以通过良好的设计改善
直接内存:
在所有的网络通信和应用程序中,每个TCP的socket的内核中都一个发送缓冲区(SO_SNDBUF)和一个接收缓冲区(SO_RECVBUF),可以使用相关套接字选项来更改缓冲区大小。
当某个应用进程调用write时,内核从该应用进程的缓冲区中复制所有数据到所写套接字的发送缓冲区。如果该套接字的发送缓冲区容不下该应用进行的所有数据,假设该套接字是阻塞的,则该应用进行将被投入睡眠
内核将不从write系统调用返回,知道应用进程缓冲区中的所有数据都复制到套接字发送缓冲区。因此从写一个TCP套接字的write调用成功返回仅仅代表我们可以重新使用原来的应用缓冲区,并不表示对端的TCP或应用进程已接收到数据。
Java程序自然也要遵守上述的规则。但在Java中存在着堆、垃圾回收机制等特性,所以在实际的IO中,在JVM内部存在着这样一种机制:
在IO读写上,如果使用堆内存,JDK会先创建一个DirectBuffer,再去执行真正的写操作。这是因为,当我们把一个地址通过JNI传递给底层的C库的时候,有一个基本的要求,就是这个地址上的内容不能失效。然而,在GC管理下的对象会在Java堆中移动 的。也就是说,有可能我们把一个地址传递给底层的write,但是这段内存却因为GC整理内存而失效了。所以必须要把待发送的数据放到一个GC管不着的地方这就是调用native方法之前,数据一定要在堆外存的原因。
可见,DirectBuffer并没有节省什么内存拷贝,只是因为HeapBuffer必须多做一次拷贝,使用DirectBuffer就会少一次内存拷贝。想必没有使用堆内存的Java,使用直接内存的Java程序当然更快一点。
从垃圾回收的角度而言,直接内存不受GC(新生代的Minor GC)影响,只有当执行老年代的Full GC的时候才会顺带回收直接内存,整理内存的压力也比数据放到HeapBuffer要小。
堆外内存的优缺点
堆外内存相比于堆内内存有几个又是:
1)减少垃圾回收的工作,因为垃圾回收会暂停其他的工作STW
2)加快了复制速度。因为堆内存在flush到远程时,会先复制到直接内存,然后再发送,而堆外内存相当于省掉了这部分工作
缺点:
1)堆外内存难以控制,如果内存泄漏,那么很难排查
2)堆外内存相对来说,不适合存储很复杂的对象,一般简单的对象或者扁平化的比较合适
零拷贝:
零拷贝从广义上来讲,只要减少了一次拷贝或者上下文切换,我们就可以称为零拷贝。
Linux的IO机制与DMA
在早期计算机中,用户进程需要读取磁盘数据,需要CPU中断和CPU参与,因此效率较低,发起IO请求,每次的IO中断都给CPU带来上下文切换。因此出现, DMA。
DMA(Direct Memory Access,直接内存存取),是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于CPU的大量中断负载
DMA控制接,接管了数据读写请求,减少CPU负担,这样一来,CPU能高效工作了。现代硬盘基本都支持DMA。
实际IO读取,涉及两个过程:
1)DMA等待数据准备好,把磁盘数据读取到操作系统内核缓冲区
2)用户进程,将内核缓冲区的数据copy到用户空间
这两个过程都是阻塞的。
传统数据传送机制
比如:读取文件,再用socket发送出去,实际经过四次copy,伪代码:buffer=File.read()
Socket.send(buffer)
1、第一次,将磁盘文件读取到操作系统内核缓冲区
2、第二次,将内核缓冲区的数据,copy到应用程序的buffer
3、第三次,将application硬程序buffer中的数据,copy到socket网络发送缓冲区(属于操作系统内核的缓冲区)
4、第四次:将socket buffer的数据,copy到网卡,由网卡进行网络传输
分析上述的过程,虽然引入DMA来接管CPU的中断请求,但四次copy是存在“不必要是拷贝”的。实际上并不需要第二个和第三个数据副本。应用程序除了缓存数据并将其传回套接字缓冲区之外什么都不做。相反,数据可以直接从读缓冲区传输到套接字缓冲区。
显然,第二次和第三次数据copy,其实在这种场景下没有什么帮助反而带来开销,这也正是零拷贝出现的背景和意义。
同时,read和send都属于系统调用,每次调用都牵涉到两次上下文切换:
总结下,传统的数据传送所消耗的成本:4次拷贝,4次上下文切换
4次拷贝,其中两次是DMAcopy,两次是CPU copy
Linux支持的(常见)零拷贝
目的:减少IO流中不必要的拷贝,当然零拷贝需要OS支持,也就是需要kernel暴露API
mmap内存映射:
硬盘上文件的位置和应用程序缓冲区(application buffers)进行映射(建立一种一一对应关系),由于mmap()将文件直接映射到用户空间,所以实际文件读取时根据这个映射关系,直接将文件从硬盘拷贝到用户空间,只进行一次数据copy,不再有文件内容从硬盘copy到内核空间的一个缓冲区。
mmap内存映射将会经历:3次copy:1次CPU copy,2次DMAcopy,以及4次上下文切换
sendfile
Linux2.1支持的sendfile
当调用sendfile()时,DMA将磁盘数据复制到kernel buffer,然后将内核中的kernel buffer直接copy到socket buffer。在硬件支持的情况下,甚至数据都并不需要真正复制到socket关联的缓冲区内。取而代之的是,只有记录数据位置和长度的描述符被加入到socket缓冲区中,DMA模块将数据直接从内核缓冲区传递给协议引擎,从而消除了遗留的最后一次复制。
一旦数据全都copy到socket buffer,sendfile()系统调用将会return,代表数据转化的完成。socket buffer里的数据就能在网络出阿叔了
sendfile会经历:3次copy:1次CPUcopy,2次DMA copy。硬件支持的情况下,则是2次copy:0次CPU copy,2次DMA copy;以及2次上下文切换
splice:
Linux从2.6.17支持splice
数据从磁盘读取到OS内核缓冲区后,在内核缓冲区直接可直接将其转成内核空间其他数据buffer,而不需要copy到用户空间。
如下图所示,从磁盘读取到内核buffer后,在内核空间直接与socket buffer建立pipe管道。
和sendfile()不同的是,splice()不需要硬件支持。
注意splice和sendfile的不同,sendfile是将磁盘数据加载到kernel buffer后,需要一次CPU copy,copy到socket buffer。而splice是更进一步,连这个CPU copy也不需要了,直接将两个内核空间的buffer进行pipe(内存共享)
splice会经历:2次copy:0次CPU copy,2次DMA copy;以及2次上下文切换
Java生态圈中的零拷贝
Linux提供的零拷贝技术Java并不是全支持,支持2种(内存映射mmap、sendfile)
NIO提供的内存映射MappedByteBuffer:
NIO中的FileChannel.map()方法其实就是采用了操作系统中的内存映射方式,底层就是调用Linux mmap()实现的。
将内核缓冲区的内存和用户缓冲区的内存做了一个地址映射。这种方式适合读取大文件,同时也能读文件内容进行修改,但是如果其后要通过SocketChannel发送,还是需要CPU进行数据拷贝。
NIO提供的sendfile:
Java NIO中提供的FileChannel拥有transferTo和transferFrom两个方法,可直接把FileChannel中的数据拷贝到另外一个Channel,或者直接把另外一个Channel中的数据拷贝到FileChannel。该接口常被用于高效的网络/文件的数据传输和大文件。在操作系统支持的情况下,通过该方法传输数据并不需要将源数据从内核态拷贝到用户态,再从用户态拷贝到目标通道的内核态,同时也避免了两次用户态和内核态间的上下文切换,也即使用了“零拷贝”,所以其性能一般高于Java IO中提供的方法。