java 网络编程
- java.io中最为核心的一个概念是流(Stream),面向流的编程。java中,一个流要么是输入流,要么是输出流,不可能既是输入流又是输出流。
- Socket又称"套接字",应用程序通常通过"套接字"向网络发出请求或者应答网络请求。
Socket和ServerSocket类库位于java.net包中。ServerSocket用于服务器端,Socket是创建网络连接时使用的。在连接成功时,应用程序两端都会产生一个Socket实列,操作这个实例,完成所需的会话。对于一个网络连接来说,套接字是平等的,
不因为在服务器端或在客户端而产生不同的级别。不管是Socket还是ServerSocket它们的工作都是通过SocketImpl类及其子类完成的。 - 套接字之间的连接过程可以分为四个步骤:服务器监听,客户端请求服务器,服务器确认,客户端确认,进行通信。
- 服务器监听:是服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。
- 客户端请求:是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
- 服务器端连接确认:是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器段套接字的描述发给客户端。
- 客户端连接确认:一旦客户端确认了此描述,连接就建立好了。双方开始进行通信。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
简单的demo:
客户端:
public static void main(String[] args) {
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try {
socket = new Socket("localhost", 8899);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
//向服务器端发送数据
out.println("hi server!");
out.println("我是客户端");
String response = in.readLine();
System.out.println("Client: " + response);
} catch (Exception e) {
e.printStackTrace();
} finally {
if(in != null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(out != null){
try {
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
socket = null;
}
}
}
服务器端:
public class Server {
public static void main(String[] args) {
ServerSocket server = null;
try {
server = new ServerSocket(8899);
System.out.println("server start .. ");
//进行阻塞
Socket socket = server.accept();
//新建一个线程执行客户端的任务
new Thread(new ServerHandler(socket)).start();
} catch (Exception e) {
e.printStackTrace();
} finally {
if(server != null){
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
server = null;
}
}
}
服务端handler:
public class ServerHandler implements Runnable{
private Socket socket ;
public ServerHandler(Socket socket){
this.socket = socket;
}
@Override
public void run() {
BufferedReader in = null;
PrintWriter out = null;
try {
in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
out = new PrintWriter(this.socket.getOutputStream(), true);
String body;
while(true){
body = in.readLine();
if(body == null) break;
System.out.println("Server :" + body);
out.println("服务器端回送响的应数据.");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if(in != null){
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(out != null){
try {
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
socket = null;
}
}
}
BIO
弊端:
- 第一,只能同时处理一个连接,要管理多个并发客户端,需要为每个新的客户端Socket创建一个Thread。在这种情况下,任何时候都可能有大量的线程处于休眠状态,只是等待输入或者输出数据就绪,这算是一种资源浪费。
- 第二,需要为每个线程的调用栈都分配内存,其默认值大小区间为64kb到1Mb,具体取决于操作系统。
- 即使java虚拟机(jvm)在物理上可以支持非常大数量的线程,但是远在达到该极限之前,上下文切换所带来的开销就会带来麻烦,例如,在达到10000个连接的时候。
1.1.1 Java NIO
- NIO开始的时候是新的输入/输出(New Input/Output)的英文缩写,也可以被称作非阻塞(Non-blocking I/O)。
- 阻塞:应用程序在获取网络数据的时候,如果网络传输数据很慢,那么程序就一直等着,直到传输完毕为止。
- 非阻塞:应用程序直接可以获取已经准备就绪好的数据,无需等待。
-
我们都是把数据从Channel读到Buffer中,然后再从Buffer中读到程序中,绝对不可能数据从Channel直接读到程序中。
IO中一个流不可能既是输入流又是输出流,Channel把数据读到Buffer,buffer将数据读到程序中,程序也可以将数据写到buffer中。 -
除了数组之外,Buffer还提供了对于数据的结构化访问方式,并且可以追踪到系统的读写过程。
-
Java中的7种原生数据类型都有各自对应的Buffer类型,如IntBuffer,LongBuffer,ByteBuffer,CharBuffer等等,并没有BooleanBuffer类型。
-
Channel指的是可以向其写入数据或是从中读取数据的对象,它类似于java.io中的stream。
-
所有数据的读写都是通过Buffer来进行的,永远不会出现直接向Channel写入数据的情况,或是直接从Channel读取数据的情况。
-
与Stream不同的是,Channel是双向的,一个流只可能是InputStream或是OutputStream,Channel打开后则可以进行读写,写入或是读写。
-
由于Channel是双向的,因此它能更好的反映出底层操作系统的真实情况,在Linux系统中,底层操作系统的通道就是双向的。
NIO三个最重要的组件
Buffer(缓冲区),Channer(管道,通道) Selector(选择器,多路复用器)
-
Buffer:Buffer是一个对象,它包含一些要写入或者要读取的数据。在NIO类库中加入Buffer对象,体现了NIO与原IO的一个重要的区别。在面向流的IO中,可以将数据直接写入或读取到Stream对象中。在NIO库中,所有数据都是用缓存区处理的(读写)。缓冲区实质上是一个数组,通常它是一个字节数组(ByteBuffer),也可以使用其它类型的数组。这个数组为缓冲区提供了数组的访问读写等操作属性,如position,capacity,limit等概念。
Buffer类型:我们最常用的就是ByteBuffer,实际上每一种java基本类型都对于了一种缓存区(除了Boolean型),ByteBuffer,CharBuffer,ShortBuffer,IntBuffer
,LongBuffer,FloatBuffer,DoubleBuffer -
Channel:
通道(Channel),网络数据通过Channel读取和写入,通道与流不同之处在于通道是双向的,而流只是一个方向上移动(一个流必须是InputStream或者OutputStream的子类),而通道可以用于读,写或者二者同时进行,最关键的是可以与多路复用器结合起来,有多种的状态位,方便多路复用器去识别。事实上通道分为两大类,一类是网络读写的(SelectableChannel),一类是用于文件操作的(FileChannel),我们使用的SocketChannel和ServerSocketChannel都是SelectableChannel的子类。 -
Selector:
-
多路复用器(Selector),他是NIO编程的基础,非常重要。多路复用器提供选择已经就绪的任务的能力。
-
简单来说,就是Selector会不断的轮询注册在其上的通道(Channel),如果某个通道发生了读写操作,这个通道就处于就绪状态,会被Selector轮询出来,然后通过SelectKey可以取得就绪的Channel集合,从而进行后续的IO操作。
-
一个多路复用器(Selector)可以负责成千上万Channel通道,没有上限,这也是JDK使用了epoll代替了传统的select实现,获得连接句柄没有限制。这也就意味着我们只要一个线程负责Selector的轮询,就可以接入成千上万个客户端,这是JDK NIO库的巨大进步。
-
Selector线程就类似一个管理者(Master),管理了成千上万个管理,然后轮询那个通道的数据已经准备好,通知cpu执行io的读取或写入操作。
-
Selector模式:当IO事件(管道)注册到选择器以后,selector会分配给每个管道一个key值,相当于标签。selector选择器是以轮询的方式进行查找注册的所有IO事件(管道),当我们的IO时间(管道)准备就绪后,select就会识别,会通过key值来找到相应的管道,进行相关的数据处理操作(从管道里读或写数据,写到我们的数据缓冲区中)。
Demo1
了解一下Buffer概念
import java.nio.IntBuffer;
import java.security.SecureRandom;
public class NioTest1 {
public static void main(String[] args) {
//IntBuffer缓冲区分配10个长度
IntBuffer buffer = IntBuffer.allocate(10);
//将数据写到buffer中
for(int i=0;i<buffer.capacity();i++){
int randomNumber = new SecureRandom().nextInt(20);
buffer.put(randomNumber);
}
//使用flip实现读写的切换
buffer.flip();
//将buffer数据读出来
while (buffer.hasRemaining()){
System.out.println(buffer.get());
}
}
}
Demo2
管道与Buffer结合使用,我们都是将数据从Channel读到Buffer中(涉及到Buffer就是写动作),然后Buffer读到程序中,绝对不可能数据直接从Channel直接读到程序中。
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NioTest2 {
public static void main(String[] args) throws Exception{
FileInputStream fileInputStream = new FileInputStream("NioTest2.txt");
FileChannel fileChannel = fileInputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
fileChannel.read(byteBuffer);
//定义程序的反转,由读到写
byteBuffer.flip();
while(byteBuffer.remaining() > 0){
byte b = byteBuffer.get();
System.out.println("Character: "+(char)b);
}
fileInputStream.close();
}
}
Demo3
import java.nio.IntBuffer;
import java.security.SecureRandom;
public class NioTest1 {
public static void main(String[] args) {
//IntBuffer缓冲区分配10个长度
IntBuffer buffer = IntBuffer.allocate(10);
System.out.println("capacity:"+buffer.capacity());
//将数据写到buffer中
for(int i=0;i<5;i++){
int randomNumber = new SecureRandom().nextInt(20);
buffer.put(randomNumber);
}
System.out.println("before flip limit: "+buffer.limit()); //10
//使用flip实现读写的切换
buffer.flip();
System.out.println("after flip limit: "+buffer.limit()); //5
System.out.println("enter while loop");
//将buffer数据读出来
while (buffer.hasRemaining()){
System.out.println("position: "+buffer.position());
System.out.println("limit: "+buffer.limit()); //5
System.out.println("capacity: "+buffer.capacity()); //10
System.out.println(buffer.get());
}
}
}
关于NIO Buffer中的3个重要状态属性的含义:position,limit与capacity。flip方法为什么在读写转换的时候调用。
java的api对其的描述:
A buffer is a linear, finite sequence of elements of a specific primitive type. Aside from its content, the essential properties of a buffer are its capacity, limit, and position:
一个buffer是线性,有限的特性原生类型的序列。除了其内容外,一个buffer最基础的属性是capacity,limit,和position:
A buffer's capacity is the number of elements it contains. The capacity of a buffer is never negative and never changes.
一个buffer的capacity就是其包含的元素的数量。一个buffer的capacity不可能是负数并且不会被改变。
A buffer's limit is the index of the first element that should not be read or written. A buffer's limit is never negative and is never greater than its capacity.
一个buffer的limit指的是无法再去读或者写的下一个元素的索引。一个buffer的limit不可能是负数并且不可能大于它的capacity。
A buffer's position is the index of the next element to be read or written. A buffer's position is never negative and is never greater than its limit.
一个buffer的position是下个能被读或者写的元素的索引。一个buffer的position不可能是负数并且不可能大于它的limit。
- 调用buffer.flip()方法设置了当前的limit的值等于当前的position,并且将position的值设为0.
- 调用buffer.clear()方法设置limit的值等于capacity并且position的值设为0。
Demo4
写一个关于三个组件都使用的程序
public class NioTest4 {
public static void main(String[] args) throws Exception{
int[] ports = new int[5];
ports[0]= 5000;
ports[1]= 5001;
ports[2]= 5002;
ports[3]= 5003;
ports[4]= 5004;
//Selector对象最常见的构建方式
Selector selector = Selector.open();
for (int i = 0; i <ports.length ; i++) {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//设置channel为非阻塞模式
serverSocketChannel.configureBlocking(false);
//得到与channel相关联的Socket
ServerSocket serverSocket = serverSocketChannel.socket();
InetSocketAddress inetSocketAddress = new InetSocketAddress(ports[i]);
serverSocket.bind(inetSocketAddress);
//将channel注册到selector中,并且监听的是接收连接事件,这边只能监听客户端连接事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("监听端口:" + ports[i]);
}
while(true){
int number = selector.select();
System.out.println("number: "+number);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
System.out.println("selectedKeys: "+selectionKeys);
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while(iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
//接收到连接,我们将selector注册到一个个的ServerChannel当中,我们可以拿到每一个与之连接的Channel
if(selectionKey.isAcceptable()){
ServerSocketChannel serverSocketChannel = (ServerSocketChannel)selectionKey.channel();
//得到连接这个channel的SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
//向selector注册可读事件
socketChannel.register(selector,SelectionKey.OP_READ);
//处理完selectkey之后一定要remove掉
iterator.remove();
System.out.println("获得客户端的连接: "+socketChannel);
}else if(selectionKey.isReadable()){
SocketChannel socketChannel= (SocketChannel)selectionKey.channel();
int bytesRead = 0;
while(true){
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
byteBuffer.clear();
int read = socketChannel.read(byteBuffer);
if(read <= 0){
break;
}
byteBuffer.flip();
socketChannel.write(byteBuffer);
bytesRead +=read;
}
System.out.println("读取: "+ bytesRead+",来自于: "+socketChannel);
iterator.remove();
}
}
}
}
}
通过nc localhost 5050
,nc localhost 5051
,nc localhost 5052
等可以去测试.
Selector维护着三个set集合:
第一个就是所有当前注册到selector上的key的集合。这个set可以通过调用keys()方法来获得。
第二个就是确定的一种感兴趣的集合比如连接的动作,可读可写的动作,这个set是通过selectedKeys方法返回的,这个已经被选择的key集合总是上面的keys()方法返回的子集。
第三个就是取消的key的集合是已经被取消的集合但是其channels还没有被取消注册。这个set集合是不能直接访问的。这个已经取消的key的集合也是上面的key set集合的一个子集。一般取消的集合都要在当前的遍历中删除掉。